mirror of
https://github.com/mastodon/mastodon.git
synced 2024-05-19 10:28:08 +00:00
Merge branch 'main' of https://github.com/Colin-Marvin/mastodon-EECS481
This commit is contained in:
commit
670e0cfc6d
88
app/javascript/mastodon/features/explore/components/card.jsx
Normal file
88
app/javascript/mastodon/features/explore/components/card.jsx
Normal file
|
@ -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 = <FormattedMessage id='follow_suggestions.friends_of_friends_longer' defaultMessage='Popular among people you follow' />;
|
||||
break;
|
||||
case 'similar_to_recently_followed':
|
||||
label = <FormattedMessage id='follow_suggestions.similar_to_recently_followed_longer' defaultMessage='Similar to profiles you recently followed' />;
|
||||
break;
|
||||
case 'featured':
|
||||
label = <FormattedMessage id='follow_suggestions.featured_longer' defaultMessage='Hand-picked by the {domain} team' values={{ domain }} />;
|
||||
break;
|
||||
case 'most_followed':
|
||||
label = <FormattedMessage id='follow_suggestions.popular_suggestion_longer' defaultMessage='Popular on {domain}' values={{ domain }} />;
|
||||
break;
|
||||
case 'most_interactions':
|
||||
label = <FormattedMessage id='follow_suggestions.popular_suggestion_longer' defaultMessage='Popular on {domain}' values={{ domain }} />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='explore__suggestions__card'>
|
||||
<div className='explore__suggestions__card__source'>
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<div className='explore__suggestions__card__body'>
|
||||
<Link to={`/@${account.get('acct')}`}><Avatar account={account} size={48} /></Link>
|
||||
|
||||
<div className='explore__suggestions__card__body__main'>
|
||||
<div className='explore__suggestions__card__body__main__name-button'>
|
||||
<Link className='explore__suggestions__card__body__main__name-button__name' to={`/@${account.get('acct')}`}><DisplayName account={account} /></Link>
|
||||
<IconButton iconComponent={CloseIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||
<Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} secondary={following} onClick={handleFollow} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
source: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']),
|
||||
};
|
|
@ -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 (
|
||||
<div className='explore__suggestions scrollable' data-nosnippet>
|
||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
||||
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||
<Card
|
||||
key={suggestion.get('account')}
|
||||
id={suggestion.get('account')}
|
||||
source={suggestion.getIn(['sources', 0])}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
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.',
|
||||
},
|
||||
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'
|
||||
| 'mark_statuses_as_sensitive'
|
||||
| 'delete_statuses'
|
||||
| 'sensitive'
|
||||
| 'silence'
|
||||
| 'suspend';
|
||||
id: string;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export const ModerationWarning: React.FC<Props> = ({ action, id, hidden }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/disputes/strikes/${id}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='notification__moderation-warning'
|
||||
>
|
||||
<Icon id='warning' icon={WarningIcon} />
|
||||
|
||||
<div className='notification__moderation-warning__content'>
|
||||
<p>{intl.formatMessage(messages[action])}</p>
|
||||
<span className='link-button'>
|
||||
<FormattedMessage
|
||||
id='notification.moderation-warning.learn_more'
|
||||
defaultMessage='Learn more'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
|
@ -26,6 +26,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';
|
||||
|
||||
|
@ -40,6 +41,7 @@ const messages = defineMessages({
|
|||
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) => {
|
||||
|
@ -383,6 +385,27 @@ class Notification extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderModerationWarning (notification) {
|
||||
const { intl, unread, hidden } = this.props;
|
||||
const warning = notification.get('moderation_warning');
|
||||
|
||||
if (!warning) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-moderation-warning focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.moderationWarning), notification.get('created_at'))}>
|
||||
<ModerationWarning
|
||||
action={warning.get('action')}
|
||||
id={warning.get('id')}
|
||||
hidden={hidden}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
renderAdminSignUp (notification, account, link) {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
|
@ -456,6 +479,8 @@ class Notification extends ImmutablePureComponent {
|
|||
return this.renderPoll(notification, account);
|
||||
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':
|
||||
|
|
|
@ -536,11 +536,11 @@
|
|||
"onboarding.follows.empty": "Bedauerlicherweise können aktuell keine Ergebnisse angezeigt werden. Du kannst die Suche verwenden oder den Reiter „Entdecken“ auswählen, um neue Leute zum Folgen zu finden – oder du versuchst es später erneut.",
|
||||
"onboarding.follows.lead": "Deine Startseite ist der primäre Anlaufpunkt, um Mastodon zu erleben. Je mehr Profilen du folgst, umso aktiver und interessanter wird sie. Damit du direkt loslegen kannst, gibt es hier ein paar Vorschläge:",
|
||||
"onboarding.follows.title": "Personalisiere deine Startseite",
|
||||
"onboarding.profile.discoverable": "Mein Profil auffindbar machen",
|
||||
"onboarding.profile.discoverable": "Mein Profil darf entdeckt werden",
|
||||
"onboarding.profile.discoverable_hint": "Wenn du entdeckt werden möchtest, dann können deine Beiträge in Suchergebnissen und Trends erscheinen. Dein Profil kann ebenfalls anderen mit ähnlichen Interessen vorgeschlagen werden.",
|
||||
"onboarding.profile.display_name": "Anzeigename",
|
||||
"onboarding.profile.display_name_hint": "Dein richtiger Name oder dein Fantasiename …",
|
||||
"onboarding.profile.lead": "Du kannst das später in den Einstellungen vervollständigen, wo noch mehr Anpassungsmöglichkeiten zur Verfügung stehen.",
|
||||
"onboarding.profile.lead": "Du kannst dein Profil später in den Einstellungen vervollständigen. Dort stehen weitere Anpassungsmöglichkeiten zur Verfügung.",
|
||||
"onboarding.profile.note": "Über mich",
|
||||
"onboarding.profile.note_hint": "Du kannst andere @Profile erwähnen oder #Hashtags verwenden …",
|
||||
"onboarding.profile.save_and_continue": "Speichern und fortfahren",
|
||||
|
@ -556,16 +556,16 @@
|
|||
"onboarding.start.title": "Du hast es geschafft!",
|
||||
"onboarding.steps.follow_people.body": "Interessanten Profilen zu folgen ist das, was Mastodon ausmacht.",
|
||||
"onboarding.steps.follow_people.title": "Personalisiere deine Startseite",
|
||||
"onboarding.steps.publish_status.body": "Begrüße die Welt mit Text, Fotos, Videos oder Umfragen {emoji}",
|
||||
"onboarding.steps.publish_status.body": "Begrüße die Welt mit Text, Fotos, Videos oder Umfragen. {emoji}",
|
||||
"onboarding.steps.publish_status.title": "Erstelle deinen ersten Beitrag",
|
||||
"onboarding.steps.setup_profile.body": "Mit einem vollständigen Profil interagieren andere eher mit dir.",
|
||||
"onboarding.steps.setup_profile.title": "Personalisiere dein Profil",
|
||||
"onboarding.steps.share_profile.body": "Lass deine Freund*innen wissen, wie sie dich auf Mastodon finden können",
|
||||
"onboarding.steps.share_profile.body": "Lass deine Freund*innen wissen, wie sie dich auf Mastodon finden können.",
|
||||
"onboarding.steps.share_profile.title": "Teile dein Mastodon-Profil",
|
||||
"onboarding.tips.2fa": "<strong>Wusstest du schon?</strong> Du kannst die Sicherheit deines Kontos erhöhen, indem du die Zwei-Faktor-Authentisierung in deinen Kontoeinstellungen aktivierst. Dafür ist keine Telefonnummer notwendig und es funktioniert jede beliebige TOTP-App!",
|
||||
"onboarding.tips.accounts_from_other_servers": "<strong>Wusstest du schon?</strong> Da Mastodon dezentralisiert ist, werden einige Profile, denen du begegnest, auf anderen Servern als deinem bereitgestellt. Und trotzdem kannst du uneingeschränkt mit ihnen interagieren! Der Servername befindet sich in der zweiten Hälfte ihres Profilnamens!",
|
||||
"onboarding.tips.migration": "<strong>Wusstest du schon?</strong> Wenn du das Gefühl hast, dass {domain} in Zukunft nicht die richtige Serverwahl für dich ist, kannst du auf einen anderen Mastodon-Server umziehen, ohne deine Follower zu verlieren. Du kannst sogar deinen eigenen Server betreiben!",
|
||||
"onboarding.tips.verification": "<strong>Wusstest du schon?</strong> Du kannst dein Konto verifizieren, indem du auf deiner Website auf dein Mastodon-Profil verlinkst und den Link deiner Website zu deinem Profil hinzufügst. Keine Gebühren oder Dokumente erforderlich!",
|
||||
"onboarding.tips.verification": "<strong>Wusstest du schon?</strong> Du kannst dein Konto verifizieren, indem du auf deiner Website auf dein Mastodon-Profil verlinkst und den Link deiner Website zu deinem Profil hinzufügst. Völlig kostenlos und ohne Dokumente einsenden zu müssen!",
|
||||
"password_confirmation.exceeds_maxlength": "Passwortbestätigung überschreitet die maximal erlaubte Zeichenanzahl",
|
||||
"password_confirmation.mismatching": "Passwortbestätigung stimmt nicht überein",
|
||||
"picture_in_picture.restore": "Zurücksetzen",
|
||||
|
|
|
@ -308,6 +308,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}.",
|
||||
|
@ -315,6 +317,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",
|
||||
|
@ -469,6 +473,15 @@
|
|||
"notification.follow": "{name} followed you",
|
||||
"notification.follow_request": "{name} has requested to follow you",
|
||||
"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_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",
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"column.bookmarks": "Ebenrụtụakā",
|
||||
"column.home": "Be",
|
||||
"column.lists": "Ndepụta",
|
||||
"column.notifications": "Nziọkwà",
|
||||
"column.pins": "Pinned post",
|
||||
"column_header.pin": "Gbado na profaịlụ gị",
|
||||
"column_subheading.settings": "Mwube",
|
||||
|
@ -42,17 +43,28 @@
|
|||
"confirmations.reply.confirm": "Zaa",
|
||||
"confirmations.unfollow.confirm": "Kwụsị iso",
|
||||
"conversation.delete": "Hichapụ nkata",
|
||||
"disabled_account_banner.account_settings": "Mwube akaụntụ",
|
||||
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
|
||||
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
|
||||
"domain_pill.username": "Ahaojiaru",
|
||||
"embed.instructions": "Embed this status on your website by copying the code below.",
|
||||
"emoji_button.activity": "Mmemme",
|
||||
"emoji_button.label": "Tibanye emoji",
|
||||
"emoji_button.search": "Chọọ...",
|
||||
"emoji_button.symbols": "Ọdịmara",
|
||||
"empty_column.account_timeline": "No posts found",
|
||||
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
|
||||
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
|
||||
"errors.unexpected_crash.report_issue": "Kpesa nsogbu",
|
||||
"explore.trending_links": "Akụkọ",
|
||||
"firehose.all": "Ha niine",
|
||||
"follow_request.authorize": "Nye ikike",
|
||||
"footer.privacy_policy": "Iwu nzuzu",
|
||||
"getting_started.heading": "Mbido",
|
||||
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
|
||||
"home.column_settings.show_replies": "Gosi nzaghachị",
|
||||
"home.hide_announcements": "Zoo ọkwa",
|
||||
"home.show_announcements": "Gosi ọkwa",
|
||||
"keyboard_shortcuts.back": "to navigate back",
|
||||
"keyboard_shortcuts.blocked": "to open blocked users list",
|
||||
"keyboard_shortcuts.boost": "to boost",
|
||||
|
|
|
@ -298,7 +298,7 @@
|
|||
"filter_modal.select_filter.title": "この投稿をフィルターする",
|
||||
"filter_modal.title.status": "投稿をフィルターする",
|
||||
"filtered_notifications_banner.mentions": "{count, plural, one {メンション} other {メンション}}",
|
||||
"filtered_notifications_banner.pending_requests": "{count, plural, =0 {アカウント} other {#アカウント}}からの通知がブロックされています",
|
||||
"filtered_notifications_banner.pending_requests": "{count, plural, =0 {通知がブロックされているアカウントはありません} other {#アカウントからの通知がブロックされています}}",
|
||||
"filtered_notifications_banner.title": "ブロック済みの通知",
|
||||
"firehose.all": "すべて",
|
||||
"firehose.local": "このサーバー",
|
||||
|
|
|
@ -297,6 +297,7 @@
|
|||
"filter_modal.select_filter.subtitle": "Use uma categoria existente ou crie uma nova",
|
||||
"filter_modal.select_filter.title": "Filtrar esta publicação",
|
||||
"filter_modal.title.status": "Filtrar uma publicação",
|
||||
"filtered_notifications_banner.mentions": "{count, plural, one {menção} other {menções}}",
|
||||
"filtered_notifications_banner.pending_requests": "Notificações de {count, plural, =0 {no one} one {one person} other {# people}} que você talvez conheça",
|
||||
"filtered_notifications_banner.title": "Notificações filtradas",
|
||||
"firehose.all": "Tudo",
|
||||
|
|
|
@ -297,6 +297,7 @@
|
|||
"filter_modal.select_filter.subtitle": "Använd en befintlig kategori eller skapa en ny",
|
||||
"filter_modal.select_filter.title": "Filtrera detta inlägg",
|
||||
"filter_modal.title.status": "Filtrera ett inlägg",
|
||||
"filtered_notifications_banner.mentions": "{count, plural, one {mention} other {mentions}}",
|
||||
"filtered_notifications_banner.pending_requests": "Aviseringar från {count, plural, =0 {ingen} one {en person} other {# personer}} du kanske känner",
|
||||
"filtered_notifications_banner.title": "Filtrerade aviseringar",
|
||||
"firehose.all": "Allt",
|
||||
|
|
|
@ -56,6 +56,7 @@ export const notificationToMap = notification => ImmutableMap({
|
|||
status: notification.status ? notification.status.id : null,
|
||||
report: notification.report ? fromJS(notification.report) : null,
|
||||
event: notification.event ? fromJS(notification.event) : null,
|
||||
moderation_warning: notification.moderation_warning ? fromJS(notification.moderation_warning) : null,
|
||||
});
|
||||
|
||||
const normalizeNotification = (state, notification, usePendingItems) => {
|
||||
|
|
|
@ -2016,7 +2016,10 @@ a .account__avatar {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.account__relationship,
|
||||
.explore__suggestions__card {
|
||||
.icon-button {
|
||||
border: 1px solid var(--background-border-color);
|
||||
border-radius: 4px;
|
||||
|
@ -2177,7 +2180,8 @@ a.account__display-name {
|
|||
}
|
||||
}
|
||||
|
||||
.notification__relationships-severance-event {
|
||||
.notification__relationships-severance-event,
|
||||
.notification__moderation-warning {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: $secondary-text-color;
|
||||
|
@ -2964,6 +2968,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;
|
||||
|
@ -7293,10 +7366,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class Admin::AccountAction
|
|||
process_reports!
|
||||
end
|
||||
|
||||
process_email!
|
||||
process_notification!
|
||||
process_queue!
|
||||
end
|
||||
|
||||
|
@ -158,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?
|
||||
|
|
|
@ -65,7 +65,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
|
||||
|
||||
|
@ -101,7 +102,7 @@ class Admin::StatusBatchAction
|
|||
text: text
|
||||
)
|
||||
|
||||
UserMailer.warning(target_account.user, @warning).deliver_later! if warnable?
|
||||
process_notification!
|
||||
end
|
||||
|
||||
def handle_report!
|
||||
|
@ -127,6 +128,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
|
||||
|
|
|
@ -57,6 +57,9 @@ class Notification < ApplicationRecord
|
|||
severed_relationships: {
|
||||
filterable: false,
|
||||
}.freeze,
|
||||
moderation_warning: {
|
||||
filterable: false,
|
||||
}.freeze,
|
||||
'admin.sign_up': {
|
||||
filterable: false,
|
||||
}.freeze,
|
||||
|
@ -90,6 +93,7 @@ class Notification < ApplicationRecord
|
|||
belongs_to :poll, inverse_of: false
|
||||
belongs_to :report, inverse_of: false
|
||||
belongs_to :account_relationship_severance_event, inverse_of: false
|
||||
belongs_to :account_warning, inverse_of: false
|
||||
end
|
||||
|
||||
validates :type, inclusion: { in: TYPES }
|
||||
|
@ -180,7 +184,7 @@ class Notification < ApplicationRecord
|
|||
return unless new_record?
|
||||
|
||||
case activity_type
|
||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report'
|
||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report', 'AccountWarning'
|
||||
self.from_account_id = activity&.account_id
|
||||
when 'Mention'
|
||||
self.from_account_id = activity&.status&.account_id
|
||||
|
|
16
app/serializers/rest/account_warning_serializer.rb
Normal file
16
app/serializers/rest/account_warning_serializer.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::AccountWarningSerializer < ActiveModel::Serializer
|
||||
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
|
15
app/serializers/rest/appeal_serializer.rb
Normal file
15
app/serializers/rest/appeal_serializer.rb
Normal file
|
@ -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
|
|
@ -7,6 +7,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
|||
belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
|
||||
belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer
|
||||
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
|
||||
|
@ -23,4 +24,8 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
|||
def relationship_severance_event?
|
||||
object.type == :severed_relationships
|
||||
end
|
||||
|
||||
def moderation_warning_event?
|
||||
object.type == :moderation_warning
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ class NotifyService < BaseService
|
|||
update
|
||||
poll
|
||||
status
|
||||
moderation_warning
|
||||
# TODO: this probably warrants an email notification
|
||||
severed_relationships
|
||||
).freeze
|
||||
|
@ -22,7 +23,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?
|
||||
|
||||
|
@ -75,6 +76,7 @@ class NotifyService < BaseService
|
|||
admin.report
|
||||
poll
|
||||
update
|
||||
account_warning
|
||||
).freeze
|
||||
|
||||
def initialize(notification)
|
||||
|
|
|
@ -1046,7 +1046,7 @@ de:
|
|||
apply_for_account: Konto beantragen
|
||||
captcha_confirmation:
|
||||
help_html: Falls du Probleme beim Lösen des CAPTCHA hast, dann kannst uns über %{email} kontaktieren und wir werden versuchen, dir zu helfen.
|
||||
hint_html: Fast geschafft! Wir müssen uns vergewissern, dass du ein Mensch bist (damit wir Spam verhindern können!). Bitte löse das CAPTCHA und klicke auf „Weiter“.
|
||||
hint_html: Fast geschafft! Wir müssen uns vergewissern, dass du ein Mensch bist (damit wir Spam verhindern können!). Bitte löse das CAPTCHA und klicke auf „Fortfahren“.
|
||||
title: Sicherheitsüberprüfung
|
||||
confirmations:
|
||||
awaiting_review: Deine E-Mail-Adresse wurde bestätigt und das Team von %{domain} überprüft nun deine Registrierung. Sobald es dein Konto genehmigt, wirst du eine E-Mail erhalten.
|
||||
|
|
|
@ -25,7 +25,7 @@ de:
|
|||
explanation_when_pending: Du hast dich für eine Einladung bei %{host} mit dieser E-Mail-Adresse beworben. Sobald du deine E-Mail-Adresse bestätigt hast, werden wir deine Anfrage überprüfen. Du kannst dich in dieser Zeit nicht anmelden. Wenn deine Anfrage abgelehnt wird, werden deine Daten entfernt – von dir ist keine weitere Handlung notwendig. Wenn du das nicht warst, dann kannst du diese E-Mail ignorieren.
|
||||
extra_html: Bitte beachte auch die <a href="%{terms_path}">Serverregeln</a> und <a href="%{policy_path}">unsere Datenschutzerklärung</a>.
|
||||
subject: 'Mastodon: Anleitung zum Bestätigen deines Kontos auf %{instance}'
|
||||
title: Verifiziere E-Mail-Adresse
|
||||
title: Verifiziere deine E-Mail-Adresse
|
||||
email_changed:
|
||||
explanation: 'Die E-Mail-Adresse deines Kontos wird geändert zu:'
|
||||
extra: Wenn du deine E-Mail-Adresse nicht geändert hast, ist es wahrscheinlich, dass sich jemand Zugang zu deinem Konto verschafft hat. Bitte ändere sofort dein Passwort oder kontaktiere die Administrator*innen des Servers, wenn du aus deinem Konto ausgesperrt bist.
|
||||
|
|
|
@ -174,6 +174,7 @@ es-MX:
|
|||
read:filters: ver tus filtros
|
||||
read:follows: ver a quién sigues
|
||||
read:lists: ver tus listas
|
||||
read:me: leer solo la información básica de tu cuenta
|
||||
read:mutes: ver a quién has silenciado
|
||||
read:notifications: ver tus notificaciones
|
||||
read:reports: ver tus informes
|
||||
|
|
|
@ -174,6 +174,7 @@ es:
|
|||
read:filters: ver tus filtros
|
||||
read:follows: ver a quién sigues
|
||||
read:lists: ver tus listas
|
||||
read:me: leer solo la información básica de tu cuenta
|
||||
read:mutes: ver a quién has silenciado
|
||||
read:notifications: ver tus notificaciones
|
||||
read:reports: ver tus informes
|
||||
|
|
|
@ -174,6 +174,7 @@ pt-BR:
|
|||
read:filters: ver seus filtros
|
||||
read:follows: ver quem você segue
|
||||
read:lists: ver suas listas
|
||||
read:me: ler só as informações básicas da sua conta
|
||||
read:mutes: ver seus silenciados
|
||||
read:notifications: ver suas notificações
|
||||
read:reports: ver suas denúncias
|
||||
|
|
|
@ -174,6 +174,7 @@ sv:
|
|||
read:filters: se dina filter
|
||||
read:follows: se vem du följer
|
||||
read:lists: se dina listor
|
||||
read:me: läs endast den grundläggande informationen för ditt konto
|
||||
read:mutes: se dina tystningar
|
||||
read:notifications: se dina notiser
|
||||
read:reports: se dina rapporter
|
||||
|
|
|
@ -1671,6 +1671,8 @@ pt-BR:
|
|||
domain_block: Suspensão do servidor (%{target_name})
|
||||
user_domain_block: Você bloqueou %{target_name}
|
||||
lost_followers: Seguidores perdidos
|
||||
lost_follows: Seguidores perdidos
|
||||
preamble: Você poderá perder seguidores e seguidores quando bloquear um domínio ou quando os seus moderadores decidirem suspender um servidor remoto. Quando isso acontecer, você poderá baixar listas de relações desfeitas, a serem inspecionadas e possivelmente importadas para outro servidor.
|
||||
purged: As informações sobre este servidor foram eliminadas pelos administradores do seu servidor.
|
||||
type: Evento
|
||||
statuses:
|
||||
|
|
|
@ -116,6 +116,7 @@ pt-BR:
|
|||
sign_up_requires_approval: Novas inscrições exigirão sua aprovação
|
||||
severity: Escolha o que acontecerá com as solicitações deste IP
|
||||
rule:
|
||||
hint: Opcional. Forneça mais detalhes sobre a regra
|
||||
text: Descreva uma regra ou requisito para os usuários neste servidor. Tente mantê-la curta e simples.
|
||||
sessions:
|
||||
otp: 'Digite o código de dois fatores gerado pelo aplicativo no seu celular ou use um dos códigos de recuperação:'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue