From 0b1710f6baf09aa95849bb97f4eab56f0b929997 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Thu, 21 Nov 2024 00:07:14 +0100 Subject: [PATCH] WIP: Notifications for Report Notes --- .../admin/report_notes_controller.rb | 8 +++ .../mastodon/api_types/notifications.ts | 16 ++++- app/javascript/mastodon/api_types/reports.ts | 7 ++ .../notification_admin_report_note.tsx | 66 +++++++++++++++++++ .../components/notification_group.tsx | 9 +++ .../mastodon/models/notification_group.ts | 37 ++++++++++- .../mastodon/reducers/notifications.js | 1 + app/javascript/mastodon/reducers/settings.js | 3 + app/mailers/admin_mailer.rb | 8 +++ app/models/notification.rb | 7 +- app/models/notification_group.rb | 1 + .../rest/notification_group_serializer.rb | 5 ++ .../rest/notification_serializer.rb | 5 ++ .../rest/report_note_serializer.rb | 11 ++++ app/services/notify_service.rb | 2 + .../admin_mailer/new_report_note.text.erb | 5 ++ config/locales/en.yml | 3 + 17 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 app/javascript/mastodon/features/notifications_v2/components/notification_admin_report_note.tsx create mode 100644 app/serializers/rest/report_note_serializer.rb create mode 100644 app/views/admin_mailer/new_report_note.text.erb diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index 6b16c29fc7..62c402c8e0 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -19,6 +19,14 @@ module Admin log_action :reopen, @report end + User.those_who_can(:manage_reports).includes(:account).find_each do |u| + # Prevent notifications to the author of the report note: + next if @report_note.account.id == u.account.id + + LocalNotificationWorker.perform_async(u.account_id, @report_note.id, 'ReportNote', 'admin.report_note') + AdminMailer.with(recipient: u.account).new_report_note(@report_note).deliver_later if u.allows_report_emails? + end + redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg') else @report_notes = @report.notes.chronological.includes(:account) diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts index 190d8c8396..7e70b8f086 100644 --- a/app/javascript/mastodon/api_types/notifications.ts +++ b/app/javascript/mastodon/api_types/notifications.ts @@ -3,7 +3,7 @@ import type { AccountWarningAction } from 'mastodon/models/notification_group'; import type { ApiAccountJSON } from './accounts'; -import type { ApiReportJSON } from './reports'; +import type { ApiReportJSON, ApiReportNoteJSON } from './reports'; import type { ApiStatusJSON } from './statuses'; // See app/model/notification.rb @@ -18,6 +18,7 @@ export const allNotificationTypes = [ 'update', 'admin.sign_up', 'admin.report', + 'admin.report_note', 'moderation_warning', 'severed_relationships', 'annual_report', @@ -39,6 +40,7 @@ export type NotificationType = | 'severed_relationships' | 'admin.sign_up' | 'admin.report' + | 'admin.report_note' | 'annual_report'; export interface BaseNotificationJSON { @@ -80,6 +82,16 @@ interface ReportNotificationJSON extends BaseNotificationJSON { report: ApiReportJSON; } +interface ReportNoteNotificationGroupJSON extends BaseNotificationGroupJSON { + type: 'admin.report_note'; + report_note: ApiReportNoteJSON; +} + +interface ReportNoteNotificationJSON extends BaseNotificationJSON { + type: 'admin.report_note'; + report_note: ApiReportNoteJSON; +} + type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up'; interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON { type: SimpleNotificationTypes; @@ -144,6 +156,7 @@ interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON { export type ApiNotificationJSON = | SimpleNotificationJSON | ReportNotificationJSON + | ReportNoteNotificationJSON | AccountRelationshipSeveranceNotificationJSON | NotificationWithStatusJSON | ModerationWarningNotificationJSON; @@ -151,6 +164,7 @@ export type ApiNotificationJSON = export type ApiNotificationGroupJSON = | SimpleNotificationGroupJSON | ReportNotificationGroupJSON + | ReportNoteNotificationGroupJSON | AccountRelationshipSeveranceNotificationGroupJSON | NotificationGroupWithStatusJSON | ModerationWarningNotificationGroupJSON diff --git a/app/javascript/mastodon/api_types/reports.ts b/app/javascript/mastodon/api_types/reports.ts index b11cfdd2eb..8513bd8feb 100644 --- a/app/javascript/mastodon/api_types/reports.ts +++ b/app/javascript/mastodon/api_types/reports.ts @@ -14,3 +14,10 @@ export interface ApiReportJSON { rule_ids: string[]; target_account: ApiAccountJSON; } + +export interface ApiReportNoteJSON { + id: string; + content: string; + created_at: string; + report: ApiReportJSON; +} diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_admin_report_note.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_admin_report_note.tsx new file mode 100644 index 0000000000..ececb2bf83 --- /dev/null +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_admin_report_note.tsx @@ -0,0 +1,66 @@ +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; +import { Icon } from 'mastodon/components/icon'; +import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; +import type { NotificationGroupAdminReportNote } from 'mastodon/models/notification_group'; +import { useAppSelector } from 'mastodon/store'; + +export const NotificationAdminReportNote: React.FC<{ + notification: NotificationGroupAdminReportNote; + unread?: boolean; +}> = ({ notification, notification: { reportNote }, unread }) => { + const account = useAppSelector((state) => + state.accounts.get(notification.sampleAccountIds[0] ?? '0'), + ); + + if (!account) return null; + + const domain = account.acct.split('@')[1]; + + const values = { + name: {domain ?? `@${account.acct}`}, + reportId: reportNote.report.id, + }; + + const message = ( + + ); + + return ( + +
+ +
+ +
+
+
+ {message} + +
+
+ + {reportNote.content.length > 0 && ( +
+ “{reportNote.content}” +
+ )} +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx index d5eb851985..38bed39e28 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx @@ -8,6 +8,7 @@ import type { NotificationGroup as NotificationGroupModel } from 'mastodon/model import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { NotificationAdminReport } from './notification_admin_report'; +import { NotificationAdminReportNote } from './notification_admin_report_note'; import { NotificationAdminSignUp } from './notification_admin_sign_up'; import { NotificationAnnualReport } from './notification_annual_report'; import { NotificationFavourite } from './notification_favourite'; @@ -136,6 +137,14 @@ export const NotificationGroup: React.FC<{ /> ); break; + case 'admin.report_note': + content = ( + + ); + break; case 'moderation_warning': content = ( { + report: Report; +} +export interface NotificationGroupAdminReportNote + extends BaseNotification<'admin.report_note'> { + reportNote: ReportNote; +} + export type NotificationGroup = | NotificationGroupFavourite | NotificationGroupReblog @@ -94,6 +105,7 @@ export type NotificationGroup = | NotificationGroupSeveredRelationships | NotificationGroupAdminSignUp | NotificationGroupAdminReport + | NotificationGroupAdminReportNote | NotificationGroupAnnualReport; function createReportFromJSON(reportJSON: ApiReportJSON): Report { @@ -104,6 +116,16 @@ function createReportFromJSON(reportJSON: ApiReportJSON): Report { }; } +function createReportNoteFromJSON( + reportNoteJSON: ApiReportNoteJSON, +): ReportNote { + const { report, ...reportNote } = reportNoteJSON; + return { + report: createReportFromJSON(report), + ...reportNote, + }; +} + function createAccountWarningFromJSON( warningJSON: ApiAccountWarningJSON, ): AccountWarning { @@ -153,6 +175,14 @@ export function createNotificationGroupFromJSON( ...groupWithoutTargetAccount, }; } + case 'admin.report_note': { + const { report_note, ...groupWithoutReportNote } = group; + return { + reportNote: createReportNoteFromJSON(report_note), + sampleAccountIds, + ...groupWithoutReportNote, + }; + } case 'severed_relationships': return { ...group, @@ -207,6 +237,11 @@ export function createNotificationGroupFromNotificationJSON( return { ...group, statusId: notification.status?.id }; case 'admin.report': return { ...group, report: createReportFromJSON(notification.report) }; + case 'admin.report_note': + return { + ...group, + reportNote: createReportNoteFromJSON(notification.report_note), + }; case 'severed_relationships': return { ...group, diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index c99a619b52..8ebff00faa 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -56,6 +56,7 @@ export const notificationToMap = notification => ImmutableMap({ created_at: notification.created_at, status: notification.status ? notification.status.id : null, report: notification.report ? fromJS(notification.report) : null, + report_note: notification.report_note ? fromJS(notification.report_note) : null, event: notification.event ? fromJS(notification.event) : null, moderation_warning: notification.moderation_warning ? fromJS(notification.moderation_warning) : null, }); diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index fc02ac7186..9bb5f14ba7 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -41,6 +41,7 @@ const initialState = ImmutableMap({ update: false, 'admin.sign_up': false, 'admin.report': false, + 'admin.report_note': false, }), quickFilter: ImmutableMap({ @@ -64,6 +65,7 @@ const initialState = ImmutableMap({ update: true, 'admin.sign_up': true, 'admin.report': true, + 'admin.report_note': true, }), sounds: ImmutableMap({ @@ -77,6 +79,7 @@ const initialState = ImmutableMap({ update: true, 'admin.sign_up': true, 'admin.report': true, + 'admin.report_note': true, }), group: ImmutableMap({ diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index fffbbb3c6d..98caf7c0eb 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -21,6 +21,14 @@ class AdminMailer < ApplicationMailer end end + def new_report_note(report_note) + @report_note = report_note + + locale_for_account(@me) do + mail subject: default_i18n_subject(instance: @instance, id: @report_note.report.id) + end + end + def new_appeal(appeal) @appeal = appeal diff --git a/app/models/notification.rb b/app/models/notification.rb index e7ada3399a..6781430350 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -73,6 +73,9 @@ class Notification < ApplicationRecord 'admin.report': { filterable: false, }.freeze, + 'admin.report_note': { + filterable: false, + }.freeze, }.freeze TYPES = PROPERTIES.keys.freeze @@ -85,6 +88,7 @@ class Notification < ApplicationRecord poll: [poll: :status], update: :status, 'admin.report': [report: :target_account], + 'admin.report_note': [report_note: :report], }.freeze belongs_to :account, optional: true @@ -99,6 +103,7 @@ class Notification < ApplicationRecord belongs_to :favourite, inverse_of: :notification belongs_to :poll, inverse_of: false belongs_to :report, inverse_of: false + belongs_to :report_note, inverse_of: false belongs_to :account_relationship_severance_event, inverse_of: false belongs_to :account_warning, inverse_of: false belongs_to :generated_annual_report, inverse_of: false @@ -192,7 +197,7 @@ class Notification < ApplicationRecord return unless new_record? case activity_type - when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report' + when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report', 'ReportNote' self.from_account_id = activity&.account_id when 'Mention' self.from_account_id = activity&.status&.account_id diff --git a/app/models/notification_group.rb b/app/models/notification_group.rb index 9331b9406f..c0f8a898ad 100644 --- a/app/models/notification_group.rb +++ b/app/models/notification_group.rb @@ -49,6 +49,7 @@ class NotificationGroup < ActiveModelSerializers::Model delegate :type, :target_status, :report, + :report_note, :account_relationship_severance_event, :account_warning, :generated_annual_report, diff --git a/app/serializers/rest/notification_group_serializer.rb b/app/serializers/rest/notification_group_serializer.rb index f4af842e38..c48b64a40f 100644 --- a/app/serializers/rest/notification_group_serializer.rb +++ b/app/serializers/rest/notification_group_serializer.rb @@ -11,6 +11,7 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer attribute :sample_account_ids attribute :status_id, if: :status_type? belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer + belongs_to :report_note, if: :report_note_type?, serializer: REST::ReportNoteSerializer belongs_to :account_relationship_severance_event, key: :event, if: :relationship_severance_event?, serializer: REST::AccountRelationshipSeveranceEventSerializer belongs_to :account_warning, key: :moderation_warning, if: :moderation_warning_event?, serializer: REST::AccountWarningSerializer belongs_to :generated_annual_report, key: :annual_report, if: :annual_report_event?, serializer: REST::AnnualReportEventSerializer @@ -31,6 +32,10 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer object.type == :'admin.report' end + def report_note_type? + object.type == :'admin.report_note' + end + def relationship_severance_event? object.type == :severed_relationships end diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index 320bc86961..2bef222888 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -9,6 +9,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer 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 + belongs_to :report_note, if: :report_note_type?, serializer: REST::ReportNoteSerializer belongs_to :account_relationship_severance_event, key: :event, if: :relationship_severance_event?, serializer: REST::AccountRelationshipSeveranceEventSerializer belongs_to :account_warning, key: :moderation_warning, if: :moderation_warning_event?, serializer: REST::AccountWarningSerializer @@ -28,6 +29,10 @@ class REST::NotificationSerializer < ActiveModel::Serializer object.type == :'admin.report' end + def report_note_type? + object.type == :'admin.report_note' + end + def relationship_severance_event? object.type == :severed_relationships end diff --git a/app/serializers/rest/report_note_serializer.rb b/app/serializers/rest/report_note_serializer.rb new file mode 100644 index 0000000000..e7180fe9a4 --- /dev/null +++ b/app/serializers/rest/report_note_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class REST::ReportNoteSerializer < ActiveModel::Serializer + attributes :id, :content, :created_at + + has_one :report, serializer: REST::ReportSerializer + + def id + object.id.to_s + end +end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 0cf56c5a24..1c7b0de33c 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -6,6 +6,7 @@ class NotifyService < BaseService # TODO: the severed_relationships and annual_report types probably warrants email notifications NON_EMAIL_TYPES = %i( admin.report + admin.report_note admin.sign_up update poll @@ -23,6 +24,7 @@ class NotifyService < BaseService NON_FILTERABLE_TYPES = %i( admin.sign_up admin.report + admin.report_note poll update account_warning diff --git a/app/views/admin_mailer/new_report_note.text.erb b/app/views/admin_mailer/new_report_note.text.erb new file mode 100644 index 0000000000..d36abb9857 --- /dev/null +++ b/app/views/admin_mailer/new_report_note.text.erb @@ -0,0 +1,5 @@ +<%= raw t('admin_mailer.new_report_note.body', report_id: @report_note.report.id, account: @report_note.account.pretty_acct) %> + +> <%= raw word_wrap(@report_note.content, break_sequence: "\n> ") %> + +<%= raw t('application_mailer.view')%> <%= admin_report_url(@report_note.report, anchor: dom_id(@report_note)) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 2971fe1f25..7c9d584aff 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1052,6 +1052,9 @@ en: body: "%{reporter} has reported %{target}" body_remote: Someone from %{domain} has reported %{target} subject: New report for %{instance} (#%{id}) + new_report_note: + body: "%{account} has added a report note to Report #%{report_id}:" + subject: "New report note for %{instance} (Report #%{id})" new_software_updates: body: New Mastodon versions have been released, you may want to update! subject: New Mastodon versions are available for %{instance}!