WIP: Notifications for Report Notes

This commit is contained in:
Emelia Smith 2024-11-21 00:07:14 +01:00
parent 2526b32ad3
commit 0b1710f6ba
No known key found for this signature in database
17 changed files with 191 additions and 3 deletions

View file

@ -19,6 +19,14 @@ module Admin
log_action :reopen, @report log_action :reopen, @report
end 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') redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
else else
@report_notes = @report.notes.chronological.includes(:account) @report_notes = @report.notes.chronological.includes(:account)

View file

@ -3,7 +3,7 @@
import type { AccountWarningAction } from 'mastodon/models/notification_group'; import type { AccountWarningAction } from 'mastodon/models/notification_group';
import type { ApiAccountJSON } from './accounts'; import type { ApiAccountJSON } from './accounts';
import type { ApiReportJSON } from './reports'; import type { ApiReportJSON, ApiReportNoteJSON } from './reports';
import type { ApiStatusJSON } from './statuses'; import type { ApiStatusJSON } from './statuses';
// See app/model/notification.rb // See app/model/notification.rb
@ -18,6 +18,7 @@ export const allNotificationTypes = [
'update', 'update',
'admin.sign_up', 'admin.sign_up',
'admin.report', 'admin.report',
'admin.report_note',
'moderation_warning', 'moderation_warning',
'severed_relationships', 'severed_relationships',
'annual_report', 'annual_report',
@ -39,6 +40,7 @@ export type NotificationType =
| 'severed_relationships' | 'severed_relationships'
| 'admin.sign_up' | 'admin.sign_up'
| 'admin.report' | 'admin.report'
| 'admin.report_note'
| 'annual_report'; | 'annual_report';
export interface BaseNotificationJSON { export interface BaseNotificationJSON {
@ -80,6 +82,16 @@ interface ReportNotificationJSON extends BaseNotificationJSON {
report: ApiReportJSON; 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'; type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON { interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
type: SimpleNotificationTypes; type: SimpleNotificationTypes;
@ -144,6 +156,7 @@ interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON {
export type ApiNotificationJSON = export type ApiNotificationJSON =
| SimpleNotificationJSON | SimpleNotificationJSON
| ReportNotificationJSON | ReportNotificationJSON
| ReportNoteNotificationJSON
| AccountRelationshipSeveranceNotificationJSON | AccountRelationshipSeveranceNotificationJSON
| NotificationWithStatusJSON | NotificationWithStatusJSON
| ModerationWarningNotificationJSON; | ModerationWarningNotificationJSON;
@ -151,6 +164,7 @@ export type ApiNotificationJSON =
export type ApiNotificationGroupJSON = export type ApiNotificationGroupJSON =
| SimpleNotificationGroupJSON | SimpleNotificationGroupJSON
| ReportNotificationGroupJSON | ReportNotificationGroupJSON
| ReportNoteNotificationGroupJSON
| AccountRelationshipSeveranceNotificationGroupJSON | AccountRelationshipSeveranceNotificationGroupJSON
| NotificationGroupWithStatusJSON | NotificationGroupWithStatusJSON
| ModerationWarningNotificationGroupJSON | ModerationWarningNotificationGroupJSON

View file

@ -14,3 +14,10 @@ export interface ApiReportJSON {
rule_ids: string[]; rule_ids: string[];
target_account: ApiAccountJSON; target_account: ApiAccountJSON;
} }
export interface ApiReportNoteJSON {
id: string;
content: string;
created_at: string;
report: ApiReportJSON;
}

View file

@ -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: <bdi>{domain ?? `@${account.acct}`}</bdi>,
reportId: reportNote.report.id,
};
const message = (
<FormattedMessage
id='notification.admin.report_note'
defaultMessage='{name} added a report note to Report #{reportId}'
values={values}
/>
);
return (
<a
href={`/admin/reports/${reportNote.report.id}#report_note_${reportNote.id}`}
target='_blank'
rel='noopener noreferrer'
className={classNames(
'notification-group notification-group--link notification-group--admin-report focusable',
{ 'notification-group--unread': unread },
)}
>
<div className='notification-group__icon'>
<Icon id='flag' icon={FlagIcon} />
</div>
<div className='notification-group__main'>
<div className='notification-group__main__header'>
<div className='notification-group__main__header__label'>
{message}
<RelativeTimestamp timestamp={reportNote.created_at} />
</div>
</div>
{reportNote.content.length > 0 && (
<div className='notification-group__embedded-status__content'>
{reportNote.content}
</div>
)}
</div>
</a>
);
};

View file

@ -8,6 +8,7 @@ import type { NotificationGroup as NotificationGroupModel } from 'mastodon/model
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { NotificationAdminReport } from './notification_admin_report'; import { NotificationAdminReport } from './notification_admin_report';
import { NotificationAdminReportNote } from './notification_admin_report_note';
import { NotificationAdminSignUp } from './notification_admin_sign_up'; import { NotificationAdminSignUp } from './notification_admin_sign_up';
import { NotificationAnnualReport } from './notification_annual_report'; import { NotificationAnnualReport } from './notification_annual_report';
import { NotificationFavourite } from './notification_favourite'; import { NotificationFavourite } from './notification_favourite';
@ -136,6 +137,14 @@ export const NotificationGroup: React.FC<{
/> />
); );
break; break;
case 'admin.report_note':
content = (
<NotificationAdminReportNote
unread={unread}
notification={notificationGroup}
/>
);
break;
case 'moderation_warning': case 'moderation_warning':
content = ( content = (
<NotificationModerationWarning <NotificationModerationWarning

View file

@ -8,7 +8,10 @@ import type {
NotificationType, NotificationType,
NotificationWithStatusType, NotificationWithStatusType,
} from 'mastodon/api_types/notifications'; } from 'mastodon/api_types/notifications';
import type { ApiReportJSON } from 'mastodon/api_types/reports'; import type {
ApiReportJSON,
ApiReportNoteJSON,
} from 'mastodon/api_types/reports';
// Maximum number of avatars displayed in a notification group // Maximum number of avatars displayed in a notification group
// This corresponds to the max lenght of `group.sampleAccountIds` // This corresponds to the max lenght of `group.sampleAccountIds`
@ -81,6 +84,14 @@ export interface NotificationGroupAdminReport
report: Report; report: Report;
} }
interface ReportNote extends Omit<ApiReportNoteJSON, 'report'> {
report: Report;
}
export interface NotificationGroupAdminReportNote
extends BaseNotification<'admin.report_note'> {
reportNote: ReportNote;
}
export type NotificationGroup = export type NotificationGroup =
| NotificationGroupFavourite | NotificationGroupFavourite
| NotificationGroupReblog | NotificationGroupReblog
@ -94,6 +105,7 @@ export type NotificationGroup =
| NotificationGroupSeveredRelationships | NotificationGroupSeveredRelationships
| NotificationGroupAdminSignUp | NotificationGroupAdminSignUp
| NotificationGroupAdminReport | NotificationGroupAdminReport
| NotificationGroupAdminReportNote
| NotificationGroupAnnualReport; | NotificationGroupAnnualReport;
function createReportFromJSON(reportJSON: ApiReportJSON): Report { 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( function createAccountWarningFromJSON(
warningJSON: ApiAccountWarningJSON, warningJSON: ApiAccountWarningJSON,
): AccountWarning { ): AccountWarning {
@ -153,6 +175,14 @@ export function createNotificationGroupFromJSON(
...groupWithoutTargetAccount, ...groupWithoutTargetAccount,
}; };
} }
case 'admin.report_note': {
const { report_note, ...groupWithoutReportNote } = group;
return {
reportNote: createReportNoteFromJSON(report_note),
sampleAccountIds,
...groupWithoutReportNote,
};
}
case 'severed_relationships': case 'severed_relationships':
return { return {
...group, ...group,
@ -207,6 +237,11 @@ export function createNotificationGroupFromNotificationJSON(
return { ...group, statusId: notification.status?.id }; return { ...group, statusId: notification.status?.id };
case 'admin.report': case 'admin.report':
return { ...group, report: createReportFromJSON(notification.report) }; return { ...group, report: createReportFromJSON(notification.report) };
case 'admin.report_note':
return {
...group,
reportNote: createReportNoteFromJSON(notification.report_note),
};
case 'severed_relationships': case 'severed_relationships':
return { return {
...group, ...group,

View file

@ -56,6 +56,7 @@ export const notificationToMap = notification => ImmutableMap({
created_at: notification.created_at, created_at: notification.created_at,
status: notification.status ? notification.status.id : null, status: notification.status ? notification.status.id : null,
report: notification.report ? fromJS(notification.report) : 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, event: notification.event ? fromJS(notification.event) : null,
moderation_warning: notification.moderation_warning ? fromJS(notification.moderation_warning) : null, moderation_warning: notification.moderation_warning ? fromJS(notification.moderation_warning) : null,
}); });

View file

@ -41,6 +41,7 @@ const initialState = ImmutableMap({
update: false, update: false,
'admin.sign_up': false, 'admin.sign_up': false,
'admin.report': false, 'admin.report': false,
'admin.report_note': false,
}), }),
quickFilter: ImmutableMap({ quickFilter: ImmutableMap({
@ -64,6 +65,7 @@ const initialState = ImmutableMap({
update: true, update: true,
'admin.sign_up': true, 'admin.sign_up': true,
'admin.report': true, 'admin.report': true,
'admin.report_note': true,
}), }),
sounds: ImmutableMap({ sounds: ImmutableMap({
@ -77,6 +79,7 @@ const initialState = ImmutableMap({
update: true, update: true,
'admin.sign_up': true, 'admin.sign_up': true,
'admin.report': true, 'admin.report': true,
'admin.report_note': true,
}), }),
group: ImmutableMap({ group: ImmutableMap({

View file

@ -21,6 +21,14 @@ class AdminMailer < ApplicationMailer
end end
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) def new_appeal(appeal)
@appeal = appeal @appeal = appeal

View file

@ -73,6 +73,9 @@ class Notification < ApplicationRecord
'admin.report': { 'admin.report': {
filterable: false, filterable: false,
}.freeze, }.freeze,
'admin.report_note': {
filterable: false,
}.freeze,
}.freeze }.freeze
TYPES = PROPERTIES.keys.freeze TYPES = PROPERTIES.keys.freeze
@ -85,6 +88,7 @@ class Notification < ApplicationRecord
poll: [poll: :status], poll: [poll: :status],
update: :status, update: :status,
'admin.report': [report: :target_account], 'admin.report': [report: :target_account],
'admin.report_note': [report_note: :report],
}.freeze }.freeze
belongs_to :account, optional: true belongs_to :account, optional: true
@ -99,6 +103,7 @@ class Notification < ApplicationRecord
belongs_to :favourite, inverse_of: :notification belongs_to :favourite, inverse_of: :notification
belongs_to :poll, inverse_of: false belongs_to :poll, inverse_of: false
belongs_to :report, 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_relationship_severance_event, inverse_of: false
belongs_to :account_warning, inverse_of: false belongs_to :account_warning, inverse_of: false
belongs_to :generated_annual_report, inverse_of: false belongs_to :generated_annual_report, inverse_of: false
@ -192,7 +197,7 @@ class Notification < ApplicationRecord
return unless new_record? return unless new_record?
case activity_type 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 self.from_account_id = activity&.account_id
when 'Mention' when 'Mention'
self.from_account_id = activity&.status&.account_id self.from_account_id = activity&.status&.account_id

View file

@ -49,6 +49,7 @@ class NotificationGroup < ActiveModelSerializers::Model
delegate :type, delegate :type,
:target_status, :target_status,
:report, :report,
:report_note,
:account_relationship_severance_event, :account_relationship_severance_event,
:account_warning, :account_warning,
:generated_annual_report, :generated_annual_report,

View file

@ -11,6 +11,7 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer
attribute :sample_account_ids attribute :sample_account_ids
attribute :status_id, if: :status_type? attribute :status_id, if: :status_type?
belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer 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_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 :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 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' object.type == :'admin.report'
end end
def report_note_type?
object.type == :'admin.report_note'
end
def relationship_severance_event? def relationship_severance_event?
object.type == :severed_relationships object.type == :severed_relationships
end end

View file

@ -9,6 +9,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer 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_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 :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' object.type == :'admin.report'
end end
def report_note_type?
object.type == :'admin.report_note'
end
def relationship_severance_event? def relationship_severance_event?
object.type == :severed_relationships object.type == :severed_relationships
end end

View file

@ -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

View file

@ -6,6 +6,7 @@ class NotifyService < BaseService
# TODO: the severed_relationships and annual_report types probably warrants email notifications # TODO: the severed_relationships and annual_report types probably warrants email notifications
NON_EMAIL_TYPES = %i( NON_EMAIL_TYPES = %i(
admin.report admin.report
admin.report_note
admin.sign_up admin.sign_up
update update
poll poll
@ -23,6 +24,7 @@ class NotifyService < BaseService
NON_FILTERABLE_TYPES = %i( NON_FILTERABLE_TYPES = %i(
admin.sign_up admin.sign_up
admin.report admin.report
admin.report_note
poll poll
update update
account_warning account_warning

View file

@ -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)) %>

View file

@ -1052,6 +1052,9 @@ en:
body: "%{reporter} has reported %{target}" body: "%{reporter} has reported %{target}"
body_remote: Someone from %{domain} has reported %{target} body_remote: Someone from %{domain} has reported %{target}
subject: New report for %{instance} (#%{id}) 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: new_software_updates:
body: New Mastodon versions have been released, you may want to update! body: New Mastodon versions have been released, you may want to update!
subject: New Mastodon versions are available for %{instance}! subject: New Mastodon versions are available for %{instance}!