about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-06-27 09:30:15 +0200
committerGitHub <noreply@github.com>2022-06-27 09:30:15 +0200
commit2936f42a14cfdca70d4a9653dab29382315945e7 (patch)
tree182a6660f1d8304ad893582732fb85db00e250aa
parent602f291da9f4fa157233b29dc6ac96e784203c98 (diff)
Add notifications for new reports (#18697)
-rw-r--r--app/javascript/mastodon/actions/notifications.js6
-rw-r--r--app/javascript/mastodon/components/icon_button.js12
-rw-r--r--app/javascript/mastodon/features/notifications/components/column_settings.js13
-rw-r--r--app/javascript/mastodon/features/notifications/components/notification.js30
-rw-r--r--app/javascript/mastodon/features/notifications/components/report.js62
-rw-r--r--app/javascript/mastodon/features/notifications/containers/notification_container.js4
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json33
-rw-r--r--app/javascript/mastodon/locales/en.json7
-rw-r--r--app/javascript/mastodon/reducers/notifications.js3
-rw-r--r--app/javascript/mastodon/reducers/settings.js3
-rw-r--r--app/javascript/mastodon/selectors/index.js17
-rw-r--r--app/javascript/styles/mastodon/components.scss39
-rw-r--r--app/models/notification.rb5
-rw-r--r--app/serializers/rest/admin/report_serializer.rb2
-rw-r--r--app/serializers/rest/notification_serializer.rb5
-rw-r--r--app/serializers/rest/report_serializer.rb5
-rw-r--r--app/services/report_service.rb4
-rw-r--r--config/locales/en.yml2
18 files changed, 235 insertions, 17 deletions
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 96cf628d6..84dfbeef3 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -91,6 +91,10 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
         dispatch(importFetchedStatus(notification.status));
       }
 
+      if (notification.report) {
+        dispatch(importFetchedAccount(notification.report.target_account));
+      }
+
       dispatch({
         type: NOTIFICATIONS_UPDATE,
         notification,
@@ -134,6 +138,7 @@ const excludeTypesFromFilter = filter => {
     'status',
     'update',
     'admin.sign_up',
+    'admin.report',
   ]);
 
   return allTypes.filterNot(item => item === filter).toJS();
@@ -179,6 +184,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
 
       dispatch(importFetchedAccounts(response.data.map(item => item.account)));
       dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
+      dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
 
       dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
       fetchRelatedRelationships(dispatch, response.data);
diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js
index 6a653675b..81743a1db 100644
--- a/app/javascript/mastodon/components/icon_button.js
+++ b/app/javascript/mastodon/components/icon_button.js
@@ -132,8 +132,16 @@ export default class IconButton extends React.PureComponent {
     );
 
     if (href) {
-      contents = (
-        <a href={href} target='_blank' rel='noopener noreferrer'>
+      return (
+        <a
+          href={href}
+          aria-label={title}
+          title={title}
+          target='_blank'
+          rel='noopener noreferrer'
+          className={classes}
+          style={style}
+        >
           {contents}
         </a>
       );
diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js
index 1cdb24086..61df79b46 100644
--- a/app/javascript/mastodon/features/notifications/components/column_settings.js
+++ b/app/javascript/mastodon/features/notifications/components/column_settings.js
@@ -178,6 +178,19 @@ export default class ColumnSettings extends React.PureComponent {
             </div>
           </div>
         )}
+
+        {isStaff && (
+          <div role='group' aria-labelledby='notifications-admin-report'>
+            <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span>
+
+            <div className='column-settings__row'>
+              <SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} />
+              {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'admin.report']} onChange={this.onPushChange} label={pushStr} />}
+              <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} />
+              <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
+            </div>
+          </div>
+        )}
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js
index 9198e9c9d..0af71418c 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.js
+++ b/app/javascript/mastodon/features/notifications/components/notification.js
@@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { me } from 'mastodon/initial_state';
 import StatusContainer from 'mastodon/containers/status_container';
 import AccountContainer from 'mastodon/containers/account_container';
+import Report from './report';
 import FollowRequestContainer from '../containers/follow_request_container';
 import Icon from 'mastodon/components/icon';
 import Permalink from 'mastodon/components/permalink';
@@ -21,6 +22,7 @@ const messages = defineMessages({
   status: { id: 'notification.status', defaultMessage: '{name} just posted' },
   update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
   adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
+  adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
 });
 
 const notificationForScreenReader = (intl, message, timestamp) => {
@@ -367,6 +369,32 @@ class Notification extends ImmutablePureComponent {
     );
   }
 
+  renderAdminReport (notification, account, link) {
+    const { intl, unread, report } = this.props;
+
+    const targetAccount = report.get('target_account');
+    const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
+    const targetLink = <bdi><Permalink className='notification__display-name' href={targetAccount.get('url')} title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
+
+    return (
+      <HotKeys handlers={this.getHandlers()}>
+        <div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: account.get('acct'), target: notification.getIn(['report', 'target_account', 'acct']) }), notification.get('created_at'))}>
+          <div className='notification__message'>
+            <div className='notification__favourite-icon-wrapper'>
+              <Icon id='flag' fixedWidth />
+            </div>
+
+            <span title={notification.get('created_at')}>
+              <FormattedMessage id='notification.admin.report' defaultMessage='{name} reported {target}' values={{ name: link, target: targetLink }} />
+            </span>
+          </div>
+
+          <Report account={account} report={notification.get('report')} hidden={this.props.hidden} />
+        </div>
+      </HotKeys>
+    );
+  }
+
   render () {
     const { notification } = this.props;
     const account          = notification.get('account');
@@ -392,6 +420,8 @@ class Notification extends ImmutablePureComponent {
       return this.renderPoll(notification, account);
     case 'admin.sign_up':
       return this.renderAdminSignUp(notification, account, link);
+    case 'admin.report':
+      return this.renderAdminReport(notification, account, link);
     }
 
     return null;
diff --git a/app/javascript/mastodon/features/notifications/components/report.js b/app/javascript/mastodon/features/notifications/components/report.js
new file mode 100644
index 000000000..3ce3eb9d3
--- /dev/null
+++ b/app/javascript/mastodon/features/notifications/components/report.js
@@ -0,0 +1,62 @@
+import React, { Fragment } from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import AvatarOverlay from 'mastodon/components/avatar_overlay';
+import RelativeTimestamp from 'mastodon/components/relative_timestamp';
+
+const messages = defineMessages({
+  openReport: { id: 'report_notification.open', defaultMessage: 'Open report' },
+  other: { id: 'report_notification.categories.other', defaultMessage: 'Other' },
+  spam: { id: 'report_notification.categories.spam', defaultMessage: 'Spam' },
+  violation: { id: 'report_notification.categories.violation', defaultMessage: 'Rule violation' },
+});
+
+export default @injectIntl
+class Report extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.map.isRequired,
+    report: ImmutablePropTypes.map.isRequired,
+    hidden: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+  };
+
+  render () {
+    const { intl, hidden, report, account } = this.props;
+
+    if (!report) {
+      return null;
+    }
+
+    if (hidden) {
+      return (
+        <Fragment>
+          {report.get('id')}
+        </Fragment>
+      );
+    }
+
+    return (
+      <div className='notification__report'>
+        <div className='notification__report__avatar'>
+          <AvatarOverlay account={report.get('target_account')} friend={account} />
+        </div>
+
+        <div className='notification__report__details'>
+          <div>
+            <RelativeTimestamp timestamp={report.get('created_at')} short={false} /> · <FormattedMessage id='report_notification.attached_statuses' defaultMessage='{count, plural, one {{count} post} other {{count} posts}} attached' values={{ count: report.get('status_ids').size }} />
+            <br />
+            <strong>{intl.formatMessage(messages[report.get('category')])}</strong>
+          </div>
+
+          <div className='notification__report__actions'>
+            <a href={`/admin/reports/${report.get('id')}`} className='button' target='_blank' rel='noopener noreferrer'>{intl.formatMessage(messages.openReport)}</a>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js
index 5c984197f..8bd5b3d78 100644
--- a/app/javascript/mastodon/features/notifications/containers/notification_container.js
+++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js
@@ -1,5 +1,5 @@
 import { connect } from 'react-redux';
-import { makeGetNotification, makeGetStatus } from '../../../selectors';
+import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
 import Notification from '../components/notification';
 import { initBoostModal } from '../../../actions/boosts';
 import { mentionCompose } from '../../../actions/compose';
@@ -18,12 +18,14 @@ import { boostModal } from '../../../initial_state';
 const makeMapStateToProps = () => {
   const getNotification = makeGetNotification();
   const getStatus = makeGetStatus();
+  const getReport = makeGetReport();
 
   const mapStateToProps = (state, props) => {
     const notification = getNotification(state, props.notification, props.accountId);
     return {
       notification: notification,
       status: notification.get('status') ? getStatus(state, { id: notification.get('status') }) : null,
+      report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
     };
   };
 
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 250987be3..7cd913493 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -2532,6 +2532,10 @@
       {
         "defaultMessage": "New sign-ups:",
         "id": "notifications.column_settings.admin.sign_up"
+      },
+      {
+        "defaultMessage": "New reports:",
+        "id": "notifications.column_settings.admin.report"
       }
     ],
     "path": "app/javascript/mastodon/features/notifications/components/column_settings.json"
@@ -2626,6 +2630,10 @@
         "id": "notification.admin.sign_up"
       },
       {
+        "defaultMessage": "{name} reported {target}",
+        "id": "notification.admin.report"
+      },
+      {
         "defaultMessage": "{name} has requested to follow you",
         "id": "notification.follow_request"
       }
@@ -2656,6 +2664,31 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Open report",
+        "id": "report_notification.open"
+      },
+      {
+        "defaultMessage": "Other",
+        "id": "report_notification.categories.other"
+      },
+      {
+        "defaultMessage": "Spam",
+        "id": "report_notification.categories.spam"
+      },
+      {
+        "defaultMessage": "Rule violation",
+        "id": "report_notification.categories.violation"
+      },
+      {
+        "defaultMessage": "{count, plural, one {{count} post} other {{count} posts}} attached",
+        "id": "report_notification.attached_statuses"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/notifications/components/report.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Are you sure you want to permanently clear all your notifications?",
         "id": "notifications.clear_confirmation"
       },
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index a0a1e1cdf..9082c4202 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -314,6 +314,7 @@
   "navigation_bar.preferences": "Preferences",
   "navigation_bar.public_timeline": "Federated timeline",
   "navigation_bar.security": "Security",
+  "notification.admin.report": "{name} reported {target}",
   "notification.admin.sign_up": "{name} signed up",
   "notification.favourite": "{name} favourited your post",
   "notification.follow": "{name} followed you",
@@ -326,6 +327,7 @@
   "notification.update": "{name} edited a post",
   "notifications.clear": "Clear notifications",
   "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+  "notifications.column_settings.admin.report": "New reports:",
   "notifications.column_settings.admin.sign_up": "New sign-ups:",
   "notifications.column_settings.alert": "Desktop notifications",
   "notifications.column_settings.favourite": "Favourites:",
@@ -431,6 +433,11 @@
   "report.thanks.title_actionable": "Thanks for reporting, we'll look into this.",
   "report.unfollow": "Unfollow @{name}",
   "report.unfollow_explanation": "You are following this account. To not see their posts in your home feed anymore, unfollow them.",
+  "report_notification.attached_statuses": "{count, plural, one {{count} post} other {{count} posts}} attached",
+  "report_notification.categories.other": "Other",
+  "report_notification.categories.spam": "Spam",
+  "report_notification.categories.violation": "Rule violation",
+  "report_notification.open": "Open report",
   "search.placeholder": "Search",
   "search_popout.search_format": "Advanced search format",
   "search_popout.tips.full_text": "Simple text returns posts you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index b587b6d0f..4b460bc10 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -28,7 +28,7 @@ import {
 } from '../actions/app';
 import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
 import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
-import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import compareId from '../compare_id';
 
 const initialState = ImmutableMap({
@@ -52,6 +52,7 @@ const notificationToMap = notification => ImmutableMap({
   account: notification.account.id,
   created_at: notification.created_at,
   status: notification.status ? notification.status.id : null,
+  report: notification.report ? fromJS(notification.report) : null,
 });
 
 const normalizeNotification = (state, notification, usePendingItems) => {
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index afffce917..f9d3236e4 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -39,6 +39,7 @@ const initialState = ImmutableMap({
       status: false,
       update: false,
       'admin.sign_up': false,
+      'admin.report': false,
     }),
 
     quickFilter: ImmutableMap({
@@ -60,6 +61,7 @@ const initialState = ImmutableMap({
       status: true,
       update: true,
       'admin.sign_up': true,
+      'admin.report': true,
     }),
 
     sounds: ImmutableMap({
@@ -72,6 +74,7 @@ const initialState = ImmutableMap({
       status: true,
       update: true,
       'admin.sign_up': true,
+      'admin.report': true,
     }),
   }),
 
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 3121774b3..fbd25b605 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -152,14 +152,15 @@ export const getAlerts = createSelector([getAlertsBase], (base) => {
   return arr;
 });
 
-export const makeGetNotification = () => {
-  return createSelector([
-    (_, base)             => base,
-    (state, _, accountId) => state.getIn(['accounts', accountId]),
-  ], (base, account) => {
-    return base.set('account', account);
-  });
-};
+export const makeGetNotification = () => createSelector([
+  (_, base)             => base,
+  (state, _, accountId) => state.getIn(['accounts', accountId]),
+], (base, account) => base.set('account', account));
+
+export const makeGetReport = () => createSelector([
+  (_, base) => base,
+  (state, _, targetAccountId) => state.getIn(['accounts', targetAccountId]),
+], (base, targetAccount) => base.set('target_account', targetAccount));
 
 export const getAccountGallery = createSelector([
   (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 1ada1fcf7..7e3ce3de2 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1355,6 +1355,8 @@ a .account__avatar {
 .account__avatar-overlay {
   @include avatar-size(48px);
 
+  position: relative;
+
   &-base {
     @include avatar-radius;
     @include avatar-size(36px);
@@ -1620,6 +1622,33 @@ a.account__display-name {
   }
 }
 
+.notification__report {
+  padding: 8px 10px;
+  padding-left: 68px;
+  position: relative;
+  border-bottom: 1px solid lighten($ui-base-color, 8%);
+  min-height: 54px;
+
+  &__details {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    color: $darker-text-color;
+    font-size: 15px;
+    line-height: 22px;
+
+    strong {
+      font-weight: 500;
+    }
+  }
+
+  &__avatar {
+    position: absolute;
+    left: 10px;
+    top: 10px;
+  }
+}
+
 .notification__message {
   margin: 0 10px 0 68px;
   padding: 8px 0 0;
@@ -2360,6 +2389,16 @@ a.account__display-name {
       padding-top: 15px;
     }
 
+    .notification__report {
+      padding: 15px 15px 15px (48px + 15px * 2);
+      min-height: 48px + 2px;
+
+      &__avatar {
+        left: 15px;
+        top: 17px;
+      }
+    }
+
     .status {
       padding: 15px 15px 15px (48px + 15px * 2);
       min-height: 48px + 2px;
diff --git a/app/models/notification.rb b/app/models/notification.rb
index ba94b54d1..bbc63c1c0 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -37,6 +37,7 @@ class Notification < ApplicationRecord
     poll
     update
     admin.sign_up
+    admin.report
   ).freeze
 
   TARGET_STATUS_INCLUDES_BY_TYPE = {
@@ -46,6 +47,7 @@ class Notification < ApplicationRecord
     favourite: [favourite: :status],
     poll: [poll: :status],
     update: :status,
+    'admin.report': [report: :target_account],
   }.freeze
 
   belongs_to :account, optional: true
@@ -58,6 +60,7 @@ class Notification < ApplicationRecord
   belongs_to :follow_request, foreign_key: 'activity_id', optional: true
   belongs_to :favourite,      foreign_key: 'activity_id', optional: true
   belongs_to :poll,           foreign_key: 'activity_id', optional: true
+  belongs_to :report,         foreign_key: 'activity_id', optional: true
 
   validates :type, inclusion: { in: TYPES }
 
@@ -146,7 +149,7 @@ class Notification < ApplicationRecord
     return unless new_record?
 
     case activity_type
-    when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll'
+    when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report'
       self.from_account_id = activity&.account_id
     when 'Mention'
       self.from_account_id = activity&.status&.account_id
diff --git a/app/serializers/rest/admin/report_serializer.rb b/app/serializers/rest/admin/report_serializer.rb
index 237f41d8e..44b4726e4 100644
--- a/app/serializers/rest/admin/report_serializer.rb
+++ b/app/serializers/rest/admin/report_serializer.rb
@@ -2,7 +2,7 @@
 
 class REST::Admin::ReportSerializer < ActiveModel::Serializer
   attributes :id, :action_taken, :action_taken_at, :category, :comment,
-             :created_at, :updated_at
+             :forwarded, :created_at, :updated_at
 
   has_one :account, serializer: REST::Admin::AccountSerializer
   has_one :target_account, serializer: REST::Admin::AccountSerializer
diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb
index 69b81f6de..137fc53dd 100644
--- a/app/serializers/rest/notification_serializer.rb
+++ b/app/serializers/rest/notification_serializer.rb
@@ -5,6 +5,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
 
   def id
     object.id.to_s
@@ -13,4 +14,8 @@ class REST::NotificationSerializer < ActiveModel::Serializer
   def status_type?
     [:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type)
   end
+
+  def report_type?
+    object.type == :'admin.report'
+  end
 end
diff --git a/app/serializers/rest/report_serializer.rb b/app/serializers/rest/report_serializer.rb
index ecb88d653..de68dfc6d 100644
--- a/app/serializers/rest/report_serializer.rb
+++ b/app/serializers/rest/report_serializer.rb
@@ -1,7 +1,10 @@
 # frozen_string_literal: true
 
 class REST::ReportSerializer < ActiveModel::Serializer
-  attributes :id, :action_taken
+  attributes :id, :action_taken, :action_taken_at, :category, :comment,
+             :forwarded, :created_at, :status_ids, :rule_ids
+
+  has_one :target_account, serializer: REST::AccountSerializer
 
   def id
     object.id.to_s
diff --git a/app/services/report_service.rb b/app/services/report_service.rb
index d251bb33f..70212a6a7 100644
--- a/app/services/report_service.rb
+++ b/app/services/report_service.rb
@@ -39,8 +39,8 @@ class ReportService < BaseService
     return if @report.unresolved_siblings?
 
     User.staff.includes(:account).each do |u|
-      next unless u.allows_report_emails?
-      AdminMailer.new_report(u.account, @report).deliver_later
+      LocalNotificationWorker.perform_async(u.account_id, @report.id, 'Report', 'admin.report')
+      AdminMailer.new_report(u.account, @report).deliver_later if u.allows_report_emails?
     end
   end
 
diff --git a/config/locales/en.yml b/config/locales/en.yml
index cedcc9361..5a0fc3da8 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1251,6 +1251,8 @@ en:
     copy_account_note_text: 'This user moved from %{acct}, here were your previous notes about them:'
   notification_mailer:
     admin:
+      report:
+        subject: "%{name} submitted a report"
       sign_up:
         subject: "%{name} signed up"
     digest: