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 (
+
+ );
+};
+
+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