diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 96cf628d69..84dfbeef3d 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 6a653675b8..81743a1dbe 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 = ( - + return ( + {contents} ); diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index 1cdb240867..61df79b464 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 { )} + + {isStaff && ( +
+ + +
+ + {showPushSettings && } + + +
+
+ )} ); } diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index 9198e9c9d8..0af71418c4 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 = ; + + return ( + +
+
+
+ +
+ + + + +
+ +
+
+ ); + } + 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 0000000000..3ce3eb9d32 --- /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 ( + + {report.get('id')} + + ); + } + + return ( +
+
+ +
+ +
+
+ ยท +
+ {intl.formatMessage(messages[report.get('category')])} +
+ +
+ {intl.formatMessage(messages.openReport)} +
+
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 5c984197fa..8bd5b3d782 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 250987be3f..7cd913493e 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" @@ -2625,6 +2629,10 @@ "defaultMessage": "{name} signed up", "id": "notification.admin.sign_up" }, + { + "defaultMessage": "{name} reported {target}", + "id": "notification.admin.report" + }, { "defaultMessage": "{name} has requested to follow you", "id": "notification.follow_request" @@ -2653,6 +2661,31 @@ ], "path": "app/javascript/mastodon/features/notifications/components/notifications_permission_banner.json" }, + { + "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": [ { diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index a0a1e1cdf6..9082c42025 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 b587b6d0f6..4b460bc10c 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 afffce9172..f9d3236e4a 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 3121774b3d..fbd25b605a 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 1ada1fcf7d..7e3ce3de24 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 ba94b54d12..bbc63c1c05 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 237f41d8e5..44b4726e43 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 69b81f6deb..137fc53dda 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 ecb88d653f..de68dfc6da 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 d251bb33f0..70212a6a75 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 cedcc9361c..5a0fc3da88 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: