From 5778ba10284aa78d9341d1eb4fc384e98236c810 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 7 Nov 2022 22:35:53 +0100 Subject: [PATCH 01/10] Fix validation error in SynchronizeFeaturedTagsCollectionWorker (#20018) * Fix followers count not being updated when migrating follows Fixes #19900 * Fix validation error in SynchronizeFeaturedTagsCollectionWorker Also saves remote user's chosen case for hashtags * Limit remote featured tags before validation --- app/models/featured_tag.rb | 2 + .../fetch_featured_tags_collection_service.rb | 22 ++--- ...h_featured_tags_collection_service_spec.rb | 95 +++++++++++++++++++ 3 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb index 78185b2a99..debae22121 100644 --- a/app/models/featured_tag.rb +++ b/app/models/featured_tag.rb @@ -63,6 +63,8 @@ class FeaturedTag < ApplicationRecord end def validate_featured_tags_limit + return unless account.local? + errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= LIMIT end diff --git a/app/services/activitypub/fetch_featured_tags_collection_service.rb b/app/services/activitypub/fetch_featured_tags_collection_service.rb index 5559199381..ab047a0f8b 100644 --- a/app/services/activitypub/fetch_featured_tags_collection_service.rb +++ b/app/services/activitypub/fetch_featured_tags_collection_service.rb @@ -51,21 +51,17 @@ class ActivityPub::FetchFeaturedTagsCollectionService < BaseService end def process_items(items) - names = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.map { |name| HashtagNormalizer.new.normalize(name) } - to_remove = [] - to_add = names - - FeaturedTag.where(account: @account).map(&:name).each do |name| - if names.include?(name) - to_add.delete(name) - else - to_remove << name - end - end + names = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.take(FeaturedTag::LIMIT) + tags = names.index_by { |name| HashtagNormalizer.new.normalize(name) } + normalized_names = tags.keys - FeaturedTag.includes(:tag).where(account: @account, tags: { name: to_remove }).delete_all unless to_remove.empty? + FeaturedTag.includes(:tag).references(:tag).where(account: @account).where.not(tag: { name: normalized_names }).delete_all + + FeaturedTag.includes(:tag).references(:tag).where(account: @account, tag: { name: normalized_names }).each do |featured_tag| + featured_tag.update(name: tags.delete(featured_tag.tag.name)) + end - to_add.each do |name| + tags.each_value do |name| FeaturedTag.create!(account: @account, name: name) end end diff --git a/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb new file mode 100644 index 0000000000..6ca22c9fc6 --- /dev/null +++ b/spec/services/activitypub/fetch_featured_tags_collection_service_spec.rb @@ -0,0 +1,95 @@ +require 'rails_helper' + +RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service do + let(:collection_url) { 'https://example.com/account/tags' } + let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/account') } + + let(:items) do + [ + { type: 'Hashtag', href: 'https://example.com/account/tagged/foo', name: 'Foo' }, + { type: 'Hashtag', href: 'https://example.com/account/tagged/bar', name: 'bar' }, + { type: 'Hashtag', href: 'https://example.com/account/tagged/baz', name: 'baZ' }, + ] + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: collection_url, + items: items, + }.with_indifferent_access + end + + subject { described_class.new } + + shared_examples 'sets featured tags' do + before do + subject.call(actor, collection_url) + end + + it 'sets expected tags as pinned tags' do + expect(actor.featured_tags.map(&:display_name)).to match_array ['Foo', 'bar', 'baZ'] + end + end + + describe '#call' do + context 'when the endpoint is a Collection' do + before do + stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) + end + + it_behaves_like 'sets featured tags' + end + + context 'when the account already has featured tags' do + before do + stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) + + actor.featured_tags.create!(name: 'FoO') + actor.featured_tags.create!(name: 'baz') + actor.featured_tags.create!(name: 'oh').update(name: nil) + end + + it_behaves_like 'sets featured tags' + end + + context 'when the endpoint is an OrderedCollection' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'OrderedCollection', + id: collection_url, + orderedItems: items, + }.with_indifferent_access + end + + before do + stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) + end + + it_behaves_like 'sets featured tags' + end + + context 'when the endpoint is a paginated Collection' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: collection_url, + first: { + type: 'CollectionPage', + partOf: collection_url, + items: items, + } + }.with_indifferent_access + end + + before do + stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload)) + end + + it_behaves_like 'sets featured tags' + end + end +end From e9c23545156a01bb3cc81564236495c750de56fd Mon Sep 17 00:00:00 2001 From: Zach Flanders Date: Mon, 7 Nov 2022 15:37:36 -0600 Subject: [PATCH 02/10] Fix spoiler buttons css not rendering correct color in light theme (#19960) * Updating status__content__spoiler-link css for mastodon-light theme to ensure correct rendering precedence * Adding focus css selector to status__content__spoiler-link mastodon-light theme * reformatting code to match convention of having css selectors on separate lines * fixing code format for scss linting issue --- app/javascript/styles/mastodon-light/diff.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index d928a55ed5..d960070d62 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -268,7 +268,8 @@ html { .status__content .status__content__spoiler-link { background: $ui-base-color; - &:hover { + &:hover, + &:focus { background: lighten($ui-base-color, 4%); } } From 97ee940b1ad8bbfdfb212f1bc905021d96698559 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Mon, 7 Nov 2022 18:50:47 -0800 Subject: [PATCH 03/10] Micro-optimization: use `if`/`else` instead of `Array#compact` and `Array#min` (#19906) * Technically `if`/`else` is faster than using `[value1, value2].compact.min` to find the lesser of two values, one of which may be `nil`. --- app/models/account_statuses_cleanup_policy.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/account_statuses_cleanup_policy.rb b/app/models/account_statuses_cleanup_policy.rb index 3651236531..49adc6ad05 100644 --- a/app/models/account_statuses_cleanup_policy.rb +++ b/app/models/account_statuses_cleanup_policy.rb @@ -139,7 +139,12 @@ class AccountStatusesCleanupPolicy < ApplicationRecord # Filtering on `id` rather than `min_status_age` ago will treat # non-snowflake statuses as older than they really are, but Mastodon # has switched to snowflake IDs significantly over 2 years ago anyway. - max_id = [max_id, Mastodon::Snowflake.id_at(min_status_age.seconds.ago, with_random: false)].compact.min + snowflake_id = Mastodon::Snowflake.id_at(min_status_age.seconds.ago, with_random: false) + + if max_id.nil? || snowflake_id < max_id + max_id = snowflake_id + end + Status.where(Status.arel_table[:id].lteq(max_id)) end From ffef3ac03b5c4b25330c64bf766a2f32675a0829 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 8 Nov 2022 03:52:52 +0100 Subject: [PATCH 04/10] Fix opening the language picker scrolling the single-column view to the top (#19983) Fixes #19915 --- .../features/compose/components/language_dropdown.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.js b/app/javascript/mastodon/features/compose/components/language_dropdown.js index e48fa60ffb..bf56fd0fae 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.js @@ -51,6 +51,15 @@ class LanguageDropdownMenu extends React.PureComponent { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); this.setState({ mounted: true }); + + // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need + // to wait for a frame before focusing + requestAnimationFrame(() => { + if (this.node) { + const element = this.node.querySelector('input[type="search"]'); + if (element) element.focus(); + } + }); } componentWillUnmount () { @@ -226,7 +235,7 @@ class LanguageDropdownMenu extends React.PureComponent { // react-overlays
- +
From 18c710be23145829b4cd0486fefbc69b79f3c97a Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 8 Nov 2022 03:53:06 +0100 Subject: [PATCH 05/10] Add password autocomplete hints (#20071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #20067 Our password autocomplete hints were “off” but that does not prevent current browsers from trying to autocomplete them anyway, so use `current-password` and `new-password` so they don't put a newly-generated password in a password confirmation prompt, or the old password for a password renewal prompt. --- app/views/auth/challenges/new.html.haml | 2 +- app/views/auth/passwords/edit.html.haml | 4 ++-- app/views/auth/registrations/edit.html.haml | 6 +++--- app/views/auth/sessions/new.html.haml | 2 +- app/views/settings/deletes/show.html.haml | 2 +- app/views/settings/migration/redirects/new.html.haml | 2 +- app/views/settings/migrations/show.html.haml | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/views/auth/challenges/new.html.haml b/app/views/auth/challenges/new.html.haml index 9aef2c35d6..ff4b7a506f 100644 --- a/app/views/auth/challenges/new.html.haml +++ b/app/views/auth/challenges/new.html.haml @@ -5,7 +5,7 @@ = f.input :return_to, as: :hidden .field-group - = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off', :autofocus => true }, label: t('challenge.prompt'), required: true + = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'current-password', :autofocus => true }, label: t('challenge.prompt'), required: true .actions = f.button :button, t('challenge.confirm'), type: :submit diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml index 114a744542..c7dbebe756 100644 --- a/app/views/auth/passwords/edit.html.haml +++ b/app/views/auth/passwords/edit.html.haml @@ -8,9 +8,9 @@ = f.input :reset_password_token, as: :hidden .fields-group - = f.input :password, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, required: true + = f.input :password, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, required: true .fields-group - = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }, required: true + = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'new-password' }, required: true .actions = f.button :button, t('auth.set_new_password'), type: :submit diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index df929e3e80..c642c2293b 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -13,13 +13,13 @@ .fields-row__column.fields-group.fields-row__column-6 = f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended? .fields-row__column.fields-group.fields-row__column-6 - = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended?, hint: false + = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'current-password' }, required: true, disabled: current_account.suspended?, hint: false .fields-row .fields-row__column.fields-group.fields-row__column-6 - = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: t('simple_form.hints.defaults.password'), disabled: current_account.suspended? + = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: t('simple_form.hints.defaults.password'), disabled: current_account.suspended? .fields-row__column.fields-group.fields-row__column-6 - = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }, disabled: current_account.suspended? + = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'new-password' }, disabled: current_account.suspended? .actions = f.button :button, t('generic.save_changes'), type: :submit, class: 'button', disabled: current_account.suspended? diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index a4323d1d9a..943618e390 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -12,7 +12,7 @@ - else = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false .fields-group - = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false + = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'current-password' }, hint: false .actions = f.button :button, t('auth.login'), type: :submit diff --git a/app/views/settings/deletes/show.html.haml b/app/views/settings/deletes/show.html.haml index ddf0908794..c08ee85b0b 100644 --- a/app/views/settings/deletes/show.html.haml +++ b/app/views/settings/deletes/show.html.haml @@ -21,7 +21,7 @@ %hr.spacer/ - if current_user.encrypted_password.present? - = f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_password') + = f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'current-password' }, hint: t('deletes.confirm_password') - else = f.input :username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_username') diff --git a/app/views/settings/migration/redirects/new.html.haml b/app/views/settings/migration/redirects/new.html.haml index 017450f4b9..d7868e900d 100644 --- a/app/views/settings/migration/redirects/new.html.haml +++ b/app/views/settings/migration/redirects/new.html.haml @@ -19,7 +19,7 @@ .fields-row__column.fields-group.fields-row__column-6 - if current_user.encrypted_password.present? - = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true + = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'current-password' }, required: true - else = f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true diff --git a/app/views/settings/migrations/show.html.haml b/app/views/settings/migrations/show.html.haml index 14bebb19b0..1ecf7302a9 100644 --- a/app/views/settings/migrations/show.html.haml +++ b/app/views/settings/migrations/show.html.haml @@ -48,7 +48,7 @@ .fields-row__column.fields-group.fields-row__column-6 - if current_user.encrypted_password.present? - = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown? + = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'current-password' }, required: true, disabled: on_cooldown? - else = f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown? From f48dcd025b0b0bda3b493ea90c60d338a505985f Mon Sep 17 00:00:00 2001 From: James Tucker Date: Mon, 7 Nov 2022 19:00:27 -0800 Subject: [PATCH 06/10] Improve performance by avoiding method cache busts (#19957) Switch to monkey-patching http.rb rather than a runtime extend of each response, so as to avoid busting the global method cache. A guard is included that will provide developer feedback in development and test environments should the monkey patch ever collide. --- app/lib/request.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/lib/request.rb b/app/lib/request.rb index 648aa30850..1ea86862db 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -62,8 +62,6 @@ class Request end begin - response = response.extend(ClientLimit) - # If we are using a persistent connection, we have to # read every response to be able to move forward at all. # However, simply calling #to_s or #flush may not be safe, @@ -181,6 +179,14 @@ class Request end end + if ::HTTP::Response.methods.include?(:body_with_limit) && !Rails.env.production? + abort 'HTTP::Response#body_with_limit is already defined, the monkey patch will not be applied' + else + class ::HTTP::Response + include Request::ClientLimit + end + end + class Socket < TCPSocket class << self def open(host, *args) From 793cc2096d0f99bb70318d3d734fe162e8e9b28b Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 8 Nov 2022 04:06:54 +0100 Subject: [PATCH 07/10] Fix redrafting a currently-editing post not leaving edit mode (#20023) --- app/javascript/mastodon/reducers/compose.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index e4601e4715..ad384bd0b5 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -452,6 +452,7 @@ export default function compose(state = initialState, action) { map.set('idempotencyKey', uuid()); map.set('sensitive', action.status.get('sensitive')); map.set('language', action.status.get('language')); + map.set('id', null); if (action.status.get('spoiler_text').length > 0) { map.set('spoiler', true); From 9cb2ad6dbf911287e971eb5625eae58ef183966f Mon Sep 17 00:00:00 2001 From: Zach Flanders Date: Mon, 7 Nov 2022 15:37:36 -0600 Subject: [PATCH 08/10] [Glitch] Fix spoiler buttons css not rendering correct color in light theme Port e9c23545156a01bb3cc81564236495c750de56fd to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/mastodon-light/diff.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 37a49ce9f1..1fe3d7a511 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -268,7 +268,8 @@ html { .status__content .status__content__spoiler-link { background: $ui-base-color; - &:hover { + &:hover, + &:focus { background: lighten($ui-base-color, 4%); } } From 59e51cfea4d0fa09eae6dde8ba312152d4468d6f Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 8 Nov 2022 03:52:52 +0100 Subject: [PATCH 09/10] [Glitch] Fix opening the language picker scrolling the single-column view to the top Port ffef3ac03b5c4b25330c64bf766a2f32675a0829 to glitch-soc Signed-off-by: Claire --- .../features/compose/components/language_dropdown.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js index 31f1d4e730..a3256aa9b0 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js @@ -51,6 +51,15 @@ class LanguageDropdownMenu extends React.PureComponent { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); this.setState({ mounted: true }); + + // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need + // to wait for a frame before focusing + requestAnimationFrame(() => { + if (this.node) { + const element = this.node.querySelector('input[type="search"]'); + if (element) element.focus(); + } + }); } componentWillUnmount () { @@ -226,7 +235,7 @@ class LanguageDropdownMenu extends React.PureComponent { // react-overlays
- +
From e9aa9c43ec4d296e71dd7295e127e0e2f6e7068a Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 8 Nov 2022 04:06:54 +0100 Subject: [PATCH 10/10] [Glitch] Fix redrafting a currently-editing post not leaving edit mode Port 793cc2096d0f99bb70318d3d734fe162e8e9b28b to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/reducers/compose.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index b739456da5..460af39553 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -573,6 +573,7 @@ export default function compose(state = initialState, action) { 'advanced_options', map => map.merge(new ImmutableMap({ do_not_federate })) ); + map.set('id', null); if (action.status.get('spoiler_text').length > 0) { map.set('spoiler', true);