Merge commit '4d2a148ccbedc818c98fd712a0b44869c1019321' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2026-02-25 19:11:22 +01:00
85 changed files with 1684 additions and 841 deletions

View File

@@ -9,7 +9,6 @@ on:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- '.prettier*'
- 'stylelint.config.js'
- '**/*.css'
- '**/*.scss'
@@ -21,7 +20,6 @@ on:
- 'package.json'
- 'yarn.lock'
- '.nvmrc'
- '.prettier*'
- 'stylelint.config.js'
- '**/*.css'
- '**/*.scss'

View File

@@ -10,7 +10,6 @@ on:
- 'yarn.lock'
- 'tsconfig.json'
- '.nvmrc'
- '.prettier*'
- 'eslint.config.mjs'
- '**/*.js'
- '**/*.jsx'
@@ -24,7 +23,6 @@ on:
- 'yarn.lock'
- 'tsconfig.json'
- '.nvmrc'
- '.prettier*'
- 'eslint.config.mjs'
- '**/*.js'
- '**/*.jsx'

View File

@@ -13,7 +13,7 @@ class Admin::Reports::ActionsController < Admin::BaseController
case action_from_button
when 'delete', 'mark_as_sensitive'
Admin::StatusBatchAction.new(status_batch_action_params).save!
Admin::ModerationAction.new(moderation_action_params).save!
when 'silence', 'suspend'
Admin::AccountAction.new(account_action_params).save!
else
@@ -25,9 +25,8 @@ class Admin::Reports::ActionsController < Admin::BaseController
private
def status_batch_action_params
def moderation_action_params
shared_params
.merge(status_ids: @report.status_ids)
end
def account_action_params

View File

@@ -78,8 +78,6 @@ module Admin
'report'
elsif params[:remove_from_report]
'remove_from_report'
elsif params[:delete]
'delete'
end
end
end

View File

@@ -19,7 +19,7 @@ module Admin::ActionLogsHelper
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
log.human_identifier.present? ? link_to(log.human_identifier, admin_instance_path(log.human_identifier)) : I18n.t('admin.action_logs.unavailable_instance')
when 'Status'
when 'Status', 'Collection'
link_to log.human_identifier, log.permalink
when 'AccountWarning'
link_to log.human_identifier, disputes_strike_path(log.target_id)

View File

@@ -182,15 +182,25 @@ function loaded() {
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
if (target.value && target.value.length > 0) {
const checkedUsername = target.value;
if (checkedUsername && checkedUsername.length > 0) {
axios
.get('/api/v1/accounts/lookup', { params: { acct: target.value } })
.get('/api/v1/accounts/lookup', {
params: { acct: checkedUsername },
})
.then(() => {
target.setCustomValidity(formatMessage(messages.usernameTaken));
// Only update the validity if the result is for the currently-typed username
if (checkedUsername === target.value) {
target.setCustomValidity(formatMessage(messages.usernameTaken));
}
return true;
})
.catch(() => {
target.setCustomValidity('');
// Only update the validity if the result is for the currently-typed username
if (checkedUsername === target.value) {
target.setCustomValidity('');
}
});
} else {
target.setCustomValidity('');

View File

@@ -20,18 +20,7 @@ export interface EmojiHTMLProps {
}
export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
(
{
extraEmojis,
htmlString,
as: asProp = 'div', // Rename for syntax highlighting
className,
onElement,
onAttribute,
...props
},
ref,
) => {
({ extraEmojis, htmlString, onElement, onAttribute, ...props }, ref) => {
const contents = useMemo(
() =>
htmlStringToComponents(htmlString, {
@@ -44,12 +33,7 @@ export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
return (
<CustomEmojiProvider emojis={extraEmojis}>
<AnimateEmojiProvider
{...props}
as={asProp}
className={className}
ref={ref}
>
<AnimateEmojiProvider {...props} ref={ref}>
{contents}
</AnimateEmojiProvider>
</CustomEmojiProvider>

View File

@@ -23,7 +23,17 @@ export type MiniCardProps = OmitUnion<
export const MiniCard = forwardRef<HTMLDivElement, MiniCardProps>(
(
{ label, value, className, hidden, icon, iconId, iconClassName, ...props },
{
label,
value,
className,
hidden,
icon,
iconId,
iconClassName,
children,
...props
},
ref,
) => {
if (!label) {
@@ -50,6 +60,7 @@ export const MiniCard = forwardRef<HTMLDivElement, MiniCardProps>(
)}
<dt className={classes.label}>{label}</dt>
<dd className={classes.value}>{value}</dd>
{children}
</div>
);
},

View File

@@ -1,125 +0,0 @@
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router';
import { AccountBio } from '@/mastodon/components/account_bio';
import { Column } from '@/mastodon/components/column';
import { ColumnBackButton } from '@/mastodon/components/column_back_button';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import type { AccountId } from '@/mastodon/hooks/useAccountId';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import { createAppSelector, useAppSelector } from '@/mastodon/store';
import { AccountHeader } from '../account_timeline/components/account_header';
import { AccountHeaderFields } from '../account_timeline/components/fields';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import classes from './styles.module.css';
const selectIsProfileEmpty = createAppSelector(
[(state) => state.accounts, (_, accountId: AccountId) => accountId],
(accounts, accountId) => {
// Null means still loading, otherwise it's a boolean.
if (!accountId) {
return null;
}
const account = accounts.get(accountId);
if (!account) {
return null;
}
return !account.note && !account.fields.size;
},
);
export const AccountAbout: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const accountId = useAccountId();
const { blockedBy, hidden, suspended } = useAccountVisibility(accountId);
const forceEmptyState = blockedBy || hidden || suspended;
const isProfileEmpty = useAppSelector((state) =>
selectIsProfileEmpty(state, accountId),
);
if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
if (!accountId || isProfileEmpty === null) {
return (
<Column bindToDocument={!multiColumn}>
<LoadingIndicator />
</Column>
);
}
const showEmptyMessage = forceEmptyState || isProfileEmpty;
return (
<Column bindToDocument={!multiColumn}>
<ColumnBackButton />
<div className='scrollable scrollable--flex'>
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
<div className={classes.wrapper}>
{!showEmptyMessage ? (
<>
<AccountBio
accountId={accountId}
className={`${classes.bio} account__header__content`}
/>
<AccountHeaderFields accountId={accountId} />
</>
) : (
<div className='empty-column-indicator'>
<EmptyMessage accountId={accountId} />
</div>
)}
</div>
</div>
</Column>
);
};
const EmptyMessage: FC<{ accountId: string }> = ({ accountId }) => {
const { blockedBy, hidden, suspended } = useAccountVisibility(accountId);
const currentUserId = useAppSelector(
(state) => state.meta.get('me') as string | null,
);
const { acct } = useParams<{ acct?: string }>();
if (suspended) {
return (
<FormattedMessage
id='empty_column.account_suspended'
defaultMessage='Account suspended'
/>
);
} else if (hidden) {
return <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
return (
<FormattedMessage
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/>
);
} else if (accountId === currentUserId) {
return (
<FormattedMessage
id='empty_column.account_about.me'
defaultMessage='You have not added any information about yourself yet.'
/>
);
}
return (
<FormattedMessage
id='empty_column.account_about.other'
defaultMessage='{acct} has not added any information about themselves yet.'
values={{ acct }}
/>
);
};

View File

@@ -1,7 +0,0 @@
.wrapper {
padding: 16px;
}
.bio {
color: var(--color-text-primary);
}

View File

@@ -17,8 +17,15 @@ import BundleColumnError from 'mastodon/features/ui/components/bundle_column_err
import Column from 'mastodon/features/ui/components/column';
import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import {
fetchAccountCollections,
selectAccountCollections,
} from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { CollectionListItem } from '../collections/detail/collection_list_item';
import { areCollectionsEnabled } from '../collections/utils';
import { EmptyMessage } from './components/empty_message';
import { FeaturedTag } from './components/featured_tag';
import type { TagMap } from './components/featured_tag';
@@ -42,6 +49,9 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
if (accountId) {
void dispatch(fetchFeaturedTags({ accountId }));
void dispatch(fetchEndorsedAccounts({ accountId }));
if (areCollectionsEnabled()) {
void dispatch(fetchAccountCollections({ accountId }));
}
}
}, [accountId, dispatch]);
@@ -64,6 +74,14 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
ImmutableList(),
) as ImmutableList<string>,
);
const { collections, status } = useAppSelector((state) =>
selectAccountCollections(state, accountId ?? null),
);
const publicCollections = collections.filter(
// This filter only applies when viewing your own profile, where the endpoint
// returns all collections, but we hide unlisted ones here to avoid confusion
(item) => item.discoverable,
);
if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
@@ -101,6 +119,25 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
{accountId && (
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
)}
{publicCollections.length > 0 && status === 'idle' && (
<>
<h4 className='column-subheading'>
<FormattedMessage
id='account.featured.collections'
defaultMessage='Collections'
/>
</h4>
<section>
{publicCollections.map((item, index) => (
<CollectionListItem
key={item.id}
collection={item}
withoutBorder={index === publicCollections.length - 1}
/>
))}
</section>
</>
)}
{!featuredTags.isEmpty() && (
<>
<h4 className='column-subheading'>

View File

@@ -1,5 +1,12 @@
import type { AccountFieldShape } from '@/mastodon/models/account';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
export function isRedesignEnabled() {
return isServerFeatureEnabled('profile_redesign');
}
export interface AccountField extends AccountFieldShape {
nameHasEmojis: boolean;
value_plain: string;
valueHasEmojis: boolean;
}

View File

@@ -210,18 +210,14 @@ export const AccountHeader: React.FC<{
<AccountNote accountId={accountId} />
))}
{(!isRedesign || layout === 'single-column') && (
<>
<AccountBio
accountId={accountId}
className={classNames(
'account__header__content',
isRedesign && redesignClasses.bio,
)}
/>
<AccountHeaderFields accountId={accountId} />
</>
)}
<AccountBio
accountId={accountId}
className={classNames(
'account__header__content',
isRedesign && redesignClasses.bio,
)}
/>
<AccountHeaderFields accountId={accountId} />
</div>
<AccountNumberFields accountId={accountId} />

View File

@@ -1,25 +1,31 @@
import { useCallback, useMemo, useState } from 'react';
import type { FC, Key } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import type { FC } from 'react';
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import htmlConfig from '@/config/html-tags.json';
import IconVerified from '@/images/icons/icon_verified.svg?react';
import { openModal } from '@/mastodon/actions/modal';
import { AccountFields } from '@/mastodon/components/account_fields';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
import type { EmojiHTMLProps } from '@/mastodon/components/emoji/html';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
import { Icon } from '@/mastodon/components/icon';
import { IconButton } from '@/mastodon/components/icon_button';
import { MiniCard } from '@/mastodon/components/mini_card';
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
import { useAccount } from '@/mastodon/hooks/useAccount';
import type { Account, AccountFieldShape } from '@/mastodon/models/account';
import type { OnElementHandler } from '@/mastodon/utils/html';
import { useResizeObserver } from '@/mastodon/hooks/useObserver';
import type { Account } from '@/mastodon/models/account';
import { useAppDispatch } from '@/mastodon/store';
import MoreIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { cleanExtraEmojis } from '../../emoji/normalize';
import type { AccountField } from '../common';
import { isRedesignEnabled } from '../common';
import { useFieldHtml } from '../hooks/useFieldHtml';
import classes from './redesign.module.scss';
@@ -74,172 +80,310 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => {
() => cleanExtraEmojis(account.emojis),
[account.emojis],
);
const textHasCustomEmoji = useCallback(
(text?: string | null) => {
if (!emojis || !text) {
return false;
}
for (const emoji of Object.keys(emojis)) {
if (text.includes(`:${emoji}:`)) {
return true;
}
}
return false;
},
[emojis],
);
const fields: AccountField[] = useMemo(() => {
const fields = account.fields.toJS();
if (!emojis) {
return fields.map((field) => ({
...field,
nameHasEmojis: false,
value_plain: field.value_plain ?? '',
valueHasEmojis: false,
}));
}
const shortcodes = Object.keys(emojis);
return fields.map((field) => ({
...field,
nameHasEmojis: shortcodes.some((code) =>
field.name.includes(`:${code}:`),
),
value_plain: field.value_plain ?? '',
valueHasEmojis: shortcodes.some((code) =>
field.value_plain?.includes(`:${code}:`),
),
}));
}, [account.fields, emojis]);
const htmlHandlers = useElementHandledLink({
hashtagAccountId: account.id,
});
if (account.fields.isEmpty()) {
const { wrapperRef } = useColumnWrap();
if (fields.length === 0) {
return null;
}
return (
<CustomEmojiProvider emojis={emojis}>
<dl className={classes.fieldList}>
{account.fields.map((field, key) => (
<FieldRow
key={key}
{...field.toJSON()}
htmlHandlers={htmlHandlers}
textHasCustomEmoji={textHasCustomEmoji}
/>
<dl className={classes.fieldList} ref={wrapperRef}>
{fields.map((field, key) => (
<FieldRow key={key} field={field} htmlHandlers={htmlHandlers} />
))}
</dl>
</CustomEmojiProvider>
);
};
const FieldRow: FC<
{
textHasCustomEmoji: (text?: string | null) => boolean;
htmlHandlers: ReturnType<typeof useElementHandledLink>;
} & AccountFieldShape
> = ({
textHasCustomEmoji,
htmlHandlers,
name,
name_emojified,
value_emojified,
value_plain,
verified_at,
}) => {
const FieldRow: FC<{
htmlHandlers: ReturnType<typeof useElementHandledLink>;
field: AccountField;
}> = ({ htmlHandlers, field }) => {
const intl = useIntl();
const [showAll, setShowAll] = useState(false);
const handleClick = useCallback(() => {
setShowAll((prev) => !prev);
}, []);
const {
name,
name_emojified,
nameHasEmojis,
value_emojified,
value_plain,
valueHasEmojis,
verified_at,
} = field;
const { wrapperRef, isLabelOverflowing, isValueOverflowing } =
useFieldOverflow();
const dispatch = useAppDispatch();
const handleOverflowClick = useCallback(() => {
dispatch(
openModal({
modalType: 'ACCOUNT_FIELD_OVERFLOW',
modalProps: { field },
}),
);
}, [dispatch, field]);
return (
/* eslint-disable -- This method of showing field contents is not very accessible, but it's what we've got for now */
<div
<MiniCard
className={classNames(
classes.fieldRow,
classes.fieldItem,
verified_at && classes.fieldVerified,
showAll && classes.fieldShowAll,
)}
onClick={handleClick}
/* eslint-enable */
>
<FieldHTML
as='dt'
text={name}
textEmojified={name_emojified}
textHasCustomEmoji={textHasCustomEmoji(name)}
titleLength={50}
className='translate'
{...htmlHandlers}
/>
<dd>
label={
<FieldHTML
as='span'
text={value_plain ?? ''}
textEmojified={value_emojified}
textHasCustomEmoji={textHasCustomEmoji(value_plain ?? '')}
titleLength={120}
text={name}
textEmojified={name_emojified}
textHasCustomEmoji={nameHasEmojis}
className='translate'
isOverflowing={isLabelOverflowing}
onOverflowClick={handleOverflowClick}
{...htmlHandlers}
/>
{verified_at && (
<Icon
id='verified'
icon={IconVerified}
className={classes.fieldVerifiedIcon}
aria-label={intl.formatMessage(verifyMessage, {
date: intl.formatDate(verified_at, dateFormatOptions),
})}
noFill
/>
)}
</dd>
</div>
}
value={
<FieldHTML
text={value_plain}
textEmojified={value_emojified}
textHasCustomEmoji={valueHasEmojis}
isOverflowing={isValueOverflowing}
onOverflowClick={handleOverflowClick}
{...htmlHandlers}
/>
}
ref={wrapperRef}
>
{verified_at && (
<Icon
id='verified'
icon={IconVerified}
className={classes.fieldVerifiedIcon}
aria-label={intl.formatMessage(verifyMessage, {
date: intl.formatDate(verified_at, dateFormatOptions),
})}
noFill
/>
)}
</MiniCard>
);
};
const FieldHTML: FC<
{
as?: 'span' | 'dt';
text: string;
textEmojified: string;
textHasCustomEmoji: boolean;
titleLength: number;
} & Omit<EmojiHTMLProps, 'htmlString'>
> = ({
as,
type FieldHTMLProps = {
text: string;
textEmojified: string;
textHasCustomEmoji: boolean;
isOverflowing?: boolean;
onOverflowClick?: () => void;
} & Omit<EmojiHTMLProps, 'htmlString'>;
const FieldHTML: FC<FieldHTMLProps> = ({
className,
extraEmojis,
text,
textEmojified,
textHasCustomEmoji,
titleLength,
isOverflowing,
onOverflowClick,
onElement,
...props
}) => {
const handleElement: OnElementHandler = useCallback(
(element, props, children, extra) => {
if (element instanceof HTMLAnchorElement) {
// Don't allow custom emoji and links in the same field to prevent verification spoofing.
if (textHasCustomEmoji) {
return (
<span {...filterAttributesForSpan(props)} key={props.key as Key}>
{children}
</span>
);
}
return onElement?.(element, props, children, extra);
}
return undefined;
},
[onElement, textHasCustomEmoji],
);
const intl = useIntl();
const handleElement = useFieldHtml(textHasCustomEmoji, onElement);
return (
const html = (
<EmojiHTML
as={as}
as='span'
htmlString={textEmojified}
title={showTitleOnLength(text, titleLength)}
className={className}
onElement={handleElement}
data-contents
{...props}
/>
);
if (!isOverflowing) {
return html;
}
return (
<>
{html}
<IconButton
icon='ellipsis'
iconComponent={MoreIcon}
title={intl.formatMessage({
id: 'account.field_overflow',
defaultMessage: 'Show full content',
})}
className={classes.fieldOverflowButton}
onClick={onOverflowClick}
/>
</>
);
};
function filterAttributesForSpan(props: Record<string, unknown>) {
const validAttributes: Record<string, unknown> = {};
for (const key of Object.keys(props)) {
if (key in htmlConfig.tags.span.attributes) {
validAttributes[key] = props[key];
function useColumnWrap() {
const listRef = useRef<HTMLDListElement | null>(null);
const handleRecalculate = useCallback(() => {
const listEle = listRef.current;
if (!listEle) {
return;
}
}
return validAttributes;
// Calculate dimensions from styles and element size to determine column spans.
const styles = getComputedStyle(listEle);
const gap = parseFloat(styles.columnGap || styles.gap || '0');
const columnCount = parseInt(styles.getPropertyValue('--cols')) || 2;
const listWidth = listEle.offsetWidth;
const colWidth = (listWidth - gap * (columnCount - 1)) / columnCount;
// Matrix to hold the grid layout.
const itemGrid: { ele: HTMLElement; span: number }[][] = [];
// First, determine the column span for each item and populate the grid matrix.
let currentRow = 0;
for (const child of listEle.children) {
if (!(child instanceof HTMLElement)) {
continue;
}
// This uses a data attribute to detect which elements to measure that overflow.
const contents = child.querySelectorAll('[data-contents]');
const childStyles = getComputedStyle(child);
const padding =
parseFloat(childStyles.paddingLeft) +
parseFloat(childStyles.paddingRight);
const contentWidth =
Math.max(
...Array.from(contents).map((content) => content.scrollWidth),
) + padding;
const contentSpan = Math.ceil(contentWidth / colWidth);
const maxColSpan = Math.min(contentSpan, columnCount);
const curRow = itemGrid[currentRow] ?? [];
const availableCols =
columnCount - curRow.reduce((carry, curr) => carry + curr.span, 0);
// Move to next row if current item doesn't fit.
if (maxColSpan > availableCols) {
currentRow++;
}
itemGrid[currentRow] = (itemGrid[currentRow] ?? []).concat({
ele: child,
span: maxColSpan,
});
}
// Next, iterate through the grid matrix and set the column spans and row breaks.
for (const row of itemGrid) {
let remainingRowSpan = columnCount;
for (let i = 0; i < row.length; i++) {
const item = row[i];
if (!item) {
break;
}
const { ele, span } = item;
if (i < row.length - 1) {
ele.dataset.cols = span.toString();
remainingRowSpan -= span;
} else {
// Last item in the row takes up remaining space to fill the row.
ele.dataset.cols = remainingRowSpan.toString();
break;
}
}
}
}, []);
const observer = useResizeObserver(handleRecalculate);
const wrapperRefCallback = useCallback(
(element: HTMLDListElement | null) => {
if (element) {
listRef.current = element;
observer.observe(element);
}
},
[observer],
);
return { wrapperRef: wrapperRefCallback };
}
function showTitleOnLength(value: string | null, maxLength: number) {
if (value && value.length > maxLength) {
return value;
}
return undefined;
function useFieldOverflow() {
const [isLabelOverflowing, setIsLabelOverflowing] = useState(false);
const [isValueOverflowing, setIsValueOverflowing] = useState(false);
const wrapperRef = useRef<HTMLElement | null>(null);
const handleRecalculate = useCallback(() => {
const wrapperEle = wrapperRef.current;
if (!wrapperEle) return;
const wrapperStyles = getComputedStyle(wrapperEle);
const maxWidth =
wrapperEle.offsetWidth -
(parseFloat(wrapperStyles.paddingLeft) +
parseFloat(wrapperStyles.paddingRight));
const label = wrapperEle.querySelector<HTMLSpanElement>(
'dt > [data-contents]',
);
const value = wrapperEle.querySelector<HTMLSpanElement>(
'dd > [data-contents]',
);
setIsLabelOverflowing(label ? label.scrollWidth > maxWidth : false);
setIsValueOverflowing(value ? value.scrollWidth > maxWidth : false);
}, []);
const observer = useResizeObserver(handleRecalculate);
const wrapperRefCallback = useCallback(
(element: HTMLElement | null) => {
if (element) {
wrapperRef.current = element;
observer.observe(element);
}
},
[observer],
);
return {
isLabelOverflowing,
isValueOverflowing,
wrapperRef: wrapperRefCallback,
};
}

View File

@@ -214,64 +214,90 @@ svg.badgeIcon {
}
.fieldList {
--cols: 4;
position: relative;
display: grid;
grid-template-columns: 160px 1fr;
column-gap: 12px;
grid-template-columns: repeat(var(--cols), 1fr);
gap: 4px;
margin: 16px 0;
border-top: 0.5px solid var(--color-border-primary);
@container (width < 420px) {
grid-template-columns: 100px 1fr;
--cols: 2;
}
}
.fieldRow {
display: grid;
grid-column: 1 / -1;
align-items: start;
grid-template-columns: subgrid;
padding: 8px;
border-bottom: 0.5px solid var(--color-border-primary);
.fieldItem {
--col-span: 1;
> :is(dt, dd) {
&:not(.fieldShowAll) {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
grid-column: span var(--col-span);
position: relative;
@for $col from 2 through 4 {
&[data-cols='#{$col}'] {
--col-span: #{$col};
}
}
> dt {
color: var(--color-text-secondary);
dt {
font-weight: normal;
}
> dd {
display: flex;
align-items: center;
gap: 4px;
dd {
font-weight: 500;
}
a {
color: inherit;
text-decoration: none;
:is(dt, dd) {
text-overflow: initial;
&:hover,
&:focus {
text-decoration: underline;
// Override the MiniCard link styles
a {
color: inherit;
font-weight: inherit;
&:hover,
&:focus {
color: inherit;
text-decoration: underline;
}
}
}
}
.fieldVerified {
background-color: var(--color-bg-success-softer);
dt {
padding-right: 24px;
}
}
.fieldVerifiedIcon {
width: 16px;
height: 16px;
position: absolute;
top: 8px;
right: 8px;
}
.fieldOverflowButton {
--default-bg-color: var(--color-bg-secondary-solid);
--hover-bg-color: color-mix(
in oklab,
var(--color-bg-brand-base),
var(--default-bg-color) var(--overlay-strength-brand)
);
position: absolute;
right: 8px;
padding: 0 2px;
transition: background-color 0.2s ease-in-out;
border: 2px solid var(--color-bg-primary);
> svg {
width: 16px;
height: 12px;
}
}
.fieldNumbersWrapper {

View File

@@ -5,23 +5,15 @@ import { FormattedMessage } from 'react-intl';
import type { NavLinkProps } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import { useLayout } from '@/mastodon/hooks/useLayout';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss';
export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
const { layout } = useLayout();
if (isRedesignEnabled()) {
return (
<div className={classes.tabs}>
{layout !== 'single-column' && (
<NavLink exact to={`/@${acct}/about`}>
<FormattedMessage id='account.about' defaultMessage='About' />
</NavLink>
)}
<NavLink isActive={isActive} to={`/@${acct}/posts`}>
<NavLink isActive={isActive} to={`/@${acct}`}>
<FormattedMessage id='account.activity' defaultMessage='Activity' />
</NavLink>
<NavLink exact to={`/@${acct}/media`}>

View File

@@ -0,0 +1,38 @@
import type { Key } from 'react';
import { useCallback } from 'react';
import htmlConfig from '@/config/html-tags.json';
import type { OnElementHandler } from '@/mastodon/utils/html';
export function useFieldHtml(
hasCustomEmoji: boolean,
onElement?: OnElementHandler,
): OnElementHandler {
return useCallback(
(element, props, children, extra) => {
if (element instanceof HTMLAnchorElement) {
// Don't allow custom emoji and links in the same field to prevent verification spoofing.
if (hasCustomEmoji) {
return (
<span {...filterAttributesForSpan(props)} key={props.key as Key}>
{children}
</span>
);
}
return onElement?.(element, props, children, extra);
}
return undefined;
},
[onElement, hasCustomEmoji],
);
}
function filterAttributesForSpan(props: Record<string, unknown>) {
const validAttributes: Record<string, unknown> = {};
for (const key of Object.keys(props)) {
if (key in htmlConfig.tags.span.attributes) {
validAttributes[key] = props[key];
}
}
return validAttributes;
}

View File

@@ -0,0 +1,44 @@
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import type { AccountField } from '../common';
import { useFieldHtml } from '../hooks/useFieldHtml';
import classes from './styles.module.css';
export const AccountFieldModal: FC<{
onClose: () => void;
field: AccountField;
}> = ({ onClose, field }) => {
const handleLabelElement = useFieldHtml(field.nameHasEmojis);
const handleValueElement = useFieldHtml(field.valueHasEmojis);
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__confirmation'>
<EmojiHTML
as='p'
htmlString={field.name_emojified}
onElement={handleLabelElement}
/>
<EmojiHTML
as='p'
htmlString={field.value_emojified}
onElement={handleValueElement}
className={classes.fieldValue}
/>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<button onClick={onClose} className='link-button' type='button'>
<FormattedMessage id='lightbox.close' defaultMessage='Close' />
</button>
</div>
</div>
</div>
);
};

View File

@@ -13,7 +13,7 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
import classes from './modals.module.css';
import classes from './styles.module.css';
const messages = defineMessages({
newTitle: {

View File

@@ -19,3 +19,9 @@
outline: var(--outline-focus-default);
outline-offset: 2px;
}
.fieldValue {
color: var(--color-text-primary);
font-weight: 600;
margin-top: 4px;
}

View File

@@ -2,15 +2,17 @@
display: flex;
align-items: center;
gap: 16px;
margin-inline: 10px;
padding-inline-end: 5px;
border-bottom: 1px solid var(--color-border-primary);
padding-inline: 16px;
&:not(.wrapperWithoutBorder) {
border-bottom: 1px solid var(--color-border-primary);
}
}
.content {
position: relative;
flex-grow: 1;
padding: 15px 5px;
padding-block: 15px;
}
.link {

View File

@@ -67,13 +67,18 @@ export const CollectionMetaData: React.FC<{
export const CollectionListItem: React.FC<{
collection: ApiCollectionJSON;
}> = ({ collection }) => {
withoutBorder?: boolean;
}> = ({ collection, withoutBorder }) => {
const { id, name } = collection;
const linkId = useId();
return (
<article
className={classNames(classes.wrapper, 'focusable')}
className={classNames(
classes.wrapper,
'focusable',
withoutBorder && classes.wrapperWithoutBorder,
)}
tabIndex={-1}
aria-labelledby={linkId}
>

View File

@@ -2,11 +2,16 @@ import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { matchPath } from 'react-router';
import { useAccount } from '@/mastodon/hooks/useAccount';
import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
import { openModal } from 'mastodon/actions/modal';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { IconButton } from 'mastodon/components/icon_button';
import { me } from 'mastodon/initial_state';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import { useAppDispatch } from 'mastodon/store';
import { messages as editorMessages } from '../editor';
@@ -16,10 +21,18 @@ const messages = defineMessages({
id: 'collections.view_collection',
defaultMessage: 'View collection',
},
viewOtherCollections: {
id: 'collections.view_other_collections_by_user',
defaultMessage: 'View other collections by this user',
},
delete: {
id: 'collections.delete_collection',
defaultMessage: 'Delete collection',
},
report: {
id: 'collections.report_collection',
defaultMessage: 'Report this collection',
},
more: { id: 'status.more', defaultMessage: 'More' },
});
@@ -31,9 +44,11 @@ export const CollectionMenu: React.FC<{
const dispatch = useAppDispatch();
const intl = useIntl();
const { id, name } = collection;
const { id, name, account_id } = collection;
const isOwnCollection = account_id === me;
const ownerAccount = useAccount(account_id);
const handleDeleteClick = useCallback(() => {
const openDeleteConfirmation = useCallback(() => {
dispatch(
openModal({
modalType: 'CONFIRM_DELETE_COLLECTION',
@@ -45,34 +60,83 @@ export const CollectionMenu: React.FC<{
);
}, [dispatch, id, name]);
const menu = useMemo(() => {
const commonItems = [
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
},
{
text: intl.formatMessage(editorMessages.editDetails),
to: `/collections/${id}/edit/details`,
},
null,
{
text: intl.formatMessage(messages.delete),
action: handleDeleteClick,
dangerous: true,
},
];
const openReportModal = useCallback(() => {
dispatch(
openModal({
modalType: 'REPORT_COLLECTION',
modalProps: {
collection,
},
}),
);
}, [collection, dispatch]);
if (context === 'list') {
return [
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
const menu = useMemo(() => {
if (isOwnCollection) {
const commonItems: MenuItem[] = [
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
},
{
text: intl.formatMessage(editorMessages.editDetails),
to: `/collections/${id}/edit/details`,
},
null,
...commonItems,
{
text: intl.formatMessage(messages.delete),
action: openDeleteConfirmation,
dangerous: true,
},
];
if (context === 'list') {
return [
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
null,
...commonItems,
];
} else {
return commonItems;
}
} else if (ownerAccount) {
const items: MenuItem[] = [
{
text: intl.formatMessage(messages.report),
action: openReportModal,
},
];
const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`;
// Don't show menu link to featured collections while on that very page
if (
!matchPath(location.pathname, {
path: featuredCollectionsPath,
exact: true,
})
) {
items.unshift(
...[
{
text: intl.formatMessage(messages.viewOtherCollections),
to: featuredCollectionsPath,
},
null,
],
);
}
return items;
} else {
return commonItems;
return [];
}
}, [intl, id, handleDeleteClick, context]);
}, [
isOwnCollection,
intl,
id,
openDeleteConfirmation,
context,
ownerAccount,
openReportModal,
]);
return (
<Dropdown scrollKey='collections' items={menu}>

View File

@@ -5,6 +5,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useParams } from 'react-router';
import { useRelationship } from '@/mastodon/hooks/useRelationship';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { showAlert } from 'mastodon/actions/alerts';
@@ -79,7 +80,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
collection,
}) => {
const intl = useIntl();
const { name, description, tag } = collection;
const { name, description, tag, account_id } = collection;
const dispatch = useAppDispatch();
const handleShare = useCallback(() => {
@@ -114,7 +115,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
{description && <p className={classes.description}>{description}</p>}
<AuthorNote id={collection.account_id} />
<CollectionMetaData
extended
extended={account_id === me}
collection={collection}
className={classes.metaData}
/>
@@ -123,6 +124,28 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
);
};
const CollectionAccountItem: React.FC<{
accountId: string | undefined;
collectionOwnerId: string;
}> = ({ accountId, collectionOwnerId }) => {
const relationship = useRelationship(accountId);
if (!accountId) {
return null;
}
// When viewing your own collection, only show the Follow button
// for accounts you're not following (anymore).
// Otherwise, always show the follow button in its various states.
const withoutButton =
accountId === me ||
!relationship ||
(collectionOwnerId === me &&
(relationship.following || relationship.requested));
return <Account minimal={withoutButton} withMenu={false} id={accountId} />;
};
export const CollectionDetailPage: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
@@ -163,11 +186,13 @@ export const CollectionDetailPage: React.FC<{
collection ? <CollectionHeader collection={collection} /> : null
}
>
{collection?.items.map(({ account_id }) =>
account_id ? (
<Account key={account_id} minimal id={account_id} />
) : null,
)}
{collection?.items.map(({ account_id }) => (
<CollectionAccountItem
key={account_id}
accountId={account_id}
collectionOwnerId={collection.account_id}
/>
))}
</ScrollableList>
<Helmet>

View File

@@ -14,7 +14,7 @@ import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import {
fetchAccountCollections,
selectMyCollections,
selectAccountCollections,
} from 'mastodon/reducers/slices/collections';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
@@ -31,7 +31,9 @@ export const Collections: React.FC<{
const dispatch = useAppDispatch();
const intl = useIntl();
const me = useAppSelector((state) => state.meta.get('me') as string);
const { collections, status } = useAppSelector(selectMyCollections);
const { collections, status } = useAppSelector((state) =>
selectAccountCollections(state, me),
);
useEffect(() => {
void dispatch(fetchAccountCollections({ accountId: me }));

View File

@@ -1,121 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback, useEffect, useRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import { OrderedSet, List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { shallowEqual } from 'react-redux';
import Toggle from 'react-toggle';
import { fetchAccount } from 'mastodon/actions/accounts';
import { Button } from 'mastodon/components/button';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
});
const selectRepliedToAccountIds = createSelector(
[
(state) => state.get('statuses'),
(_, statusIds) => statusIds,
],
(statusesMap, statusIds) => statusIds.map((statusId) => statusesMap.getIn([statusId, 'in_reply_to_account_id'])),
{
resultEqualityCheck: shallowEqual,
}
);
const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedDomains, onSubmit, onChangeComment, onToggleDomain }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const loadedRef = useRef(false);
const handleClick = useCallback(() => onSubmit(), [onSubmit]);
const handleChange = useCallback((e) => onChangeComment(e.target.value), [onChangeComment]);
const handleToggleDomain = useCallback(e => onToggleDomain(e.target.value, e.target.checked), [onToggleDomain]);
const handleKeyDown = useCallback((e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
handleClick();
}
}, [handleClick]);
// Memoize accountIds since we don't want it to trigger `useEffect` on each render
const accountIds = useAppSelector((state) => domain ? selectRepliedToAccountIds(state, statusIds) : ImmutableList());
// While we could memoize `availableDomains`, it is pretty inexpensive to recompute
const accountsMap = useAppSelector((state) => state.get('accounts'));
const availableDomains = domain ? OrderedSet([domain]).union(accountIds.map((accountId) => accountsMap.getIn([accountId, 'acct'], '').split('@')[1]).filter(domain => !!domain)) : OrderedSet();
useEffect(() => {
if (loadedRef.current) {
return;
}
loadedRef.current = true;
// First, pre-select known domains
availableDomains.forEach((domain) => {
onToggleDomain(domain, true);
});
// Then, fetch missing replied-to accounts
const unknownAccounts = OrderedSet(accountIds.filter(accountId => accountId && !accountsMap.has(accountId)));
unknownAccounts.forEach((accountId) => {
dispatch(fetchAccount(accountId));
});
});
return (
<>
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3>
<textarea
className='report-dialog-modal__textarea'
placeholder={intl.formatMessage(messages.placeholder)}
value={comment}
onChange={handleChange}
onKeyDown={handleKeyDown}
disabled={isSubmitting}
/>
{isRemote && (
<>
<p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
{ availableDomains.map((domain) => (
<label className='report-dialog-modal__toggle' key={`toggle-${domain}`}>
<Toggle checked={selectedDomains.includes(domain)} disabled={isSubmitting} onChange={handleToggleDomain} value={domain} />
<FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />
</label>
))}
</>
)}
<div className='flex-spacer' />
<div className='report-dialog-modal__actions'>
<Button onClick={handleClick} disabled={isSubmitting}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
</div>
</>
);
};
Comment.propTypes = {
comment: PropTypes.string.isRequired,
domain: PropTypes.string,
statusIds: ImmutablePropTypes.list.isRequired,
isRemote: PropTypes.bool,
isSubmitting: PropTypes.bool,
selectedDomains: ImmutablePropTypes.set.isRequired,
onSubmit: PropTypes.func.isRequired,
onChangeComment: PropTypes.func.isRequired,
onToggleDomain: PropTypes.func.isRequired,
};
export default Comment;

View File

@@ -0,0 +1,217 @@
import { useCallback, useEffect, useId, useRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import type { Map } from 'immutable';
import { OrderedSet } from 'immutable';
import { shallowEqual } from 'react-redux';
import Toggle from 'react-toggle';
import { fetchAccount } from 'mastodon/actions/accounts';
import { Button } from 'mastodon/components/button';
import type { Status } from 'mastodon/models/status';
import type { RootState } from 'mastodon/store';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from 'mastodon/store';
const messages = defineMessages({
placeholder: {
id: 'report.placeholder',
defaultMessage: 'Type or paste additional comments',
},
});
const selectRepliedToAccountIds = createAppSelector(
[
(state: RootState) => state.statuses,
(_: unknown, statusIds: string[]) => statusIds,
],
(statusesMap: Map<string, Status>, statusIds: string[]) =>
statusIds.map(
(statusId) =>
statusesMap.getIn([statusId, 'in_reply_to_account_id']) as string,
),
{
memoizeOptions: {
resultEqualityCheck: shallowEqual,
},
},
);
interface Props {
modalTitle?: React.ReactNode;
comment: string;
domain?: string;
statusIds: string[];
isRemote?: boolean;
isSubmitting?: boolean;
selectedDomains: string[];
submitError?: React.ReactNode;
onSubmit: () => void;
onChangeComment: (newComment: string) => void;
onToggleDomain: (toggledDomain: string, checked: boolean) => void;
}
const Comment: React.FC<Props> = ({
modalTitle,
comment,
domain,
statusIds,
isRemote,
isSubmitting,
selectedDomains,
submitError,
onSubmit,
onChangeComment,
onToggleDomain,
}) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const loadedRef = useRef(false);
const handleSubmit = useCallback(() => {
onSubmit();
}, [onSubmit]);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChangeComment(e.target.value);
},
[onChangeComment],
);
const handleToggleDomain = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onToggleDomain(e.target.value, e.target.checked);
},
[onToggleDomain],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
handleSubmit();
}
},
[handleSubmit],
);
// Memoize accountIds since we don't want it to trigger `useEffect` on each render
const accountIds = useAppSelector((state) =>
domain ? selectRepliedToAccountIds(state, statusIds) : [],
);
// While we could memoize `availableDomains`, it is pretty inexpensive to recompute
const accountsMap = useAppSelector((state) => state.accounts);
const availableDomains = domain
? OrderedSet([domain]).union(
accountIds
.map(
(accountId) =>
(accountsMap.getIn([accountId, 'acct'], '') as string).split(
'@',
)[1],
)
.filter((domain): domain is string => !!domain),
)
: OrderedSet<string>();
useEffect(() => {
if (loadedRef.current) {
return;
}
loadedRef.current = true;
// First, pre-select known domains
availableDomains.forEach((domain) => {
onToggleDomain(domain, true);
});
// Then, fetch missing replied-to accounts
const unknownAccounts = OrderedSet(
accountIds.filter(
(accountId) => accountId && !accountsMap.has(accountId),
),
);
unknownAccounts.forEach((accountId) => {
dispatch(fetchAccount(accountId));
});
});
const titleId = useId();
return (
<>
<h3 className='report-dialog-modal__title' id={titleId}>
{modalTitle ?? (
<FormattedMessage
id='report.comment.title'
defaultMessage='Is there anything else you think we should know?'
/>
)}
</h3>
<textarea
className='report-dialog-modal__textarea'
placeholder={intl.formatMessage(messages.placeholder)}
aria-labelledby={titleId}
value={comment}
onChange={handleChange}
onKeyDown={handleKeyDown}
disabled={isSubmitting}
/>
{isRemote && (
<>
<p className='report-dialog-modal__lead'>
<FormattedMessage
id='report.forward_hint'
defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?'
/>
</p>
{availableDomains.map((domain) => (
<label
className='report-dialog-modal__toggle'
key={`toggle-${domain}`}
htmlFor={`input-${domain}`}
>
<Toggle
checked={selectedDomains.includes(domain)}
disabled={isSubmitting}
onChange={handleToggleDomain}
value={domain}
id={`input-${domain}`}
/>
<FormattedMessage
id='report.forward'
defaultMessage='Forward to {target}'
values={{ target: domain }}
/>
</label>
))}
</>
)}
{submitError}
<div className='flex-spacer' />
<div className='report-dialog-modal__actions'>
<Button onClick={handleSubmit} disabled={isSubmitting}>
<FormattedMessage id='report.submit' defaultMessage='Submit report' />
</Button>
</div>
</>
);
};
// eslint-disable-next-line import/no-default-export
export default Comment;

View File

@@ -10,6 +10,7 @@ import {
BlockModal,
DomainBlockModal,
ReportModal,
ReportCollectionModal,
EmbedModal,
ListAdder,
CompareHistoryModal,
@@ -77,6 +78,7 @@ export const MODAL_COMPONENTS = {
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,
'REPORT': ReportModal,
'REPORT_COLLECTION': ReportCollectionModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
'FOCAL_POINT': () => Promise.resolve({ default: AltTextModal }),
@@ -90,6 +92,7 @@ export const MODAL_COMPONENTS = {
'ANNUAL_REPORT': AnnualReportModal,
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
'ACCOUNT_NOTE': () => import('@/mastodon/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
'ACCOUNT_FIELD_OVERFLOW': () => import('@/mastodon/features/account_timeline/modals/field_modal').then(module => ({ default: module.AccountFieldModal })),
'ACCOUNT_EDIT_NAME': () => import('@/mastodon/features/account_edit/components/name_modal').then(module => ({ default: module.NameModal })),
'ACCOUNT_EDIT_BIO': () => import('@/mastodon/features/account_edit/components/bio_modal').then(module => ({ default: module.BioModal })),
};

View File

@@ -0,0 +1,173 @@
import { useCallback, useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Callout } from '@/mastodon/components/callout';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { submitReport } from 'mastodon/actions/reports';
import { fetchServer } from 'mastodon/actions/server';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Button } from 'mastodon/components/button';
import { IconButton } from 'mastodon/components/icon_button';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useAppDispatch } from 'mastodon/store';
import Comment from '../../report/comment';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const CollectionThanks: React.FC<{
onClose: () => void;
}> = ({ onClose }) => {
return (
<>
<h3 className='report-dialog-modal__title'>
<FormattedMessage
id='report.thanks.title_actionable'
defaultMessage="Thanks for reporting, we'll look into this."
/>
</h3>
<div className='flex-spacer' />
<div className='report-dialog-modal__actions'>
<Button onClick={onClose}>
<FormattedMessage id='report.close' defaultMessage='Done' />
</Button>
</div>
</>
);
};
export const ReportCollectionModal: React.FC<{
collection: ApiCollectionJSON;
onClose: () => void;
}> = ({ collection, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { id: collectionId, name, account_id } = collection;
const account = useAccount(account_id);
useEffect(() => {
dispatch(fetchServer());
}, [dispatch]);
const [submitState, setSubmitState] = useState<
'idle' | 'submitting' | 'submitted' | 'error'
>('idle');
const [step, setStep] = useState<'comment' | 'thanks'>('comment');
const [comment, setComment] = useState('');
const [selectedDomains, setSelectedDomains] = useState<string[]>([]);
const handleDomainToggle = useCallback((domain: string, checked: boolean) => {
if (checked) {
setSelectedDomains((domains) => [...domains, domain]);
} else {
setSelectedDomains((domains) => domains.filter((d) => d !== domain));
}
}, []);
const handleSubmit = useCallback(() => {
setSubmitState('submitting');
dispatch(
submitReport(
{
account_id,
status_ids: [],
collection_ids: [collectionId],
forward_to_domains: selectedDomains,
comment,
forward: selectedDomains.length > 0,
category: 'spam',
},
() => {
setSubmitState('submitted');
setStep('thanks');
},
() => {
setSubmitState('error');
},
),
);
}, [account_id, comment, dispatch, collectionId, selectedDomains]);
if (!account) {
return null;
}
const domain = account.get('acct').split('@')[1];
const isRemote = !!domain;
let stepComponent;
switch (step) {
case 'comment':
stepComponent = (
<Comment
modalTitle={
<FormattedMessage
id='report.collection_comment'
defaultMessage='Why do you want to report this collection?'
/>
}
submitError={
submitState === 'error' && (
<Callout
variant='error'
title={
<FormattedMessage
id='report.submission_error'
defaultMessage='Report could not be submitted'
/>
}
>
<FormattedMessage
id='report.submission_error_details'
defaultMessage='Please check your network connection and try again later.'
/>
</Callout>
)
}
onSubmit={handleSubmit}
isSubmitting={submitState === 'submitting'}
isRemote={isRemote}
comment={comment}
domain={domain}
onChangeComment={setComment}
statusIds={[]}
selectedDomains={selectedDomains}
onToggleDomain={handleDomainToggle}
/>
);
break;
case 'thanks':
stepComponent = <CollectionThanks onClose={onClose} />;
}
return (
<div className='modal-root__modal report-dialog-modal'>
<div className='report-modal__target'>
<IconButton
className='report-modal__close'
title={intl.formatMessage(messages.close)}
icon='times'
iconComponent={CloseIcon}
onClick={onClose}
/>
<FormattedMessage
id='report.target'
defaultMessage='Report {target}'
values={{ target: <strong>{name}</strong> }}
/>
</div>
<div className='report-dialog-modal__container'>{stepComponent}</div>
</div>
);
};

View File

@@ -22,7 +22,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
import { isClientFeatureEnabled, isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { isClientFeatureEnabled } from '@/mastodon/utils/environment';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache';
@@ -80,7 +80,6 @@ import {
PrivacyPolicy,
TermsOfService,
AccountFeatured,
AccountAbout,
AccountEdit,
AccountEditFeaturedTags,
Quotes,
@@ -166,36 +165,6 @@ class SwitchingColumnsArea extends PureComponent {
}
const profileRedesignRoutes = [];
if (isServerFeatureEnabled('profile_redesign')) {
profileRedesignRoutes.push(
<WrappedRoute key="posts" path={['/@:acct/posts', '/accounts/:id/posts']} exact component={AccountTimeline} content={children} />,
);
// Check if we're in single-column mode. Confusingly, the singleColumn prop includes mobile.
if (this.props.layout === 'single-column') {
// When in single column mode (desktop w/o advanced view), redirect both the root and about to the posts tab.
profileRedesignRoutes.push(
<Redirect key="acct-redirect" from='/@:acct' to='/@:acct/posts' exact />,
<Redirect key="id-redirect" from='/accounts/:id' to='/accounts/:id/posts' exact />,
<Redirect key="about-acct-redirect" from='/@:acct/about' to='/@:acct/posts' exact />,
<Redirect key="about-id-redirect" from='/accounts/:id/about' to='/accounts/:id/posts' exact />,
);
} else {
// Otherwise, provide and redirect to the /about page.
profileRedesignRoutes.push(
<WrappedRoute key="about" path={['/@:acct/about', '/accounts/:id/about']} component={AccountAbout} content={children} />,
<Redirect key="acct-redirect" from='/@:acct' to='/@:acct/about' exact />,
<Redirect key="id-redirect" from='/accounts/:id' to='/accounts/:id/about' exact />
);
}
} else {
profileRedesignRoutes.push(
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />,
// If the redesign is not enabled but someone shares an /about link, redirect to the root.
<Redirect key="about-acct-redirect" from='/@:acct/about' to='/@:acct' exact />,
<Redirect key="about-id-redirect" from='/accounts/:id/about' to='/accounts/:id' exact />
);
}
if (isClientFeatureEnabled('profile_editing')) {
profileRedesignRoutes.push(
<WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />,
@@ -257,6 +226,7 @@ class SwitchingColumnsArea extends PureComponent {
{...profileRedesignRoutes}
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />

View File

@@ -93,11 +93,6 @@ export function AccountFeatured() {
return import('../../account_featured');
}
export function AccountAbout() {
return import('../../account_about')
.then((module) => ({ default: module.AccountAbout }));
}
export function AccountEdit() {
return import('../../account_edit')
.then((module) => ({ default: module.AccountEdit }));
@@ -172,6 +167,11 @@ export function ReportModal () {
return import('../components/report_modal');
}
export function ReportCollectionModal () {
return import('../components/report_collection_modal')
.then((module) => ({ default: module.ReportCollectionModal }));;
}
export function IgnoreNotificationsModal () {
return import('../components/ignore_notifications_modal');
}

View File

@@ -0,0 +1,29 @@
import { useEffect, useRef } from 'react';
export function useResizeObserver(callback: ResizeObserverCallback) {
const observerRef = useRef<ResizeObserver | null>(null);
observerRef.current ??= new ResizeObserver(callback);
useEffect(() => {
const observer = observerRef.current;
return () => {
observer?.disconnect();
};
}, []);
return observerRef.current;
}
export function useMutationObserver(callback: MutationCallback) {
const observerRef = useRef<MutationObserver | null>(null);
observerRef.current ??= new MutationObserver(callback);
useEffect(() => {
const observer = observerRef.current;
return () => {
observer?.disconnect();
};
}, []);
return observerRef.current;
}

View File

@@ -1,6 +1,8 @@
import type { MutableRefObject, RefCallback } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
import { useMutationObserver, useResizeObserver } from './useObserver';
/**
* Hook to manage overflow of items in a container with a "more" button.
*
@@ -182,48 +184,30 @@ export function useOverflowObservers({
// This is the item container element.
const listRef = useRef<HTMLElement | null>(null);
// Set up observers to watch for size and content changes.
const resizeObserverRef = useRef<ResizeObserver | null>(null);
const mutationObserverRef = useRef<MutationObserver | null>(null);
// Helper to get or create the resize observer.
const resizeObserver = useCallback(() => {
const observer = (resizeObserverRef.current ??= new ResizeObserver(
onRecalculate,
));
return observer;
}, [onRecalculate]);
const resizeObserver = useResizeObserver(onRecalculate);
// Iterate through children and observe them for size changes.
const handleChildrenChange = useCallback(() => {
const listEle = listRef.current;
const observer = resizeObserver();
if (listEle) {
for (const child of listEle.children) {
if (child instanceof HTMLElement) {
observer.observe(child);
resizeObserver.observe(child);
}
}
}
onRecalculate();
}, [onRecalculate, resizeObserver]);
// Helper to get or create the mutation observer.
const mutationObserver = useCallback(() => {
const observer = (mutationObserverRef.current ??= new MutationObserver(
handleChildrenChange,
));
return observer;
}, [handleChildrenChange]);
const mutationObserver = useMutationObserver(handleChildrenChange);
// Set up observers.
const handleObserve = useCallback(() => {
if (wrapperRef.current) {
resizeObserver().observe(wrapperRef.current);
resizeObserver.observe(wrapperRef.current);
}
if (listRef.current) {
mutationObserver().observe(listRef.current, { childList: true });
mutationObserver.observe(listRef.current, { childList: true });
handleChildrenChange();
}
}, [handleChildrenChange, mutationObserver, resizeObserver]);
@@ -233,12 +217,12 @@ export function useOverflowObservers({
const wrapperRefCallback = useCallback(
(node: HTMLElement | null) => {
if (node) {
wrapperRef.current = node;
wrapperRef.current = node; // eslint-disable-line react-hooks/immutability -- https://github.com/facebook/react/issues/34955
handleObserve();
if (typeof onWrapperRef === 'function') {
onWrapperRef(node);
} else if (onWrapperRef && 'current' in onWrapperRef) {
onWrapperRef.current = node;
onWrapperRef.current = node; // eslint-disable-line react-hooks/immutability -- https://github.com/facebook/react/issues/34955
}
}
},
@@ -254,28 +238,13 @@ export function useOverflowObservers({
if (typeof onListRef === 'function') {
onListRef(node);
} else if (onListRef && 'current' in onListRef) {
onListRef.current = node;
onListRef.current = node; // eslint-disable-line react-hooks/immutability -- https://github.com/facebook/react/issues/34955
}
}
},
[handleObserve, onListRef],
);
useEffect(() => {
handleObserve();
return () => {
if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect();
resizeObserverRef.current = null;
}
if (mutationObserverRef.current) {
mutationObserverRef.current.disconnect();
mutationObserverRef.current = null;
}
};
}, [handleObserve]);
return {
wrapperRefCallback,
listRefCallback,

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Fremhævede hashtags hjælper brugere med at finde og interagere med din profil. De vises som filtre i aktivitetsvisningen på din profilside.",
"account_edit_tags.search_placeholder": "Angiv et hashtag…",
"account_edit_tags.suggestions": "Forslag:",
"account_edit_tags.tag_status_count": "{count} indlæg",
"account_edit_tags.tag_status_count": "{count, plural, one {# indlæg} other {# indlæg}}",
"account_note.placeholder": "Klik for at tilføje notat",
"admin.dashboard.daily_retention": "Brugerfastholdelsesrate pr. dag efter tilmelding",
"admin.dashboard.monthly_retention": "Brugerfastholdelsesrate pr. måned efter tilmelding",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "Ingen samlinger endnu.",
"collections.old_last_post_note": "Seneste indlæg er fra over en uge siden",
"collections.remove_account": "Fjern denne konto",
"collections.report_collection": "Anmeld denne samling",
"collections.search_accounts_label": "Søg efter konti for at tilføje…",
"collections.search_accounts_max_reached": "Du har tilføjet det maksimale antal konti",
"collections.sensitive": "Sensitivt",
"collections.topic_hint": "Tilføj et hashtag, der hjælper andre med at forstå det overordnede emne for denne samling.",
"collections.view_collection": "Vis samling",
"collections.view_other_collections_by_user": "Se andre samlinger af denne bruger",
"collections.visibility_public": "Offentlig",
"collections.visibility_public_hint": "Kan opdages i søgeresultater og andre områder, hvor anbefalinger vises.",
"collections.visibility_title": "Synlighed",
@@ -976,6 +978,7 @@
"report.category.title_account": "profil",
"report.category.title_status": "indlæg",
"report.close": "Udført",
"report.collection_comment": "Hvorfor vil du anmelde denne samling?",
"report.comment.title": "Er der andet, som vi bør vide?",
"report.forward": "Videresend til {target}",
"report.forward_hint": "Kontoen er fra en anden server. Send også en anonymiseret kopi af anmeldelsen dertil?",
@@ -997,6 +1000,8 @@
"report.rules.title": "Hvilke regler overtrædes?",
"report.statuses.subtitle": "Vælg alle relevante",
"report.statuses.title": "Er der indlæg, som kan bekræfte denne anmeldelse?",
"report.submission_error": "Anmeldelse kunne ikke indsendes",
"report.submission_error_details": "Tjek din netværksforbindelse eller prøv igen senere.",
"report.submit": "Indsend",
"report.target": "Anmelder {target}",
"report.thanks.take_action": "Her er mulighederne for at styre, hvad du ser på Mastodon:",

View File

@@ -88,7 +88,7 @@
"account.menu.hide_reblogs": "Geteilte Beiträge in der Timeline ausblenden",
"account.menu.mention": "Erwähnen",
"account.menu.mute": "Konto stummschalten",
"account.menu.note.description": "Nur für Sie sichtbar",
"account.menu.note.description": "Nur für dich sichtbar",
"account.menu.open_original_page": "Auf {domain} ansehen",
"account.menu.remove_follower": "Follower entfernen",
"account.menu.report": "Konto melden",
@@ -104,11 +104,11 @@
"account.muted": "Stummgeschaltet",
"account.muting": "Stummgeschaltet",
"account.mutual": "Ihr folgt einander",
"account.name.help.domain": "{domain} ist der Server, auf dem das Profil und die Beiträge des Benutzers gespeichert sind.",
"account.name.help.domain_self": "{domain} ist Ihr Server, auf dem Ihr Profil und Ihre Beiträge gespeichert sind.",
"account.name.help.footer": "Genauso wie Sie E-Mails über verschiedene E-Mail-Anbieter versenden können, Sie können auch mit Personen auf anderen Mastodon Servers interagieren und mit allen auf anderen sozialen Apps, die nach denselben Regeln wie Mastodon funktionieren (dem ActivityPub Protokoll).",
"account.name.help.domain": "{domain} ist der Server, auf dem das Profil registriert ist und die Beiträge verwaltet werden.",
"account.name.help.domain_self": "{domain} ist der Server, auf dem du registriert bist und deine Beiträge verwaltet werden.",
"account.name.help.footer": "So wie du E-Mails an andere trotz unterschiedlicher E-Mail-Clients senden kannst, so kannst du auch mit anderen Profilen auf unterschiedlichen Mastodon-Servern interagieren. Wenn andere soziale Apps die gleichen Kommunikationsregeln (das ActivityPub-Protokoll) wie Mastodon verwenden, dann funktioniert die Kommunikation auch dort.",
"account.name.help.header": "Deine Adresse im Fediverse ist wie eine E-Mail-Adresse",
"account.name.help.username": "{username} ist der Profilname dieses Kontos auf diesem Server. Jemand auf einem anderen Server könnte denselben Profilnamen haben.",
"account.name.help.username": "{username} ist der Profilname auf deren Server. Es ist möglich, dass jemand auf einem anderen Server den gleichen Profilnamen hat.",
"account.name.help.username_self": "{username} ist dein Profilname auf diesem Server. Es ist möglich, dass jemand auf einem anderen Server den gleichen Profilnamen hat.",
"account.name_info": "Was bedeutet das?",
"account.no_bio": "Keine Beschreibung verfügbar.",
@@ -153,10 +153,10 @@
"account_edit.column_title": "Profil bearbeiten",
"account_edit.custom_fields.placeholder": "Ergänze deine Pronomen, weiterführenden Links oder etwas anderes, das du teilen möchtest.",
"account_edit.custom_fields.title": "Zusatzfelder",
"account_edit.display_name.placeholder": "Ihr Anzeigename ist der Name, der in Ihrem Profil und auf Ihrer \"Timeline\" angezeigt wird.",
"account_edit.display_name.placeholder": "Dein Anzeigename wird auf deinem Profil und in Timelines angezeigt.",
"account_edit.display_name.title": "Anzeigename",
"account_edit.featured_hashtags.item": "Hashtags",
"account_edit.featured_hashtags.placeholder": "Helfen Sie anderen dabei, Ihre Lieblingsthemen zu identifizieren und schnell darauf zuzugreifen.",
"account_edit.featured_hashtags.placeholder": "Präsentiere deine Lieblingsthemen und ermögliche anderen einen schnellen Zugriff darauf.",
"account_edit.featured_hashtags.title": "Vorgestellte Hashtags",
"account_edit.name_modal.add_title": "Anzeigenamen hinzufügen",
"account_edit.name_modal.edit_title": "Anzeigenamen bearbeiten",
@@ -164,10 +164,10 @@
"account_edit.profile_tab.title": "Profil-Tab-Einstellungen",
"account_edit.save": "Speichern",
"account_edit_tags.column_title": "Vorgestellte Hashtags bearbeiten",
"account_edit_tags.help_text": "Vorgestellte Hashtags können dabei helfen, dein Profil zu entdecken und Kontakt mit dir aufzunehmen. Diese erscheinen als Filter in der Aktivitätenübersicht deines Profils.",
"account_edit_tags.help_text": "Vorgestellte Hashtags können dabei helfen, dein Profil zu entdecken und besser mit dir zu interagieren. Sie erscheinen in der Aktivitätenübersicht deines Profils und dienen als Filter.",
"account_edit_tags.search_placeholder": "Gib einen Hashtag ein …",
"account_edit_tags.suggestions": "Vorschläge:",
"account_edit_tags.tag_status_count": "{count} Beiträge",
"account_edit_tags.tag_status_count": "{count, plural, one {# Beitrag} other {# Beiträge}}",
"account_note.placeholder": "Klicken, um private Anmerkung hinzuzufügen",
"admin.dashboard.daily_retention": "Verweildauer der Nutzer*innen pro Tag seit der Registrierung",
"admin.dashboard.monthly_retention": "Verweildauer der Nutzer*innen pro Monat seit der Registrierung",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.",
"collections.old_last_post_note": "Neuester Beitrag mehr als eine Woche alt",
"collections.remove_account": "Dieses Konto entfernen",
"collections.report_collection": "Sammlung melden",
"collections.search_accounts_label": "Suche nach Konten, um sie hinzuzufügen …",
"collections.search_accounts_max_reached": "Du hast die Höchstzahl an Konten hinzugefügt",
"collections.sensitive": "Inhaltswarnung",
"collections.topic_hint": "Ein Hashtag für diese Sammlung kann anderen dabei helfen, dein Anliegen besser einordnen zu können.",
"collections.view_collection": "Sammlungen anzeigen",
"collections.view_other_collections_by_user": "Andere Sammlungen dieses Kontos ansehen",
"collections.visibility_public": "Öffentlich",
"collections.visibility_public_hint": "Wird in den Suchergebnissen und anderen Bereichen mit Empfehlungen angezeigt.",
"collections.visibility_title": "Sichtbarkeit",
@@ -976,6 +978,7 @@
"report.category.title_account": "Profil",
"report.category.title_status": "Beitrag",
"report.close": "Fertig",
"report.collection_comment": "Weshalb möchtest du diese Sammlung melden?",
"report.comment.title": "Gibt es noch etwas, das wir wissen sollten?",
"report.forward": "Meldung auch an den externen Server {target} weiterleiten",
"report.forward_hint": "Das gemeldete Konto befindet sich auf einem anderen Server. Soll zusätzlich eine anonymisierte Kopie deiner Meldung an diesen Server geschickt werden?",
@@ -997,6 +1000,8 @@
"report.rules.title": "Gegen welche Regeln wurde verstoßen?",
"report.statuses.subtitle": "Wähle alle zutreffenden Inhalte aus",
"report.statuses.title": "Gibt es Beiträge, die diese Meldung stützen?",
"report.submission_error": "Meldung konnte nicht übermittelt werden",
"report.submission_error_details": "Bitte prüfe deine Internetverbindung und probiere es später erneut.",
"report.submit": "Senden",
"report.target": "{target} melden",
"report.thanks.take_action": "Das sind deine Möglichkeiten zu bestimmen, was du auf Mastodon sehen möchtest:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Οι αναδεδειγμένες ετικέτες βοηθούν τους χρήστες να ανακαλύψουν και να αλληλεπιδράσουν με το προφίλ σας. Εμφανίζονται ως φίλτρα στην προβολή Δραστηριότητας της σελίδας προφίλ σας.",
"account_edit_tags.search_placeholder": "Εισάγετε μια ετικέτα…",
"account_edit_tags.suggestions": "Προτάσεις:",
"account_edit_tags.tag_status_count": "{count} αναρτήσεις",
"account_edit_tags.tag_status_count": "{count, plural, one {# ανάρτηση} other {# αναρτήσεις}}",
"account_note.placeholder": "Κάνε κλικ για να προσθέσεις σημείωση",
"admin.dashboard.daily_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά ημέρα",
"admin.dashboard.monthly_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά μήνα",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "Καμία συλλογή ακόμη.",
"collections.old_last_post_note": "Τελευταία ανάρτηση πριν από μια εβδομάδα",
"collections.remove_account": "Αφαίρεση λογαριασμού",
"collections.report_collection": "Αναφορά αυτής της συλλογής",
"collections.search_accounts_label": "Αναζήτηση λογαριασμών για προσθήκη…",
"collections.search_accounts_max_reached": "Έχετε προσθέσει τον μέγιστο αριθμό λογαριασμών",
"collections.sensitive": "Ευαίσθητο",
"collections.topic_hint": "Προσθέστε μια ετικέτα που βοηθά άλλους να κατανοήσουν το κύριο θέμα αυτής της συλλογής.",
"collections.view_collection": "Προβολή συλλογής",
"collections.view_other_collections_by_user": "Δείτε άλλες συλλογές από αυτόν τον χρήστη",
"collections.visibility_public": "Δημόσια",
"collections.visibility_public_hint": "Ανιχνεύσιμη στα αποτελέσματα αναζήτησης και σε άλλα σημεία όπου εμφανίζονται προτάσεις.",
"collections.visibility_title": "Ορατότητα",
@@ -976,13 +978,14 @@
"report.category.title_account": "προφίλ",
"report.category.title_status": "ανάρτηση",
"report.close": "Τέλος",
"report.collection_comment": "Γιατί θέλετε να αναφέρετε αυτήν τη συλλογή;",
"report.comment.title": "Υπάρχει κάτι άλλο που νομίζεις ότι θα πρέπει να γνωρίζουμε;",
"report.forward": "Προώθηση προς {target}",
"report.forward_hint": "Ο λογαριασμός είναι από διαφορετικό διακομιστή. Να σταλεί ανώνυμο αντίγραφο της αναφοράς και εκεί;",
"report.mute": "Σίγαση",
"report.mute_explanation": "Δεν θα βλέπεις τις αναρτήσεις τους. Εκείνοι μπορούν ακόμη να σε ακολουθούν και να βλέπουν τις αναρτήσεις σου χωρίς να γνωρίζουν ότι είναι σε σίγαση.",
"report.next": "Επόμενο",
"report.placeholder": "Επιπλέον σχόλια",
"report.placeholder": "Επιπρόσθετα σχόλια",
"report.reasons.dislike": "Δεν μου αρέσει",
"report.reasons.dislike_description": "Δεν είναι κάτι που θα ήθελες να δεις",
"report.reasons.legal": "Είναι παράνομο",
@@ -997,6 +1000,8 @@
"report.rules.title": "Ποιοι κανόνες παραβιάζονται;",
"report.statuses.subtitle": "Επίλεξε όλα όσα ισχύουν",
"report.statuses.title": "Υπάρχουν αναρτήσεις που τεκμηριώνουν αυτή την αναφορά;",
"report.submission_error": "Δεν ήταν δυνατή η υποβολή της αναφοράς",
"report.submission_error_details": "Παρακαλούμε ελέγξτε τη σύνδεση δικτύου σας και προσπαθήστε ξανά αργότερα.",
"report.submit": "Υποβολή",
"report.target": "Αναφορά {target}",
"report.thanks.take_action": "Αυτές είναι οι επιλογές σας για να ελέγχετε τι βλέπετε στο Mastodon:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Featured hashtags help users discover and interact with your profile. They appear as filters on your Profile pages Activity view.",
"account_edit_tags.search_placeholder": "Enter a hashtag…",
"account_edit_tags.suggestions": "Suggestions:",
"account_edit_tags.tag_status_count": "{count} posts",
"account_edit_tags.tag_status_count": "{count, plural, one {# post} other {# posts}}",
"account_note.placeholder": "Click to add note",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "No collections yet.",
"collections.old_last_post_note": "Last posted over a week ago",
"collections.remove_account": "Remove this account",
"collections.report_collection": "Report this collection",
"collections.search_accounts_label": "Search for accounts to add…",
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
"collections.sensitive": "Sensitive",
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.",
"collections.view_collection": "View collection",
"collections.view_other_collections_by_user": "View other collections by this user",
"collections.visibility_public": "Public",
"collections.visibility_public_hint": "Discoverable in search results and other areas where recommendations appear.",
"collections.visibility_title": "Visibility",
@@ -976,6 +978,7 @@
"report.category.title_account": "profile",
"report.category.title_status": "post",
"report.close": "Done",
"report.collection_comment": "Why do you want to report this collection?",
"report.comment.title": "Is there anything else you think we should know?",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymised copy of the report there as well?",
@@ -997,6 +1000,8 @@
"report.rules.title": "Which rules are being violated?",
"report.statuses.subtitle": "Select all that apply",
"report.statuses.title": "Are there any posts that back up this report?",
"report.submission_error": "Report could not be submitted",
"report.submission_error_details": "Please check your network connection and try again later.",
"report.submit": "Submit",
"report.target": "Reporting {target}",
"report.thanks.take_action": "Here are your options for controlling what you see on Mastodon:",

View File

@@ -13,7 +13,6 @@
"about.not_available": "This information has not been made available on this server.",
"about.powered_by": "Decentralized social media powered by {mastodon}",
"about.rules": "Server rules",
"account.about": "About",
"account.account_note_header": "Personal note",
"account.activity": "Activity",
"account.add_note": "Add a personal note",
@@ -45,9 +44,11 @@
"account.familiar_followers_two": "Followed by {name1} and {name2}",
"account.featured": "Featured",
"account.featured.accounts": "Profiles",
"account.featured.collections": "Collections",
"account.featured.hashtags": "Hashtags",
"account.featured_tags.last_status_at": "Last post on {date}",
"account.featured_tags.last_status_never": "No posts",
"account.field_overflow": "Show full content",
"account.filters.all": "All activity",
"account.filters.boosts_toggle": "Show boosts",
"account.filters.posts_boosts": "Posts and boosts",
@@ -306,11 +307,13 @@
"collections.no_collections_yet": "No collections yet.",
"collections.old_last_post_note": "Last posted over a week ago",
"collections.remove_account": "Remove this account",
"collections.report_collection": "Report this collection",
"collections.search_accounts_label": "Search for accounts to add…",
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
"collections.sensitive": "Sensitive",
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.",
"collections.view_collection": "View collection",
"collections.view_other_collections_by_user": "View other collections by this user",
"collections.visibility_public": "Public",
"collections.visibility_public_hint": "Discoverable in search results and other areas where recommendations appear.",
"collections.visibility_title": "Visibility",
@@ -496,8 +499,6 @@
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.account_about.me": "You have not added any information about yourself yet.",
"empty_column.account_about.other": "{acct} has not added any information about themselves yet.",
"empty_column.account_featured.me": "You have not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friends accounts on your profile?",
"empty_column.account_featured.other": "{acct} has not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friends accounts on your profile?",
"empty_column.account_featured_other.unknown": "This account has not featured anything yet.",
@@ -976,6 +977,7 @@
"report.category.title_account": "profile",
"report.category.title_status": "post",
"report.close": "Done",
"report.collection_comment": "Why do you want to report this collection?",
"report.comment.title": "Is there anything else you think we should know?",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
@@ -997,6 +999,8 @@
"report.rules.title": "Which rules are being violated?",
"report.statuses.subtitle": "Select all that apply",
"report.statuses.title": "Are there any posts that back up this report?",
"report.submission_error": "Report could not be submitted",
"report.submission_error_details": "Please check your network connection and try again later.",
"report.submit": "Submit",
"report.target": "Reporting {target}",
"report.thanks.take_action": "Here are your options for controlling what you see on Mastodon:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Las etiquetas destacadas ayudan a los usuarios a descubrir e interactuar con tu perfil. Las etiquetas destacadas aparecen como filtros en la vista de actividad de la página de tu perfil.",
"account_edit_tags.search_placeholder": "Ingresá una etiqueta…",
"account_edit_tags.suggestions": "Sugerencias:",
"account_edit_tags.tag_status_count": "{count} mensajes",
"account_edit_tags.tag_status_count": "{count, plural, one {voto} other {votos}}",
"account_note.placeholder": "Hacé clic par agregar una nota",
"admin.dashboard.daily_retention": "Tasa de retención de usuarios por día, después del registro",
"admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes, después del registro",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "No hay colecciones aún.",
"collections.old_last_post_note": "Último mensaje hace más de una semana",
"collections.remove_account": "Eliminar esta cuenta",
"collections.report_collection": "Denunciar esta colección",
"collections.search_accounts_label": "Buscar cuentas para agregar…",
"collections.search_accounts_max_reached": "Agregaste el número máximo de cuentas",
"collections.sensitive": "Sensible",
"collections.topic_hint": "Agregá una etiqueta que ayude a otros usuarios a entender el tema principal de esta colección.",
"collections.view_collection": "Abrir colección",
"collections.view_other_collections_by_user": "Ver otras colecciones de este usuario",
"collections.visibility_public": "Pública",
"collections.visibility_public_hint": "Puede ser descubierta en los resultados de búsqueda y en otras áreas donde aparezcan recomendaciones.",
"collections.visibility_title": "Visibilidad",
@@ -976,6 +978,7 @@
"report.category.title_account": "perfil",
"report.category.title_status": "mensaje",
"report.close": "Listo",
"report.collection_comment": "¿Por qué querés denunciar esta colección?",
"report.comment.title": "¿Hay algo más que creés que deberíamos saber?",
"report.forward": "Reenviar a {target}",
"report.forward_hint": "La cuenta es de otro servidor. ¿Querés enviar una copia anonimizada del informe también ahí?",
@@ -997,6 +1000,8 @@
"report.rules.title": "¿Qué reglas se están violando?",
"report.statuses.subtitle": "Seleccioná todo lo que corresponda",
"report.statuses.title": "¿Hay algún mensaje que respalde esta denuncia?",
"report.submission_error": "No se pudo enviar la denuncia",
"report.submission_error_details": "Por favor, revisá tu conexión a Internet e intentá de nuevo más tarde.",
"report.submit": "Enviar",
"report.target": "Denunciando a {target}",
"report.thanks.take_action": "Acá están tus opciones para controlar lo que ves en Mastodon:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Las etiquetas destacadas ayudan a los usuarios a descubrir tu perfil e interactuar con él. Aparecen como filtros en la vista Actividad de tu página de perfil.",
"account_edit_tags.search_placeholder": "Introduce una etiqueta…",
"account_edit_tags.suggestions": "Sugerencias:",
"account_edit_tags.tag_status_count": "{count} publicaciones",
"account_edit_tags.tag_status_count": "{count, plural,one {# publicación} other {# publicaciones}}",
"account_note.placeholder": "Haz clic para añadir una nota",
"admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después de unirse",
"admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después de unirse",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "No hay colecciones todavía.",
"collections.old_last_post_note": "Última publicación hace más de una semana",
"collections.remove_account": "Eliminar esta cuenta",
"collections.report_collection": "Reportar esta colección",
"collections.search_accounts_label": "Buscar cuentas para añadir…",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
"collections.sensitive": "Sensible",
"collections.topic_hint": "Añade una etiqueta que ayude a los demás a comprender el tema principal de esta colección.",
"collections.view_collection": "Ver colección",
"collections.view_other_collections_by_user": "Ver otras colecciones de este usuario",
"collections.visibility_public": "Pública",
"collections.visibility_public_hint": "Visible en los resultados de búsqueda y otras áreas donde aparecen recomendaciones.",
"collections.visibility_title": "Visibilidad",
@@ -976,6 +978,7 @@
"report.category.title_account": "perfil",
"report.category.title_status": "publicación",
"report.close": "Realizado",
"report.collection_comment": "¿Por qué quieres reportar esta colección?",
"report.comment.title": "¿Hay algo más que creas que deberíamos saber?",
"report.forward": "Reenviar a {target}",
"report.forward_hint": "La cuenta es de otro servidor. ¿Enviar también una copia anónima del informe allí?",
@@ -997,6 +1000,8 @@
"report.rules.title": "¿Cuáles reglas se están infringiendo?",
"report.statuses.subtitle": "Seleccione todas las que apliquen",
"report.statuses.title": "¿Hay alguna publicación que respalde esta denuncia?",
"report.submission_error": "No se pudo enviar el informe",
"report.submission_error_details": "Por favor, comprueba tu conexión a internet y vuelve a intentarlo más tarde.",
"report.submit": "Enviar",
"report.target": "Denunciando a {target}",
"report.thanks.take_action": "Aquí están tus opciones para controlar lo que ves en Mastodon:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Las etiquetas destacadas ayudan a los usuarios a descubrir e interactuar con tu perfil. Aparecen como filtros en la vista de actividad de tu página de perfil.",
"account_edit_tags.search_placeholder": "Introduce una etiqueta…",
"account_edit_tags.suggestions": "Sugerencias:",
"account_edit_tags.tag_status_count": "{count} publicaciones",
"account_edit_tags.tag_status_count": "{count, plural, one {# publicación} other {# publicaciones}}",
"account_note.placeholder": "Haz clic para añadir nota",
"admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después del registro",
"admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después del registro",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "Aún no hay colecciones.",
"collections.old_last_post_note": "Última publicación hace más de una semana",
"collections.remove_account": "Borrar esta cuenta",
"collections.report_collection": "Reportar esta colección",
"collections.search_accounts_label": "Buscar cuentas para añadir…",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
"collections.sensitive": "Sensible",
"collections.topic_hint": "Añadir una etiqueta que ayude a otros a entender el tema principal de esta colección.",
"collections.view_collection": "Ver colección",
"collections.view_other_collections_by_user": "Ver otras colecciones de este usuario",
"collections.visibility_public": "Pública",
"collections.visibility_public_hint": "Puede mostrarse en los resultados de búsqueda y en otros lugares donde aparezcan recomendaciones.",
"collections.visibility_title": "Visibilidad",
@@ -976,6 +978,7 @@
"report.category.title_account": "perfil",
"report.category.title_status": "publicación",
"report.close": "Hecho",
"report.collection_comment": "¿Por qué quieres reportar esta colección?",
"report.comment.title": "¿Hay algo más que creas que deberíamos saber?",
"report.forward": "Reenviar a {target}",
"report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?",
@@ -997,6 +1000,8 @@
"report.rules.title": "¿Qué normas se están violando?",
"report.statuses.subtitle": "Selecciona todos los que correspondan",
"report.statuses.title": "¿Hay alguna publicación que respalde este informe?",
"report.submission_error": "No se pudo enviar el reporte",
"report.submission_error_details": "Comprueba tu conexión de red e intenta volver a intentarlo más tarde.",
"report.submit": "Enviar",
"report.target": "Reportando {target}",
"report.thanks.take_action": "Aquí están tus opciones para controlar lo que ves en Mastodon:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Esiteltävät aihetunnisteet auttavat käyttäjiä löytämään profiilisi ja olemaan vuorovaikutuksessa sen kanssa. Ne näkyvät suodattimina profiilisivusi Toiminta-näkymässä.",
"account_edit_tags.search_placeholder": "Syötä aihetunniste…",
"account_edit_tags.suggestions": "Ehdotuksia:",
"account_edit_tags.tag_status_count": "{count} julkaisua",
"account_edit_tags.tag_status_count": "{count, plural, one {# julkaisu} other {# julkaisua}}",
"account_note.placeholder": "Lisää muistiinpano napsauttamalla",
"admin.dashboard.daily_retention": "Käyttäjien pysyvyys päivittäin rekisteröitymisen jälkeen",
"admin.dashboard.monthly_retention": "Käyttäjien pysyvyys kuukausittain rekisteröitymisen jälkeen",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "Ei vielä kokoelmia.",
"collections.old_last_post_note": "Julkaissut viimeksi yli viikko sitten",
"collections.remove_account": "Poista tämä tili",
"collections.report_collection": "Raportoi tämä kokoelma",
"collections.search_accounts_label": "Hae lisättäviä tilejä…",
"collections.search_accounts_max_reached": "Olet lisännyt enimmäismäärän tilejä",
"collections.sensitive": "Arkaluonteinen",
"collections.topic_hint": "Lisää aihetunniste, joka auttaa muita ymmärtämään tämän kokoelman pääaiheen.",
"collections.view_collection": "Näytä kokoelma",
"collections.view_other_collections_by_user": "Näytä muut tämän käyttäjän kokoelmat",
"collections.visibility_public": "Julkinen",
"collections.visibility_public_hint": "Löydettävissä hakutuloksista ja muualta, jossa ilmenee suosituksia.",
"collections.visibility_title": "Näkyvyys",
@@ -976,6 +978,7 @@
"report.category.title_account": "profiili",
"report.category.title_status": "julkaisu",
"report.close": "Valmis",
"report.collection_comment": "Miksi haluat raportoida tämän kokoelman?",
"report.comment.title": "Onko vielä jotain muuta, mitä meidän pitäisi tietää?",
"report.forward": "Välitä palvelimelle {target}",
"report.forward_hint": "Tämä tili on toisella palvelimella. Haluatko lähettää nimettömän raportin myös sinne?",
@@ -997,6 +1000,8 @@
"report.rules.title": "Mitä sääntöjä rikotaan?",
"report.statuses.subtitle": "Valitse kaikki sopivat",
"report.statuses.title": "Onko julkaisuja, jotka tukevat tätä raporttia?",
"report.submission_error": "Raporttia ei voitu lähettää",
"report.submission_error_details": "Tarkista verkkoyhteytesi ja yritä uudelleen myöhemmin.",
"report.submit": "Lähetä",
"report.target": "Raportoidaan {target}",
"report.thanks.take_action": "Tässä on vaihtoehtosi hallita näkemääsi Mastodonissa:",

View File

@@ -167,7 +167,6 @@
"account_edit_tags.help_text": "Sermerkt frámerki hjálpa brúkarum at varnast og virka saman við vanga tínum. Tey síggjast sum filtur á virksemisvísingini av vanga tínum.",
"account_edit_tags.search_placeholder": "Áset eitt frámerki…",
"account_edit_tags.suggestions": "Uppskot:",
"account_edit_tags.tag_status_count": "{count} postar",
"account_note.placeholder": "Klikka fyri at leggja viðmerking afturat",
"admin.dashboard.daily_retention": "Hvussu nógvir brúkarar eru eftir, síðani tey skrásettu seg, roknað í døgum",
"admin.dashboard.monthly_retention": "Hvussu nógvir brúkarar eru eftir síðani tey skrásettu seg, roknað í mánaðum",

View File

@@ -15,7 +15,7 @@
"about.rules": "Règles du serveur",
"account.about": "À propos",
"account.account_note_header": "Note personnelle",
"account.activity": "Activités",
"account.activity": "Activité",
"account.add_note": "Ajouter une note personnelle",
"account.add_or_remove_from_list": "Ajouter ou enlever de listes",
"account.badges.admin": "Admin",
@@ -145,6 +145,9 @@
"account_edit.bio.title": "Présentation",
"account_edit.bio_modal.add_title": "Ajouter une présentation",
"account_edit.bio_modal.edit_title": "Modifier la présentation",
"account_edit.button.add": "Ajouter {item}",
"account_edit.button.delete": "Supprimer {item}",
"account_edit.button.edit": "Modifier {item}",
"account_edit.char_counter": "{currentLength}/{maxLength} caractères",
"account_edit.column_button": "Terminé",
"account_edit.column_title": "Modifier le profil",
@@ -152,6 +155,7 @@
"account_edit.custom_fields.title": "Champs personnalisés",
"account_edit.display_name.placeholder": "Votre nom public est le nom qui apparaît sur votre profil et dans les fils d'actualités.",
"account_edit.display_name.title": "Nom public",
"account_edit.featured_hashtags.item": "hashtags",
"account_edit.featured_hashtags.placeholder": "Aider les autres à identifier et à accéder rapidement à vos sujets préférés.",
"account_edit.featured_hashtags.title": "Hashtags mis en avant",
"account_edit.name_modal.add_title": "Ajouter un nom public",
@@ -159,6 +163,11 @@
"account_edit.profile_tab.subtitle": "Personnaliser les onglets de votre profil et leur contenu.",
"account_edit.profile_tab.title": "Paramètres de l'onglet du profil",
"account_edit.save": "Enregistrer",
"account_edit_tags.column_title": "Modifier les hashtags mis en avant",
"account_edit_tags.help_text": "Les hashtags mis en avant aident les personnes à découvrir et interagir avec votre profil. Ils apparaissent comme des filtres dans la vue « Activité » de votre profil.",
"account_edit_tags.search_placeholder": "Saisir un hashtag…",
"account_edit_tags.suggestions": "Suggestions :",
"account_edit_tags.tag_status_count": "{count, plural, one {# message} other {# messages}}",
"account_note.placeholder": "Cliquez pour ajouter une note",
"admin.dashboard.daily_retention": "Taux de rétention des comptes par jour après inscription",
"admin.dashboard.monthly_retention": "Taux de rétention des comptes par mois après inscription",
@@ -297,6 +306,7 @@
"collections.no_collections_yet": "Aucune collection pour le moment.",
"collections.old_last_post_note": "Dernière publication il y a plus d'une semaine",
"collections.remove_account": "Supprimer ce compte",
"collections.report_collection": "Signaler cette collection",
"collections.search_accounts_label": "Chercher des comptes à ajouter…",
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
"collections.sensitive": "Sensible",
@@ -768,7 +778,7 @@
"not_signed_in_indicator.not_signed_in": "Vous devez vous connecter pour accéder à cette ressource.",
"notification.admin.report": "{name} a signalé {target}",
"notification.admin.report_account": "{name} a signalé {count, plural, one {un message} other {# messages}} de {target} pour {category}",
"notification.admin.report_account_other": "{name} a signalé {count, plural, one {un message} other {# messages}} depuis {target}",
"notification.admin.report_account_other": "{name} a signalé {count, plural, one {un message} other {# messages}} de {target}",
"notification.admin.report_statuses": "{name} a signalé {target} pour {category}",
"notification.admin.report_statuses_other": "{name} a signalé {target}",
"notification.admin.sign_up": "{name} s'est inscrit·e",
@@ -967,6 +977,7 @@
"report.category.title_account": "ce profil",
"report.category.title_status": "ce message",
"report.close": "Terminé",
"report.collection_comment": "Pourquoi souhaitez-vous signaler cette collection ?",
"report.comment.title": "Y a-t-il autre chose que nous devrions savoir?",
"report.forward": "Transférer à {target}",
"report.forward_hint": "Le compte provient dun autre serveur. Envoyer une copie anonyme du rapport là-bas également?",
@@ -988,6 +999,8 @@
"report.rules.title": "Quelles règles sont enfreintes?",
"report.statuses.subtitle": "Sélectionnez toutes les réponses appropriées",
"report.statuses.title": "Existe-t-il des messages pour étayer ce rapport?",
"report.submission_error": "Le signalement na pas pu être envoyé",
"report.submission_error_details": "Veuillez vérifier votre connexion réseau et réessayer plus tard.",
"report.submit": "Envoyer",
"report.target": "Signalement de {target}",
"report.thanks.take_action": "Voici les possibilités que vous avez pour contrôler ce que vous voyez sur Mastodon:",

View File

@@ -15,7 +15,7 @@
"about.rules": "Règles du serveur",
"account.about": "À propos",
"account.account_note_header": "Note personnelle",
"account.activity": "Activités",
"account.activity": "Activité",
"account.add_note": "Ajouter une note personnelle",
"account.add_or_remove_from_list": "Ajouter ou retirer des listes",
"account.badges.admin": "Admin",
@@ -145,6 +145,9 @@
"account_edit.bio.title": "Présentation",
"account_edit.bio_modal.add_title": "Ajouter une présentation",
"account_edit.bio_modal.edit_title": "Modifier la présentation",
"account_edit.button.add": "Ajouter {item}",
"account_edit.button.delete": "Supprimer {item}",
"account_edit.button.edit": "Modifier {item}",
"account_edit.char_counter": "{currentLength}/{maxLength} caractères",
"account_edit.column_button": "Terminé",
"account_edit.column_title": "Modifier le profil",
@@ -152,6 +155,7 @@
"account_edit.custom_fields.title": "Champs personnalisés",
"account_edit.display_name.placeholder": "Votre nom public est le nom qui apparaît sur votre profil et dans les fils d'actualités.",
"account_edit.display_name.title": "Nom public",
"account_edit.featured_hashtags.item": "hashtags",
"account_edit.featured_hashtags.placeholder": "Aider les autres à identifier et à accéder rapidement à vos sujets préférés.",
"account_edit.featured_hashtags.title": "Hashtags mis en avant",
"account_edit.name_modal.add_title": "Ajouter un nom public",
@@ -159,6 +163,11 @@
"account_edit.profile_tab.subtitle": "Personnaliser les onglets de votre profil et leur contenu.",
"account_edit.profile_tab.title": "Paramètres de l'onglet du profil",
"account_edit.save": "Enregistrer",
"account_edit_tags.column_title": "Modifier les hashtags mis en avant",
"account_edit_tags.help_text": "Les hashtags mis en avant aident les personnes à découvrir et interagir avec votre profil. Ils apparaissent comme des filtres dans la vue « Activité » de votre profil.",
"account_edit_tags.search_placeholder": "Saisir un hashtag…",
"account_edit_tags.suggestions": "Suggestions :",
"account_edit_tags.tag_status_count": "{count, plural, one {# message} other {# messages}}",
"account_note.placeholder": "Cliquez pour ajouter une note",
"admin.dashboard.daily_retention": "Taux de rétention des utilisateur·rice·s par jour après inscription",
"admin.dashboard.monthly_retention": "Taux de rétention des utilisateur·rice·s par mois après inscription",
@@ -297,6 +306,7 @@
"collections.no_collections_yet": "Aucune collection pour le moment.",
"collections.old_last_post_note": "Dernière publication il y a plus d'une semaine",
"collections.remove_account": "Supprimer ce compte",
"collections.report_collection": "Signaler cette collection",
"collections.search_accounts_label": "Chercher des comptes à ajouter…",
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
"collections.sensitive": "Sensible",
@@ -768,7 +778,7 @@
"not_signed_in_indicator.not_signed_in": "Vous devez vous connecter pour accéder à cette ressource.",
"notification.admin.report": "{name} a signalé {target}",
"notification.admin.report_account": "{name} a signalé {count, plural, one {un message} other {# messages}} de {target} pour {category}",
"notification.admin.report_account_other": "{name} a signalé {count, plural, one {un message} other {# messages}} depuis {target}",
"notification.admin.report_account_other": "{name} a signalé {count, plural, one {un message} other {# messages}} de {target}",
"notification.admin.report_statuses": "{name} a signalé {target} pour {category}",
"notification.admin.report_statuses_other": "{name} a signalé {target}",
"notification.admin.sign_up": "{name} s'est inscrit·e",
@@ -967,6 +977,7 @@
"report.category.title_account": "ce profil",
"report.category.title_status": "ce message",
"report.close": "Terminé",
"report.collection_comment": "Pourquoi souhaitez-vous signaler cette collection ?",
"report.comment.title": "Y a-t-il autre chose que nous devrions savoir ?",
"report.forward": "Transférer à {target}",
"report.forward_hint": "Le compte provient dun autre serveur. Envoyer également une copie anonyme du rapport?",
@@ -988,6 +999,8 @@
"report.rules.title": "Quelles règles sont enfreintes ?",
"report.statuses.subtitle": "Sélectionnez toutes les réponses appropriées",
"report.statuses.title": "Existe-t-il des messages pour étayer ce rapport ?",
"report.submission_error": "Le signalement na pas pu être envoyé",
"report.submission_error_details": "Veuillez vérifier votre connexion réseau et réessayer plus tard.",
"report.submit": "Envoyer",
"report.target": "Signalement de {target}",
"report.thanks.take_action": "Voici les possibilités que vous avez pour contrôler ce que vous voyez sur Mastodon :",
@@ -996,7 +1009,7 @@
"report.thanks.title_actionable": "Merci pour votre signalement, nous allons investiguer.",
"report.unfollow": "Ne plus suivre @{name}",
"report.unfollow_explanation": "Vous êtes abonné à ce compte. Pour ne plus voir ses messages dans votre fil principal, retirez-le de votre liste d'abonnements.",
"report_notification.attached_statuses": "{count, plural, one {{count} message lié} other {{count} messages liés}}",
"report_notification.attached_statuses": "{count, plural, one {{count} message joint} other {{count} messages joints}}",
"report_notification.categories.legal": "Légal",
"report_notification.categories.legal_sentence": "contenu illégal",
"report_notification.categories.other": "Autre",

View File

@@ -145,6 +145,9 @@
"account_edit.bio.title": "Beathaisnéis",
"account_edit.bio_modal.add_title": "Cuir beathaisnéis leis",
"account_edit.bio_modal.edit_title": "Cuir beathaisnéis in eagar",
"account_edit.button.add": "Cuir {item} leis",
"account_edit.button.delete": "Scrios {item}",
"account_edit.button.edit": "Cuir {item} in eagar",
"account_edit.char_counter": "{currentLength}/{maxLength} carachtair",
"account_edit.column_button": "Déanta",
"account_edit.column_title": "Cuir Próifíl in Eagar",
@@ -152,6 +155,7 @@
"account_edit.custom_fields.title": "Réimsí saincheaptha",
"account_edit.display_name.placeholder": "Is é dainm taispeána an chaoi a bhfeictear dainm ar do phróifíl agus in amlínte.",
"account_edit.display_name.title": "Ainm taispeána",
"account_edit.featured_hashtags.item": "haischlibeanna",
"account_edit.featured_hashtags.placeholder": "Cabhraigh le daoine eile do thopaicí is fearr leat a aithint, agus rochtain thapa a bheith acu orthu.",
"account_edit.featured_hashtags.title": "Haischlibeanna Réadmhaoine",
"account_edit.name_modal.add_title": "Cuir ainm taispeána leis",
@@ -159,6 +163,11 @@
"account_edit.profile_tab.subtitle": "Saincheap na cluaisíní ar do phróifíl agus a bhfuil á thaispeáint iontu.",
"account_edit.profile_tab.title": "Socruithe an chluaisín próifíle",
"account_edit.save": "Sábháil",
"account_edit_tags.column_title": "Cuir haischlibeanna le feiceáil in eagar",
"account_edit_tags.help_text": "Cuidíonn haischlibeanna le húsáideoirí do phróifíl a aimsiú agus idirghníomhú léi. Feictear iad mar scagairí ar radharc Gníomhaíochta do leathanaigh Phróifíle.",
"account_edit_tags.search_placeholder": "Cuir isteach haischlib…",
"account_edit_tags.suggestions": "Moltaí:",
"account_edit_tags.tag_status_count": "{count, plural, one {# post} two {# poist} few {# poist} many {# poist} other {# poist}}",
"account_note.placeholder": "Cliceáil chun nóta a chuir leis",
"admin.dashboard.daily_retention": "Ráta coinneála an úsáideora de réir an lae tar éis clárú",
"admin.dashboard.monthly_retention": "Ráta coinneála na n-úsáideoirí de réir na míosa tar éis dóibh clárú",
@@ -283,6 +292,7 @@
"collections.detail.curated_by_you": "Curtha i dtoll a chéile agatsa",
"collections.detail.loading": "Ag lódáil an bhailiúcháin…",
"collections.detail.share": "Comhroinn an bailiúchán seo",
"collections.edit_details": "Cuir sonraí in eagar",
"collections.error_loading_collections": "Tharla earráid agus iarracht á déanamh do bhailiúcháin a luchtú.",
"collections.hints.accounts_counter": "{count} / {max} cuntais",
"collections.hints.add_more_accounts": "Cuir ar a laghad {count, plural, one {# cuntas} two {# cuntais} few {# cuntais} many {# cuntais} other {# cuntais}} leis chun leanúint ar aghaidh",
@@ -291,13 +301,18 @@
"collections.manage_accounts": "Bainistigh cuntais",
"collections.mark_as_sensitive": "Marcáil mar íogair",
"collections.mark_as_sensitive_hint": "Folaíonn sé cur síos agus cuntais an bhailiúcháin taobh thiar de rabhadh ábhair. Beidh ainm an bhailiúcháin le feiceáil fós.",
"collections.name_length_hint": "Teorainn 40 carachtar",
"collections.new_collection": "Bailiúchán nua",
"collections.no_collections_yet": "Gan aon bhailiúcháin fós.",
"collections.old_last_post_note": "Postáilte go deireanach breis agus seachtain ó shin",
"collections.remove_account": "Bain an cuntas seo",
"collections.report_collection": "Tuairiscigh an bailiúchán seo",
"collections.search_accounts_label": "Cuardaigh cuntais le cur leis…",
"collections.search_accounts_max_reached": "Tá an líon uasta cuntas curtha leis agat",
"collections.sensitive": "Íogair",
"collections.topic_hint": "Cuir haischlib leis a chabhraíonn le daoine eile príomhábhar an bhailiúcháin seo a thuiscint.",
"collections.view_collection": "Féach ar bhailiúchán",
"collections.view_other_collections_by_user": "Féach ar bhailiúcháin eile ón úsáideoir seo",
"collections.visibility_public": "Poiblí",
"collections.visibility_public_hint": "Infheicthe i dtorthaí cuardaigh agus i réimsí eile ina bhfuil moltaí le feiceáil.",
"collections.visibility_title": "Infheictheacht",
@@ -386,6 +401,9 @@
"confirmations.discard_draft.post.title": "An bhfuil tú ag iarraidh do dhréachtphost a chaitheamh amach?",
"confirmations.discard_edit_media.confirm": "Faigh réidh de",
"confirmations.discard_edit_media.message": "Tá athruithe neamhshlánaithe don tuarascáil gné nó réamhamharc agat, faigh réidh dóibh ar aon nós?",
"confirmations.follow_to_collection.confirm": "Lean agus cuir leis an mbailiúchán",
"confirmations.follow_to_collection.message": "Ní mór duit a bheith ag leanúint {name} le go gcuirfidh tú iad le bailiúchán.",
"confirmations.follow_to_collection.title": "Lean an cuntas?",
"confirmations.follow_to_list.confirm": "Lean agus cuir leis an liosta",
"confirmations.follow_to_list.message": "Ní mór duit {name} a leanúint chun iad a chur le liosta.",
"confirmations.follow_to_list.title": "Lean an t-úsáideoir?",
@@ -960,6 +978,7 @@
"report.category.title_account": "próifíl",
"report.category.title_status": "postáil",
"report.close": "Déanta",
"report.collection_comment": "Cén fáth ar mhaith leat an bailiúchán seo a thuairisciú?",
"report.comment.title": "An bhfuil aon rud eile ba chóir dúinn a fhios agat, dar leat?",
"report.forward": "Seol ar aghaidh chun {target}",
"report.forward_hint": "Is ó fhreastalaí eile an cuntas. Cuir cóip gan ainm den tuarascáil ansin freisin?",
@@ -981,6 +1000,8 @@
"report.rules.title": "Cén rialacha atá á sárú?",
"report.statuses.subtitle": "Roghnaigh gach atá i bhfeidhm",
"report.statuses.title": "An bhfuil aon phoist a thacaíonn leis an tuarascáil seo?",
"report.submission_error": "Níorbh fhéidir an tuarascáil a chur isteach",
"report.submission_error_details": "Seiceáil do nasc líonra agus déan iarracht arís ar ball.",
"report.submit": "Cuir isteach",
"report.target": "Ag tuairisciú {target}",
"report.thanks.take_action": "Seo do roghanna chun an méid a fheiceann tú ar Mastodon a rialú:",

View File

@@ -145,6 +145,9 @@
"account_edit.bio.title": "Sobre ti",
"account_edit.bio_modal.add_title": "Engadir biografía",
"account_edit.bio_modal.edit_title": "Editar biografía",
"account_edit.button.add": "Engadir {item}",
"account_edit.button.delete": "Eliminar {item}",
"account_edit.button.edit": "Editar {item}",
"account_edit.char_counter": "{currentLength}/{maxLength} caracteres",
"account_edit.column_button": "Feito",
"account_edit.column_title": "Editar perfil",
@@ -152,6 +155,7 @@
"account_edit.custom_fields.title": "Campos personalizados",
"account_edit.display_name.placeholder": "O nome público é o nome que aparece no perfil e nas cronoloxías.",
"account_edit.display_name.title": "Nome público",
"account_edit.featured_hashtags.item": "cancelos",
"account_edit.featured_hashtags.placeholder": "Facilita que te identifiquen, e da acceso rápido aos teus intereses favoritos.",
"account_edit.featured_hashtags.title": "Cancelos destacados",
"account_edit.name_modal.add_title": "Engadir nome público",
@@ -159,6 +163,11 @@
"account_edit.profile_tab.subtitle": "Personaliza as pestanas e o seu contido no teu perfil.",
"account_edit.profile_tab.title": "Perfil e axustes das pestanas",
"account_edit.save": "Gardar",
"account_edit_tags.column_title": "Editar cancelos destacados",
"account_edit_tags.help_text": "Os cancelos destacados axúdanlle ás usuarias a atopar e interactuar co teu perfil. Aparecen como filtros na túa páxina de perfil na vista Actividade.",
"account_edit_tags.search_placeholder": "Escribe un cancelo…",
"account_edit_tags.suggestions": "Suxestións:",
"account_edit_tags.tag_status_count": "{count, plural, one {# publicación} other {# publicacións}}",
"account_note.placeholder": "Preme para engadir nota",
"admin.dashboard.daily_retention": "Ratio de retención de usuarias diaria após rexistrarse",
"admin.dashboard.monthly_retention": "Ratio de retención de usuarias mensual após o rexistro",
@@ -297,11 +306,13 @@
"collections.no_collections_yet": "Aínda non tes coleccións.",
"collections.old_last_post_note": "Hai máis dunha semana da última publicación",
"collections.remove_account": "Retirar esta conta",
"collections.report_collection": "Denunciar esta colección",
"collections.search_accounts_label": "Buscar contas para engadir…",
"collections.search_accounts_max_reached": "Acadaches o máximo de contas permitidas",
"collections.sensitive": "Sensible",
"collections.topic_hint": "Engadir un cancelo para que axudar a que outras persoas coñezan a temática desta colección.",
"collections.view_collection": "Ver colección",
"collections.view_other_collections_by_user": "Ver outras coleccións desta usuaria",
"collections.visibility_public": "Pública",
"collections.visibility_public_hint": "Pódese atopar nos resultados das buscas e noutras áreas onde se mostran recomendacións.",
"collections.visibility_title": "Visibilidade",
@@ -967,6 +978,7 @@
"report.category.title_account": "perfil",
"report.category.title_status": "publicación",
"report.close": "Feito",
"report.collection_comment": "Por que queres denunciar esta colección?",
"report.comment.title": "Hai algo máis que creas debamos saber?",
"report.forward": "Reenviar a {target}",
"report.forward_hint": "A conta é doutro servidor. Enviar unha copia anónima da denuncia aló tamén?",
@@ -988,6 +1000,8 @@
"report.rules.title": "Que regras foron incumpridas?",
"report.statuses.subtitle": "Elixe todo o que corresponda",
"report.statuses.title": "Hai algunha publicación que apoie esta denuncia?",
"report.submission_error": "Non se puido enviar a denuncia",
"report.submission_error_details": "Comproba a conexión á rede e volve a intentalo máis tarde.",
"report.submit": "Enviar",
"report.target": "Denunciar a {target}",
"report.thanks.take_action": "Aquí tes unhas opcións para controlar o que ves en Mastodon:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "תגיות נבחרות עוזרות למשתמשים לגלות ולהשתמש בפרופיל שלך. הן יופיעו כסננים במבט הפעילויות על עמוד הפרופיל שלך.",
"account_edit_tags.search_placeholder": "הזנת תגית…",
"account_edit_tags.suggestions": "הצעות:",
"account_edit_tags.tag_status_count": "{count} הודעות",
"account_edit_tags.tag_status_count": "{count, plural, one {הודעה אחת} two {הודעותיים} other {# הודעות}}",
"account_note.placeholder": "יש ללחוץ כדי להוסיף הערות",
"admin.dashboard.daily_retention": "קצב שימור משתמשים יומי אחרי ההרשמה",
"admin.dashboard.monthly_retention": "קצב שימור משתמשים (פר חודש) אחרי ההרשמה",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "עוד אין אוספים.",
"collections.old_last_post_note": "פרסמו לאחרונה לפני יותר משבוע",
"collections.remove_account": "הסר חשבון זה",
"collections.report_collection": "דיווח על אוסף זה",
"collections.search_accounts_label": "לחפש חשבונות להוספה…",
"collections.search_accounts_max_reached": "הגעת למספר החשבונות המירבי",
"collections.sensitive": "רגיש",
"collections.topic_hint": "הוספת תגית שמסייעת לאחרים להבין את הנושא הראשי של האוסף.",
"collections.view_collection": "צפיה באוסף",
"collections.view_other_collections_by_user": "צפייה באוספים אחרים של משתמש.ת אלו",
"collections.visibility_public": "פומבי",
"collections.visibility_public_hint": "זמין לגילוי בתוצאות חיפוש ושאר אזורים בהם מופיעות המלצות.",
"collections.visibility_title": "ניראות",
@@ -976,6 +978,7 @@
"report.category.title_account": "פרופיל",
"report.category.title_status": "הודעה",
"report.close": "בוצע",
"report.collection_comment": "מדוע ברצונכם לדווח על האוסף הזה?",
"report.comment.title": "האם יש דבר נוסף שלדעתך חשוב שנדע?",
"report.forward": "קדם ל-{target}",
"report.forward_hint": "חשבון זה הוא משרת אחר. האם לשלוח בנוסף עותק אנונימי לשם?",
@@ -997,6 +1000,8 @@
"report.rules.title": "אילו חוקים מופרים?",
"report.statuses.subtitle": "בחר/י את כל המתאימים",
"report.statuses.title": "האם ישנן הודעות התומכות בדיווח זה?",
"report.submission_error": "לא ניתן לבצע את הדיווח",
"report.submission_error_details": "נא לבדוק את חיבור הרשת ולנסות שוב מאוחר יותר.",
"report.submit": "שליחה",
"report.target": "דיווח על {target}",
"report.thanks.take_action": "הנה כמה אפשרויות לשליטה בתצוגת מסטודון:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Myllumerki með aukið vægi hjálpa lesendum að finna og eiga við notandasíðuna þína. Þau birtast sem síur í virkniflipa notandasíðunnar þinnar.",
"account_edit_tags.search_placeholder": "Settu inn myllumerki…",
"account_edit_tags.suggestions": "Tillögur:",
"account_edit_tags.tag_status_count": "{count} færslur",
"account_edit_tags.tag_status_count": "{count, plural, one {# færsla} other {# færslur}}",
"account_note.placeholder": "Smelltu til að bæta við minnispunkti",
"admin.dashboard.daily_retention": "Hlutfall virkra notenda eftir nýskráningu eftir dögum",
"admin.dashboard.monthly_retention": "Hlutfall virkra notenda eftir nýskráningu eftir mánuðum",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "Engin söfn ennþá.",
"collections.old_last_post_note": "Birti síðast fyrir meira en viku síðan",
"collections.remove_account": "Fjarlægja þennan aðgang",
"collections.report_collection": "Kæra þetta safn",
"collections.search_accounts_label": "Leita að aðgöngum til að bæta við…",
"collections.search_accounts_max_reached": "Þú hefur þegar bætt við leyfilegum hámarksfjölda aðganga",
"collections.sensitive": "Viðkvæmt",
"collections.topic_hint": "Bættu við myllumerki sem hjálpar öðrum að skilja aðalefni þessa safns.",
"collections.view_collection": "Skoða safn",
"collections.view_other_collections_by_user": "Skoða önnur söfn frá þessum notanda",
"collections.visibility_public": "Opinbert",
"collections.visibility_public_hint": "Hægt að finna í leitarniðurstöðum og öðrum þeim þáttum þar sem meðmæli birtast.",
"collections.visibility_title": "Sýnileiki",
@@ -976,6 +978,7 @@
"report.category.title_account": "notandasnið",
"report.category.title_status": "færsla",
"report.close": "Lokið",
"report.collection_comment": "Hvers vegna viltu kæra þetta safn?",
"report.comment.title": "Er eitthvað annað sem þú heldur að við ættum að vita?",
"report.forward": "Áframsenda til {target}",
"report.forward_hint": "Notandaaðgangurinn er af öðrum vefþjóni. Á einnig að senda nafnlaust afrit af kærunni þangað?",
@@ -997,6 +1000,8 @@
"report.rules.title": "Hvaða reglur eru brotnar?",
"report.statuses.subtitle": "Veldu allt sem á við",
"report.statuses.title": "Eru einhverjar færslur sem styðja þessa kæru?",
"report.submission_error": "Ekki var hægt að senda inn kæruna",
"report.submission_error_details": "Athugaðu nettenginguna þína og prófaðu aftur síðar.",
"report.submit": "Senda inn",
"report.target": "Kæri {target}",
"report.thanks.take_action": "Hér eru nokkrir valkostir til að stýra hvað þú sérð á Mastodon:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Gli hashtag in evidenza aiutano gli utenti a scoprire e interagire con il tuo profilo. Appaiono come filtri nella visualizzazione Attività della tua pagina del profilo.",
"account_edit_tags.search_placeholder": "Inserisci un hashtag…",
"account_edit_tags.suggestions": "Suggerimenti:",
"account_edit_tags.tag_status_count": "{count} post",
"account_edit_tags.tag_status_count": "{count, plural, one {# post} other {# post}}",
"account_note.placeholder": "Clicca per aggiungere una nota",
"admin.dashboard.daily_retention": "Tasso di ritenzione dell'utente per giorno, dopo la registrazione",
"admin.dashboard.monthly_retention": "Tasso di ritenzione dell'utente per mese, dopo la registrazione",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "Nessuna collezione ancora.",
"collections.old_last_post_note": "Ultimo post più di una settimana fa",
"collections.remove_account": "Rimuovi questo account",
"collections.report_collection": "Segnala questa collezione",
"collections.search_accounts_label": "Cerca account da aggiungere…",
"collections.search_accounts_max_reached": "Hai aggiunto il numero massimo di account",
"collections.sensitive": "Sensibile",
"collections.topic_hint": "Aggiungi un hashtag che aiuti gli altri a comprendere l'argomento principale di questa collezione.",
"collections.view_collection": "Visualizza la collezione",
"collections.view_other_collections_by_user": "Visualizza altre collezioni da questo utente",
"collections.visibility_public": "Pubblica",
"collections.visibility_public_hint": "Scopribile nei risultati di ricerca e in altre aree in cui compaiono i suggerimenti.",
"collections.visibility_title": "Visibilità",
@@ -976,6 +978,7 @@
"report.category.title_account": "profilo",
"report.category.title_status": "post",
"report.close": "Fatto",
"report.collection_comment": "Perché vuoi segnalare questa collezione?",
"report.comment.title": "C'è altro che pensi che dovremmo sapere?",
"report.forward": "Inoltra a {target}",
"report.forward_hint": "Il profilo proviene da un altro server. Inviare anche lì una copia anonima del rapporto?",
@@ -997,6 +1000,8 @@
"report.rules.title": "Quali regole sono violate?",
"report.statuses.subtitle": "Seleziona tutte le risposte pertinenti",
"report.statuses.title": "Ci sono dei post a sostegno di questa segnalazione?",
"report.submission_error": "Non è stato possibile inviare la segnalazione",
"report.submission_error_details": "Si prega di controllare la tua connessione di rete e riprovare più tardi.",
"report.submit": "Invia",
"report.target": "Segnalando {target}",
"report.thanks.take_action": "Ecco le tue opzioni per controllare cosa vedi su Mastodon:",

View File

@@ -167,7 +167,6 @@
"account_edit_tags.help_text": "Aanbevolen hashtags helpen gebruikers je profiel te ontdekken en te communiceren. Ze verschijnen als filters op de activiteitenweergave van je pagina.",
"account_edit_tags.search_placeholder": "Voer een hashtag in…",
"account_edit_tags.suggestions": "Suggesties:",
"account_edit_tags.tag_status_count": "{count} berichten",
"account_note.placeholder": "Klik om een opmerking toe te voegen",
"admin.dashboard.daily_retention": "Retentiegraad van gebruikers per dag, vanaf registratie",
"admin.dashboard.monthly_retention": "Retentiegraad van gebruikers per maand, vanaf registratie",

View File

@@ -145,6 +145,9 @@
"account_edit.bio.title": "Om meg",
"account_edit.bio_modal.add_title": "Skriv om deg sjølv",
"account_edit.bio_modal.edit_title": "Endre bio",
"account_edit.button.add": "Legg til {item}",
"account_edit.button.delete": "Slett {item}",
"account_edit.button.edit": "Rediger {item}",
"account_edit.char_counter": "{currentLength}/{maxLength} teikn",
"account_edit.column_button": "Ferdig",
"account_edit.column_title": "Rediger profil",
@@ -152,6 +155,7 @@
"account_edit.custom_fields.title": "Eigne felt",
"account_edit.display_name.placeholder": "Det synlege namnet ditt er det som syner på profilen din og i tidsliner.",
"account_edit.display_name.title": "Synleg namn",
"account_edit.featured_hashtags.item": "emneknaggar",
"account_edit.featured_hashtags.placeholder": "Hjelp andre å finna og få rask tilgang til favorittemna dine.",
"account_edit.featured_hashtags.title": "Utvalde emneknaggar",
"account_edit.name_modal.add_title": "Legg til synleg namn",
@@ -159,6 +163,11 @@
"account_edit.profile_tab.subtitle": "Tilpass fanene på profilen din og kva dei syner.",
"account_edit.profile_tab.title": "Innstillingar for profilfane",
"account_edit.save": "Lagre",
"account_edit_tags.column_title": "Rediger utvalde emneknaggar",
"account_edit_tags.help_text": "Utvalde emneknaggar hjelper folk å oppdaga og samhandla med profilen din. Dei blir viste som filter på aktivitetsoversikta på profilsida di.",
"account_edit_tags.search_placeholder": "Skriv ein emneknagg…",
"account_edit_tags.suggestions": "Framlegg:",
"account_edit_tags.tag_status_count": "{count, plural, one {# innlegg} other {# innlegg}}",
"account_note.placeholder": "Klikk for å leggja til merknad",
"admin.dashboard.daily_retention": "Mengda brukarar aktive ved dagar etter registrering",
"admin.dashboard.monthly_retention": "Mengda brukarar aktive ved månader etter registrering",

View File

@@ -106,9 +106,9 @@
"account.mutual": "Vocês se seguem",
"account.name.help.domain": "{domain} é o servidor que hospeda o perfil e publicações do usuário.",
"account.name.help.domain_self": "{domain} é o seu servidor que hospeda seu perfil e publicações.",
"account.name.help.footer": "Da mesma forma que você pode enviar emails para pessoas utilizando diferentes clientes de email, você pode interagir com pessoas em outros servidores do Mastodon e com qualquer um em outros aplicativos sociais regidos pelo mesmo conjunto de regras que o Mastodon (chamadas de Protocolo ActivityPub).",
"account.name.help.header": "Um nome de usuário é como um endereço de email",
"account.name.help.username": "{username} é o nome de usuário desta conta no servidor dela. Alguém em outro servidor pode ter o mesmo nome de usuário.",
"account.name.help.footer": "Assim como pode enviar mensagens eletrônicas de serviços diferentes, você pode interagir com pessoas de outros servidores Mastodon e qualquer pessoa em um aplicativo alimentado com as regras utilizadas pelo Mastodon (protocolo ActivityPub).",
"account.name.help.header": "Um identificador é como um endereço de endereço eletrônico",
"account.name.help.username": "{username} é o nome de usuário da conta neste servidor. Alguém em outro servidor pode ter o mesmo nome de usuário.",
"account.name.help.username_self": "{username} é seu nome de usuário neste servidor. Alguém em outro servidor pode ter o mesmo nome de usuário.",
"account.name_info": "O que isto significa?",
"account.no_bio": "Nenhuma descrição fornecida.",
@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Hashtags em destaque ajudam os usuários a descobrir e interagir com seu perfil. Elas aparecem como filtros na visualização de Atividade da sua página de Perfil.",
"account_edit_tags.search_placeholder": "Insira uma hashtag…",
"account_edit_tags.suggestions": "Sugestões:",
"account_edit_tags.tag_status_count": "{count} publicações",
"account_edit_tags.tag_status_count": "{count, plural, one {# publicação} other {# publicações}}",
"account_note.placeholder": "Nota pessoal sobre este perfil aqui",
"admin.dashboard.daily_retention": "Taxa de retenção de usuários por dia, após a inscrição",
"admin.dashboard.monthly_retention": "Taxa de retenção de usuários por mês, após a inscrição",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Hashtag-ët e zgjedhur i ndihmojnë përdoruesit të zbulojnë dhe ndërveprojnë me profilin tuaj. Ata duken si filtra te pamja Veprimtari e faqes tuaj të Profilit.",
"account_edit_tags.search_placeholder": "Jepni një hashtag…",
"account_edit_tags.suggestions": "Sugjerime:",
"account_edit_tags.tag_status_count": "{count} postime",
"account_edit_tags.tag_status_count": "{count, plural, one {# postim} other {# postime}}",
"account_note.placeholder": "Klikoni për të shtuar shënim",
"admin.dashboard.daily_retention": "Shkallë mbajtjeje përdoruesi, në ditë, pas regjistrimit",
"admin.dashboard.monthly_retention": "Shkallë mbajtjeje përdoruesi, në muaj, pas regjistrimit",
@@ -303,11 +303,13 @@
"collections.no_collections_yet": "Ende pa koleksione.",
"collections.old_last_post_note": "Të postuarat e fundit gjatë një jave më parë",
"collections.remove_account": "Hiqe këtë llogari",
"collections.report_collection": "Raportojeni këtë koleksion",
"collections.search_accounts_label": "Kërkoni për llogari për shtim…",
"collections.search_accounts_max_reached": "Keni shtuar numrin maksimum të llogarive",
"collections.sensitive": "Rezervat",
"collections.topic_hint": "Shtoni një hashtag që ndihmon të tjerët të kuptojnë temën kryesore të këtij koleksion.",
"collections.view_collection": "Shiheni koleksionin",
"collections.view_other_collections_by_user": "Shihni koleksione të tjera nga ky përdorues",
"collections.visibility_public": "Publik",
"collections.visibility_public_hint": "I zbulueshëm në përfundime kërkimi dhe fusha të tjera ku shfaqen rekomandime.",
"collections.visibility_title": "Dukshmëri",
@@ -973,6 +975,7 @@
"report.category.title_account": "profil",
"report.category.title_status": "postim",
"report.close": "U bë",
"report.collection_comment": "Pse doni ta raportoni këtë koleksion?",
"report.comment.title": "Ka ndonjë gjë tjetër që do të duhej ta dinim?",
"report.forward": "Përcillja {target}",
"report.forward_hint": "Llogaria është nga një shërbyes tjetër. Të dërgohet edhe një kopje e anonimizuar e raportimit?",
@@ -994,6 +997,8 @@
"report.rules.title": "Cilat rregulla po cenohen?",
"report.statuses.subtitle": "Përzgjidhni gjithçka që ka vend",
"report.statuses.title": "A ka postime që dëshmojnë problemet e këtij raporti?",
"report.submission_error": "Raportimi su parashtrua dot",
"report.submission_error_details": "Ju lutemi, kontrolloni lidhjen tuaj në rrjet dhe riprovoni më vonë.",
"report.submit": "Parashtroje",
"report.target": "Raportim i {target}",
"report.thanks.take_action": "Ja mundësitë tuaja për të kontrolluar çshihni në Mastodon:",

View File

@@ -874,6 +874,7 @@
"report.rules.title": "Vilka regler överträds?",
"report.statuses.subtitle": "Välj alla som stämmer",
"report.statuses.title": "Finns det några inlägg som stöder denna rapport?",
"report.submission_error_details": "Kontrollera din nätverksanslutning och försök igen senare.",
"report.submit": "Skicka",
"report.target": "Rapporterar {target}",
"report.thanks.take_action": "Här är dina alternativ för att bestämma vad du ser på Mastodon:",

View File

@@ -145,6 +145,9 @@
"account_edit.bio.title": "Kişisel bilgiler",
"account_edit.bio_modal.add_title": "Kişisel bilgi ekle",
"account_edit.bio_modal.edit_title": "Kişisel bilgiyi düzenle",
"account_edit.button.add": "{item} ekle",
"account_edit.button.delete": "{item} sil",
"account_edit.button.edit": "{item} düzenle",
"account_edit.char_counter": "{currentLength}/{maxLength} karakter",
"account_edit.column_button": "Tamamlandı",
"account_edit.column_title": "Profili Düzenle",
@@ -152,6 +155,7 @@
"account_edit.custom_fields.title": "Özel alanlar",
"account_edit.display_name.placeholder": "Görünen adınız profilinizde ve zaman akışlarında adınızın nasıl göründüğüdür.",
"account_edit.display_name.title": "Görünen ad",
"account_edit.featured_hashtags.item": "etiketler",
"account_edit.featured_hashtags.placeholder": "Başkalarının favori konularınızı tanımlamasına ve bunlara hızlı bir şekilde erişmesine yardımcı olun.",
"account_edit.featured_hashtags.title": "Öne çıkan etiketler",
"account_edit.name_modal.add_title": "Görünen ad ekle",
@@ -159,6 +163,11 @@
"account_edit.profile_tab.subtitle": "Profilinizdeki sekmeleri ve bunların görüntülediği bilgileri özelleştirin.",
"account_edit.profile_tab.title": "Profil sekme ayarları",
"account_edit.save": "Kaydet",
"account_edit_tags.column_title": "Öne çıkarılmış etiketleri düzenle",
"account_edit_tags.help_text": "Öne çıkan etiketler kullanıcıların profilinizi keşfetmesine ve etkileşim kurmasına yardımcı olur. Profil sayfanızın Etkinlik görünümünde filtreler olarak görünürler.",
"account_edit_tags.search_placeholder": "Bir etiket girin…",
"account_edit_tags.suggestions": "Öneriler:",
"account_edit_tags.tag_status_count": "{count, plural, one {# gönderi} other {# gönderi}}",
"account_note.placeholder": "Not eklemek için tıklayın",
"admin.dashboard.daily_retention": "Kayıttan sonra günlük kullanıcı saklama oranı",
"admin.dashboard.monthly_retention": "Kayıttan sonra aylık kullanıcı saklama oranı",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "Hashtag thường dùng giúp bạn mọi người khám phá và tương tác với hồ sơ của bạn. Chúng xuất hiện như những bộ lọc trên phần Hoạt động hồ sơ.",
"account_edit_tags.search_placeholder": "Nhập một hashtag…",
"account_edit_tags.suggestions": "Được đề xuất:",
"account_edit_tags.tag_status_count": "{count} tút",
"account_edit_tags.tag_status_count": "{count, plural, other {# tút}}",
"account_note.placeholder": "Nhấn để thêm",
"admin.dashboard.daily_retention": "Tỉ lệ người dùng sau đăng ký ở lại theo ngày",
"admin.dashboard.monthly_retention": "Tỉ lệ người dùng ở lại sau khi đăng ký",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "Chưa có collection.",
"collections.old_last_post_note": "Đăng lần cuối hơn một tuần trước",
"collections.remove_account": "Gỡ tài khoản này",
"collections.report_collection": "Báo cáo collection này",
"collections.search_accounts_label": "Tìm tài khoản để thêm…",
"collections.search_accounts_max_reached": "Bạn đã đạt đến số lượng tài khoản tối đa",
"collections.sensitive": "Nhạy cảm",
"collections.topic_hint": "Thêm hashtag giúp người khác hiểu chủ đề chính của collection này.",
"collections.view_collection": "Xem collection",
"collections.view_other_collections_by_user": "View những collection khác từ tài khoản này",
"collections.visibility_public": "Công khai",
"collections.visibility_public_hint": "Có thể tìm thấy trong kết quả tìm kiếm và các khu vực khác nơi xuất hiện đề xuất.",
"collections.visibility_title": "Hiển thị",
@@ -976,6 +978,7 @@
"report.category.title_account": "Người",
"report.category.title_status": "Tút",
"report.close": "Xong",
"report.collection_comment": "Vì sao bạn muốn báo cáo collection này?",
"report.comment.title": "Có điều gì mà chúng tôi cần biết không?",
"report.forward": "Chuyển đến {target}",
"report.forward_hint": "Người này thuộc máy chủ khác. Gửi một báo cáo ẩn danh tới máy chủ đó?",
@@ -997,6 +1000,8 @@
"report.rules.title": "Vi phạm nội quy nào?",
"report.statuses.subtitle": "Chọn tất cả những gì phù hợp",
"report.statuses.title": "Bạn muốn báo cáo tút nào?",
"report.submission_error": "Không thể gửi báo cáo",
"report.submission_error_details": "Kiểm tra kết nối mạng và thử lại sau.",
"report.submit": "Gửi đi",
"report.target": "Báo cáo {target}",
"report.thanks.take_action": "Đây là cách kiểm soát những thứ mà bạn thấy:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "精选话题标签可以帮助他人发现并与你的个人资料互动。这些标签会作为过滤器条件出现在你个人资料页面的活动视图中。",
"account_edit_tags.search_placeholder": "输入话题标签…",
"account_edit_tags.suggestions": "建议:",
"account_edit_tags.tag_status_count": "{count} 条嘟文",
"account_edit_tags.tag_status_count": "{count, plural, other {# 条嘟文}}",
"account_note.placeholder": "点击添加备注",
"admin.dashboard.daily_retention": "注册后用户留存率(按日计算)",
"admin.dashboard.monthly_retention": "注册后用户留存率(按月计算)",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "尚无收藏列表。",
"collections.old_last_post_note": "上次发言于一周多以前",
"collections.remove_account": "移除此账号",
"collections.report_collection": "举报此收藏列表",
"collections.search_accounts_label": "搜索要添加的账号…",
"collections.search_accounts_max_reached": "你添加的账号数量已达上限",
"collections.sensitive": "敏感内容",
"collections.topic_hint": "添加话题标签,帮助他人了解此收藏列表的主题。",
"collections.view_collection": "查看收藏列表",
"collections.view_other_collections_by_user": "查看此用户的其他收藏列表",
"collections.visibility_public": "公开",
"collections.visibility_public_hint": "可在搜索结果及其他推荐功能可用的区域被发现。",
"collections.visibility_title": "可见性",
@@ -976,6 +978,7 @@
"report.category.title_account": "账号",
"report.category.title_status": "嘟文",
"report.close": "完成",
"report.collection_comment": "举报此收藏列表的原因是什么?",
"report.comment.title": "还有什么你认为我们应该知道的吗?",
"report.forward": "转发举报至 {target}",
"report.forward_hint": "这名用户来自另一个服务器。是否要向那个服务器发送一条匿名的举报?",
@@ -997,6 +1000,8 @@
"report.rules.title": "违反了哪些规则?",
"report.statuses.subtitle": "选择全部适用选项",
"report.statuses.title": "是否有任何嘟文可以支持这一报告?",
"report.submission_error": "无法提交举报",
"report.submission_error_details": "请检查网络连接,然后再试一次。",
"report.submit": "提交",
"report.target": "举报 {target}",
"report.thanks.take_action": "以下是你控制你在 Mastodon 上能看到哪些内容的选项:",

View File

@@ -167,7 +167,7 @@
"account_edit_tags.help_text": "推薦主題標籤幫助其他人發現並與您的個人檔案互動。它們將作為過濾器出現於您個人檔案頁面之動態中。",
"account_edit_tags.search_placeholder": "請輸入主題標籤…",
"account_edit_tags.suggestions": "建議:",
"account_edit_tags.tag_status_count": "{count} 則嘟文",
"account_edit_tags.tag_status_count": "{count, plural, other {# 則嘟文}}",
"account_note.placeholder": "點擊以新增備註",
"admin.dashboard.daily_retention": "註冊後使用者存留率(日)",
"admin.dashboard.monthly_retention": "註冊後使用者存留率(月)",
@@ -306,11 +306,13 @@
"collections.no_collections_yet": "您沒有任何收藏名單。",
"collections.old_last_post_note": "上次發表嘟文已超過一週",
"collections.remove_account": "移除此帳號",
"collections.report_collection": "檢舉此收藏名單",
"collections.search_accounts_label": "搜尋帳號以加入...",
"collections.search_accounts_max_reached": "您新增之帳號數已達上限",
"collections.sensitive": "敏感內容",
"collections.topic_hint": "新增主題標籤以協助其他人瞭解此收藏名單之主題。",
"collections.view_collection": "檢視收藏名單",
"collections.view_other_collections_by_user": "檢視此使用者之其他收藏名單",
"collections.visibility_public": "公開",
"collections.visibility_public_hint": "可於搜尋結果與其他推薦處可見。",
"collections.visibility_title": "可見性",
@@ -767,7 +769,7 @@
"navigation_bar.mutes": "已靜音的使用者",
"navigation_bar.opened_in_classic_interface": "預設於經典網頁介面中開啟嘟文、帳號與其他特定頁面。",
"navigation_bar.preferences": "偏好設定",
"navigation_bar.privacy_and_reach": "隱私權觸及",
"navigation_bar.privacy_and_reach": "隱私權觸及",
"navigation_bar.search": "搜尋",
"navigation_bar.search_trends": "搜尋 / 熱門趨勢",
"navigation_panel.collapse_followed_tags": "收合已跟隨主題標籤選單",
@@ -976,6 +978,7 @@
"report.category.title_account": "個人檔案",
"report.category.title_status": "嘟文",
"report.close": "已完成",
"report.collection_comment": "您檢舉此收藏名單的原因是?",
"report.comment.title": "有什麼其他您想讓我們知道的嗎?",
"report.forward": "轉寄到 {target}",
"report.forward_hint": "這個帳號屬於其他伺服器。要向該伺服器發送匿名的檢舉訊息嗎?",
@@ -997,6 +1000,8 @@
"report.rules.title": "違反了哪些規則?",
"report.statuses.subtitle": "請選擇所有適用的選項",
"report.statuses.title": "是否有能佐證這份檢舉之嘟文?",
"report.submission_error": "無法送出檢舉",
"report.submission_error_details": "請檢查您的網路連線並稍候重試。",
"report.submit": "送出",
"report.target": "檢舉 {target}",
"report.thanks.take_action": "以下是控制您想於 Mastodon 看到什麼內容之選項:",

View File

@@ -229,14 +229,16 @@ interface AccountCollectionQuery {
collections: ApiCollectionJSON[];
}
export const selectMyCollections = createAppSelector(
export const selectAccountCollections = createAppSelector(
[
(state) => state.meta.get('me') as string,
(_, accountId: string | null) => accountId,
(state) => state.collections.accountCollections,
(state) => state.collections.collections,
],
(me, collectionsByAccountId, collectionsMap) => {
const myCollectionsQuery = collectionsByAccountId[me];
(accountId, collectionsByAccountId, collectionsMap) => {
const myCollectionsQuery = accountId
? collectionsByAccountId[accountId]
: null;
if (!myCollectionsQuery) {
return {

View File

@@ -1,11 +1,6 @@
# frozen_string_literal: true
class Admin::AccountAction
include ActiveModel::Model
include ActiveModel::Attributes
include AccountableConcern
include Authorization
class Admin::AccountAction < Admin::BaseAction
TYPES = %w(
none
disable
@@ -15,49 +10,13 @@ class Admin::AccountAction
).freeze
attr_accessor :target_account,
:current_account,
:type,
:text,
:report_id,
:warning_preset_id
attr_reader :warning
attribute :include_statuses, :boolean, default: true
attribute :send_email_notification, :boolean, default: true
alias send_email_notification? send_email_notification
alias include_statuses? include_statuses
validates :type, :target_account, :current_account, presence: true
validates :type, inclusion: { in: TYPES }
def save
return false unless valid?
ApplicationRecord.transaction do
process_action!
process_strike!
process_reports!
end
process_notification!
process_queue!
true
end
def save!
raise ActiveRecord::RecordInvalid, self unless save
end
def report
@report ||= Report.find(report_id) if report_id.present?
end
def with_report?
!report.nil?
end
validates :target_account, presence: true
class << self
def types_for_account(account)
@@ -84,6 +43,17 @@ class Admin::AccountAction
private
def process_action!
ApplicationRecord.transaction do
handle_type!
process_strike!
process_reports!
end
process_notification!
process_queue!
end
def handle_type!
case type
when 'disable'
handle_disable!
@@ -96,20 +66,6 @@ class Admin::AccountAction
end
end
def process_strike!
@warning = target_account.strikes.create!(
account: current_account,
report: report,
action: type,
text: text_for_warning,
status_ids: status_ids
)
# A log entry is only interesting if the warning contains
# custom text from someone. Otherwise it's just noise.
log_action(:create, @warning) if @warning.text.present? && type == 'none'
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
@@ -161,17 +117,6 @@ class Admin::AccountAction
queue_suspension_worker! if type == 'suspend'
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
def status_ids
report.status_ids if with_report? && include_statuses?
end

View File

@@ -0,0 +1,65 @@
# frozen_string_literal: true
class Admin::BaseAction
include ActiveModel::Model
include ActiveModel::Attributes
include AccountableConcern
include Authorization
attr_accessor :current_account,
:type,
:text,
:report_id
attr_reader :warning
attribute :send_email_notification, :boolean, default: true
alias send_email_notification? send_email_notification
validates :type, :current_account, presence: true
validates :type, inclusion: { in: ->(a) { a.class::TYPES } }
def save
return false unless valid?
process_action!
true
end
def save!
raise ActiveRecord::RecordInvalid, self unless save
end
def report
@report ||= Report.find(report_id) if report_id.present?
end
def with_report?
!report.nil?
end
private
def process_strike!(action = type)
@warning = target_account.strikes.create!(
account: current_account,
report: report,
action:,
text: text_for_warning,
status_ids: status_ids
)
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
end

View File

@@ -0,0 +1,120 @@
# frozen_string_literal: true
class Admin::ModerationAction < Admin::BaseAction
TYPES = %w(
delete
mark_as_sensitive
).freeze
validates :report_id, presence: true
private
def status_ids
report.status_ids
end
def statuses
@statuses ||= Status.with_discarded.where(id: status_ids).reorder(nil)
end
def collections
report.collections
end
def process_action!
case type
when 'delete'
handle_delete!
when 'mark_as_sensitive'
handle_mark_as_sensitive!
end
end
def handle_delete!
statuses.each { |status| authorize([:admin, status], :destroy?) }
collections.each { |collection| authorize([:admin, collection], :destroy?) }
ApplicationRecord.transaction do
delete_statuses!
delete_collections!
resolve_report!
process_strike!(:delete_statuses)
create_tombstones! unless target_account.local?
end
process_notification!
RemovalWorker.push_bulk(status_ids) { |status_id| [status_id, { 'preserve' => target_account.local?, 'immediate' => !target_account.local? }] }
end
def handle_mark_as_sensitive!
collections.each { |collection| authorize([:admin, collection], :update?) }
# Can't use a transaction here because UpdateStatusService queues
# Sidekiq jobs
mark_statuses_as_sensitive!
mark_collections_as_sensitive!
resolve_report!
process_strike!(:mark_statuses_as_sensitive)
process_notification!
end
def delete_statuses!
statuses.each do |status|
status.discard_with_reblogs
log_action(:destroy, status)
end
end
def delete_collections!
collections.each do |collection|
collection.destroy!
log_action(:destroy, collection)
end
end
def create_tombstones!
(statuses + collections).each { |record| Tombstone.find_or_create_by(uri: record.uri, account: target_account, by_moderator: true) }
end
def mark_statuses_as_sensitive!
representative_account = Account.representative
statuses.includes(:media_attachments, preview_cards_status: :preview_card).find_each do |status|
next if status.discarded? || !(status.with_media? || status.with_preview_card?)
authorize([:admin, status], :update?)
if target_account.local?
UpdateStatusService.new.call(status, representative_account.id, sensitive: true)
else
status.update(sensitive: true)
end
log_action(:update, status)
end
end
def mark_collections_as_sensitive!
collections.each do |collection|
UpdateCollectionService.new.call(collection, sensitive: true)
log_action(:update, collection)
end
end
def resolve_report!
report.resolve!(current_account)
log_action(:resolve, report)
end
def target_account
report.target_account
end
def text_for_warning = text
end

View File

@@ -1,20 +1,12 @@
# frozen_string_literal: true
class Admin::StatusBatchAction
include ActiveModel::Model
include ActiveModel::Attributes
include AccountableConcern
include Authorization
class Admin::StatusBatchAction < Admin::BaseAction
TYPES = %w(
report
remove_from_report
).freeze
attr_accessor :current_account, :type,
:status_ids, :report_id,
:text
attribute :send_email_notification, :boolean
def save!
process_action!
end
attr_accessor :status_ids
private
@@ -26,10 +18,6 @@ class Admin::StatusBatchAction
return if status_ids.empty?
case type
when 'delete'
handle_delete!
when 'mark_as_sensitive'
handle_mark_as_sensitive!
when 'report'
handle_report!
when 'remove_from_report'
@@ -37,71 +25,6 @@ class Admin::StatusBatchAction
end
end
def handle_delete!
statuses.each { |status| authorize([:admin, status], :destroy?) }
ApplicationRecord.transaction do
statuses.each do |status|
status.discard_with_reblogs
log_action(:destroy, status)
end
if with_report?
report.resolve!(current_account)
log_action(:resolve, report)
end
@warning = target_account.strikes.create!(
action: :delete_statuses,
account: current_account,
report: report,
status_ids: status_ids,
text: text
)
statuses.each { |status| Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) } unless target_account.local?
end
process_notification!
RemovalWorker.push_bulk(status_ids) { |status_id| [status_id, { 'preserve' => target_account.local?, 'immediate' => !target_account.local? }] }
end
def handle_mark_as_sensitive!
representative_account = Account.representative
# Can't use a transaction here because UpdateStatusService queues
# Sidekiq jobs
statuses.includes(:media_attachments, preview_cards_status: :preview_card).find_each do |status|
next if status.discarded? || !(status.with_media? || status.with_preview_card?)
authorize([:admin, status], :update?)
if target_account.local?
UpdateStatusService.new.call(status, representative_account.id, sensitive: true)
else
status.update(sensitive: true)
end
log_action(:update, status)
if with_report?
report.resolve!(current_account)
log_action(:resolve, report)
end
end
@warning = target_account.strikes.create!(
action: :mark_statuses_as_sensitive,
account: current_account,
report: report,
status_ids: status_ids,
text: text
)
process_notification!
end
def handle_report!
@report = Report.new(report_params) unless with_report?
@report.status_ids = (@report.status_ids + allowed_status_ids).uniq
@@ -117,25 +40,6 @@ class Admin::StatusBatchAction
report.save!
end
def report
@report ||= Report.find(report_id) if report_id.present?
end
def with_report?
!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
def target_account
@target_account ||= statuses.first.account
end

View File

@@ -69,6 +69,14 @@ class Collection < ApplicationRecord
:featured_collection
end
def to_log_human_identifier
account.acct
end
def to_log_permalink
ActivityPub::TagManager.instance.uri_for(self)
end
private
def tag_is_usable

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
class Admin::CollectionPolicy < ApplicationPolicy
def index?
role.can?(:manage_reports, :manage_users)
end
def show?
role.can?(:manage_reports, :manage_users)
end
def destroy?
role.can?(:manage_reports)
end
def update?
role.can?(:manage_reports)
end
end

View File

@@ -5,7 +5,7 @@
= link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(report), method: :post, class: 'button'
.report-actions__item__description
= t('admin.reports.actions.resolve_description_html')
- if statuses.any? { |status| (status.with_media? || status.with_preview_card?) && !status.discarded? }
- if report.collections.any? || statuses.any? { |status| (status.with_media? || status.with_preview_card?) && !status.discarded? }
.report-actions__item
.report-actions__item__button
= form.button t('admin.reports.mark_as_sensitive'),
@@ -18,8 +18,8 @@
= form.button t('admin.reports.delete_and_resolve'),
name: :delete,
class: 'button button--destructive',
disabled: statuses.empty?,
title: statuses.empty? ? t('admin.reports.actions_no_posts') : ''
disabled: (report.collections + statuses).empty?,
title: (report.collections + statuses).empty? ? t('admin.reports.actions_no_posts') : ''
.report-actions__item__description
= t('admin.reports.actions.delete_description_html')
.report-actions__item

View File

@@ -1952,7 +1952,7 @@ da:
content_warnings:
hide: Skjul indlæg
show: Vis mere
default_language: Samme som UI-sproget
default_language: Samme som grænsefladesproget
disallowed_hashtags:
one: 'indeholdte et ikke tilladt hashtag: %{tags}'
other: 'indeholdte de ikke tilladte etiketter: %{tags}'

View File

@@ -83,6 +83,10 @@ de:
access_denied: Diese Anfrage wurde von den Inhaber*innen oder durch den Autorisierungsserver abgelehnt.
credential_flow_not_configured: Das Konto konnte nicht gefunden werden, da Doorkeeper.configure.resource_owner_from_credentials nicht konfiguriert ist.
invalid_client: 'Client-Authentisierung ist fehlgeschlagen: Client unbekannt, keine Authentisierung mitgeliefert oder Authentisierungsmethode wird nicht unterstützt.'
invalid_code_challenge_method:
one: Die code_challenge_method muss %{challenge_methods} sein.
other: Die code_challenge_method muss eine von %{challenge_methods} sein.
zero: Der Berechtigungsserver unterstützt PKCE nicht, da keine akzeptierten code_challenge_method Werte vorhanden sind.
invalid_grant: Die beigefügte Autorisierung ist ungültig, abgelaufen, wurde widerrufen oder einem anderen Client ausgestellt, oder der Weiterleitungs-URI stimmt nicht mit der Autorisierungs-Anfrage überein.
invalid_redirect_uri: Der beigefügte Weiterleitungs-URI ist ungültig.
invalid_request:

View File

@@ -267,6 +267,7 @@ en:
demote_user_html: "%{name} demoted user %{target}"
destroy_announcement_html: "%{name} deleted announcement %{target}"
destroy_canonical_email_block_html: "%{name} unblocked email with the hash %{target}"
destroy_collection_html: "%{name} removed collection by %{target}"
destroy_custom_emoji_html: "%{name} deleted emoji %{target}"
destroy_domain_allow_html: "%{name} disallowed federation with domain %{target}"
destroy_domain_block_html: "%{name} unblocked domain %{target}"
@@ -306,6 +307,7 @@ en:
unsilence_account_html: "%{name} undid limit of %{target}'s account"
unsuspend_account_html: "%{name} unsuspended %{target}'s account"
update_announcement_html: "%{name} updated announcement %{target}"
update_collection_html: "%{name} updated collection by %{target}"
update_custom_emoji_html: "%{name} updated emoji %{target}"
update_domain_block_html: "%{name} updated domain block for %{target}"
update_ip_block_html: "%{name} changed rule for IP %{target}"

View File

@@ -692,6 +692,7 @@ fr-CA:
cancel: Annuler
category: Catégorie
category_description_html: La raison pour laquelle ce compte et/ou ce contenu a été signalé sera citée dans la communication avec le compte signalé
collections: Collections (%{count})
comment:
none: Aucun
comment_description_html: 'Pour fournir plus d''informations, %{name} a écrit :'
@@ -727,6 +728,7 @@ fr-CA:
resolved_msg: Signalement résolu avec succès!
skip_to_actions: Passer aux actions
status: Statut
statuses: Messages (%{count})
statuses_description_html: Le contenu offensant sera cité dans la communication avec le compte signalé
summary:
action_preambles:

View File

@@ -692,6 +692,7 @@ fr:
cancel: Annuler
category: Catégorie
category_description_html: La raison pour laquelle ce compte et/ou ce contenu a été signalé sera citée dans la communication avec le compte signalé
collections: Collections (%{count})
comment:
none: Aucun
comment_description_html: 'Pour fournir plus d''informations, %{name} a écrit :'
@@ -727,6 +728,7 @@ fr:
resolved_msg: Signalement résolu avec succès!
skip_to_actions: Passer aux actions
status: Statut
statuses: Messages (%{count})
statuses_description_html: Le contenu offensant sera cité dans la communication avec le compte signalé
summary:
action_preambles:

View File

@@ -731,6 +731,7 @@ ga:
cancel: Cealaigh
category: Catagóir
category_description_html: Luafar an chúis ar tuairiscíodh an cuntas seo agus/nó an t-ábhar seo i gcumarsáid leis an gcuntas tuairiscithe
collections: Bailiúcháin (%{count})
comment:
none: Dada
comment_description_html: 'Chun tuilleadh eolais a sholáthar, scríobh %{name}:'
@@ -766,6 +767,7 @@ ga:
resolved_msg: D'éirigh le réiteach an tuairisc!
skip_to_actions: Léim ar ghníomhartha
status: Stádas
statuses: Poist (%{count})
statuses_description_html: Luafar ábhar ciontach i gcumarsáid leis an gcuntas tuairiscithe
summary:
action_preambles:

View File

@@ -689,6 +689,7 @@ gl:
cancel: Cancelar
category: Categoría
category_description_html: A razón para denunciar esta conta ou contido será citada na comunicación coa conta denunciada
collections: Coleccións (%{count})
comment:
none: Ningún
comment_description_html: 'Como información engadida, %{name} escribiu:'
@@ -724,6 +725,7 @@ gl:
resolved_msg: Resolveuse con éxito a denuncia!
skip_to_actions: Ir a accións
status: Estado
statuses: Publicacións (%{count})
statuses_description_html: O contido ofensivo será citado na comunicación coa conta denunciada
summary:
action_preambles:

View File

@@ -660,7 +660,7 @@ ko:
add_to_report: 신고에 더 추가하기
already_suspended_badges:
local: 이 서버에서 이미 정지되었습니다
remote: 서버에서 이미 정지되었습니다
remote: 이미 해당 서버에서 정지함
are_you_sure: 확실합니까?
assign_to_self: 나에게 할당하기
assigned: 할당된 중재자

View File

@@ -345,6 +345,9 @@ nn:
accounts: Kontoar
collection_title: Samling av %{name}
contents: Innhald
number_of_accounts:
one: 1 konto
other: "%{count} kontoar"
open: Opna
view_publicly: Vis offentleg
critical_update_pending: Kritisk oppdatering ventar
@@ -686,6 +689,7 @@ nn:
cancel: Avbryt
category: Kategori
category_description_html: Årsaka til at kontoen og/eller innhaldet vart rapportert vil bli inkludert i kommunikasjonen med den rapporterte kontoen
collections: Samlingar (%{count})
comment:
none: Ingen
comment_description_html: 'For å gje meir informasjon, skreiv %{name}:'
@@ -721,6 +725,7 @@ nn:
resolved_msg: Rapporten er løyst!
skip_to_actions: Gå til handlingar
status: Status
statuses: Innlegg (%{count})
statuses_description_html: Støytande innhald vil bli inkludert i kommunikasjonen med den rapporterte kontoen
summary:
action_preambles:

View File

@@ -60,7 +60,7 @@ zh-TW:
setting_boost_modal: 當啟用時,轉嘟前將先開啟確認對話框,您能於其變更轉嘟之可見性。
setting_default_quote_policy_private: Mastodon 上發佈之僅限跟隨者嘟文無法被其他使用者引用。
setting_default_quote_policy_unlisted: 當其他人引用您時,他們的嘟文也會自熱門時間軸隱藏。
setting_default_sensitive: 敏感內容媒體預設隱藏,且按一下即可重新顯示
setting_default_sensitive: 敏感內容媒體預設隱藏,且按一下即可重新顯示
setting_display_media_default: 隱藏標為敏感內容的媒體
setting_display_media_hide_all: 總是隱藏所有媒體
setting_display_media_show_all: 總是顯示標為敏感內容的媒體
@@ -272,7 +272,7 @@ zh-TW:
type: 匯入類型
username: 使用者名稱
username_or_email: 使用者名稱或電子郵件地址
whole_word: 詞彙
whole_word: 整詞彙
email_domain_block:
with_dns_records: 包括網域的 MX 記錄與 IP 位址
featured_tag:

View File

@@ -689,6 +689,7 @@ tr:
cancel: İptal et
category: Kategori
category_description_html: Bu hesap ve/veya içeriğin bildirilme gerekçesi, bildirilen hesapla iletişimde alıntılanacaktır
collections: Koleksiyonlar (%{count})
comment:
none: Yok
comment_description_html: 'Daha fazla bilgi vermek için %{name} şunu yazdı:'
@@ -724,6 +725,7 @@ tr:
resolved_msg: Şikayet başarıyla çözümlendi!
skip_to_actions: İşlemlere atla
status: Durum
statuses: Gönderiler (%{count})
statuses_description_html: İncitici içerik, bildirilen hesapla iletişimde alıntılanacaktır
summary:
action_preambles:

View File

@@ -675,7 +675,7 @@ zh-CN:
cancel: 取消
category: 类别
category_description_html: 在与被举报账号的通信时,将引用该账号和/或内容被举报的原因
collections: 收藏列表%{count}
collections: 收藏列表 (%{count})
comment:
none: 没有
comment_description_html: "%{name} 补充道:"
@@ -711,7 +711,7 @@ zh-CN:
resolved_msg: 举报处理成功!
skip_to_actions: 跳转到操作
status: 状态
statuses: 嘟文%{count}
statuses: 嘟文 (%{count})
statuses_description_html: 在与该账号的通信中将引用违规内容
summary:
action_preambles:

View File

@@ -1754,7 +1754,7 @@ zh-TW:
too_many_options: 不能包含多於 %{max} 個項目
vote: 投票
posting_defaults:
explanation: 這些設定將作為您建立新嘟文時之預設值,但您能於編輯器中編輯個別嘟文之設定。
explanation: 這些設定將作為您建立新嘟文時之預設值,但您能於嘟文編輯器中編輯個別嘟文之設定。
preferences:
other: 其他
posting_defaults: 嘟文預設值
@@ -1767,7 +1767,7 @@ zh-TW:
reach_hint_html: 控制您希望被新使用者探索或跟隨之方式。想使您的嘟文出現於探索頁面嗎?想使其他人透過他們的跟隨建議找到您嗎?想自動接受所有新跟隨者嗎?或是想逐一控制跟隨請求嗎?
search: 搜尋
search_hint_html: 控制您希望如何被發現。您想透過您的公開嘟文被人們發現嗎?您想透過網頁搜尋被 Mastodon 以外的人找到您的個人檔案嗎?請注意,公開資訊可能無法全面地被所有搜尋引擎所排除。
title: 隱私權觸及
title: 隱私權觸及
privacy_policy:
title: 隱私權政策
reactions:

View File

@@ -155,9 +155,7 @@ Rails.application.routes.draw do
constraints(username: %r{[^@/.]+}) do
with_options to: 'accounts#show' do
get '/@:username', as: :short_account
get '/@:username/posts'
get '/@:username/featured'
get '/@:username/about'
get '/@:username/with_replies', as: :short_account_with_replies
get '/@:username/media', as: :short_account_media
get '/@:username/tagged/:tag', as: :short_account_tag

View File

@@ -0,0 +1,105 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::ModerationAction do
subject do
described_class.new(
current_account:,
type:,
report_id:,
text:
)
end
let(:current_account) { Fabricate(:admin_user).account }
let(:target_account) { Fabricate(:account) }
let(:statuses) { Fabricate.times(2, :status, account: target_account) }
let(:status_ids) { statuses.map(&:id) }
let(:report) { Fabricate(:report, target_account:, status_ids:) }
let(:report_id) { report.id }
let(:text) { 'test' }
describe '#save!' do
context 'when `type` is `delete`' do
let(:type) { 'delete' }
it 'discards the statuses' do
subject.save!
statuses.each do |status|
expect(status.reload).to be_discarded
end
expect(report.reload).to be_action_taken
end
context 'with attached collections', feature: :collections do
let(:status_ids) { [] }
let(:collections) { Fabricate.times(2, :collection, account: target_account) }
before do
report.collections = collections
end
it 'deletes the collections and creates an action log' do
expect { subject.save! }.to change(Collection, :count).by(-2)
.and change(Admin::ActionLog, :count).by(3)
end
end
context 'with a remote collection', feature: :collections do
let(:status_ids) { [] }
let(:collection) { Fabricate(:remote_collection) }
let(:target_account) { collection.account }
before do
report.collections << collection
end
it 'creates a tombstone' do
expect { subject.save! }.to change(Tombstone, :count).by(1)
expect(Tombstone.last.uri).to eq collection.uri
end
end
end
context 'when `type` is `mark_as_sensitive`' do
let(:type) { 'mark_as_sensitive' }
before do
preview_card = Fabricate(:preview_card)
statuses.each do |status|
PreviewCardsStatus.create!(status:, preview_card:)
end
end
it 'marks the statuses as sensitive' do
subject.save!
statuses.each do |status|
expect(status.reload).to be_sensitive
end
expect(report.reload).to be_action_taken
end
context 'with attached collections', feature: :collections do
let(:status_ids) { [] }
let(:collections) { Fabricate.times(2, :collection, account: target_account) }
before do
report.collections = collections
end
it 'marks the collections as sensitive' do
subject.save!
collections.each do |collection|
expect(collection.reload).to be_sensitive
end
expect(report.reload).to be_action_taken
end
end
end
end
end

View File

@@ -22,39 +22,6 @@ RSpec.describe Admin::StatusBatchAction do
let(:text) { 'test' }
describe '#save!' do
context 'when `type` is `delete`' do
let(:type) { 'delete' }
it 'discards the statuses' do
subject.save!
statuses.each do |status|
expect(status.reload).to be_discarded
end
expect(report.reload).to be_action_taken
end
end
context 'when `type` is `mark_as_sensitive`' do
let(:type) { 'mark_as_sensitive' }
before do
preview_card = Fabricate(:preview_card)
statuses.each do |status|
PreviewCardsStatus.create!(status:, preview_card:)
end
end
it 'marks the statuses as sensitive' do
subject.save!
statuses.each do |status|
expect(status.reload).to be_sensitive
end
expect(report.reload).to be_action_taken
end
end
context 'when `type` is `report`' do
let(:report_id) { nil }
let(:type) { 'report' }

View File

@@ -138,4 +138,18 @@ RSpec.describe Collection do
expect(subject.object_type).to eq :featured_collection
end
end
describe '#to_log_human_identifier' do
subject { Fabricate(:collection) }
it 'returns the account name' do
expect(subject.to_log_human_identifier).to eq subject.account.acct
end
end
describe '#to_log_permalink' do
it 'includes the URI of the collection' do
expect(subject.to_log_permalink).to eq ActivityPub::TagManager.instance.uri_for(subject)
end
end
end

View File

@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::CollectionPolicy do
let(:policy) { described_class }
let(:admin) { Fabricate(:admin_user).account }
let(:john) { Fabricate(:account) }
let(:collection) { Fabricate(:collection) }
permissions :index?, :show?, :update?, :destroy? do
context 'with an admin' do
it 'permits' do
expect(policy).to permit(admin, Collection)
end
end
context 'with a non-admin' do
it 'denies' do
expect(policy).to_not permit(john, Collection)
end
end
end
end