-
- {spoilerButton}
-
+ {(!visible || uncached) && (
+
+ {spoilerButton}
+
+ )}
{children}
+
+ {(visible && !uncached) && (
+
+
+
+ )}
);
}
}
-export default injectIntl(MediaGallery);
+export default MediaGallery;
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 7c1d7f126d..39ee7b858f 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -457,7 +457,7 @@
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading…",
- "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
+ "media_gallery.hide": "Hide",
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
"mute_modal.hide_from_notifications": "Hide from notifications",
"mute_modal.hide_options": "Hide options",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index a69dc56cde..37f4bb9cce 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4718,22 +4718,14 @@ a.status-card {
position: absolute;
z-index: 100;
- &--minified {
- display: block;
- inset-inline-start: 4px;
- top: 4px;
- width: auto;
- height: auto;
+ &--hidden {
+ display: none;
}
&--click-thru {
pointer-events: none;
}
- &--hidden {
- display: none;
- }
-
&__overlay {
display: flex;
align-items: center;
@@ -4745,19 +4737,20 @@ a.status-card {
margin: 0;
border: 0;
color: $white;
+ line-height: 20px;
+ font-size: 14px;
&__label {
background-color: rgba($black, 0.45);
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
- border-radius: 6px;
- padding: 10px 15px;
+ border-radius: 8px;
+ padding: 12px 16px;
display: flex;
align-items: center;
justify-content: center;
- gap: 8px;
+ gap: 4px;
flex-direction: column;
- font-weight: 500;
- font-size: 14px;
+ font-weight: 600;
}
&__action {
@@ -6838,10 +6831,32 @@ a.status-card {
z-index: 9999;
}
-.media-gallery__item__badges {
+.media-gallery__actions {
position: absolute;
bottom: 6px;
- inset-inline-start: 6px;
+ inset-inline-end: 6px;
+ display: flex;
+ gap: 2px;
+ z-index: 2;
+
+ &__pill {
+ display: block;
+ color: $white;
+ border: 0;
+ background: rgba($black, 0.65);
+ backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
+ padding: 3px 12px;
+ border-radius: 99px;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 20px;
+ }
+}
+
+.media-gallery__item__badges {
+ position: absolute;
+ bottom: 8px;
+ inset-inline-start: 8px;
display: flex;
gap: 2px;
}
@@ -6854,18 +6869,13 @@ a.status-card {
color: $white;
background: rgba($black, 0.65);
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
- padding: 2px 6px;
+ padding: 3px 8px;
border-radius: 4px;
- font-size: 11px;
+ font-size: 12px;
font-weight: 700;
z-index: 1;
pointer-events: none;
- line-height: 18px;
-
- .icon {
- width: 15px;
- height: 15px;
- }
+ line-height: 20px;
}
.attachment-list {
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 926df4e96f..56f7b893f3 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -12,6 +12,41 @@ code {
margin: 50px auto;
}
+.form-section {
+ border-radius: 8px;
+ background: var(--surface-background-color);
+ padding: 24px;
+ margin-bottom: 24px;
+}
+
+.fade-out-top {
+ position: relative;
+ overflow: hidden;
+ height: 160px;
+
+ &::after {
+ content: '';
+ display: block;
+ background: linear-gradient(
+ to bottom,
+ var(--surface-background-color),
+ transparent
+ );
+ position: absolute;
+ top: 0;
+ inset-inline-start: 0;
+ width: 100%;
+ height: 100px;
+ pointer-events: none;
+ }
+
+ & > div {
+ position: absolute;
+ inset-inline-start: 0;
+ bottom: 0;
+ }
+}
+
.indicator-icon {
display: flex;
align-items: center;
diff --git a/app/lib/entity_cache.rb b/app/lib/entity_cache.rb
index 80b0046eea..e647dcab7f 100644
--- a/app/lib/entity_cache.rb
+++ b/app/lib/entity_cache.rb
@@ -27,7 +27,7 @@ class EntityCache
end
unless uncached_ids.empty?
- uncached = CustomEmoji.where(shortcode: shortcodes, domain: domain, disabled: false).index_by(&:shortcode)
+ uncached = CustomEmoji.enabled.where(shortcode: shortcodes, domain: domain).index_by(&:shortcode)
uncached.each_value { |item| Rails.cache.write(to_key(:emoji, item.shortcode, domain), item, expires_in: MAX_EXPIRATION) }
end
diff --git a/app/models/account.rb b/app/models/account.rb
index a025792256..6063b3e0dd 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -51,6 +51,7 @@
# reviewed_at :datetime
# requested_review_at :datetime
# indexable :boolean default(FALSE), not null
+# attribution_domains :string default([]), is an Array
#
class Account < ApplicationRecord
@@ -88,6 +89,7 @@ class Account < ApplicationRecord
include Account::Merging
include Account::Search
include Account::StatusesSearch
+ include Account::AttributionDomains
include DomainMaterializable
include DomainNormalizable
include Paginable
diff --git a/app/models/announcement_reaction.rb b/app/models/announcement_reaction.rb
index 9881892c4b..f953402b7e 100644
--- a/app/models/announcement_reaction.rb
+++ b/app/models/announcement_reaction.rb
@@ -27,7 +27,7 @@ class AnnouncementReaction < ApplicationRecord
private
def set_custom_emoji
- self.custom_emoji = CustomEmoji.local.find_by(disabled: false, shortcode: name) if name.present?
+ self.custom_emoji = CustomEmoji.local.enabled.find_by(shortcode: name) if name.present?
end
def queue_publish
diff --git a/app/models/concerns/account/attribution_domains.rb b/app/models/concerns/account/attribution_domains.rb
new file mode 100644
index 0000000000..37a498a150
--- /dev/null
+++ b/app/models/concerns/account/attribution_domains.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Account::AttributionDomains
+ extend ActiveSupport::Concern
+
+ included do
+ validates :attribution_domains_as_text, domain: { multiline: true }, lines: { maximum: 100 }, if: -> { local? && will_save_change_to_attribution_domains? }
+ end
+
+ def attribution_domains_as_text
+ self[:attribution_domains].join("\n")
+ end
+
+ def attribution_domains_as_text=(str)
+ self[:attribution_domains] = str.split.filter_map do |line|
+ line.strip.delete_prefix('*.')
+ end
+ end
+
+ def can_be_attributed_from?(domain)
+ segments = domain.split('.')
+ variants = segments.map.with_index { |_, i| segments[i..].join('.') }.to_set
+ self[:attribution_domains].to_set.intersect?(variants)
+ end
+end
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 7ea50e85d8..ec334800e4 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -51,9 +51,10 @@ class CustomEmoji < ApplicationRecord
scope :local, -> { where(domain: nil) }
scope :remote, -> { where.not(domain: nil) }
+ scope :enabled, -> { where(disabled: false) }
scope :alphabetic, -> { order(domain: :asc, shortcode: :asc) }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) }
- scope :listed, -> { local.where(disabled: false).where(visible_in_picker: true) }
+ scope :listed, -> { local.enabled.where(visible_in_picker: true) }
remotable_attachment :image, LIMIT
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 4ab48ff204..a6281e23b9 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -8,7 +8,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
context_extensions :manually_approves_followers, :featured, :also_known_as,
:moved_to, :property_value, :discoverable, :olm, :suspended,
- :memorial, :indexable
+ :memorial, :indexable, :attribution_domains
attributes :id, :type, :following, :followers,
:inbox, :outbox, :featured, :featured_tags,
@@ -25,6 +25,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
attribute :moved_to, if: :moved?
attribute :also_known_as, if: :also_known_as?
attribute :suspended, if: :suspended?
+ attribute :attribution_domains, if: -> { object.attribution_domains.any? }
class EndpointsSerializer < ActivityPub::Serializer
include RoutingHelper
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index b667e97f4d..1e2d614d72 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -117,6 +117,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.discoverable = @json['discoverable'] || false
@account.indexable = @json['indexable'] || false
@account.memorial = @json['memorial'] || false
+ @account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) }
end
def set_fetchable_key!
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 36d5c490a6..7662fc1f29 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -153,12 +153,13 @@ class FetchLinkCardService < BaseService
return if html.nil?
link_details_extractor = LinkDetailsExtractor.new(@url, @html, @html_charset)
- provider = PreviewCardProvider.matching_domain(Addressable::URI.parse(link_details_extractor.canonical_url).normalized_host)
- linked_account = ResolveAccountService.new.call(link_details_extractor.author_account, suppress_errors: true) if link_details_extractor.author_account.present? && provider&.trendable?
+ domain = Addressable::URI.parse(link_details_extractor.canonical_url).normalized_host
+ provider = PreviewCardProvider.matching_domain(domain)
+ linked_account = ResolveAccountService.new.call(link_details_extractor.author_account, suppress_errors: true) if link_details_extractor.author_account.present?
@card = PreviewCard.find_or_initialize_by(url: link_details_extractor.canonical_url) if link_details_extractor.canonical_url != @card.url
@card.assign_attributes(link_details_extractor.to_preview_card_attributes)
- @card.author_account = linked_account
+ @card.author_account = linked_account if linked_account&.can_be_attributed_from?(domain) || provider&.trendable?
@card.save_with_optional_image! unless @card.title.blank? && @card.html.blank?
end
end
diff --git a/app/validators/domain_validator.rb b/app/validators/domain_validator.rb
index 3a951f9a7e..718fd190f1 100644
--- a/app/validators/domain_validator.rb
+++ b/app/validators/domain_validator.rb
@@ -1,22 +1,29 @@
# frozen_string_literal: true
class DomainValidator < ActiveModel::EachValidator
+ MAX_DOMAIN_LENGTH = 256
+ MIN_LABEL_LENGTH = 1
+ MAX_LABEL_LENGTH = 63
+ ALLOWED_CHARACTERS_RE = /^[a-z0-9\-]+$/i
+
def validate_each(record, attribute, value)
return if value.blank?
- domain = if options[:acct]
- value.split('@').last
- else
- value
- end
+ (options[:multiline] ? value.split : [value]).each do |domain|
+ _, domain = domain.split('@') if options[:acct]
- record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(domain)
+ next if domain.blank?
+
+ record.errors.add(attribute, options[:multiline] ? :invalid_domain_on_line : :invalid, value: domain) unless compliant?(domain)
+ end
end
private
def compliant?(value)
- Addressable::URI.new.tap { |uri| uri.host = value }
+ uri = Addressable::URI.new
+ uri.host = value
+ uri.normalized_host.size < MAX_DOMAIN_LENGTH && uri.normalized_host.split('.').all? { |label| label.size.between?(MIN_LABEL_LENGTH, MAX_LABEL_LENGTH) && label =~ ALLOWED_CHARACTERS_RE }
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
false
end
diff --git a/app/validators/lines_validator.rb b/app/validators/lines_validator.rb
new file mode 100644
index 0000000000..27a108bb2c
--- /dev/null
+++ b/app/validators/lines_validator.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class LinesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if value.blank?
+
+ record.errors.add(attribute, :too_many_lines, limit: options[:maximum]) if options[:maximum].present? && value.split.size > options[:maximum]
+ end
+end
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 1a3a111b33..a76dc75f63 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -21,7 +21,7 @@
%link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/
%link{ rel: 'manifest', href: manifest_path(format: :json) }/
= theme_color_tags current_theme
- %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/
+ %meta{ name: 'mobile-web-app-capable', content: 'yes' }/
%title= html_title
diff --git a/app/views/settings/verifications/show.html.haml b/app/views/settings/verifications/show.html.haml
index 4fb2918018..5318b0767d 100644
--- a/app/views/settings/verifications/show.html.haml
+++ b/app/views/settings/verifications/show.html.haml
@@ -5,7 +5,9 @@
%h2= t('settings.profile')
= render partial: 'settings/shared/profile_navigation'
-.simple_form
+.simple_form.form-section
+ %h3= t('verification.website_verification')
+
%p.lead= t('verification.hint_html')
%h4= t('verification.here_is_how')
@@ -28,3 +30,33 @@
%span.verified-badge
= material_symbol 'check', class: 'verified-badge__mark'
%span= field.value
+
+= simple_form_for @account, url: settings_verification_path, html: { method: :put, class: 'form-section' } do |f|
+ = render 'shared/error_messages', object: @account
+
+ %h3= t('author_attribution.title')
+
+ %p.lead= t('author_attribution.hint_html')
+
+ .fields-row
+ .fields-row__column.fields-row__column-6
+ .fields-group
+ = f.input :attribution_domains_as_text, as: :text, wrapper: :with_block_label, input_html: { placeholder: "example1.com\nexample2.com\nexample3.com", rows: 4 }
+ .fields-row__column.fields-row__column-6
+ .fields-group.fade-out-top
+ %div
+ .status-card.expanded.bottomless
+ .status-card__image
+ = image_tag frontend_asset_url('images/preview.png'), alt: '', class: 'status-card__image-image'
+ .status-card__content
+ %span.status-card__host
+ %span= t('author_attribution.s_blog', name: @account.username)
+ ·
+ %time.time-ago{ datetime: 1.year.ago.to_date.iso8601 }
+ %strong.status-card__title= t('author_attribution.example_title')
+ .more-from-author
+ = logo_as_symbol(:icon)
+ = t('author_attribution.more_from_html', name: link_to(root_url, class: 'story__details__shared__author-link') { image_tag(@account.avatar.url, class: 'account__avatar', width: 16, height: 16, alt: '') + content_tag(:bdi, display_name(@account)) })
+
+ .actions
+ = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml
index a53c7c6e9e..e135856036 100644
--- a/config/locales/activerecord.en.yml
+++ b/config/locales/activerecord.en.yml
@@ -15,6 +15,12 @@ en:
user/invite_request:
text: Reason
errors:
+ attributes:
+ domain:
+ invalid: is not a valid domain name
+ messages:
+ invalid_domain_on_line: "%{value} is not a valid domain name"
+ too_many_lines: is over the limit of %{limit} lines
models:
account:
attributes:
diff --git a/config/locales/an.yml b/config/locales/an.yml
index 9afc9e881d..41eeee4614 100644
--- a/config/locales/an.yml
+++ b/config/locales/an.yml
@@ -1017,8 +1017,6 @@ an:
your_appeal_approved: S'aprebó la tuya apelación
your_appeal_pending: Has ninviau una apelación
your_appeal_rejected: La tuya apelación ha estau refusada
- domain_validator:
- invalid_domain: no ye un nombre de dominio valido
errors:
'400': La solicitut que has ninviau no ye valida u yera malformada.
'403': No tiens permiso pa acceder ta esta pachina.
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index ee05684b6c..06cea7ecb3 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -1239,8 +1239,6 @@ ar:
your_appeal_approved: تمت الموافقة على طعنك
your_appeal_pending: لقد قمت بتقديم طعن
your_appeal_rejected: تم رفض طعنك
- domain_validator:
- invalid_domain: ليس بإسم نطاق صالح
edit_profile:
basic_information: معلومات أساسية
hint_html: "