diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 6f6a3235ea..c8b1178d77 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -133,9 +133,6 @@ export function updateNotifications(notification, intlMessages, intlLocale) { if (notification.status) { dispatch(importFetchedStatus(notification.status)); } - if (notification.statuses) { - dispatch(importFetchedStatuses(notification.statuses)); - } if (notification.report) { dispatch(importFetchedAccount(notification.report.target_account)); @@ -182,7 +179,6 @@ const excludeTypesFromFilter = filter => { 'status', 'list_status', 'update', - 'account_warning', 'admin.sign_up', 'admin.report', ]); @@ -241,10 +237,7 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses( - response.data.map(item => item.status).filter(status => !!status) - .concat(response.data.flatMap(item => item.statuses || [])) - )); + 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)); diff --git a/app/javascript/mastodon/features/explore/components/card.jsx b/app/javascript/mastodon/features/explore/components/card.jsx new file mode 100644 index 0000000000..316203060a --- /dev/null +++ b/app/javascript/mastodon/features/explore/components/card.jsx @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import { useCallback } from 'react'; + +import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { useDispatch, useSelector } from 'react-redux'; + +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { followAccount, unfollowAccount } from 'mastodon/actions/accounts'; +import { dismissSuggestion } from 'mastodon/actions/suggestions'; +import { Avatar } from 'mastodon/components/avatar'; +import { Button } from 'mastodon/components/button'; +import { DisplayName } from 'mastodon/components/display_name'; +import { IconButton } from 'mastodon/components/icon_button'; +import { domain } from 'mastodon/initial_state'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" }, +}); + +export const Card = ({ id, source }) => { + const intl = useIntl(); + const account = useSelector(state => state.getIn(['accounts', id])); + const relationship = useSelector(state => state.getIn(['relationships', id])); + const dispatch = useDispatch(); + const following = relationship?.get('following') ?? relationship?.get('requested'); + + const handleFollow = useCallback(() => { + if (following) { + dispatch(unfollowAccount(id)); + } else { + dispatch(followAccount(id)); + } + }, [id, following, dispatch]); + + const handleDismiss = useCallback(() => { + dispatch(dismissSuggestion(id)); + }, [id, dispatch]); + + let label; + + switch (source) { + case 'friends_of_friends': + label = ; + break; + case 'similar_to_recently_followed': + label = ; + break; + case 'featured': + label = ; + break; + case 'most_followed': + label = ; + break; + case 'most_interactions': + label = ; + break; + } + + return ( +
+
+ {label} +
+ +
+ + +
+
+ + +
+
+
+
+ ); +}; + +Card.propTypes = { + id: PropTypes.string.isRequired, + source: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']), +}; diff --git a/app/javascript/mastodon/features/explore/suggestions.jsx b/app/javascript/mastodon/features/explore/suggestions.jsx index ba33c4d081..101ec0d195 100644 --- a/app/javascript/mastodon/features/explore/suggestions.jsx +++ b/app/javascript/mastodon/features/explore/suggestions.jsx @@ -10,9 +10,10 @@ import { connect } from 'react-redux'; import { fetchSuggestions } from 'mastodon/actions/suggestions'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; -import AccountCard from 'mastodon/features/directory/components/account_card'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; +import { Card } from './components/card'; + const mapStateToProps = state => ({ suggestions: state.getIn(['suggestions', 'items']), isLoading: state.getIn(['suggestions', 'isLoading']), @@ -54,7 +55,11 @@ class Suggestions extends PureComponent { return (
{isLoading ? : suggestions.map(suggestion => ( - + ))}
); diff --git a/app/javascript/mastodon/features/notifications/components/moderation_warning.tsx b/app/javascript/mastodon/features/notifications/components/moderation_warning.tsx new file mode 100644 index 0000000000..acd1f3c028 --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/moderation_warning.tsx @@ -0,0 +1,83 @@ +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import WarningIcon from '@/material-icons/400-24px/warning-fill.svg?react'; +import { Icon } from 'mastodon/components/icon'; + +// This needs to be kept in sync with app/models/account_warning.rb +const messages = defineMessages({ + none: { + id: 'notification.moderation_warning.action_none', + defaultMessage: 'Your account has received a moderation warning.', + }, + disable: { + id: 'notification.moderation_warning.action_disable', + defaultMessage: 'Your account has been disabled.', + }, + force_cw: { + id: 'notification.moderation_warning.action_force_cw', + defaultMessage: 'Some of your posts have been added content-warning text.', + }, + mark_statuses_as_sensitive: { + id: 'notification.moderation_warning.action_mark_statuses_as_sensitive', + defaultMessage: 'Some of your posts have been marked as sensitive.', + }, + delete_statuses: { + id: 'notification.moderation_warning.action_delete_statuses', + defaultMessage: 'Some of your posts have been removed.', + }, + sensitive: { + id: 'notification.moderation_warning.action_sensitive', + defaultMessage: 'Your posts will be marked as sensitive from now on.', + }, + silence: { + id: 'notification.moderation_warning.action_silence', + defaultMessage: 'Your account has been limited.', + }, + suspend: { + id: 'notification.moderation_warning.action_suspend', + defaultMessage: 'Your account has been suspended.', + }, +}); + +interface Props { + action: + | 'none' + | 'disable' + | 'force_cw' + | 'mark_statuses_as_sensitive' + | 'delete_statuses' + | 'sensitive' + | 'silence' + | 'suspend'; + id: string; + hidden: boolean; +} + +export const ModerationWarning: React.FC = ({ action, id, hidden }) => { + const intl = useIntl(); + + if (hidden) { + return null; + } + + return ( + + + +
+

{intl.formatMessage(messages[action])}

+ + + +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index a3cb8c3934..8db95ce526 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -10,7 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; -import DangerousIcon from '@/material-icons/400-24px/dangerous-fill.svg?react'; import EditIcon from '@/material-icons/400-24px/edit.svg?react'; import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; @@ -30,6 +29,7 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import FollowRequestContainer from '../containers/follow_request_container'; +import { ModerationWarning } from './moderation_warning'; import { RelationshipsSeveranceEvent } from './relationships_severance_event'; import Report from './report'; @@ -44,18 +44,10 @@ const messages = defineMessages({ listStatus: { id: 'notification.list_status', defaultMessage: '{name} post is added on {listName}' }, statusReference: { id: 'notification.status_reference', defaultMessage: '{name} refered your post' }, update: { id: 'notification.update', defaultMessage: '{name} edited a post' }, - warning: { id: 'notification.warning', defaultMessage: 'You have been warned and did something. Check your mailbox' }, - warning_none: { id: 'notification.warning.none', defaultMessage: 'You have been warned. Check your mailbox.' }, - warning_disable: { id: 'notification.warning.disable', defaultMessage: 'You have been warned and disabled account. Check your mailbox.' }, - warning_force_cw: { id: 'notification.warning.force_cw', defaultMessage: 'You have been warned and one or more statuses have been added warning messages. Check your mailbox.' }, - warning_mark_statuses_as_sensitive: { id: 'notification.warning.mark_statuses_as_sensitive', defaultMessage: 'You have been warned and some statuses have been marked as sensitive. Check your mailbox.' }, - warning_delete_statuses: { id: 'notification.warning.delete_statuses', defaultMessage: 'You have been warned and one or more statuses have been deleted. Check your mailbox.' }, - warning_sensitive: { id: 'notification.warning.sensitive', defaultMessage: 'You have been warned and your account has been marked as sensitive. Check your mailbox.' }, - warning_silence: { id: 'notification.warning.silence', defaultMessage: 'You have been warned and your account has been silenced. Check your mailbox.' }, - warning_suspend: { id: 'notification.warning.suspend', defaultMessage: 'You have been warned and your account has been suspended. Check your mailbox.' }, adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' }, adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' }, relationshipsSevered: { id: 'notification.relationships_severance_event', defaultMessage: 'Lost connections with {name}' }, + moderationWarning: { id: 'notification.moderation_warning', defaultMessage: 'Your have received a moderation warning' }, }); const notificationForScreenReader = (intl, message, timestamp) => { @@ -482,47 +474,6 @@ class Notification extends ImmutablePureComponent { ); } - - renderWarning (notification) { - const { intl, unread } = this.props; - - const preMessageKey = `warning_${notification.getIn(['account_warning', 'action'])}`; - const messageKey = Object.keys(messages).includes(preMessageKey) ? preMessageKey : 'warning'; - const text = notification.getIn(['account_warning', 'text']); - - return ( - -
-
-
- -
- - - {intl.formatMessage(messages[messageKey])} - -
- - {text &&
{text}
} - - {notification.get('statuses').map((status_id) => ( -
-
- ); - } renderRelationshipsSevered (notification) { const { intl, unread, hidden } = this.props; @@ -547,6 +498,27 @@ class Notification extends ImmutablePureComponent { ); } + renderModerationWarning (notification) { + const { intl, unread, hidden } = this.props; + const warning = notification.get('moderation_warning'); + + if (!warning) { + return null; + } + + return ( + +
+
+
+ ); + } + renderAdminSignUp (notification, account, link) { const { intl, unread } = this.props; @@ -626,10 +598,10 @@ class Notification extends ImmutablePureComponent { return this.renderUpdate(notification, link); case 'poll': return this.renderPoll(notification, account); - case 'warning': - return this.renderWarning(notification); case 'severed_relationships': return this.renderRelationshipsSevered(notification); + case 'moderation_warning': + return this.renderModerationWarning(notification); case 'admin.sign_up': return this.renderAdminSignUp(notification, account, link); case 'admin.report': diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 8068b0cd00..ebefc09039 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -411,6 +411,8 @@ "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_suggestions.curated_suggestion": "Staff pick", "follow_suggestions.dismiss": "Don't show again", + "follow_suggestions.featured_longer": "Hand-picked by the {domain} team", + "follow_suggestions.friends_of_friends_longer": "Popular among people you follow", "follow_suggestions.hints.featured": "This profile has been hand-picked by the {domain} team.", "follow_suggestions.hints.friends_of_friends": "This profile is popular among the people you follow.", "follow_suggestions.hints.most_followed": "This profile is one of the most followed on {domain}.", @@ -418,6 +420,8 @@ "follow_suggestions.hints.similar_to_recently_followed": "This profile is similar to the profiles you have most recently followed.", "follow_suggestions.personalized_suggestion": "Personalized suggestion", "follow_suggestions.popular_suggestion": "Popular suggestion", + "follow_suggestions.popular_suggestion_longer": "Popular on {domain}", + "follow_suggestions.similar_to_recently_followed_longer": "Similar to profiles you recently followed", "follow_suggestions.view_all": "View all", "follow_suggestions.who_to_follow": "Who to follow", "followed_tags": "Followed hashtags", @@ -586,6 +590,16 @@ "notification.follow_request": "{name} has requested to follow you", "notification.list_status": "{name} post is added to {listName}", "notification.mention": "{name} mentioned you", + "notification.moderation-warning.learn_more": "Learn more", + "notification.moderation_warning": "Your have received a moderation warning", + "notification.moderation_warning.action_delete_statuses": "Some of your posts have been removed.", + "notification.moderation_warning.action_disable": "Your account has been disabled.", + "notification.moderation_warning.action_force_cw": "Some of your posts have been added content-warning text.", + "notification.moderation_warning.action_mark_statuses_as_sensitive": "Some of your posts have been marked as sensitive.", + "notification.moderation_warning.action_none": "Your account has received a moderation warning.", + "notification.moderation_warning.action_sensitive": "Your posts will be marked as sensitive from now on.", + "notification.moderation_warning.action_silence": "Your account has been limited.", + "notification.moderation_warning.action_suspend": "Your account has been suspended.", "notification.own_poll": "Your poll has ended", "notification.poll": "A poll you have voted in has ended", "notification.reblog": "{name} boosted your post", @@ -597,15 +611,6 @@ "notification.status": "{name} just posted", "notification.status_reference": "{name} quoted your post", "notification.update": "{name} edited a post", - "notification.warning": "You have been warned and did something. Check your mailbox", - "notification.warning.delete_statuses": "You have been warned and one or more statuses have been deleted. Check your mailbox.", - "notification.warning.disable": "You have been warned and disabled account. Check your mailbox.", - "notification.warning.force_cw": "You have been warned and one or more statuses have been added warning messages. Check your mailbox.", - "notification.warning.mark_statuses_as_sensitive": "You have been warned and some statuses have been marked as sensitive. Check your mailbox.", - "notification.warning.none": "You have been warned. Check your mailbox.", - "notification.warning.sensitive": "You have been warned and your account has been marked as sensitive. Check your mailbox.", - "notification.warning.silence": "You have been warned and your account has been silenced. Check your mailbox.", - "notification.warning.suspend": "You have been warned and your account has been suspended. Check your mailbox.", "notification_requests.accept": "Accept", "notification_requests.dismiss": "Dismiss", "notification_requests.notifications_from": "Notifications from {name}", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index a381cd87cd..683b597602 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -585,15 +585,6 @@ "notification.status": "{name}さんが投稿しました", "notification.status_reference": "{name}さんがあなたの投稿を引用しました", "notification.update": "{name}さんが投稿を編集しました", - "notification.warning": "あなたは警告を出され、処分が実行されました。詳細はメールをご確認ください", - "notification.warning.none": "あなたは警告を出されました。詳細はメールをご確認ください。", - "notification.warning.delete_statuses": "あなたは警告を出され、1つまたは複数の投稿が削除されました。詳細はメールをご確認ください。", - "notification.warning.disable": "あなたは警告を出され、アカウントが無効化されました。詳細はメールをご確認ください。", - "notification.warning.force_cw": "あなたは警告を出され、投稿に警告文が追加されました。詳細はメールをご確認ください。", - "notification.warning.mark_statuses_as_sensitive": "あなたは警告を出され、投稿が閲覧注意としてマークされました。詳細はメールをご確認ください。", - "notification.warning.sensitive": "あなたは警告を出され、アカウントがセンシティブ指定されました。詳細はメールをご確認ください。", - "notification.warning.silence": "あなたは警告を出され、サイレンスされました。詳細はメールをご確認ください。", - "notification.warning.suspended": "あなたは警告を出され、サスペンドされました。詳細はメールをご確認ください。", "notification_requests.accept": "受け入れる", "notification_requests.dismiss": "無視", "notification_requests.notifications_from": "{name}からの通知", diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 1d0bca6103..14a3269127 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -55,11 +55,10 @@ export const notificationToMap = notification => ImmutableMap({ created_at: notification.created_at, emoji_reaction: ImmutableMap(notification.emoji_reaction), status: notification.status ? notification.status.id : null, - statuses: notification.statuses ? notification.statuses.map((status) => status.id) : null, list: notification.list ? ImmutableMap(notification.list) : null, report: notification.report ? fromJS(notification.report) : null, - account_warning: notification.account_warning ? ImmutableMap(notification.account_warning) : null, event: notification.event ? fromJS(notification.event) : null, + moderation_warning: notification.moderation_warning ? fromJS(notification.moderation_warning) : null, }); const normalizeNotification = (state, notification, usePendingItems) => { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0d6915e3be..7de31d12cc 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2086,11 +2086,10 @@ a .account__avatar { display: flex; align-items: center; gap: 8px; +} - .fa-times { - color: $ui-secondary-color; - } - +.account__relationship, +.explore__suggestions__card { .icon-button { border: 1px solid var(--background-border-color); border-radius: 4px; @@ -2256,7 +2255,8 @@ a.account__display-name { } } -.notification__relationships-severance-event { +.notification__relationships-severance-event, +.notification__moderation-warning { display: flex; gap: 16px; color: $secondary-text-color; @@ -3062,6 +3062,75 @@ $ui-header-logo-wordmark-width: 99px; display: none; } +.explore__suggestions__card { + padding: 12px 16px; + gap: 8px; + display: flex; + flex-direction: column; + border-bottom: 1px solid var(--background-border-color); + + &:last-child { + border-bottom: 0; + } + + &__source { + padding-inline-start: 60px; + font-size: 13px; + line-height: 16px; + color: $dark-text-color; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + &__body { + display: flex; + gap: 12px; + align-items: center; + + &__main { + flex: 1 1 auto; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + + &__name-button { + display: flex; + align-items: center; + gap: 8px; + + &__name { + display: block; + color: inherit; + text-decoration: none; + flex: 1 1 auto; + min-width: 0; + } + + .button { + min-width: 80px; + } + + .display-name { + font-size: 15px; + line-height: 20px; + color: $secondary-text-color; + + strong { + font-weight: 700; + } + + &__account { + color: $darker-text-color; + display: block; + } + } + } + } + } +} + @media screen and (max-width: $no-gap-breakpoint - 1px) { .columns-area__panels__pane--compositional { display: none; @@ -7599,10 +7668,11 @@ a.status-card { content: ''; position: absolute; bottom: -1px; - left: 0; - width: 100%; + left: 50%; + transform: translateX(-50%); + width: 40px; height: 3px; - border-radius: 4px; + border-radius: 4px 4px 0 0; background: $highlight-text-color; } } diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index c85d733619..3700ce4cd6 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -52,9 +52,8 @@ class Admin::AccountAction process_reports! end - process_email! + process_notification! process_queue! - notify! end def report @@ -108,10 +107,6 @@ class Admin::AccountAction log_action(:create, @warning) if @warning.text.present? && type == 'none' end - def notify! - LocalNotificationWorker.perform_async(target_account.id, @warning.id, 'AccountWarning', 'warning') if @warning && %w(none sensitive silence).include?(type) - end - def process_reports! # If we're doing "mark as resolved" on a single report, # then we want to keep other reports open in case they @@ -163,8 +158,11 @@ class Admin::AccountAction queue_suspension_worker! if type == 'suspend' end - def process_email! - UserMailer.warning(target_account.user, warning).deliver_later! if warnable? + def process_notification! + return unless warnable? + + UserMailer.warning(target_account.user, warning).deliver_later! + LocalNotificationWorker.perform_async(target_account.id, warning.id, 'AccountWarning', 'moderation_warning') end def warnable? diff --git a/app/models/admin/status_batch_action.rb b/app/models/admin/status_batch_action.rb index d295b01e28..832ddc56ac 100644 --- a/app/models/admin/status_batch_action.rb +++ b/app/models/admin/status_batch_action.rb @@ -17,7 +17,6 @@ class Admin::StatusBatchAction def save! process_action! - notify! end private @@ -68,7 +67,8 @@ class Admin::StatusBatchAction statuses.each { |status| Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) } unless target_account.local? end - UserMailer.warning(target_account.user, @warning).deliver_later! if warnable? + process_notification! + RemovalWorker.push_bulk(status_ids) { |status_id| [status_id, { 'preserve' => target_account.local?, 'immediate' => !target_account.local? }] } end @@ -104,7 +104,7 @@ class Admin::StatusBatchAction text: text ) - UserMailer.warning(target_account.user, @warning).deliver_later! if warnable? + process_notification! end def handle_force_cw! @@ -140,7 +140,7 @@ class Admin::StatusBatchAction text: text ) - UserMailer.warning(target_account.user, @warning).deliver_later! if warnable? + process_notification! end def handle_report! @@ -158,10 +158,6 @@ class Admin::StatusBatchAction report.save! end - def notify! - LocalNotificationWorker.perform_async(target_account.id, @warning.id, 'AccountWarning', 'warning') if warnable? && @warning - end - def report @report ||= Report.find(report_id) if report_id.present? end @@ -170,6 +166,13 @@ class Admin::StatusBatchAction !report.nil? end + def process_notification! + return unless warnable? + + UserMailer.warning(target_account.user, @warning).deliver_later! + LocalNotificationWorker.perform_async(target_account.id, @warning.id, 'AccountWarning', 'moderation_warning') + end + def warnable? send_email_notification && target_account.local? end diff --git a/app/models/notification.rb b/app/models/notification.rb index 3aa1e27412..d14b54a087 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -30,7 +30,7 @@ class Notification < ApplicationRecord 'EmojiReaction' => :emoji_reaction, 'StatusReference' => :status_reference, 'Poll' => :poll, - 'AccountWarning' => :warning, + 'AccountWarning' => :moderation_warning, }.freeze PROPERTIES = { @@ -70,10 +70,10 @@ class Notification < ApplicationRecord update: { filterable: false, }.freeze, - warning: { + severed_relationships: { filterable: false, }.freeze, - severed_relationships: { + moderation_warning: { filterable: false, }.freeze, 'admin.sign_up': { @@ -208,15 +208,6 @@ class Notification < ApplicationRecord end end - def from_account_web - case activity_type - when 'AccountWarning' - account_warning&.target_account - else - from_account - end - end - after_initialize :set_from_account before_validation :set_from_account diff --git a/app/serializers/rest/account_warning_serializer.rb b/app/serializers/rest/account_warning_serializer.rb index 3f5940b71c..a0ef341d25 100644 --- a/app/serializers/rest/account_warning_serializer.rb +++ b/app/serializers/rest/account_warning_serializer.rb @@ -1,5 +1,16 @@ # frozen_string_literal: true class REST::AccountWarningSerializer < ActiveModel::Serializer - attributes :id, :action, :text, :status_ids + attributes :id, :action, :text, :status_ids, :created_at + + has_one :target_account, serializer: REST::AccountSerializer + has_one :appeal, serializer: REST::AppealSerializer + + def id + object.id.to_s + end + + def status_ids + object&.status_ids&.map(&:to_s) + end end diff --git a/app/serializers/rest/appeal_serializer.rb b/app/serializers/rest/appeal_serializer.rb new file mode 100644 index 0000000000..a24cabc272 --- /dev/null +++ b/app/serializers/rest/appeal_serializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class REST::AppealSerializer < ActiveModel::Serializer + attributes :text, :state + + def state + if object.approved? + 'approved' + elsif object.rejected? + 'rejected' + else + 'pending' + end + end +end diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index cd309820bd..6f223a7d75 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -3,20 +3,24 @@ class REST::NotificationSerializer < ActiveModel::Serializer attributes :id, :type, :created_at - has_many :statuses, serializer: REST::StatusSerializer, if: :warning_type? - - belongs_to :from_account_web, 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 :report, if: :report_type?, serializer: REST::ReportSerializer belongs_to :emoji_reaction, if: :emoji_reaction_type?, serializer: REST::NotifyEmojiReactionSerializer - belongs_to :account_warning, if: :warning_type?, serializer: REST::AccountWarningSerializer belongs_to :list, if: :list_status_type?, serializer: REST::ListSerializer 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 def id object.id.to_s end + def from_account + return object.account if moderation_warning_event? # Hide moderator account + + object.from_account + end + def status_type? [:favourite, :emoji_reaction, :reaction, :reblog, :status_reference, :status, :list_status, :mention, :poll, :update].include?(object.type) end @@ -25,10 +29,6 @@ class REST::NotificationSerializer < ActiveModel::Serializer object.type == :'admin.report' end - def warning_type? - object.type == :warning - end - def emoji_reaction_type? object.type == :emoji_reaction end @@ -45,7 +45,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer object.type == :severed_relationships end - def statuses - Status.where(id: object.account_warning.status_ids).to_a + def moderation_warning_event? + object.type == :moderation_warning end end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 7978ecee36..3716d38f92 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -12,7 +12,7 @@ class NotifyService < BaseService status_reference status list_status - warning + moderation_warning # TODO: this probably warrants an email notification severed_relationships ).freeze @@ -26,7 +26,7 @@ class NotifyService < BaseService def dismiss? blocked = @recipient.unavailable? - blocked ||= from_self? && @notification.type != :poll && @notification.type != :severed_relationships + blocked ||= from_self? && %i(poll severed_relationships moderation_warning).exclude?(@notification.type) return blocked if message? && from_staff? @@ -79,6 +79,7 @@ class NotifyService < BaseService admin.report poll update + account_warning ).freeze def initialize(notification) diff --git a/config/locales/en.yml b/config/locales/en.yml index ffca1bceaf..9af88c4519 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1507,6 +1507,7 @@ en: title_actions: delete_statuses: Post removal disable: Freezing of account + force_cw: Add content-warning to posts mark_statuses_as_sensitive: Marking of posts as sensitive none: Warning sensitive: Marking of account as sensitive diff --git a/config/locales/ja.yml b/config/locales/ja.yml index a60dba3519..9753ecd374 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1500,6 +1500,7 @@ ja: title_actions: delete_statuses: 投稿の削除 disable: アカウント凍結 + force_cw: 投稿に警告文を追加 mark_statuses_as_sensitive: 投稿を閲覧注意としてマーク none: 警告 sensitive: アカウントを閲覧注意としてマーク diff --git a/db/migrate/20240426000034_move_account_warning_notifications.rb b/db/migrate/20240426000034_move_account_warning_notifications.rb new file mode 100644 index 0000000000..fdabf0218c --- /dev/null +++ b/db/migrate/20240426000034_move_account_warning_notifications.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class MoveAccountWarningNotifications < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + class Notification < ApplicationRecord; end + + def up + Notification.where(type: 'warning').in_batches.update_all(type: 'moderation_warning') + end + + def down + Notification.where(type: 'moderation_warning').in_batches.update_all(type: 'warning') + end +end diff --git a/db/schema.rb b/db/schema.rb index 3ce8f434d1..d3f7d7f68b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_04_01_222541) do +ActiveRecord::Schema[7.1].define(version: 2024_04_26_000034) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/lib/tasks/dangerous.rake b/lib/tasks/dangerous.rake index cd2da0677e..3c00bbe74b 100644 --- a/lib/tasks/dangerous.rake +++ b/lib/tasks/dangerous.rake @@ -98,6 +98,7 @@ namespace :dangerous do 20240326231854 20240327234026 20240401222541 + 20240426000034 ) # Removed: account_groups target_tables = %w( diff --git a/spec/models/admin/account_action_spec.rb b/spec/models/admin/account_action_spec.rb index 9bc9f8061d..a9dcf352dc 100644 --- a/spec/models/admin/account_action_spec.rb +++ b/spec/models/admin/account_action_spec.rb @@ -69,22 +69,22 @@ RSpec.describe Admin::AccountAction do end end - it 'creates Admin::ActionLog' do + it 'sends notification, log the action, and closes other reports', :aggregate_failures do + other_report = Fabricate(:report, target_account: target_account) + + emails = [] expect do - subject - end.to change(Admin::ActionLog, :count).by 1 - end + emails = capture_emails { subject } + end.to (change(Admin::ActionLog.where(action: type), :count).by 1) + .and(change { other_report.reload.action_taken? }.from(false).to(true)) - it 'calls process_email!' do - allow(account_action).to receive(:process_email!) - subject - expect(account_action).to have_received(:process_email!) - end + expect(emails).to contain_exactly( + have_attributes( + to: contain_exactly(target_account.user.email) + ) + ) - it 'calls process_reports!' do - allow(account_action).to receive(:process_reports!) - subject - expect(account_action).to have_received(:process_reports!) + expect(LocalNotificationWorker).to have_enqueued_sidekiq_job(target_account.id, anything, 'AccountWarning', 'moderation_warning') end end