Add fallback attributes to notifications for new and infrequent notifications (#38832)

This commit is contained in:
Claire 2026-04-29 17:53:29 +02:00 committed by GitHub
parent afeb63d287
commit 5b395774c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 225 additions and 14 deletions

View File

@ -16,7 +16,7 @@ class Api::V1::NotificationsController < Api::BaseController
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
end
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships, supported_notification_types: params[:supported_types]
end
def unread_count
@ -29,7 +29,7 @@ class Api::V1::NotificationsController < Api::BaseController
def show
@notification = current_account.notifications.without_suspended.find(params[:id])
render json: @notification, serializer: REST::NotificationSerializer
render json: @notification, serializer: REST::NotificationSerializer, supported_notification_types: params[:supported_types]
end
def clear
@ -89,6 +89,8 @@ class Api::V1::NotificationsController < Api::BaseController
end
def pagination_params(core_params)
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params)
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered, :supported_types)
.permit(:limit, :account_id, :include_filtered, types: [], exclude_types: [], supported_types: [])
.merge(core_params)
end
end

View File

@ -33,7 +33,7 @@ class Api::V2::NotificationsController < Api::BaseController
'app.notification_grouping.expand_accounts_param' => expand_accounts_param
)
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, expand_accounts: expand_accounts_param, supported_notification_types: params[:supported_types]
end
end
@ -48,7 +48,7 @@ class Api::V2::NotificationsController < Api::BaseController
def show
@notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take!
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
render json: presenter, serializer: REST::DedupNotificationGroupSerializer, supported_notification_types: params[:supported_types]
end
def clear
@ -138,7 +138,9 @@ class Api::V2::NotificationsController < Api::BaseController
end
def pagination_params(core_params)
params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types).permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: []).merge(core_params)
params.slice(:limit, :include_filtered, :types, :exclude_types, :grouped_types, :supported_types)
.permit(:limit, :include_filtered, types: [], exclude_types: [], grouped_types: [], supported_types: [])
.merge(core_params)
end
def expand_accounts_param

View File

@ -76,6 +76,15 @@ class TextFormatter
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
h(url)
end
def link_to_mention(account, with_domain: false)
url = ActivityPub::TagManager.instance.url_for(account)
display_username = with_domain ? account.pretty_acct : account.username
<<~HTML.squish
<span class="h-card" translate="no"><a href="#{h(url)}" class="u-url mention">@<span>#{h(display_username)}</span></a></span>
HTML
end
end
private
@ -136,12 +145,7 @@ class TextFormatter
return "@#{h(entity[:screen_name])}" if account.nil?
url = ActivityPub::TagManager.instance.url_for(account)
display_username = same_username_hits&.positive? || with_domains? ? account.pretty_acct : account.username
<<~HTML.squish
<span class="h-card" translate="no"><a href="#{h(url)}" class="u-url mention">@<span>#{h(display_username)}</span></a></span>
HTML
TextFormatter.link_to_mention(account, with_domain: same_username_hits&.positive? || with_domains?)
end
def entity_cache

View File

@ -37,54 +37,71 @@ class Notification < ApplicationRecord
PROPERTIES = {
mention: {
filterable: true,
baseline: true,
}.freeze,
status: {
filterable: false,
baseline: true,
}.freeze,
reblog: {
filterable: true,
baseline: true,
}.freeze,
follow: {
filterable: true,
baseline: true,
}.freeze,
follow_request: {
filterable: true,
baseline: true,
}.freeze,
favourite: {
filterable: true,
baseline: true,
}.freeze,
poll: {
filterable: false,
baseline: true,
}.freeze,
update: {
filterable: false,
baseline: true,
}.freeze,
severed_relationships: {
filterable: false,
baseline: false,
}.freeze,
moderation_warning: {
filterable: false,
baseline: false,
}.freeze,
annual_report: {
filterable: false,
baseline: true,
}.freeze,
'admin.sign_up': {
filterable: false,
baseline: false,
}.freeze,
'admin.report': {
filterable: false,
baseline: false,
}.freeze,
quote: {
filterable: true,
baseline: true,
}.freeze,
quoted_update: {
filterable: false,
baseline: true,
}.freeze,
added_to_collection: {
filterable: true,
baseline: false,
}.freeze,
collection_update: {
filterable: false,
baseline: false,
}.freeze,
}.freeze

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
module NotificationFallbackConcern
extend ActiveSupport::Concern
def fallback
{
title: fallback_title,
summary: fallback_summary,
description: nil,
}
end
def needs_fallback?
return false if instance_options[:supported_notification_types].nil?
return false if Notification::PROPERTIES.dig(object.type, :baseline) || instance_options[:supported_notification_types].include?(object.type.to_s)
# In rare cases, a notification might be missing its activity, in which case we can't do much
case object.type
when :severed_relationships
object.account_relationship_severance_event.present?
when :'admin.report'
object.report.present?
when :added_to_collection, :collection_update
object.target_collection.present?
else
true
end
end
def fallback_title
account = object.is_a?(NotificationGroup) ? object.sample_accounts.first : object.from_account
case object.type
when :severed_relationships
I18n.t(
'notification_fallbacks.severed_relationships.title',
name: object.account_relationship_severance_event.target_name
)
when :moderation_warning
I18n.t('notification_fallbacks.moderation_warning.title')
when :'admin.sign_up'
count = object.is_a?(NotificationGroup) ? object.sample_accounts.count : 1
if count > 1
I18n.t(
'notification_fallbacks.admin_sign_up.title_and_others_html',
name: TextFormatter.link_to_mention(account),
count: count - 1
)
else
I18n.t(
'notification_fallbacks.admin_sign_up.title_html',
name: TextFormatter.link_to_mention(account)
)
end
when :'admin.report'
I18n.t(
'notification_fallbacks.admin_report.title_html',
name: account.remote? ? account.domain : TextFormatter.link_to_mention(account),
target: TextFormatter.link_to_mention(object.report.target_account)
)
when :added_to_collection
I18n.t(
'notification_fallbacks.added_to_collection.title_html',
name: TextFormatter.link_to_mention(account)
)
when :collection_update
I18n.t(
'notification_fallbacks.collection_update.title_html',
name: account
)
end
end
def fallback_summary
case object.type
when :severed_relationships
I18n.t(
'notification_fallbacks.severed_relationships.summary_html',
from: Rails.configuration.x.local_domain,
target: object.account_relationship_severance_event.target_name,
link: link_to(I18n.t('notification_fallbacks.generic.sign_in'), severed_relationships_url)
)
when :moderation_warning
I18n.t(
'notification_fallbacks.moderation_warning.summary_html',
link: link_to(I18n.t('notification_fallbacks.generic.sign_in'), disputes_strike_url(object.account_warning.id))
)
when :'admin.sign_up', :'admin.report', :added_to_collection, :collection_update
I18n.t(
'notification_fallbacks.generic.summary_html',
link: link_to(I18n.t('notification_fallbacks.generic.sign_in'), root_url)
)
end
end
end

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true
class REST::NotificationGroupSerializer < ActiveModel::Serializer
include RoutingHelper
include ActionView::Helpers::UrlHelper
include NotificationFallbackConcern
# Please update app/javascript/mastodon/api_types/notifications.ts when making changes to the attributes
attributes :group_key, :notifications_count, :type, :most_recent_notification_id
@ -8,6 +12,8 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer
attribute :page_max_id, if: :paginated?
attribute :latest_page_notification_at, if: :paginated?
attribute :fallback, if: :needs_fallback?
attribute :sample_account_ids
attribute :status_id, if: :status_type?
belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer

View File

@ -1,11 +1,17 @@
# frozen_string_literal: true
class REST::NotificationSerializer < ActiveModel::Serializer
include RoutingHelper
include ActionView::Helpers::UrlHelper
include NotificationFallbackConcern
# Please update app/javascript/mastodon/api_types/notifications.ts when making changes to the attributes
attributes :id, :type, :created_at, :group_key
attribute :filtered, if: :filtered?
attribute :fallback, if: :needs_fallback?
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer

View File

@ -1731,6 +1731,27 @@ en:
copy_account_note_text: 'This user moved from %{acct}, here were your previous notes about them:'
navigation:
toggle_menu: Toggle menu
notification_fallbacks:
added_to_collection:
title_html: "%{name} added you to a collection"
admin_report:
title_html: "%{name} reported %{target}"
admin_sign_up:
title_and_others_html:
one: "%{name} and one other signed up"
other: "%{name} and %{count} others signed up"
title_html: "%{name} signed up"
collection_update:
title_html: "%{name} updated a collection you are in"
generic:
sign_in: Sign in to the Mastodon web app
summary_html: You're on an app that does not support the most recent version of Mastodon. %link for full functionality.
moderation_warning:
summary_html: You're on an app that does not support the most recent version of Mastodon. %link
title: You have received a moderation warning.
severed_relationships:
summary_html: An admin from %{from} has suspended %{target}, which means you can no longer receive updates from them or interact with them. %{link} to retrieve a list of the lost relationships.
title: Lost connections with %{name}
notification_mailer:
admin:
report:

View File

@ -6,10 +6,16 @@ RSpec.describe REST::NotificationGroupSerializer do
subject do
serialized_record_json(
notification_group,
described_class
described_class,
options: {
scope: current_user,
scope_name: :current_user,
supported_notification_types: [],
}
)
end
let(:current_user) { Fabricate(:user) }
let(:notification_group) { NotificationGroup.new pagination_data: { latest_notification_at: 3.days.ago }, notification: Fabricate(:notification), sample_accounts: [] }
context 'when latest_page_notification_at is populated' do
@ -20,4 +26,28 @@ RSpec.describe REST::NotificationGroupSerializer do
)
end
end
shared_examples 'with fallback notifications' do |type, fabricators|
let(:activities) { fabricators.map { |fabricator| Fabricate(fabricator) } }
let(:notifications) { activities.map { |activity| Fabricate(:notification, type:, activity:, account: current_user.account) } }
let(:notification_group) { NotificationGroup.new(notification: notifications.last, sample_accounts: notifications.map(&:from_account)) }
it 'renders correctly' do
expect(subject)
.to include(
'type' => type,
'fallback' => include(
'title' => anything,
'summary' => anything
)
)
end
end
it_behaves_like 'with fallback notifications', 'severed_relationships', [:account_relationship_severance_event]
it_behaves_like 'with fallback notifications', 'moderation_warning', [:account_warning]
it_behaves_like 'with fallback notifications', 'admin.report', [:report]
it_behaves_like 'with fallback notifications', 'admin.report', [:report, :report]
it_behaves_like 'with fallback notifications', 'added_to_collection', [:collection_item]
it_behaves_like 'with fallback notifications', 'collection_update', [:collection]
end

View File

@ -6,10 +6,16 @@ RSpec.describe REST::NotificationSerializer do
subject do
serialized_record_json(
notification,
described_class
described_class,
options: {
scope: current_user,
scope_name: :current_user,
supported_notification_types: [],
}
)
end
let(:current_user) { Fabricate(:user) }
let(:notification) { Fabricate :notification }
context 'when created_at is populated' do
@ -20,4 +26,25 @@ RSpec.describe REST::NotificationSerializer do
)
end
end
shared_examples 'with fallback notifications' do |type, fabricator|
let(:notification) { Fabricate(:notification, type:, activity: Fabricate(fabricator), account: current_user.account) }
it 'renders correctly' do
expect(subject)
.to include(
'type' => type,
'fallback' => include(
'title' => anything,
'summary' => anything
)
)
end
end
it_behaves_like 'with fallback notifications', 'severed_relationships', :account_relationship_severance_event
it_behaves_like 'with fallback notifications', 'moderation_warning', :account_warning
it_behaves_like 'with fallback notifications', 'admin.report', :report
it_behaves_like 'with fallback notifications', 'added_to_collection', :collection_item
it_behaves_like 'with fallback notifications', 'collection_update', :collection
end