From e9b1c1edfe8a31adc250005016db431aa1c9ba5d Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 20 Jun 2025 06:04:14 -0400 Subject: [PATCH 1/6] Simplify `WebauthnCredential` constant limit math (#35107) --- app/models/webauthn_credential.rb | 2 +- spec/models/webauthn_credential_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/webauthn_credential.rb b/app/models/webauthn_credential.rb index d7ed1b9d40..3681ce332c 100644 --- a/app/models/webauthn_credential.rb +++ b/app/models/webauthn_credential.rb @@ -21,5 +21,5 @@ class WebauthnCredential < ApplicationRecord validates :external_id, uniqueness: true validates :nickname, uniqueness: { scope: :user_id } validates :sign_count, - numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: SIGN_COUNT_LIMIT - 1 } + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: SIGN_COUNT_LIMIT } end diff --git a/spec/models/webauthn_credential_spec.rb b/spec/models/webauthn_credential_spec.rb index 067c696c13..8abf583541 100644 --- a/spec/models/webauthn_credential_spec.rb +++ b/spec/models/webauthn_credential_spec.rb @@ -14,6 +14,6 @@ RSpec.describe WebauthnCredential do it { is_expected.to validate_uniqueness_of(:external_id) } it { is_expected.to validate_uniqueness_of(:nickname).scoped_to(:user_id) } - it { is_expected.to validate_numericality_of(:sign_count).only_integer.is_greater_than_or_equal_to(0).is_less_than_or_equal_to(described_class::SIGN_COUNT_LIMIT - 1) } + it { is_expected.to validate_numericality_of(:sign_count).only_integer.is_greater_than_or_equal_to(0).is_less_than(described_class::SIGN_COUNT_LIMIT) } end end From 54f9a1b43b6a56f497a79e303bed632173804494 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 20 Jun 2025 06:05:24 -0400 Subject: [PATCH 2/6] Extract secret size constants in `Webhook` model (#35104) --- app/models/webhook.rb | 13 ++++++++++--- spec/models/webhook_spec.rb | 2 ++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/models/webhook.rb b/app/models/webhook.rb index f9d6564c92..e3dff76365 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -25,12 +25,15 @@ class Webhook < ApplicationRecord status.updated ).freeze + SECRET_LENGTH_MIN = 12 + SECRET_SIZE = 20 + attr_writer :current_account scope :enabled, -> { where(enabled: true) } validates :url, presence: true, url: true - validates :secret, presence: true, length: { minimum: 12 } + validates :secret, presence: true, length: { minimum: SECRET_LENGTH_MIN } validates :events, presence: true validate :events_validation_error, if: :invalid_events? @@ -41,7 +44,7 @@ class Webhook < ApplicationRecord before_validation :generate_secret def rotate_secret! - update!(secret: SecureRandom.hex(20)) + update!(secret: random_secret) end def enable! @@ -93,6 +96,10 @@ class Webhook < ApplicationRecord end def generate_secret - self.secret = SecureRandom.hex(20) if secret.blank? + self.secret = random_secret if secret.blank? + end + + def random_secret + SecureRandom.hex(SECRET_SIZE) end end diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb index 59b4212d62..a712d5e7fa 100644 --- a/spec/models/webhook_spec.rb +++ b/spec/models/webhook_spec.rb @@ -8,6 +8,8 @@ RSpec.describe Webhook do describe 'Validations' do subject { Fabricate.build :webhook } + it { is_expected.to validate_length_of(:secret).is_at_least(described_class::SECRET_LENGTH_MIN) } + it { is_expected.to validate_presence_of(:events) } it { is_expected.to_not allow_values([], %w(account.invalid)).for(:events) } From 204ff46f7e8a7756d9d36300f19f09afbf353b48 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:20:24 +0200 Subject: [PATCH 3/6] chore(deps): update dependency rspec-rails to v8.0.1 (#35110) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5f8c685a89..7583b197e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -116,7 +116,7 @@ GEM base64 (0.3.0) bcp47_spec (0.2.1) bcrypt (3.1.20) - benchmark (0.4.0) + benchmark (0.4.1) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -708,7 +708,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (6.14.0) + rdoc (6.14.1) erb psych (>= 4.0.0) redcarpet (3.6.1) @@ -737,17 +737,17 @@ GEM rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.3) + rspec-core (3.13.4) rspec-support (~> 3.13.0) - rspec-expectations (3.13.4) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-github (3.0.0) rspec-core (~> 3.0) - rspec-mocks (3.13.4) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.0) + rspec-rails (8.0.1) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -760,7 +760,7 @@ GEM rspec-expectations (~> 3.0) rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) - rspec-support (3.13.3) + rspec-support (3.13.4) rubocop (1.76.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -940,7 +940,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.7.3) PLATFORMS ruby From 3f743b1a070ffe0ae2822d38f0aaedd0a78112c5 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Sat, 21 Jun 2025 10:58:12 +0200 Subject: [PATCH 4/6] fix: Fix SCSS lint warnings (#35102) --- .../styles/mastodon/components.scss | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 506e2f0264..81fde87822 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1185,6 +1185,7 @@ body > [data-popper-placement] { line-height: 20px; letter-spacing: 0.25px; display: -webkit-box; + line-clamp: 4; -webkit-line-clamp: 4; -webkit-box-orient: vertical; padding: 0; @@ -1263,6 +1264,7 @@ body > [data-popper-placement] { letter-spacing: 0.25px; padding-top: 0 !important; display: -webkit-box; + line-clamp: 4; -webkit-line-clamp: 4; -webkit-box-orient: vertical; max-height: 4 * 20px; @@ -2100,6 +2102,7 @@ body > [data-popper-placement] { overflow: hidden; text-overflow: ellipsis; display: -webkit-box; + line-clamp: 1; -webkit-line-clamp: 1; -webkit-box-orient: vertical; margin-top: 10px; @@ -3124,7 +3127,7 @@ a.account__display-name { } } -@media screen and (max-width: $no-gap-breakpoint - 1px) { +@media screen and (max-width: ($no-gap-breakpoint - 1px)) { $sidebar-width: 285px; .columns-area__panels__main { @@ -3324,7 +3327,7 @@ a.account__display-name { } } -@media screen and (max-width: $no-gap-breakpoint - 1px) { +@media screen and (max-width: ($no-gap-breakpoint - 1px)) { .columns-area__panels__pane--compositional { display: none; } @@ -3565,19 +3568,19 @@ a.account__display-name { } } - @media screen and (height <= 930px - 56px) { + @media screen and (height <= (930px - 56px)) { &__portal .trends__item:nth-child(n + 4) { display: none; } } - @media screen and (height <= 930px - (56px * 2)) { + @media screen and (height <= (930px - 56px * 2)) { &__portal .trends__item:nth-child(n + 3) { display: none; } } - @media screen and (height <= 930px - (56px * 3)) { + @media screen and (height <= (930px - 56px * 3)) { &__portal { display: none; } @@ -4110,6 +4113,7 @@ a.status-card { .status-card.expanded .status-card__title { white-space: normal; display: -webkit-box; + line-clamp: 2; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } @@ -6153,6 +6157,7 @@ a.status-card { font-size: 15px; line-height: 22px; color: $dark-text-color; + line-clamp: 4; -webkit-line-clamp: 4; -webkit-box-orient: vertical; max-height: 4 * 22px; @@ -7717,7 +7722,7 @@ a.status-card { display: flex; flex-shrink: 0; - @media screen and (max-width: $no-gap-breakpoint - 1px) { + @media screen and (max-width: ($no-gap-breakpoint - 1px)) { border-right: 0; border-left: 0; } @@ -8126,7 +8131,7 @@ noscript { } // Fallback for older browsers with no container queries support - @media screen and (max-width: 372px + 55px) { + @media screen and (max-width: (372px + 55px)) { display: none; } } @@ -8595,7 +8600,7 @@ noscript { width: 124px; flex: 0 0 auto; - @media screen and (max-width: 124px + 300px) { + @media screen and (max-width: (124px + 300px)) { display: none; } } @@ -8605,7 +8610,7 @@ noscript { flex: 0 0 auto; position: relative; - @media screen and (max-width: 124px + 300px) { + @media screen and (max-width: (124px + 300px)) { width: 100%; } } @@ -8698,7 +8703,6 @@ noscript { height: 100%; min-width: auto; min-height: auto; - vertical-align: bottom; object-fit: contain; } } @@ -8734,6 +8738,7 @@ noscript { } .emoji-picker-dropdown { + display: flex; margin: 2px; } @@ -8959,7 +8964,7 @@ noscript { .layout-single-column .explore__search-header { display: none; - @media screen and (max-width: $no-gap-breakpoint - 1px) { + @media screen and (max-width: ($no-gap-breakpoint - 1px)) { display: flex; } } @@ -9357,7 +9362,7 @@ noscript { } } - @media screen and (max-width: $no-gap-breakpoint - 1px) { + @media screen and (max-width: ($no-gap-breakpoint - 1px)) { &__choices { flex-direction: column; @@ -10902,6 +10907,7 @@ noscript { font-size: 14px; line-height: 20px; display: -webkit-box; + line-clamp: 2; -webkit-line-clamp: 2; -webkit-box-orient: vertical; max-height: 2 * 20px; From adf812efb368a67a2ea78142a36c0be6bab28fac Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Sat, 21 Jun 2025 10:59:47 +0200 Subject: [PATCH 5/6] Fix missing terms of services link (#35115) --- .../instances/terms_of_services_controller.rb | 3 +- app/models/terms_of_service.rb | 5 ++ app/serializers/initial_state_serializer.rb | 2 +- app/serializers/rest/instance_serializer.rb | 2 +- spec/models/terms_of_service_spec.rb | 51 +++++++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/instances/terms_of_services_controller.rb b/app/controllers/api/v1/instances/terms_of_services_controller.rb index 0a861dd7bb..a32438e31d 100644 --- a/app/controllers/api/v1/instances/terms_of_services_controller.rb +++ b/app/controllers/api/v1/instances/terms_of_services_controller.rb @@ -15,8 +15,9 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo if params[:date].present? TermsOfService.published.find_by!(effective_date: params[:date]) else - TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet + TermsOfService.current end end + not_found if @terms_of_service.nil? end end diff --git a/app/models/terms_of_service.rb b/app/models/terms_of_service.rb index 93fe4af9c7..59411f710b 100644 --- a/app/models/terms_of_service.rb +++ b/app/models/terms_of_service.rb @@ -16,6 +16,7 @@ class TermsOfService < ApplicationRecord scope :published, -> { where.not(published_at: nil).order(Arel.sql('coalesce(effective_date, published_at) DESC')) } scope :live, -> { published.where('effective_date IS NULL OR effective_date < now()').limit(1) } + scope :upcoming, -> { published.reorder(effective_date: :asc).where('effective_date IS NOT NULL AND effective_date > now()').limit(1) } scope :draft, -> { where(published_at: nil).order(id: :desc).limit(1) } validates :text, presence: true @@ -26,6 +27,10 @@ class TermsOfService < ApplicationRecord NOTIFICATION_ACTIVITY_CUTOFF = 1.year.freeze + def self.current + live.first || upcoming.first # For the case when none of the published terms have become effective yet + end + def published? published_at.present? end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index d47bb3cdbc..1c83eff4b2 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -110,7 +110,7 @@ class InitialStateSerializer < ActiveModel::Serializer trends_as_landing_page: Setting.trends_as_landing_page, trends_enabled: Setting.trends, version: instance_presenter.version, - terms_of_service_enabled: TermsOfService.live.exists?, + terms_of_service_enabled: TermsOfService.current.present?, } end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index db6e619819..3008df5e81 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -61,7 +61,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer status: object.status_page_url, about: about_url, privacy_policy: privacy_policy_url, - terms_of_service: TermsOfService.live.exists? ? terms_of_service_url : nil, + terms_of_service: TermsOfService.current.present? ? terms_of_service_url : nil, }, vapid: { diff --git a/spec/models/terms_of_service_spec.rb b/spec/models/terms_of_service_spec.rb index d32ba4e642..41b9e650ae 100644 --- a/spec/models/terms_of_service_spec.rb +++ b/spec/models/terms_of_service_spec.rb @@ -24,4 +24,55 @@ RSpec.describe TermsOfService do expect(subject.pluck(:id)).to match_array(user_before.id) end end + + describe '::current' do + context 'when no terms exist' do + it 'returns nil' do + expect(described_class.current).to be_nil + end + end + + context 'when only unpublished terms exist' do + before do + yesterday = Date.yesterday + travel_to yesterday do + Fabricate(:terms_of_service, published_at: nil, effective_date: yesterday) + end + Fabricate(:terms_of_service, published_at: nil, effective_date: Date.tomorrow) + end + + it 'returns nil' do + expect(described_class.current).to be_nil + end + end + + context 'when both effective and future terms exist' do + let!(:effective_terms) do + yesterday = Date.yesterday + travel_to yesterday do + Fabricate(:terms_of_service, effective_date: yesterday) + end + end + + before do + Fabricate(:terms_of_service, effective_date: Date.tomorrow) + end + + it 'returns the effective terms' do + expect(described_class.current).to eq effective_terms + end + end + + context 'when only future terms exist' do + let!(:upcoming_terms) { Fabricate(:terms_of_service, effective_date: Date.tomorrow) } + + before do + Fabricate(:terms_of_service, effective_date: 10.days.since) + end + + it 'returns the terms that are upcoming next' do + expect(described_class.current).to eq upcoming_terms + end + end + end end From ac039d5f1323c46062d004896996f50549bfa38b Mon Sep 17 00:00:00 2001 From: Claire Date: Sat, 21 Jun 2025 11:00:38 +0200 Subject: [PATCH 6/6] Fix clicking a status multiple times causing duplicate entries in browser history (#35118) --- app/javascript/mastodon/components/status.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 0f9d751eb3..29fd4234dd 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -301,7 +301,11 @@ class Status extends ImmutablePureComponent { if (newTab) { window.open(path, '_blank', 'noopener'); } else { - history.push(path); + if (history.location.pathname.replace('/deck/', '/') === path) { + history.replace(path); + } else { + history.push(path); + } } };