From 3a6451c867595fd58998ee1706589b15a69d993b Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:19:29 +0100 Subject: Add support for incoming rich text (#23913) --- app/javascript/styles/application.scss | 1 + app/javascript/styles/mastodon/rich_text.scss | 64 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 app/javascript/styles/mastodon/rich_text.scss (limited to 'app') diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 81a040108..1b2969c23 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -23,3 +23,4 @@ @import 'mastodon/dashboard'; @import 'mastodon/rtl'; @import 'mastodon/accessibility'; +@import 'mastodon/rich_text'; diff --git a/app/javascript/styles/mastodon/rich_text.scss b/app/javascript/styles/mastodon/rich_text.scss new file mode 100644 index 000000000..35901984b --- /dev/null +++ b/app/javascript/styles/mastodon/rich_text.scss @@ -0,0 +1,64 @@ +.status__content__text, +.e-content, +.reply-indicator__content { + pre, + blockquote { + margin-bottom: 20px; + white-space: pre-wrap; + unicode-bidi: plaintext; + + &:last-child { + margin-bottom: 0; + } + } + + blockquote { + padding-left: 10px; + border-left: 3px solid $darker-text-color; + color: $darker-text-color; + white-space: normal; + + p:last-child { + margin-bottom: 0; + } + } + + & > ul, + & > ol { + margin-bottom: 20px; + } + + b, + strong { + font-weight: 700; + } + + em, + i { + font-style: italic; + } + + ul, + ol { + margin-left: 2em; + + p { + margin: 0; + } + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } +} + +.reply-indicator__content { + blockquote { + border-left-color: $inverted-text-color; + color: $inverted-text-color; + } +} -- cgit From c2a046ded1d47e2504df05568e34bc6a2a6dc810 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:25:15 +0100 Subject: Fix “Remove all followers from the selected domains” being more destructive than it claims (#23805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/relationships_controller.rb | 4 ++-- app/models/form/account_batch.rb | 10 ++++------ .../remove_domains_from_followers_service.rb | 23 ++++++++++++++++++++++ app/views/relationships/show.html.haml | 2 +- spec/controllers/relationships_controller_spec.rb | 11 ++++++++++- 5 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 app/services/remove_domains_from_followers_service.rb (limited to 'app') diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 96cce55e9..baa34da22 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -60,8 +60,8 @@ class RelationshipsController < ApplicationController 'unfollow' elsif params[:remove_from_followers] 'remove_from_followers' - elsif params[:block_domains] - 'block_domains' + elsif params[:block_domains] || params[:remove_domains_from_followers] + 'remove_domains_from_followers' end end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 473622edf..5a7fc7ed1 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -17,8 +17,8 @@ class Form::AccountBatch unfollow! when 'remove_from_followers' remove_from_followers! - when 'block_domains' - block_domains! + when 'remove_domains_from_followers' + remove_domains_from_followers! when 'approve' approve! when 'reject' @@ -50,10 +50,8 @@ class Form::AccountBatch RemoveFromFollowersService.new.call(current_account, account_ids) end - def block_domains! - AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain| - [current_account.id, domain] - end + def remove_domains_from_followers! + RemoveDomainsFromFollowersService.new.call(current_account, account_domains) end def account_domains diff --git a/app/services/remove_domains_from_followers_service.rb b/app/services/remove_domains_from_followers_service.rb new file mode 100644 index 000000000..d76763409 --- /dev/null +++ b/app/services/remove_domains_from_followers_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class RemoveDomainsFromFollowersService < BaseService + include Payloadable + + def call(source_account, target_domains) + source_account.passive_relationships.where(account_id: Account.where(domain: target_domains)).find_each do |follow| + follow.destroy + + create_notification(follow) if source_account.local? && !follow.account.local? && follow.account.activitypub? + end + end + + private + + def create_notification(follow) + ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.target_account_id, follow.account.inbox_url) + end + + def build_json(follow) + Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) + end +end diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml index 2899cd514..f08e9c1df 100644 --- a/app/views/relationships/show.html.haml +++ b/app/views/relationships/show.html.haml @@ -48,7 +48,7 @@ = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('relationships.confirm_remove_selected_followers') } unless following_relationship? - = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? + = f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :remove_domains_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship? .batch-table__body - if @accounts.empty? = nothing_here 'nothing-here--under-tabs' diff --git a/spec/controllers/relationships_controller_spec.rb b/spec/controllers/relationships_controller_spec.rb index 39f455e03..53a5daa51 100644 --- a/spec/controllers/relationships_controller_spec.rb +++ b/spec/controllers/relationships_controller_spec.rb @@ -58,7 +58,7 @@ describe RelationshipsController do end context 'when select parameter is provided' do - subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, block_domains: '' } } + subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, remove_domains_from_followers: '' } } it 'soft-blocks followers from selected domains' do poopfeast.follow!(user.account) @@ -69,6 +69,15 @@ describe RelationshipsController do expect(poopfeast.following?(user.account)).to be false end + it 'does not unfollow users from selected domains' do + user.account.follow!(poopfeast) + + sign_in user, scope: :user + subject + + expect(user.account.following?(poopfeast)).to be true + end + include_examples 'authenticate user' include_examples 'redirects back to followers page' end -- cgit From b55fc883b6181fb8080fbafd53bc7a9f1896b295 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:25:36 +0100 Subject: Fix duplicate “Publish” button on mobile (#23804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mastodon/features/ui/components/header.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/features/ui/components/header.jsx b/app/javascript/mastodon/features/ui/components/header.jsx index 1384bebda..92adc47a9 100644 --- a/app/javascript/mastodon/features/ui/components/header.jsx +++ b/app/javascript/mastodon/features/ui/components/header.jsx @@ -22,8 +22,8 @@ const mapDispatchToProps = (dispatch) => ({ }, }); -export default @connect(null, mapDispatchToProps) -@withRouter +export default @withRouter +@connect(null, mapDispatchToProps) class Header extends React.PureComponent { static contextTypes = { -- cgit From f8bb4d0d6b1050de481187e9f034b8bbb649d931 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:36:18 +0100 Subject: Fix server error when failing to follow back followers from `/relationships` (#23787) --- app/controllers/relationships_controller.rb | 2 ++ app/models/form/account_batch.rb | 6 ++++++ config/locales/en.yml | 1 + 3 files changed, 9 insertions(+) (limited to 'app') diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index baa34da22..de5dc5879 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -19,6 +19,8 @@ class RelationshipsController < ApplicationController @form.save rescue ActionController::ParameterMissing # Do nothing + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound + flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow' ensure redirect_to relationships_path(filter_params) end diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 5a7fc7ed1..6a05f8163 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -35,9 +35,15 @@ class Form::AccountBatch private def follow! + error = nil + accounts.each do |target_account| FollowService.new.call(current_account, target_account) + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound => e + error ||= e end + + raise error if error.present? end def unfollow! diff --git a/config/locales/en.yml b/config/locales/en.yml index 9f8ba7ce7..d142962b5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1408,6 +1408,7 @@ en: confirm_remove_selected_followers: Are you sure you want to remove selected followers? confirm_remove_selected_follows: Are you sure you want to remove selected follows? dormant: Dormant + follow_failure: Could not follow some of the selected accounts. follow_selected_followers: Follow selected followers followers: Followers following: Following -- cgit From a1347f456e47e085ef1031b607cabd0683c496c2 Mon Sep 17 00:00:00 2001 From: Terry Garcia <10190993+TerryGarcia@users.noreply.github.com> Date: Fri, 3 Mar 2023 13:37:49 -0600 Subject: Switched bookmark and favourites around (#23701) --- app/javascript/mastodon/features/ui/components/navigation_panel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx index 9a9309be0..755b19349 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx @@ -82,8 +82,8 @@ class NavigationPanel extends React.Component { {signedIn && ( - + -- cgit From f94aa70b814159964de660ac33bc797a1e01af30 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:44:02 +0100 Subject: Fix error when displaying post history of a trendable post in the admin interface (#23574) --- app/views/admin/statuses/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml index 62b49de8c..4631e97f1 100644 --- a/app/views/admin/statuses/show.html.haml +++ b/app/views/admin/statuses/show.html.haml @@ -34,7 +34,7 @@ %td - if @status.trend.allowed? %abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank) - - elsif @status.trend.requires_review? + - elsif @status.requires_review? = t('admin.trends.pending_review') - else = t('admin.trends.not_allowed_to_trend') -- cgit From d6679d175181e425f52e5a6b062bc2c5545ce0cc Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:44:46 +0100 Subject: Add mail headers to avoid auto-replies (#23597) --- app/mailers/application_mailer.rb | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app') diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 73b623576..35f0b5fee 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -7,9 +7,17 @@ class ApplicationMailer < ActionMailer::Base helper :instance helper :formatting + after_action :set_autoreply_headers! + protected def locale_for_account(account, &block) I18n.with_locale(account.user_locale || I18n.default_locale, &block) end + + def set_autoreply_headers! + headers['Precedence'] = 'list' + headers['X-Auto-Response-Suppress'] = 'All' + headers['Auto-Submitted'] = 'auto-generated' + end end -- cgit From 6b16b77ab0f347e688f009c5a04961142b8dd203 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:45:55 +0100 Subject: Fix external authentication not running onboarding code for new users (#23458) --- app/models/concerns/omniauthable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index b0aa5be6f..41eae215b 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -61,7 +61,7 @@ module Omniauthable user.account.avatar_remote_url = nil end - user.skip_confirmation! if email_is_verified + user.confirm! if email_is_verified user.save! user end -- cgit From 5a8c651e8f0252c7135042e79396f782361302d9 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 3 Mar 2023 21:06:31 +0100 Subject: Only offer translation for supported languages (#23879) --- .rubocop.yml | 4 + .../mastodon/components/status_content.jsx | 4 +- app/javascript/mastodon/initial_state.js | 2 - app/lib/translation_service.rb | 4 + app/lib/translation_service/deepl.rb | 46 +++++++--- app/lib/translation_service/libre_translate.rb | 38 +++++--- app/models/status.rb | 10 +++ app/serializers/initial_state_serializer.rb | 1 - app/serializers/rest/status_serializer.rb | 6 +- app/services/translate_status_service.rb | 2 +- spec/lib/translation_service/deepl_spec.rb | 100 +++++++++++++++++++++ .../translation_service/libre_translate_spec.rb | 71 +++++++++++++++ spec/models/status_spec.rb | 79 ++++++++++++++++ 13 files changed, 336 insertions(+), 31 deletions(-) create mode 100644 spec/lib/translation_service/deepl_spec.rb create mode 100644 spec/lib/translation_service/libre_translate_spec.rb (limited to 'app') diff --git a/.rubocop.yml b/.rubocop.yml index 27d778edf..0a41c54b9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -97,6 +97,10 @@ Rails/Exit: - 'lib/mastodon/cli_helper.rb' - 'lib/cli.rb' +RSpec/FilePath: + CustomTransform: + DeepL: deepl + RSpec/NotToNot: EnforcedStyle: to_not diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index a1c38171f..f9c9fe079 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -6,7 +6,7 @@ import { Link } from 'react-router-dom'; import classnames from 'classnames'; import PollContainer from 'mastodon/containers/poll_container'; import Icon from 'mastodon/components/icon'; -import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state'; +import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) @@ -220,7 +220,7 @@ class StatusContent extends React.PureComponent { const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const renderReadMore = this.props.onClick && status.get('collapsed'); - const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language'); + const renderTranslate = this.props.onTranslate && status.get('translatable'); const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index d04c4a42d..919e0fc28 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -80,7 +80,6 @@ * @property {boolean} use_blurhash * @property {boolean=} use_pending_items * @property {string} version - * @property {boolean} translation_enabled */ /** @@ -132,7 +131,6 @@ export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); -export const translationEnabled = getMeta('translation_enabled'); export const languages = initialState?.languages; export const statusPageUrl = getMeta('status_page_url'); diff --git a/app/lib/translation_service.rb b/app/lib/translation_service.rb index 285f30939..5ff93674a 100644 --- a/app/lib/translation_service.rb +++ b/app/lib/translation_service.rb @@ -21,6 +21,10 @@ class TranslationService ENV['DEEPL_API_KEY'].present? || ENV['LIBRE_TRANSLATE_ENDPOINT'].present? end + def supported?(_source_language, _target_language) + false + end + def translate(_text, _source_language, _target_language) raise NotImplementedError end diff --git a/app/lib/translation_service/deepl.rb b/app/lib/translation_service/deepl.rb index 151d33d90..deff95a1d 100644 --- a/app/lib/translation_service/deepl.rb +++ b/app/lib/translation_service/deepl.rb @@ -11,33 +11,53 @@ class TranslationService::DeepL < TranslationService end def translate(text, source_language, target_language) - request(text, source_language, target_language).perform do |res| + form = { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' } + request(:post, '/v2/translate', form: form) do |res| + transform_response(res.body_with_limit) + end + end + + def supported?(source_language, target_language) + source_language.in?(languages('source')) && target_language.in?(languages('target')) + end + + private + + def languages(type) + Rails.cache.fetch("translation_service/deepl/languages/#{type}", expires_in: 7.days, race_condition_ttl: 1.minute) do + request(:get, "/v2/languages?type=#{type}") do |res| + # In DeepL, EN and PT are deprecated in favor of EN-GB/EN-US and PT-BR/PT-PT, so + # they are supported but not returned by the API. + extra = type == 'source' ? [nil] : %w(en pt) + languages = Oj.load(res.body_with_limit).map { |language| language['language'].downcase } + + languages + extra + end + end + end + + def request(verb, path, **options) + req = Request.new(verb, "#{base_url}#{path}", **options) + req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}") + req.perform do |res| case res.code when 429 raise TooManyRequestsError when 456 raise QuotaExceededError when 200...300 - transform_response(res.body_with_limit) + yield res else raise UnexpectedResponseError end end end - private - - def request(text, source_language, target_language) - req = Request.new(:post, endpoint_url, form: { text: text, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' }) - req.add_headers(Authorization: "DeepL-Auth-Key #{@api_key}") - req - end - - def endpoint_url + def base_url if @plan == 'free' - 'https://api-free.deepl.com/v2/translate' + 'https://api-free.deepl.com' else - 'https://api.deepl.com/v2/translate' + 'https://api.deepl.com' end end diff --git a/app/lib/translation_service/libre_translate.rb b/app/lib/translation_service/libre_translate.rb index 4ebe21e45..743e4d77f 100644 --- a/app/lib/translation_service/libre_translate.rb +++ b/app/lib/translation_service/libre_translate.rb @@ -9,29 +9,45 @@ class TranslationService::LibreTranslate < TranslationService end def translate(text, source_language, target_language) - request(text, source_language, target_language).perform do |res| + body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) + request(:post, '/translate', body: body) do |res| + transform_response(res.body_with_limit, source_language) + end + end + + def supported?(source_language, target_language) + languages.key?(source_language) && languages[source_language].include?(target_language) + end + + private + + def languages + Rails.cache.fetch('translation_service/libre_translate/languages', expires_in: 7.days, race_condition_ttl: 1.minute) do + request(:get, '/languages') do |res| + languages = Oj.load(res.body_with_limit).to_h { |language| [language['code'], language['targets']] } + languages[nil] = languages.values.flatten.uniq + languages + end + end + end + + def request(verb, path, **options) + req = Request.new(verb, "#{@base_url}#{path}", allow_local: true, **options) + req.add_headers('Content-Type': 'application/json') + req.perform do |res| case res.code when 429 raise TooManyRequestsError when 403 raise QuotaExceededError when 200...300 - transform_response(res.body_with_limit, source_language) + yield res else raise UnexpectedResponseError end end end - private - - def request(text, source_language, target_language) - body = Oj.dump(q: text, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) - req = Request.new(:post, "#{@base_url}/translate", body: body, allow_local: true) - req.add_headers('Content-Type': 'application/json') - req - end - def transform_response(str, source_language) json = Oj.load(str, mode: :strict) diff --git a/app/models/status.rb b/app/models/status.rb index e7ea191a8..dd7ac2edb 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -232,6 +232,16 @@ class Status < ApplicationRecord public_visibility? || unlisted_visibility? end + def translatable? + translate_target_locale = I18n.locale.to_s.split(/[_-]/).first + + distributable? && + content.present? && + language != translate_target_locale && + TranslationService.configured? && + TranslationService.configured.supported?(language, translate_target_locale) + end + alias sign? distributable? def with_media? diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 7905444e9..769ba653e 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -30,7 +30,6 @@ class InitialStateSerializer < ActiveModel::Serializer timeline_preview: Setting.timeline_preview, activity_api_enabled: Setting.activity_api_enabled, single_user_mode: Rails.configuration.x.single_user_mode, - translation_enabled: TranslationService.configured?, trends_as_landing_page: Setting.trends_as_landing_page, status_page_url: Setting.status_page_url, } diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index e0b8f32a6..a422f5b25 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer include FormattingHelper attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, - :sensitive, :spoiler_text, :visibility, :language, + :sensitive, :spoiler_text, :visibility, :language, :translatable, :uri, :url, :replies_count, :reblogs_count, :favourites_count, :edited_at @@ -50,6 +50,10 @@ class REST::StatusSerializer < ActiveModel::Serializer object.account.user_shows_application? || (current_user? && current_user.account_id == object.account_id) end + def translatable + current_user? && object.translatable? + end + def visibility # This visibility is masked behind "private" # to avoid API changes because there are no diff --git a/app/services/translate_status_service.rb b/app/services/translate_status_service.rb index 539a0d9db..92d8b62a0 100644 --- a/app/services/translate_status_service.rb +++ b/app/services/translate_status_service.rb @@ -6,7 +6,7 @@ class TranslateStatusService < BaseService include FormattingHelper def call(status, target_language) - raise Mastodon::NotPermittedError unless status.public_visibility? || status.unlisted_visibility? + raise Mastodon::NotPermittedError unless status.translatable? @status = status @content = status_content_format(@status) diff --git a/spec/lib/translation_service/deepl_spec.rb b/spec/lib/translation_service/deepl_spec.rb new file mode 100644 index 000000000..aa2473186 --- /dev/null +++ b/spec/lib/translation_service/deepl_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TranslationService::DeepL do + subject(:service) { described_class.new(plan, 'my-api-key') } + + let(:plan) { 'advanced' } + + before do + stub_request(:get, 'https://api.deepl.com/v2/languages?type=source').to_return( + body: '[{"language":"EN","name":"English"},{"language":"UK","name":"Ukrainian"}]' + ) + stub_request(:get, 'https://api.deepl.com/v2/languages?type=target').to_return( + body: '[{"language":"EN-GB","name":"English (British)"},{"language":"ZH","name":"Chinese"}]' + ) + end + + describe '#supported?' do + it 'supports included languages as source and target languages' do + expect(service.supported?('uk', 'en')).to be true + end + + it 'supports auto-detecting source language' do + expect(service.supported?(nil, 'en')).to be true + end + + it 'supports "en" and "pt" as target languages though not included in language list' do + expect(service.supported?('uk', 'en')).to be true + expect(service.supported?('uk', 'pt')).to be true + end + + it 'does not support non-included language as target language' do + expect(service.supported?('uk', 'nl')).to be false + end + + it 'does not support non-included language as source language' do + expect(service.supported?('da', 'en')).to be false + end + end + + describe '#translate' do + it 'returns translation with specified source language' do + stub_request(:post, 'https://api.deepl.com/v2/translate') + .with(body: 'text=Hasta+la+vista&source_lang=ES&target_lang=en&tag_handling=html') + .to_return(body: '{"translations":[{"detected_source_language":"ES","text":"See you soon"}]}') + + translation = service.translate('Hasta la vista', 'es', 'en') + expect(translation.detected_source_language).to eq 'es' + expect(translation.provider).to eq 'DeepL.com' + expect(translation.text).to eq 'See you soon' + end + + it 'returns translation with auto-detected source language' do + stub_request(:post, 'https://api.deepl.com/v2/translate') + .with(body: 'text=Guten+Tag&source_lang&target_lang=en&tag_handling=html') + .to_return(body: '{"translations":[{"detected_source_language":"DE","text":"Good Morning"}]}') + + translation = service.translate('Guten Tag', nil, 'en') + expect(translation.detected_source_language).to eq 'de' + expect(translation.provider).to eq 'DeepL.com' + expect(translation.text).to eq 'Good Morning' + end + end + + describe '#languages?' do + it 'returns source languages' do + expect(service.send(:languages, 'source')).to eq ['en', 'uk', nil] + end + + it 'returns target languages' do + expect(service.send(:languages, 'target')).to eq %w(en-gb zh en pt) + end + end + + describe '#request' do + before do + stub_request(:any, //) + # rubocop:disable Lint/EmptyBlock + service.send(:request, :get, '/v2/languages') { |res| } + # rubocop:enable Lint/EmptyBlock + end + + it 'uses paid plan base URL' do + expect(a_request(:get, 'https://api.deepl.com/v2/languages')).to have_been_made.once + end + + context 'with free plan' do + let(:plan) { 'free' } + + it 'uses free plan base URL' do + expect(a_request(:get, 'https://api-free.deepl.com/v2/languages')).to have_been_made.once + end + end + + it 'sends API key' do + expect(a_request(:get, 'https://api.deepl.com/v2/languages').with(headers: { Authorization: 'DeepL-Auth-Key my-api-key' })).to have_been_made.once + end + end +end diff --git a/spec/lib/translation_service/libre_translate_spec.rb b/spec/lib/translation_service/libre_translate_spec.rb new file mode 100644 index 000000000..a6cb01884 --- /dev/null +++ b/spec/lib/translation_service/libre_translate_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe TranslationService::LibreTranslate do + subject(:service) { described_class.new('https://libretranslate.example.com', 'my-api-key') } + + before do + stub_request(:get, 'https://libretranslate.example.com/languages').to_return( + body: '[{"code": "en","name": "English","targets": ["de","es"]},{"code": "da","name": "Danish","targets": ["en","de"]}]' + ) + end + + describe '#supported?' do + it 'supports included language pair' do + expect(service.supported?('en', 'de')).to be true + end + + it 'does not support reversed language pair' do + expect(service.supported?('de', 'en')).to be false + end + + it 'supports auto-detecting source language' do + expect(service.supported?(nil, 'de')).to be true + end + + it 'does not support auto-detecting for unsupported target language' do + expect(service.supported?(nil, 'pt')).to be false + end + end + + describe '#languages' do + subject(:languages) { service.send(:languages) } + + it 'includes supported source languages' do + expect(languages.keys).to eq ['en', 'da', nil] + end + + it 'includes supported target languages for source language' do + expect(languages['en']).to eq %w(de es) + end + + it 'includes supported target languages for auto-detected language' do + expect(languages[nil]).to eq %w(de es en) + end + end + + describe '#translate' do + it 'returns translation with specified source language' do + stub_request(:post, 'https://libretranslate.example.com/translate') + .with(body: '{"q":"Hasta la vista","source":"es","target":"en","format":"html","api_key":"my-api-key"}') + .to_return(body: '{"translatedText": "See you"}') + + translation = service.translate('Hasta la vista', 'es', 'en') + expect(translation.detected_source_language).to eq 'es' + expect(translation.provider).to eq 'LibreTranslate' + expect(translation.text).to eq 'See you' + end + + it 'returns translation with auto-detected source language' do + stub_request(:post, 'https://libretranslate.example.com/translate') + .with(body: '{"q":"Guten Morgen","source":"auto","target":"en","format":"html","api_key":"my-api-key"}') + .to_return(body: '{"detectedLanguage":{"confidence":92,"language":"de"},"translatedText":"Good morning"}') + + translation = service.translate('Guten Morgen', nil, 'en') + expect(translation.detected_source_language).to be_nil + expect(translation.provider).to eq 'LibreTranslate' + expect(translation.text).to eq 'Good morning' + end + end +end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 1e58c6d0d..1f6cfc796 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -114,6 +114,85 @@ RSpec.describe Status, type: :model do end end + describe '#translatable?' do + before do + allow(TranslationService).to receive(:configured?).and_return(true) + allow(TranslationService).to receive(:configured).and_return(TranslationService.new) + allow(TranslationService.configured).to receive(:supported?).with('es', 'en').and_return(true) + + subject.language = 'es' + subject.visibility = :public + end + + context 'all conditions are satisfied' do + it 'returns true' do + expect(subject.translatable?).to be true + end + end + + context 'translation service is not configured' do + it 'returns false' do + allow(TranslationService).to receive(:configured?).and_return(false) + allow(TranslationService).to receive(:configured).and_raise(TranslationService::NotConfiguredError) + expect(subject.translatable?).to be false + end + end + + context 'status language is nil' do + it 'returns true' do + subject.language = nil + allow(TranslationService.configured).to receive(:supported?).with(nil, 'en').and_return(true) + expect(subject.translatable?).to be true + end + end + + context 'status language is same as default locale' do + it 'returns false' do + subject.language = I18n.locale + expect(subject.translatable?).to be false + end + end + + context 'status language is unsupported' do + it 'returns false' do + subject.language = 'af' + allow(TranslationService.configured).to receive(:supported?).with('af', 'en').and_return(false) + expect(subject.translatable?).to be false + end + end + + context 'default locale is unsupported' do + it 'returns false' do + allow(TranslationService.configured).to receive(:supported?).with('es', 'af').and_return(false) + I18n.with_locale('af') do + expect(subject.translatable?).to be false + end + end + end + + context 'default locale has region' do + it 'returns true' do + I18n.with_locale('en-GB') do + expect(subject.translatable?).to be true + end + end + end + + context 'status text is blank' do + it 'returns false' do + subject.text = ' ' + expect(subject.translatable?).to be false + end + end + + context 'status visiblity is hidden' do + it 'returns false' do + subject.visibility = 'limited' + expect(subject.translatable?).to be false + end + end + end + describe '#content' do it 'returns the text of the status if it is not a reblog' do expect(subject.content).to eql subject.text -- cgit From ddde4e0d954fbb1338774d61f1130e4af6924496 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 21:08:22 +0100 Subject: Change `ActivityPub::DeliveryWorker` retries to be spread out more (#21956) --- app/workers/activitypub/delivery_worker.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'app') diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index d9153132b..7c1c14766 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -10,6 +10,16 @@ class ActivityPub::DeliveryWorker sidekiq_options queue: 'push', retry: 16, dead: false + # Unfortunately, we cannot control Sidekiq's jitter, so add our own + sidekiq_retry_in do |count| + # This is Sidekiq's default delay + delay = (count**4) + 15 + # Our custom jitter, that will be added to Sidekiq's built-in one. + # Sidekiq's built-in jitter is `rand(10) * (count + 1)` + jitter = rand(0.5 * (count**4)) + delay + jitter + end + HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze def perform(json, source_account_id, inbox_url, options = {}) -- cgit From 050f1669c6fc02d7a917261d16d9264512955bc6 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 21:13:55 +0100 Subject: Fix original account being unfollowed on migration before the follow request could be sent (#21957) --- app/services/follow_migration_service.rb | 40 ++++++++++++++++++++++ .../activitypub/migrated_follow_delivery_worker.rb | 17 +++++++++ app/workers/unfollow_follow_worker.rb | 8 +---- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 app/services/follow_migration_service.rb create mode 100644 app/workers/activitypub/migrated_follow_delivery_worker.rb (limited to 'app') diff --git a/app/services/follow_migration_service.rb b/app/services/follow_migration_service.rb new file mode 100644 index 000000000..cfe9093cb --- /dev/null +++ b/app/services/follow_migration_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class FollowMigrationService < FollowService + # Follow an account with the same settings as another account, and unfollow the old account once the request is sent + # @param [Account] source_account From which to follow + # @param [Account] target_account Account to follow + # @param [Account] old_target_account Account to unfollow once the follow request has been sent to the new one + # @option [Boolean] bypass_locked Whether to immediately follow the new account even if it is locked + def call(source_account, target_account, old_target_account, bypass_locked: false) + @old_target_account = old_target_account + + follow = source_account.active_relationships.find_by(target_account: old_target_account) + reblogs = follow&.show_reblogs? + notify = follow&.notify? + languages = follow&.languages + + super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true) + end + + private + + def request_follow! + follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])) + + if @target_account.local? + LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request') + UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true) + elsif @target_account.activitypub? + ActivityPub::MigratedFollowDeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, @old_target_account.id) + end + + follow_request + end + + def direct_follow! + follow = super + UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true) + follow + end +end diff --git a/app/workers/activitypub/migrated_follow_delivery_worker.rb b/app/workers/activitypub/migrated_follow_delivery_worker.rb new file mode 100644 index 000000000..17a9e515e --- /dev/null +++ b/app/workers/activitypub/migrated_follow_delivery_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker + def perform(json, source_account_id, inbox_url, old_target_account_id, options = {}) + super(json, source_account_id, inbox_url, options) + unfollow_old_account!(old_target_account_id) + end + + private + + def unfollow_old_account!(old_target_account_id) + old_target_account = Account.find(old_target_account_id) + UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true) + rescue StandardError + true + end +end diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb index 7203b4888..a4d57839d 100644 --- a/app/workers/unfollow_follow_worker.rb +++ b/app/workers/unfollow_follow_worker.rb @@ -10,13 +10,7 @@ class UnfollowFollowWorker old_target_account = Account.find(old_target_account_id) new_target_account = Account.find(new_target_account_id) - follow = follower_account.active_relationships.find_by(target_account: old_target_account) - reblogs = follow&.show_reblogs? - notify = follow&.notify? - languages = follow&.languages - - FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true) - UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true) + FollowMigrationService.new.call(follower_account, new_target_account, old_target_account, bypass_locked: bypass_locked) rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError true end -- cgit From b00f945d92b6fff58c8a8ed702a9511ddb56a3da Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Fri, 3 Mar 2023 16:49:16 -0500 Subject: Remove implied StandardError rescue (#23942) --- app/workers/activitypub/migrated_follow_delivery_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/workers/activitypub/migrated_follow_delivery_worker.rb b/app/workers/activitypub/migrated_follow_delivery_worker.rb index 17a9e515e..daf30e0ae 100644 --- a/app/workers/activitypub/migrated_follow_delivery_worker.rb +++ b/app/workers/activitypub/migrated_follow_delivery_worker.rb @@ -11,7 +11,7 @@ class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker def unfollow_old_account!(old_target_account_id) old_target_account = Account.find(old_target_account_id) UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true) - rescue StandardError + rescue true end end -- cgit From 922837dc96154b0455a4cf660c3f8369c65aacb4 Mon Sep 17 00:00:00 2001 From: Jean byroot Boussier Date: Sat, 4 Mar 2023 16:38:28 +0100 Subject: Upgrade to latest redis-rb 4.x and fix deprecations (#23616) Co-authored-by: Jean Boussier --- Gemfile.lock | 2 +- app/lib/feed_manager.rb | 20 ++++++++++---------- app/models/follow_recommendation_suppression.rb | 4 ++-- app/services/batched_remove_status_service.rb | 18 +++++++++--------- .../scheduler/follow_recommendations_scheduler.rb | 13 +++++-------- config/environments/development.rb | 2 ++ config/environments/test.rb | 2 ++ config/initializers/redis.rb | 1 + db/migrate/20170920032311_fix_reblogs_in_feeds.rb | 2 +- .../20200407202420_migrate_unavailable_inboxes.rb | 5 +++-- lib/mastodon/feeds_cli.rb | 6 +----- 11 files changed, 37 insertions(+), 38 deletions(-) create mode 100644 config/initializers/redis.rb (limited to 'app') diff --git a/Gemfile.lock b/Gemfile.lock index 51cf8147b..b8b094325 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -558,7 +558,7 @@ GEM rdf-normalize (0.5.1) rdf (~> 3.2) redcarpet (3.6.0) - redis (4.5.1) + redis (4.8.1) redis-namespace (1.10.0) redis (>= 4) redlock (1.3.2) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 8d7540e0f..7dda6b185 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -273,27 +273,27 @@ class FeedManager def clean_feeds!(type, ids) reblogged_id_sets = {} - redis.pipelined do + redis.pipelined do |pipeline| ids.each do |feed_id| - redis.del(key(type, feed_id)) reblog_key = key(type, feed_id, 'reblogs') # We collect a future for this: we don't block while getting # it, but we can iterate over it later. - reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1) - redis.del(reblog_key) + reblogged_id_sets[feed_id] = pipeline.zrange(reblog_key, 0, -1) + pipeline.del(key(type, feed_id), reblog_key) end end # Remove all of the reblog tracking keys we just removed the # references to. - redis.pipelined do - reblogged_id_sets.each do |feed_id, future| - future.value.each do |reblogged_id| - reblog_set_key = key(type, feed_id, "reblogs:#{reblogged_id}") - redis.del(reblog_set_key) - end + keys_to_delete = reblogged_id_sets.flat_map do |feed_id, future| + future.value.map do |reblogged_id| + key(type, feed_id, "reblogs:#{reblogged_id}") end end + + redis.del(keys_to_delete) unless keys_to_delete.empty? + + nil end private diff --git a/app/models/follow_recommendation_suppression.rb b/app/models/follow_recommendation_suppression.rb index a9dbbfc18..e261a2fe3 100644 --- a/app/models/follow_recommendation_suppression.rb +++ b/app/models/follow_recommendation_suppression.rb @@ -20,9 +20,9 @@ class FollowRecommendationSuppression < ApplicationRecord private def remove_follow_recommendations - redis.pipelined do + redis.pipelined do |pipeline| I18n.available_locales.each do |locale| - redis.zrem("follow_recommendations:#{locale}", account_id) + pipeline.zrem("follow_recommendations:#{locale}", account_id) end end end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 54e5f10a4..7e9b67126 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -45,9 +45,9 @@ class BatchedRemoveStatusService < BaseService # Cannot be batched @status_id_cutoff = Mastodon::Snowflake.id_at(2.weeks.ago) - redis.pipelined do + redis.pipelined do |pipeline| statuses.each do |status| - unpush_from_public_timelines(status) + unpush_from_public_timelines(status, pipeline) end end end @@ -70,22 +70,22 @@ class BatchedRemoveStatusService < BaseService end end - def unpush_from_public_timelines(status) + def unpush_from_public_timelines(status, pipeline) return unless status.public_visibility? && status.id > @status_id_cutoff payload = Oj.dump(event: :delete, payload: status.id.to_s) - redis.publish('timeline:public', payload) - redis.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload) + pipeline.publish('timeline:public', payload) + pipeline.publish(status.local? ? 'timeline:public:local' : 'timeline:public:remote', payload) if status.media_attachments.any? - redis.publish('timeline:public:media', payload) - redis.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload) + pipeline.publish('timeline:public:media', payload) + pipeline.publish(status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', payload) end status.tags.map { |tag| tag.name.mb_chars.downcase }.each do |hashtag| - redis.publish("timeline:hashtag:#{hashtag}", payload) - redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? + pipeline.publish("timeline:hashtag:#{hashtag}", payload) + pipeline.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? end end end diff --git a/app/workers/scheduler/follow_recommendations_scheduler.rb b/app/workers/scheduler/follow_recommendations_scheduler.rb index 04008a9d9..17cf3f2cc 100644 --- a/app/workers/scheduler/follow_recommendations_scheduler.rb +++ b/app/workers/scheduler/follow_recommendations_scheduler.rb @@ -20,7 +20,7 @@ class Scheduler::FollowRecommendationsScheduler Trends.available_locales.each do |locale| recommendations = if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist - FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.account_id, recommendation.rank] } + FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).map { |recommendation| [recommendation.rank, recommendation.account_id] } else [] end @@ -33,14 +33,14 @@ class Scheduler::FollowRecommendationsScheduler # Language-specific results should be above language-agnostic ones, # otherwise language-agnostic ones will always overshadow them - recommendations.map! { |(account_id, rank)| [account_id, rank + max_fallback_rank] } + recommendations.map! { |(rank, account_id)| [rank + max_fallback_rank, account_id] } added = 0 fallback_recommendations.each do |recommendation| - next if recommendations.any? { |(account_id, _)| account_id == recommendation.account_id } + next if recommendations.any? { |(_, account_id)| account_id == recommendation.account_id } - recommendations << [recommendation.account_id, recommendation.rank] + recommendations << [recommendation.rank, recommendation.account_id] added += 1 break if added >= missing @@ -49,10 +49,7 @@ class Scheduler::FollowRecommendationsScheduler redis.multi do |multi| multi.del(key(locale)) - - recommendations.each do |(account_id, rank)| - multi.zadd(key(locale), rank, account_id) - end + multi.zadd(key(locale), recommendations) end end end diff --git a/config/environments/development.rb b/config/environments/development.rb index de8762ff7..29b17a350 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -87,6 +87,8 @@ Rails.application.configure do config.x.otp_secret = ENV.fetch('OTP_SECRET', '1fc2b87989afa6351912abeebe31ffc5c476ead9bf8b3d74cbc4a302c7b69a45b40b1bbef3506ddad73e942e15ed5ca4b402bf9a66423626051104f4b5f05109') end +Redis.raise_deprecations = true + ActiveRecordQueryTrace.enabled = ENV['QUERY_TRACE_ENABLED'] == 'true' module PrivateAddressCheck diff --git a/config/environments/test.rb b/config/environments/test.rb index ef3cb2e48..9cbf31e8d 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -73,3 +73,5 @@ end # Catch serialization warnings early Sidekiq.strict_args! + +Redis.raise_deprecations = true diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb new file mode 100644 index 000000000..f2bbd1e45 --- /dev/null +++ b/config/initializers/redis.rb @@ -0,0 +1 @@ +Redis.sadd_returns_boolean = false diff --git a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb index 4ab68e8f3..7e2db0ff3 100644 --- a/db/migrate/20170920032311_fix_reblogs_in_feeds.rb +++ b/db/migrate/20170920032311_fix_reblogs_in_feeds.rb @@ -1,6 +1,6 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1] def up - redis = Redis.current + redis = RedisConfiguration.pool.checkout fm = FeedManager.instance # Old scheme: diff --git a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb index 92a3acb5d..8f9c68794 100644 --- a/db/migrate/20200407202420_migrate_unavailable_inboxes.rb +++ b/db/migrate/20200407202420_migrate_unavailable_inboxes.rb @@ -2,7 +2,8 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2] disable_ddl_transaction! def up - urls = Redis.current.smembers('unavailable_inboxes') + redis = RedisConfiguration.pool.checkout + urls = redis.smembers('unavailable_inboxes') hosts = urls.map do |url| Addressable::URI.parse(url).normalized_host @@ -14,7 +15,7 @@ class MigrateUnavailableInboxes < ActiveRecord::Migration[5.2] UnavailableDomain.create(domain: host) end - Redis.current.del(*(['unavailable_inboxes'] + Redis.current.keys('exhausted_deliveries:*'))) + redis.del(*(['unavailable_inboxes'] + redis.keys('exhausted_deliveries:*'))) end def down; end diff --git a/lib/mastodon/feeds_cli.rb b/lib/mastodon/feeds_cli.rb index 428d63a44..fcfb48740 100644 --- a/lib/mastodon/feeds_cli.rb +++ b/lib/mastodon/feeds_cli.rb @@ -53,11 +53,7 @@ module Mastodon desc 'clear', 'Remove all home and list feeds from Redis' def clear keys = redis.keys('feed:*') - - redis.pipelined do - keys.each { |key| redis.del(key) } - end - + redis.del(keys) say('OK', :green) end end -- cgit From c40d5e5a8fb02f2c603a23a1b0130b3f86a15710 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 4 Mar 2023 11:00:00 -0500 Subject: Misc coverage improvements for validators (#23928) --- app/validators/ed25519_key_validator.rb | 2 +- app/validators/ed25519_signature_validator.rb | 2 +- spec/models/import_spec.rb | 5 +++++ spec/models/one_time_key_spec.rb | 19 ++++++++++++++++++- spec/validators/email_mx_validator_spec.rb | 16 ++++++++++++++++ 5 files changed, 41 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/validators/ed25519_key_validator.rb b/app/validators/ed25519_key_validator.rb index 00a448d5a..adf49296b 100644 --- a/app/validators/ed25519_key_validator.rb +++ b/app/validators/ed25519_key_validator.rb @@ -6,7 +6,7 @@ class Ed25519KeyValidator < ActiveModel::EachValidator key = Base64.decode64(value) - record.errors[attribute] << I18n.t('crypto.errors.invalid_key') unless verified?(key) + record.errors.add(attribute, I18n.t('crypto.errors.invalid_key')) unless verified?(key) end private diff --git a/app/validators/ed25519_signature_validator.rb b/app/validators/ed25519_signature_validator.rb index 77a21b837..0e74c231e 100644 --- a/app/validators/ed25519_signature_validator.rb +++ b/app/validators/ed25519_signature_validator.rb @@ -8,7 +8,7 @@ class Ed25519SignatureValidator < ActiveModel::EachValidator signature = Base64.decode64(value) message = option_to_value(record, :message) - record.errors[attribute] << I18n.t('crypto.errors.invalid_signature') unless verified?(verify_key, signature, message) + record.errors.add(attribute, I18n.t('crypto.errors.invalid_signature')) unless verified?(verify_key, signature, message) end private diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb index 81c75a964..1c8474413 100644 --- a/spec/models/import_spec.rb +++ b/spec/models/import_spec.rb @@ -23,6 +23,11 @@ RSpec.describe Import, type: :model do expect(import).to model_have_error_on_field(:data) end + it 'is invalid with malformed data' do + import = Import.create(account: account, type: type, data: StringIO.new('\"test')) + expect(import).to model_have_error_on_field(:data) + end + it 'is invalid with too many rows in data' do import = Import.create(account: account, type: type, data: StringIO.new("foo@bar.com\n" * (ImportService::ROWS_PROCESSING_LIMIT + 10))) expect(import).to model_have_error_on_field(:data) diff --git a/spec/models/one_time_key_spec.rb b/spec/models/one_time_key_spec.rb index 2a5fe8a9d..6ff7ffc5c 100644 --- a/spec/models/one_time_key_spec.rb +++ b/spec/models/one_time_key_spec.rb @@ -2,5 +2,22 @@ require 'rails_helper' -RSpec.describe OneTimeKey, type: :model do +describe OneTimeKey do + describe 'validations' do + context 'with an invalid signature' do + let(:one_time_key) { Fabricate.build(:one_time_key, signature: 'wrong!') } + + it 'is invalid' do + expect(one_time_key).to_not be_valid + end + end + + context 'with an invalid key' do + let(:one_time_key) { Fabricate.build(:one_time_key, key: 'wrong!') } + + it 'is invalid' do + expect(one_time_key).to_not be_valid + end + end + end end diff --git a/spec/validators/email_mx_validator_spec.rb b/spec/validators/email_mx_validator_spec.rb index ffb6851d0..a11b8e01e 100644 --- a/spec/validators/email_mx_validator_spec.rb +++ b/spec/validators/email_mx_validator_spec.rb @@ -41,6 +41,22 @@ describe EmailMxValidator do expect(user.errors).to_not have_received(:add) end + it 'adds an error if the TagManager fails to normalize domain' do + double = instance_double(TagManager) + allow(TagManager).to receive(:instance).and_return(double) + allow(double).to receive(:normalize_domain).with('example.com').and_raise(Addressable::URI::InvalidURIError) + + user = double(email: 'foo@example.com', errors: double(add: nil)) + subject.validate(user) + expect(user.errors).to have_received(:add) + end + + it 'adds an error if the domain email portion is blank' do + user = double(email: 'foo@', errors: double(add: nil)) + subject.validate(user) + expect(user.errors).to have_received(:add) + end + it 'adds an error if the email domain name contains empty labels' do resolver = double -- cgit From 7f4412eeeb1d35c9345c213b2cdfbbb9ce97dabb Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Sat, 4 Mar 2023 11:16:11 -0500 Subject: User mailer spec coverage improvements (#23905) --- app/views/user_mailer/appeal_rejected.html.haml | 2 +- spec/mailers/user_mailer_spec.rb | 48 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/user_mailer/appeal_rejected.html.haml b/app/views/user_mailer/appeal_rejected.html.haml index 75cd9d023..c316a73fb 100644 --- a/app/views/user_mailer/appeal_rejected.html.haml +++ b/app/views/user_mailer/appeal_rejected.html.haml @@ -17,7 +17,7 @@ %tbody %tr %td - = image_tag full_pack_url('media/images/mailer/icon_warning.png'), alt: '' + = image_tag full_pack_url('media/images/mailer/icon_flag.png'), alt: '' %h1= t 'user_mailer.appeal_rejected.title' diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 9c22f60f1..30824e7b4 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -94,4 +94,52 @@ describe UserMailer, type: :mailer do expect(mail.body.encoded).to include strike.text end end + + describe 'webauthn_credential_deleted' do + let(:credential) { Fabricate(:webauthn_credential, user_id: receiver.id) } + let(:mail) { UserMailer.webauthn_credential_deleted(receiver, credential) } + + it 'renders webauthn credential deleted notification' do + receiver.update!(locale: nil) + expect(mail.body.encoded).to include I18n.t('devise.mailer.webauthn_credential.deleted.title') + end + + include_examples 'localized subject', + 'devise.mailer.webauthn_credential.deleted.subject' + end + + describe 'suspicious_sign_in' do + let(:ip) { '192.168.0.1' } + let(:agent) { 'NCSA_Mosaic/2.0 (Windows 3.1)' } + let(:timestamp) { Time.now.utc } + let(:mail) { UserMailer.suspicious_sign_in(receiver, ip, agent, timestamp) } + + it 'renders suspicious sign in notification' do + receiver.update!(locale: nil) + expect(mail.body.encoded).to include I18n.t('user_mailer.suspicious_sign_in.explanation') + end + + include_examples 'localized subject', + 'user_mailer.suspicious_sign_in.subject' + end + + describe 'appeal_approved' do + let(:appeal) { Fabricate(:appeal, account: receiver.account, approved_at: Time.now.utc) } + let(:mail) { UserMailer.appeal_approved(receiver, appeal) } + + it 'renders appeal_approved notification' do + expect(mail.subject).to eq I18n.t('user_mailer.appeal_approved.subject', date: I18n.l(appeal.created_at)) + expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_approved.title') + end + end + + describe 'appeal_rejected' do + let(:appeal) { Fabricate(:appeal, account: receiver.account, rejected_at: Time.now.utc) } + let(:mail) { UserMailer.appeal_rejected(receiver, appeal) } + + it 'renders appeal_rejected notification' do + expect(mail.subject).to eq I18n.t('user_mailer.appeal_rejected.subject', date: I18n.l(appeal.created_at)) + expect(mail.body.encoded).to include I18n.t('user_mailer.appeal_rejected.title') + end + end end -- cgit From cb868228bde5351b79041f341a151bb08118d31f Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Sat, 4 Mar 2023 23:18:19 +0100 Subject: Do not leave Mastodon when clicking “Back” (#23953) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mastodon/components/column_back_button.jsx | 6 +++--- app/javascript/mastodon/components/column_header.jsx | 14 +++++--------- app/javascript/mastodon/features/ui/index.jsx | 6 +++--- 3 files changed, 11 insertions(+), 15 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/components/column_back_button.jsx b/app/javascript/mastodon/components/column_back_button.jsx index 5bbf11652..5c5226b7e 100644 --- a/app/javascript/mastodon/components/column_back_button.jsx +++ b/app/javascript/mastodon/components/column_back_button.jsx @@ -15,10 +15,10 @@ export default class ColumnBackButton extends React.PureComponent { }; handleClick = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); - } else { + if (window.history && window.history.state) { this.context.router.history.goBack(); + } else { + this.context.router.history.push('/'); } }; diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx index 38f6ad60f..9ba783d90 100644 --- a/app/javascript/mastodon/components/column_header.jsx +++ b/app/javascript/mastodon/components/column_header.jsx @@ -43,14 +43,6 @@ class ColumnHeader extends React.PureComponent { animating: false, }; - historyBack = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); - } else { - this.context.router.history.goBack(); - } - }; - handleToggleClick = (e) => { e.stopPropagation(); this.setState({ collapsed: !this.state.collapsed, animating: true }); @@ -69,7 +61,11 @@ class ColumnHeader extends React.PureComponent { }; handleBackClick = () => { - this.historyBack(); + if (window.history && window.history.state) { + this.context.router.history.goBack(); + } else { + this.context.router.history.push('/'); + } }; handleTransitionEnd = () => { diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 4f0ea0450..2dd59f95d 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -474,10 +474,10 @@ class UI extends React.PureComponent { }; handleHotkeyBack = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); - } else { + if (window.history && window.history.state) { this.context.router.history.goBack(); + } else { + this.context.router.history.push('/'); } }; -- cgit From 82a6cf401244e0cc51bb5fa2aa20e58b9996a640 Mon Sep 17 00:00:00 2001 From: Nick Schonning Date: Sat, 4 Mar 2023 18:33:52 -0500 Subject: Enable Rubocop for app/views (#23874) --- .rubocop.yml | 1 - app/views/accounts/show.rss.ruby | 12 ++++++------ app/views/tags/show.rss.ruby | 12 ++++++------ app/views/well_known/host_meta/show.xml.ruby | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) (limited to 'app') diff --git a/.rubocop.yml b/.rubocop.yml index f7ed79b76..b4387a461 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -19,7 +19,6 @@ AllCops: NewCops: enable Exclude: - db/schema.rb - - 'app/views/**/*' - 'config/**/*' - 'bin/*' - 'Rakefile' diff --git a/app/views/accounts/show.rss.ruby b/app/views/accounts/show.rss.ruby index 34e29d483..7a77511ce 100644 --- a/app/views/accounts/show.rss.ruby +++ b/app/views/accounts/show.rss.ruby @@ -5,7 +5,7 @@ RSS::Builder.build do |doc| doc.image(full_asset_url(@account.avatar.url(:original)), display_name(@account), params[:tag].present? ? short_account_tag_url(@account, params[:tag]) : short_account_url(@account)) doc.last_build_date(@statuses.first.created_at) if @statuses.any? doc.icon(full_asset_url(@account.avatar.url(:original))) - doc.generator("Mastodon v#{Mastodon::Version.to_s}") + doc.generator("Mastodon v#{Mastodon::Version}") @statuses.each do |status| doc.item do |item| @@ -18,12 +18,12 @@ RSS::Builder.build do |doc| item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) end - status.ordered_media_attachments.each do |media| - item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content| - media_content.medium(media.gifv? ? 'image' : media.type.to_s) + status.ordered_media_attachments.each do |media_attachment| + item.media_content(full_asset_url(media_attachment.file.url(:original, false)), media_attachment.file.content_type, media_attachment.file.size) do |media_content| + media_content.medium(media_attachment.gifv? ? 'image' : media_attachment.type.to_s) media_content.rating(status.sensitive? ? 'adult' : 'nonadult') - media_content.description(media.description) if media.description.present? - media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail? + media_content.description(media_attachment.description) if media_attachment.description.present? + media_content.thumbnail(media_attachment.thumbnail.url(:original, false)) if media_attachment.thumbnail? end end diff --git a/app/views/tags/show.rss.ruby b/app/views/tags/show.rss.ruby index 8e0c2327b..bbda1ad4b 100644 --- a/app/views/tags/show.rss.ruby +++ b/app/views/tags/show.rss.ruby @@ -3,7 +3,7 @@ RSS::Builder.build do |doc| doc.description(I18n.t('rss.descriptions.tag', hashtag: @tag.display_name)) doc.link(tag_url(@tag)) doc.last_build_date(@statuses.first.created_at) if @statuses.any? - doc.generator("Mastodon v#{Mastodon::Version.to_s}") + doc.generator("Mastodon v#{Mastodon::Version}") @statuses.each do |status| doc.item do |item| @@ -16,12 +16,12 @@ RSS::Builder.build do |doc| item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) end - status.ordered_media_attachments.each do |media| - item.media_content(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size) do |media_content| - media_content.medium(media.gifv? ? 'image' : media.type.to_s) + status.ordered_media_attachments.each do |media_attachment| + item.media_content(full_asset_url(media_attachment.file.url(:original, false)), media_attachment.file.content_type, media_attachment.file.size) do |media_content| + media_content.medium(media_attachment.gifv? ? 'image' : media_attachment.type.to_s) media_content.rating(status.sensitive? ? 'adult' : 'nonadult') - media_content.description(media.description) if media.description.present? - media_content.thumbnail(media.thumbnail.url(:original, false)) if media.thumbnail? + media_content.description(media_attachment.description) if media_attachment.description.present? + media_content.thumbnail(media_attachment.thumbnail.url(:original, false)) if media_attachment.thumbnail? end end diff --git a/app/views/well_known/host_meta/show.xml.ruby b/app/views/well_known/host_meta/show.xml.ruby index b4e867c5f..bb5a01a1b 100644 --- a/app/views/well_known/host_meta/show.xml.ruby +++ b/app/views/well_known/host_meta/show.xml.ruby @@ -9,4 +9,4 @@ doc << Ox::Element.new('XRD').tap do |xrd| end end -('' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8') +"#{Ox.dump(doc, effort: :tolerant)}".force_encoding('UTF-8') -- cgit From 03ac84b0f4857f36a14e0b8f3196c35b00fb3b16 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 3 Mar 2023 20:25:36 +0100 Subject: [Glitch] Fix duplicate “Publish” button on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port b55fc883b6181fb8080fbafd53bc7a9f1896b295 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/features/ui/components/header.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/ui/components/header.jsx b/app/javascript/flavours/glitch/features/ui/components/header.jsx index d9ad94961..5aa045fee 100644 --- a/app/javascript/flavours/glitch/features/ui/components/header.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/header.jsx @@ -23,8 +23,8 @@ const mapDispatchToProps = (dispatch) => ({ }, }); -export default @connect(null, mapDispatchToProps) -@withRouter +export default @withRouter +@connect(null, mapDispatchToProps) class Header extends React.PureComponent { static contextTypes = { -- cgit From 9a1bd1c3e9990056e847d16b407818df4adfdf24 Mon Sep 17 00:00:00 2001 From: Terry Garcia <10190993+TerryGarcia@users.noreply.github.com> Date: Fri, 3 Mar 2023 13:37:49 -0600 Subject: [Glitch] Switched bookmark and favourites around Port a1347f456e47e085ef1031b607cabd0683c496c2 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/ui/components/navigation_panel.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx index 3b46c6eec..5a296435f 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx @@ -78,8 +78,8 @@ class NavigationPanel extends React.Component { {signedIn && ( - + -- cgit From 1317553f3a91b9372220fec8c40b537edfca2446 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 3 Mar 2023 21:06:31 +0100 Subject: [Glitch] Only offer translation for supported languages Port 5a8c651e8f0252c7135042e79396f782361302d9 to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/components/status_content.jsx | 4 ++-- app/javascript/flavours/glitch/initial_state.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index 790a5c659..f21732065 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -5,7 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl'; import Permalink from './permalink'; import classnames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; -import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'flavours/glitch/initial_state'; +import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; const textMatchesTarget = (text, origin, host) => { @@ -315,7 +315,7 @@ class StatusContent extends React.PureComponent { } = this.props; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; - const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language'); + const renderTranslate = this.props.onTranslate && status.get('translatable'); const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index c4b249db8..03be4ff6c 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -140,7 +140,6 @@ export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); -export const translationEnabled = getMeta('translation_enabled'); export const languages = initialState?.languages; export const statusPageUrl = getMeta('status_page_url'); -- cgit From 7f3b0dedebfb1e200b23f0e3e1151e1050424a34 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 5 Mar 2023 21:25:48 +0100 Subject: Refactor glitch-soc rich text SCSS --- .../glitch/styles/components/compose_form.scss | 76 +---------------- .../flavours/glitch/styles/components/status.scss | 86 +------------------ app/javascript/flavours/glitch/styles/index.scss | 1 + .../flavours/glitch/styles/rich_text.scss | 99 ++++++++++++++++++++++ 4 files changed, 102 insertions(+), 160 deletions(-) create mode 100644 app/javascript/flavours/glitch/styles/rich_text.scss (limited to 'app') diff --git a/app/javascript/flavours/glitch/styles/components/compose_form.scss b/app/javascript/flavours/glitch/styles/components/compose_form.scss index aa2cc664e..1c2e0aeb4 100644 --- a/app/javascript/flavours/glitch/styles/components/compose_form.scss +++ b/app/javascript/flavours/glitch/styles/components/compose_form.scss @@ -164,8 +164,7 @@ white-space: pre-wrap; p, - pre, - blockquote { + pre { margin-bottom: 20px; white-space: pre-wrap; @@ -174,79 +173,6 @@ } } - h1, - h2, - h3, - h4, - h5 { - margin-top: 20px; - margin-bottom: 20px; - } - - h1, - h2 { - font-weight: 700; - font-size: 18px; - } - - h2 { - font-size: 16px; - } - - h3, - h4, - h5 { - font-weight: 500; - } - - blockquote { - padding-left: 10px; - border-left: 3px solid $inverted-text-color; - color: $inverted-text-color; - white-space: normal; - - p:last-child { - margin-bottom: 0; - } - } - - b, - strong { - font-weight: 700; - } - - em, - i { - font-style: italic; - } - - sub { - font-size: smaller; - vertical-align: sub; - } - - sup { - font-size: smaller; - vertical-align: super; - } - - ul, - ol { - margin-left: 1em; - - p { - margin: 0; - } - } - - ul { - list-style-type: disc; - } - - ol { - list-style-type: decimal; - } - a { color: $lighter-text-color; text-decoration: none; diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 7f94b25e9..1a7dfe9ae 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -68,8 +68,7 @@ } p, - pre, - blockquote { + pre { margin-bottom: 20px; white-space: pre-wrap; unicode-bidi: plaintext; @@ -79,89 +78,6 @@ } } - .status__content__text, - .e-content { - overflow: hidden; - - & > ul, - & > ol { - margin-bottom: 20px; - } - - h1, - h2, - h3, - h4, - h5 { - margin-top: 20px; - margin-bottom: 20px; - } - - h1, - h2 { - font-weight: 700; - font-size: 1.2em; - } - - h2 { - font-size: 1.1em; - } - - h3, - h4, - h5 { - font-weight: 500; - } - - blockquote { - padding-left: 10px; - border-left: 3px solid $darker-text-color; - color: $darker-text-color; - white-space: normal; - - p:last-child { - margin-bottom: 0; - } - } - - b, - strong { - font-weight: 700; - } - - em, - i { - font-style: italic; - } - - sub { - font-size: smaller; - vertical-align: sub; - } - - sup { - font-size: smaller; - vertical-align: super; - } - - ul, - ol { - margin-left: 2em; - - p { - margin: 0; - } - } - - ul { - list-style-type: disc; - } - - ol { - list-style-type: decimal; - } - } - a { color: $secondary-text-color; text-decoration: none; diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index fbb02c788..1cb913c8b 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -21,3 +21,4 @@ @import 'accessibility'; @import 'rtl'; @import 'dashboard'; +@import 'rich_text'; diff --git a/app/javascript/flavours/glitch/styles/rich_text.scss b/app/javascript/flavours/glitch/styles/rich_text.scss new file mode 100644 index 000000000..e60818353 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/rich_text.scss @@ -0,0 +1,99 @@ +.status__content__text, +.e-content, +.reply-indicator__content { + pre, + blockquote { + margin-bottom: 20px; + white-space: pre-wrap; + unicode-bidi: plaintext; + + &:last-child { + margin-bottom: 0; + } + } + + blockquote { + padding-left: 10px; + border-left: 3px solid $darker-text-color; + color: $darker-text-color; + white-space: normal; + + p:last-child { + margin-bottom: 0; + } + } + + & > ul, + & > ol { + margin-bottom: 20px; + } + + h1, + h2, + h3, + h4, + h5 { + margin-top: 20px; + margin-bottom: 20px; + } + + h1, + h2 { + font-weight: 700; + font-size: 1.2em; + } + + h2 { + font-size: 1.1em; + } + + h3, + h4, + h5 { + font-weight: 500; + } + + b, + strong { + font-weight: 700; + } + + em, + i { + font-style: italic; + } + + sub { + font-size: smaller; + vertical-align: sub; + } + + sup { + font-size: smaller; + vertical-align: super; + } + + ul, + ol { + margin-left: 2em; + + p { + margin: 0; + } + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } +} + +.reply-indicator__content { + blockquote { + border-left-color: $inverted-text-color; + color: $inverted-text-color; + } +} -- cgit