Merge commit '8e00f7cc8fab9b447f80a27a972eef78186633ad' into glitch-soc/merge-upstream

This commit is contained in:
Claire committed 2026-03-27 18:15:50 +01:00
commit 30bb39fe54
86 files changed
+1142 -558

No files matched your search

@@ -75,6 +75,7 @@ interface AccountProps {
withMenu?: boolean;
withBorder?: boolean;
extraAccountInfo?: React.ReactNode;
className?: string;
children?: React.ReactNode;
}
@@ -88,6 +89,7 @@ export const Account: React.FC<AccountProps> = ({
withMenu = true,
withBorder = true,
extraAccountInfo,
className,
children,
}) => {
const intl = useIntl();
@@ -290,7 +292,7 @@ export const Account: React.FC<AccountProps> = ({
return (
<div
className={classNames('account', {
className={classNames('account', className, {
'account--minimal': minimal,
'account--without-border': !withBorder,
})}
@@ -6,6 +6,7 @@
background-color: var(--color-bg-brand-softest);
color: var(--color-text-primary);
border-radius: 12px;
font-size: 15px;
}
.icon {
@@ -7,15 +7,11 @@ import ErrorIcon from '@/material-icons/400-24px/error.svg?react';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import type { FieldStatus } from '../form_fields/form_field_wrapper';
import { Icon } from '../icon';
import classes from './styles.module.css';
export interface FieldStatus {
variant: 'error' | 'warning' | 'info' | 'success';
message?: string;
}
const iconMap: Record<FieldStatus['variant'], React.FunctionComponent> = {
error: ErrorIcon,
warning: WarningIcon,
@@ -1,9 +1,9 @@
import { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { length } from 'stringz';
import { polymorphicForwardRef } from '@/types/polymorphic';
import classes from './styles.module.scss';
@@ -14,8 +14,6 @@ interface CharacterCounterProps {
recommended?: boolean;
}
const segmenter = new Intl.Segmenter();
export const CharacterCounter = polymorphicForwardRef<
'span',
CharacterCounterProps
@@ -31,10 +29,7 @@ export const CharacterCounter = polymorphicForwardRef<
},
ref,
) => {
const currentLength = useMemo(
() => [...segmenter.segment(currentString)].length,
[currentString],
);
const currentLength = length(currentString);
return (
<Component
{...props}
@@ -13,6 +13,7 @@ export const StatusesCounter = (
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
tagName='span'
/>
);
@@ -27,6 +28,7 @@ export const FollowingCounter = (
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
tagName='span'
/>
);
@@ -41,6 +43,7 @@ export const FollowersCounter = (
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
tagName='span'
/>
);
@@ -55,5 +58,6 @@ export const FollowersYouKnowCounter = (
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
tagName='span'
/>
);
@@ -121,6 +121,7 @@ export const EditedTimestamp: React.FC<{
/>
),
}}
tagName='span'
/>
</button>
</Dropdown>
@@ -2,11 +2,9 @@ import type {
ChangeEvent,
ChangeEventHandler,
ComponentPropsWithoutRef,
Dispatch,
FC,
ReactNode,
RefObject,
SetStateAction,
} from 'react';
import { useCallback, useId, useRef } from 'react';
@@ -25,7 +23,7 @@ import { TextInput } from './text_input_field';
export type EmojiInputProps = {
value?: string;
onChange?: Dispatch<SetStateAction<string>>;
onChange?: (newValue: string) => void;
counterMax?: number;
recommended?: boolean;
} & Omit<CommonFieldWrapperProps, 'wrapperClassName'>;
@@ -138,12 +136,15 @@ const EmojiFieldWrapper: FC<
const handlePickEmoji = useCallback(
(emoji: string) => {
onChange?.((prev) => {
const position = inputRef.current?.selectionStart ?? prev.length;
return insertEmojiAtPosition(prev, emoji, position);
});
if (!value) {
onChange?.('');
return;
}
const position = inputRef.current?.selectionStart ?? value.length;
const newValue = insertEmojiAtPosition(value, emoji, position);
onChange?.(newValue);
},
[onChange, inputRef],
[inputRef, value, onChange],
);
const handleChange = useCallback(
@@ -4,10 +4,10 @@ import type { ReactNode, FC } from 'react';
import { createContext, useId } from 'react';
import { A11yLiveRegion } from 'mastodon/components/a11y_live_region';
import type { FieldStatus } from 'mastodon/components/callout_inline';
import { CalloutInline } from 'mastodon/components/callout_inline';
import classes from './fieldset.module.scss';
import type { FieldStatus } from './form_field_wrapper';
import { getFieldStatus } from './form_field_wrapper';
import formFieldWrapperClasses from './form_field_wrapper.module.scss';
@@ -8,7 +8,6 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { A11yLiveRegion } from 'mastodon/components/a11y_live_region';
import type { FieldStatus } from 'mastodon/components/callout_inline';
import { CalloutInline } from 'mastodon/components/callout_inline';
import { FieldsetNameContext } from './fieldset';
@@ -20,11 +19,16 @@ export interface InputProps {
'aria-describedby'?: string;
}
export interface FieldStatus {
variant: 'error' | 'warning' | 'info' | 'success';
message?: string;
}
interface FieldWrapperProps {
label: ReactNode;
hint?: ReactNode;
required?: boolean;
status?: FieldStatus['variant'] | FieldStatus;
status?: FieldStatus['variant'] | FieldStatus | null;
inputId?: string;
describedById?: string;
inputPlacement?: 'inline-start' | 'inline-end';
@@ -1,3 +1,4 @@
export type { FieldStatus } from './form_field_wrapper';
export { FormFieldWrapper } from './form_field_wrapper';
export { FormStack } from './form_stack';
export { Fieldset } from './fieldset';
@@ -0,0 +1,40 @@
import { NavLink } from 'react-router-dom';
import type { MastodonLocationDescriptor } from 'mastodon/components/router';
import classes from './styles.module.scss';
interface WrapperProps {
children: React.ReactNode;
}
export const NumberFields: React.FC<WrapperProps> = ({ children }) => {
return <ul className={classes.list}>{children}</ul>;
};
interface ItemProps {
label: React.ReactNode;
hint?: string;
link?: MastodonLocationDescriptor;
children: React.ReactNode;
}
export const NumberFieldsItem: React.FC<ItemProps> = ({
label,
hint,
link,
children,
}) => {
return (
<li className={classes.item} title={hint}>
{label}
{link ? (
<NavLink exact to={link}>
{children}
</NavLink>
) : (
<strong>{children}</strong>
)}
</li>
);
};
@@ -0,0 +1,32 @@
.list {
display: flex;
flex-wrap: wrap;
margin: 8px 0;
padding: 0;
gap: 4px 20px;
font-size: 13px;
color: var(--color-text-secondary);
}
.item {
@container (width < 420px) {
flex: 1 1 0px;
}
a,
strong {
display: block;
font-weight: 600;
color: var(--color-text-primary);
font-size: 15px;
}
a {
padding: 0;
&:hover,
&:focus {
text-decoration: underline;
}
}
}
@@ -26,6 +26,8 @@ export type LocationState = MastodonLocationState | null | undefined;
export type MastodonLocation = ReturnType<typeof useLocation<LocationState>>;
export type MastodonLocationDescriptor = LocationDescriptor<LocationState>;
type HistoryPath = Path | LocationDescriptor<LocationState>;
export const browserHistory = createBrowserHistory<LocationState>();
@@ -241,21 +241,23 @@ export const AccountEdit: FC = () => {
showDescription={!hasFields}
buttons={
<div className={classes.fieldButtons}>
<Button
className={classes.editButton}
onClick={handleCustomFieldReorder}
disabled={profile.fields.length <= 1}
>
<FormattedMessage
id='account_edit.custom_fields.reorder_button'
defaultMessage='Reorder fields'
{profile.fields.length > 1 && (
<Button
className={classes.editButton}
onClick={handleCustomFieldReorder}
>
<FormattedMessage
id='account_edit.custom_fields.reorder_button'
defaultMessage='Reorder fields'
/>
</Button>
)}
{profile.fields.length < maxFieldCount && (
<EditButton
label={intl.formatMessage(messages.customFieldsAddLabel)}
onClick={handleCustomFieldAdd}
/>
</Button>
<EditButton
label={intl.formatMessage(messages.customFieldsAddLabel)}
onClick={handleCustomFieldAdd}
disabled={profile.fields.length >= maxFieldCount}
/>
)}
</div>
}
>
@@ -1,11 +1,5 @@
import {
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import type { FC } from 'react';
import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
import type { FC, FocusEventHandler } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@@ -13,7 +7,7 @@ import type { Map as ImmutableMap } from 'immutable';
import { closeModal } from '@/mastodon/actions/modal';
import { Button } from '@/mastodon/components/button';
import { Callout } from '@/mastodon/components/callout';
import type { FieldStatus } from '@/mastodon/components/form_fields';
import { EmojiTextInputField } from '@/mastodon/components/form_fields';
import {
removeField,
@@ -71,6 +65,26 @@ const messages = defineMessages({
id: 'account_edit.field_edit_modal.discard_confirm',
defaultMessage: 'Discard',
},
errorBlank: {
id: 'form_error.blank',
defaultMessage: 'Field cannot be blank.',
},
warningLength: {
id: 'account_edit.field_edit_modal.length_warning',
defaultMessage:
'Recommended character limit exceeded. Mobile users might not see your field in full.',
},
warningUrlEmoji: {
id: 'account_edit.field_edit_modal.link_emoji_warning',
defaultMessage:
'We recommend against the use of custom emoji in combination with urls. Custom fields containing both will display as text only instead of as a link, in order to prevent user confusion.',
},
warningUrlProtocol: {
id: 'account_edit.field_edit_modal.url_warning',
defaultMessage:
'To add a link, please include {protocol} at the beginning.',
description: '{protocol} is https://',
},
});
// We have two different values- the hard limit set by the server,
@@ -124,34 +138,86 @@ export const EditFieldModal = forwardRef<
const { nameLimit, valueLimit } = useAppSelector(selectFieldLimits);
const isPending = useAppSelector((state) => state.profileEdit.isPending);
const disabled =
!newLabel.trim() ||
!newValue.trim() ||
!isDirty ||
!nameLimit ||
!valueLimit ||
newLabel.length > nameLimit ||
newValue.length > valueLimit;
const [fieldStatuses, setFieldStatuses] = useState<{
label?: FieldStatus;
value?: FieldStatus;
}>({});
const customEmojiCodes = useAppSelector(selectEmojiCodes);
const hasLinkAndEmoji = useMemo(() => {
const text = `${newLabel} ${newValue}`; // Combine text, as we're searching it all.
const hasLink = /https?:\/\//.test(text);
const hasEmoji = customEmojiCodes.some((code) =>
text.includes(`:${code}:`),
);
return hasLink && hasEmoji;
}, [customEmojiCodes, newLabel, newValue]);
const hasLinkWithoutProtocol = useMemo(
() => isUrlWithoutProtocol(newValue),
[newValue],
const checkField = useCallback(
(value: string): FieldStatus | null => {
if (!value.trim()) {
return {
variant: 'error',
message: intl.formatMessage(messages.errorBlank),
};
}
if (value.length > RECOMMENDED_LIMIT) {
return {
variant: 'warning',
message: intl.formatMessage(messages.warningLength, {
max: RECOMMENDED_LIMIT,
}),
};
}
const hasLink = /https?:\/\//.test(value);
const hasEmoji = customEmojiCodes.some((code) =>
value.includes(`:${code}:`),
);
if (hasLink && hasEmoji) {
return {
variant: 'warning',
message: intl.formatMessage(messages.warningUrlEmoji),
};
}
if (isUrlWithoutProtocol(value)) {
return {
variant: 'warning',
message: intl.formatMessage(messages.warningUrlProtocol, {
protocol: 'https://',
}),
};
}
return null;
},
[customEmojiCodes, intl],
);
const handleBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(event) => {
const { name, value } = event.target;
const result = checkField(value);
if (name !== 'label' && name !== 'value') {
return;
}
setFieldStatuses((statuses) => ({
...statuses,
[name]: result ?? undefined,
}));
},
[checkField],
);
const dispatch = useAppDispatch();
const handleSave = useCallback(() => {
if (disabled || isPending) {
if (isPending) {
return;
}
const labelStatus = checkField(newLabel);
const valueStatus = checkField(newValue);
if (labelStatus || valueStatus) {
setFieldStatuses({
label: labelStatus ?? undefined,
value: valueStatus ?? undefined,
});
return;
}
void dispatch(
updateField({ id: fieldKey, name: newLabel, value: newValue }),
).then(() => {
@@ -163,7 +229,7 @@ export const EditFieldModal = forwardRef<
}),
);
});
}, [disabled, dispatch, fieldKey, isPending, newLabel, newValue]);
}, [checkField, dispatch, fieldKey, isPending, newLabel, newValue]);
useImperativeHandle(
ref,
@@ -198,60 +264,33 @@ export const EditFieldModal = forwardRef<
confirm={intl.formatMessage(messages.save)}
onConfirm={handleSave}
updating={isPending}
disabled={disabled}
className={classes.wrapper}
>
<EmojiTextInputField
name='label'
value={newLabel}
onChange={setNewLabel}
onBlur={handleBlur}
label={intl.formatMessage(messages.editLabelField)}
hint={intl.formatMessage(messages.editLabelHint)}
status={fieldStatuses.label}
maxLength={nameLimit}
counterMax={RECOMMENDED_LIMIT}
recommended
/>
<EmojiTextInputField
name='value'
value={newValue}
onChange={setNewValue}
onBlur={handleBlur}
label={intl.formatMessage(messages.editValueField)}
hint={intl.formatMessage(messages.editValueHint)}
status={fieldStatuses.value}
maxLength={valueLimit}
counterMax={RECOMMENDED_LIMIT}
recommended
/>
{hasLinkAndEmoji && (
<Callout variant='warning'>
<FormattedMessage
id='account_edit.field_edit_modal.link_emoji_warning'
defaultMessage='We recommend against the use of custom emoji in combination with urls. Custom fields containing both will display as text only instead of as a link, in order to prevent user confusion.'
/>
</Callout>
)}
{(newLabel.length > RECOMMENDED_LIMIT ||
newValue.length > RECOMMENDED_LIMIT) && (
<Callout variant='warning'>
<FormattedMessage
id='account_edit.field_edit_modal.limit_warning'
defaultMessage='Recommended character limit exceeded. Mobile users might not see your field in full.'
/>
</Callout>
)}
{hasLinkWithoutProtocol && (
<Callout variant='warning'>
<FormattedMessage
id='account_edit.field_edit_modal.url_warning'
defaultMessage='To add a link, please include {protocol} at the beginning.'
description='{protocol} is https://'
values={{
protocol: <code>https://</code>,
}}
/>
</Callout>
)}
</ConfirmationModal>
);
});
@@ -13,7 +13,6 @@
> img {
object-fit: cover;
object-position: top center;
width: 100%;
height: 100%;
}
@@ -26,7 +25,7 @@
.avatar {
margin-top: -64px;
margin-left: 18px;
margin-left: 22px;
position: relative;
width: 82px;
@@ -251,7 +250,7 @@
// Section component
.section {
padding: 20px;
padding: 24px;
border-bottom: 1px solid var(--color-border-primary);
font-size: 15px;
}
@@ -37,15 +37,25 @@ const selectGalleryTimeline = createAppSelector(
(state) => state.statuses,
],
(accountId, timelines, accounts, statuses) => {
let items = emptyList;
if (!accountId) {
return null;
return {
items,
hasMore: false,
isLoading: false,
withReplies: false,
};
}
const account = accounts.get(accountId);
if (!account) {
return null;
return {
items,
hasMore: false,
isLoading: false,
withReplies: false,
};
}
let items = emptyList;
const { show_media, show_media_replies } = account;
// If the account disabled showing media, don't display anything.
if (!show_media && redesignEnabled) {
@@ -53,13 +63,13 @@ const selectGalleryTimeline = createAppSelector(
items,
hasMore: false,
isLoading: false,
showingReplies: false,
withReplies: false,
};
}
const showingReplies = show_media_replies && redesignEnabled;
const withReplies = show_media_replies && redesignEnabled;
const timeline = timelines.get(
`account:${accountId}:media${showingReplies ? ':with_replies' : ''}`,
`account:${accountId}:media${withReplies ? ':with_replies' : ''}`,
);
const statusIds = timeline?.get('items');
@@ -77,8 +87,8 @@ const selectGalleryTimeline = createAppSelector(
return {
items,
hasMore: !!timeline?.get('hasMore'),
isLoading: !!timeline?.get('isLoading'),
showingReplies,
isLoading: timeline?.get('isLoading') ? true : false,
withReplies,
};
},
);
@@ -89,11 +99,11 @@ export const AccountGallery: React.FC<{
const dispatch = useAppDispatch();
const accountId = useAccountId();
const {
isLoading = true,
hasMore = false,
items: attachments = emptyList,
showingReplies: withReplies = false,
} = useAppSelector((state) => selectGalleryTimeline(state, accountId)) ?? {};
isLoading,
items: attachments,
hasMore,
withReplies,
} = useAppSelector((state) => selectGalleryTimeline(state, accountId));
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
@@ -215,7 +225,7 @@ export const AccountGallery: React.FC<{
alwaysPrepend
append={accountId && <RemoteHint accountId={accountId} />}
scrollKey='account_gallery'
isLoading={isLoading}
showLoading={isLoading}
hasMore={!forceEmptyState && hasMore}
onLoadMore={handleLoadMore}
emptyMessage={emptyMessage}
@@ -69,7 +69,9 @@ export const FamiliarFollowers: React.FC<{ accountId: string }> = ({
<Avatar withLink key={account.id} account={account} size={28} />
))}
</AvatarGroup>
<FamiliarFollowersReadout familiarFollowers={familiarFollowers} />
<span>
<FamiliarFollowersReadout familiarFollowers={familiarFollowers} />
</span>
</div>
);
};
@@ -3,7 +3,6 @@ import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { NavLink } from 'react-router-dom';
import {
@@ -12,13 +11,15 @@ import {
StatusesCounter,
} from '@/mastodon/components/counters';
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
import {
NumberFields,
NumberFieldsItem,
} from '@/mastodon/components/number_fields';
import { ShortNumber } from '@/mastodon/components/short_number';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss';
const LegacyNumberFields: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl();
const account = useAccount(accountId);
@@ -77,56 +78,51 @@ const RedesignNumberFields: FC<{ accountId: string }> = ({ accountId }) => {
}
return (
<ul
className={classNames(
'account__header__extra__links',
classes.fieldNumbersWrapper,
)}
>
<li>
<FormattedMessage id='account.posts' defaultMessage='Posts' />
<strong>
<ShortNumber value={account.statuses_count} />
</strong>
</li>
<NumberFields>
<NumberFieldsItem
label={<FormattedMessage id='account.posts' defaultMessage='Posts' />}
hint={intl.formatNumber(account.statuses_count)}
>
<ShortNumber value={account.statuses_count} />
</NumberFieldsItem>
<li>
<FormattedMessage id='account.followers' defaultMessage='Followers' />
<NavLink
exact
to={`/@${account.acct}/followers`}
title={intl.formatNumber(account.followers_count)}
>
<ShortNumber value={account.followers_count} />
</NavLink>
</li>
<NumberFieldsItem
label={
<FormattedMessage id='account.followers' defaultMessage='Followers' />
}
hint={intl.formatNumber(account.followers_count)}
link={`/@${account.acct}/followers`}
>
<ShortNumber value={account.followers_count} />
</NumberFieldsItem>
<li>
<FormattedMessage id='account.following' defaultMessage='Following' />
<NavLink
exact
to={`/@${account.acct}/following`}
title={intl.formatNumber(account.following_count)}
>
<ShortNumber value={account.following_count} />
</NavLink>
</li>
<NumberFieldsItem
label={
<FormattedMessage id='account.following' defaultMessage='Following' />
}
hint={intl.formatNumber(account.following_count)}
link={`/@${account.acct}/following`}
>
<ShortNumber value={account.following_count} />
</NumberFieldsItem>
<li>
<FormattedMessage id='account.joined_short' defaultMessage='Joined' />
<strong>
{createdThisYear ? (
<FormattedDateWrapper
value={account.created_at}
month='short'
day='2-digit'
/>
) : (
<FormattedDateWrapper value={account.created_at} year='numeric' />
)}
</strong>
</li>
</ul>
<NumberFieldsItem
label={
<FormattedMessage id='account.joined_short' defaultMessage='Joined' />
}
hint={intl.formatDate(account.created_at)}
>
{createdThisYear ? (
<FormattedDateWrapper
value={account.created_at}
month='short'
day='2-digit'
/>
) : (
<FormattedDateWrapper value={account.created_at} year='numeric' />
)}
</NumberFieldsItem>
</NumberFields>
);
};
@@ -312,37 +312,6 @@ svg.badgeIcon {
}
}
.fieldNumbersWrapper {
display: flex;
font-size: 13px;
padding: 0;
margin: 8px 0;
gap: 20px;
li {
@container (width < 420px) {
flex: 1 1 0px;
}
}
a,
strong {
display: block;
font-weight: 600;
color: var(--color-text-primary);
font-size: 15px;
}
a {
padding: 0;
&:hover,
&:focus {
text-decoration: underline;
}
}
}
.modalCloseButton {
padding: 8px;
border-radius: 50%;
@@ -38,6 +38,7 @@ export function usePinnedStatusIds({
userId: accountId,
tagged,
pinned: true,
replies: true,
});
const dispatch = useAppDispatch();
@@ -2,6 +2,8 @@ import { useCallback, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Callout } from '@/mastodon/components/callout';
import { FollowButton } from '@/mastodon/components/follow_button';
import { openModal } from 'mastodon/actions/modal';
import type {
ApiCollectionJSON,
@@ -27,10 +29,6 @@ const messages = defineMessages({
id: 'collections.accounts.empty_title',
defaultMessage: 'This collection is empty',
},
accounts: {
id: 'collections.detail.accounts_heading',
defaultMessage: 'Accounts',
},
});
const SimpleAuthorName: React.FC<{ id: string }> = ({ id }) => {
@@ -41,8 +39,9 @@ const SimpleAuthorName: React.FC<{ id: string }> = ({ id }) => {
const AccountItem: React.FC<{
accountId: string | undefined;
collectionOwnerId: string;
withBio?: boolean;
withBorder?: boolean;
}> = ({ accountId, withBorder = true, collectionOwnerId }) => {
}> = ({ accountId, withBio = true, withBorder = true, collectionOwnerId }) => {
const relationship = useRelationship(accountId);
if (!accountId) {
@@ -59,12 +58,17 @@ const AccountItem: React.FC<{
(relationship.following || relationship.requested));
return (
<Account
minimal={withoutButton}
withMenu={false}
withBorder={withBorder}
id={accountId}
/>
<div className={classes.accountItemWrapper} data-with-border={withBorder}>
<Account
minimal
id={accountId}
withBio={withBio}
withBorder={false}
withMenu={false}
className={classes.accountItem}
/>
{!withoutButton && <FollowButton accountId={accountId} />}
</div>
);
};
@@ -131,19 +135,27 @@ const SensitiveScreen: React.FC<{
}
return (
<div className={classes.sensitiveWarning}>
<Callout
variant='warning'
title={
<FormattedMessage
id='collections.detail.sensitive_content'
defaultMessage='Sensitive content'
/>
}
primaryLabel={
<FormattedMessage
id='content_warning.show_short'
defaultMessage='Show'
/>
}
onPrimary={showAnyway}
>
<FormattedMessage
id='collections.detail.sensitive_note'
defaultMessage='This collection contains accounts and content that may be sensitive to some users.'
tagName='p'
defaultMessage='The description and accounts may not be suitable for all viewers.'
/>
<Button onClick={showAnyway}>
<FormattedMessage
id='content_warning.show'
defaultMessage='Show anyway'
/>
</Button>
</div>
</Callout>
);
};
@@ -192,13 +204,14 @@ export const CollectionAccountsList: React.FC<{
<ItemList
isLoading={isLoading}
emptyMessage={intl.formatMessage(messages.empty)}
className={classes.itemList}
>
{collection && currentUserInCollection ? (
<>
<h3 className={classes.columnSubheading}>
<FormattedMessage
id='collections.detail.author_added_you'
defaultMessage='{author} added you to this collection'
id='collections.detail.you_were_added_to_this_collection'
defaultMessage='You were added to this collection'
values={{
author: <SimpleAuthorName id={collection.account_id} />,
}}
@@ -208,9 +221,11 @@ export const CollectionAccountsList: React.FC<{
key={currentUserInCollection.account_id}
aria-posinset={1}
aria-setsize={items.length}
className={classes.youWereAddedWrapper}
>
<AccountItem
withBorder={false}
withBio={false}
accountId={currentUserInCollection.account_id}
collectionOwnerId={collection.account_id}
/>
@@ -225,18 +240,30 @@ export const CollectionAccountsList: React.FC<{
ref={listHeadingRef}
>
<FormattedMessage
id='collections.detail.other_accounts_in_collection'
defaultMessage='Others in this collection:'
id='collections.detail.other_accounts_count'
defaultMessage='{count, plural, one {# other account} other {# other accounts}}'
values={{ count: collection.item_count - 1 }}
/>
</h3>
</>
) : (
<h3
className='column-subheading sr-only'
className={classes.columnSubheading}
tabIndex={-1}
ref={listHeadingRef}
>
{intl.formatMessage(messages.accounts)}
{collection ? (
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
/>
) : (
<FormattedMessage
id='collections.detail.accounts_heading'
defaultMessage='Accounts'
/>
)}
</h3>
)}
{collection && (
@@ -43,7 +43,7 @@
width: 18px;
height: 18px;
border-radius: 8px;
color: var(--color-text-primary);
fill: var(--color-text-primary);
background: var(--color-bg-warning-softest);
}
@@ -4,24 +4,19 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useHistory, useLocation, useParams } from 'react-router';
import { Link } from 'react-router-dom';
import { openModal } from '@/mastodon/actions/modal';
import { RelativeTimestamp } from '@/mastodon/components/relative_timestamp';
import { useAccountHandle } from '@/mastodon/components/display_name/default';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Avatar } from 'mastodon/components/avatar';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import {
DisplayName,
LinkedDisplayName,
} from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import { Scrollable } from 'mastodon/components/scrollable_list/components';
import { Tag } from 'mastodon/components/tags/tag';
import { useAccount } from 'mastodon/hooks/useAccount';
import { me } from 'mastodon/initial_state';
import { domain } from 'mastodon/initial_state';
import { fetchCollection } from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
@@ -40,88 +35,29 @@ const messages = defineMessages({
},
});
const CollectionMetaData: React.FC<{
collection: ApiCollectionJSON;
extended?: boolean;
}> = ({ collection, extended }) => {
return (
<ul className={classes.metaList}>
<FormattedMessage
id='collections.account_count'
defaultMessage='{count, plural, one {# account} other {# accounts}}'
values={{ count: collection.item_count }}
tagName='li'
/>
{extended && (
<>
{collection.discoverable ? (
<FormattedMessage
id='collections.visibility_public'
defaultMessage='Public'
tagName='li'
/>
) : (
<FormattedMessage
id='collections.visibility_unlisted'
defaultMessage='Unlisted'
tagName='li'
/>
)}
{collection.sensitive && (
<FormattedMessage
id='collections.sensitive'
defaultMessage='Sensitive'
tagName='li'
/>
)}
</>
)}
<FormattedMessage
id='collections.last_updated_at'
defaultMessage='Last updated: {date}'
values={{
date: <RelativeTimestamp timestamp={collection.updated_at} long />,
}}
tagName='li'
/>
</ul>
);
};
export const AuthorNote: React.FC<{ id: string; previewMode?: boolean }> = ({
id,
// When previewMode is enabled, your own display name
// will not be replaced with "you"
previewMode = false,
}) => {
export const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
const account = useAccount(id);
const authorHandle = useAccountHandle(account, domain);
if (!account) {
return null;
}
const author = (
<span className={classes.displayNameWithAvatar}>
<Avatar size={18} account={account} />
{previewMode ? (
<DisplayName account={account} variant='simple' />
) : (
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
)}
</span>
<Link to={`/@${account.acct}`} data-hover-card-account={account.id}>
{authorHandle}
</Link>
);
const displayAsYou = id === me && !previewMode;
return (
<p className={previewMode ? classes.previewAuthorNote : classes.authorNote}>
{displayAsYou ? (
<FormattedMessage
id='collections.detail.curated_by_you'
defaultMessage='Curated by you'
/>
) : (
<FormattedMessage
id='collections.detail.curated_by_author'
defaultMessage='Curated by {author}'
values={{ author }}
/>
)}
<p className={classes.authorNote}>
<FormattedMessage
id='collections.by_account'
defaultMessage='by {account_handle}'
values={{
account_handle: author,
}}
/>
</p>
);
};
@@ -156,14 +92,12 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
}, [history, handleShare, isNewCollection, location.pathname]);
return (
<div className={classes.header}>
<header className={classes.header}>
<div className={classes.titleWithMenu}>
<div className={classes.titleWrapper}>
{tag && (
// TODO: Make non-interactive tag component
<Tag name={tag.name} className={classes.tag} />
)}
{tag && <span className={classes.tag}>#{tag.name}</span>}
<h2 className={classes.name}>{name}</h2>
<AuthorNote id={account_id} />
</div>
<div className={classes.headerButtonWrapper}>
<IconButton
@@ -181,12 +115,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
</div>
</div>
{description && <p className={classes.description}>{description}</p>}
<AuthorNote id={collection.account_id} />
<CollectionMetaData
extended={account_id === me}
collection={collection}
/>
</div>
</header>
);
};
@@ -97,7 +97,7 @@ export const CollectionShareModal: React.FC<{
<div className={classes.preview}>
<div>
<h2 className={classes.previewHeading}>{collection.name}</h2>
<AuthorNote previewMode id={collection.account_id} />
<AuthorNote id={collection.account_id} />
</div>
<AvatarGroup>
{collection.items.slice(0, 5).map(({ account_id }) => {
@@ -1,6 +1,5 @@
.header {
padding: 16px;
border-bottom: 1px solid var(--color-border-primary);
padding: 24px;
}
.titleWithMenu {
@@ -11,23 +10,46 @@
.titleWrapper {
flex-grow: 1;
display: flex;
flex-direction: column;
align-items: start;
gap: 4px;
min-width: 0;
}
.tag {
margin-bottom: 4px;
margin-inline-start: -8px;
display: inline-block;
padding: 4px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-bg-secondary);
}
.name {
font-size: 28px;
font-size: 22px;
font-weight: 500;
line-height: 1.2;
overflow-wrap: anywhere;
}
.authorNote {
font-size: 13px;
color: var(--color-text-secondary);
a {
color: inherit;
text-decoration-color: rgb(from var(--color-text-secondary) r g b / 50%);
&:hover {
text-decoration: none;
}
}
}
.description {
font-size: 15px;
margin-top: 8px;
margin-top: 12px;
}
.headerButtonWrapper {
@@ -39,79 +61,41 @@
box-sizing: content-box;
padding: 5px;
border-radius: 4px;
border: 1px solid var(--color-border-primary);
}
.authorNote {
margin-top: 8px;
font-size: 13px;
color: var(--color-text-secondary);
}
.previewAuthorNote {
font-size: 13px;
}
.metaList {
--gap: 0.75ch;
display: flex;
flex-wrap: wrap;
margin-top: 16px;
gap: var(--gap);
font-size: 15px;
& > li:not(:last-child)::after {
content: '·';
margin-inline-start: var(--gap);
}
.itemList {
padding-inline: 24px;
}
.columnSubheading {
background: var(--color-bg-secondary);
padding: 15px 20px;
padding-bottom: 12px;
font-size: 15px;
font-weight: 500;
&:focus-visible {
outline: var(--outline-focus-default);
outline-offset: -2px;
}
}
.displayNameWithAvatar {
display: inline-flex;
gap: 4px;
align-items: baseline;
a {
color: inherit;
text-decoration: underline;
&:hover,
&:focus {
text-decoration: none;
}
}
> :global(.account__avatar) {
align-self: center;
}
}
.sensitiveWarning {
.accountItemWrapper {
display: flex;
flex-direction: column;
align-items: center;
max-width: 460px;
margin: auto;
padding: 60px 30px;
gap: 20px;
text-align: center;
text-wrap: balance;
font-size: 15px;
line-height: 1.5;
cursor: default;
align-items: start;
padding-block: 16px;
&[data-with-border='true'] {
border-bottom: 1px solid var(--color-border-primary);
}
}
.accountItem {
--account-name-size: 15px;
--account-handle-color: var(--color-text-secondary);
--account-handle-size: 13px;
--account-bio-color: var(--color-text-primary);
--account-bio-size: 13px;
--account-outer-spacing: 0;
flex-grow: 1;
}
.youWereAddedWrapper {
padding-bottom: 16px;
}
.revokeControlWrapper {
@@ -119,9 +103,7 @@
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-top: -10px;
padding-bottom: 16px;
padding-inline: calc(26px + var(--avatar-width)) 16px;
margin-bottom: 8px;
:global(.button) {
min-width: 30%;
@@ -69,6 +69,7 @@ class Bundle extends PureComponent {
this.setState({ mod: mod.default });
})
.catch((error) => {
console.error('Bundle fetching error:', error);
this.setState({ mod: null });
});
};
+1
View File
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural,one {# уліковы запіс} few {# уліковыя запісы} other {# уліковых запісаў}}",
"collections.accounts.empty_description": "Дадайце да {count} уліковых запісаў, на якія Вы падпісаныя",
"collections.accounts.empty_title": "Гэтая калекцыя пустая",
"collections.by_account": "ад {account_handle}",
"collections.collection_description": "Апісанне",
"collections.collection_language": "Мова",
"collections.collection_language_none": "Няма",
+1
View File
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, one {# konto} other {# konti}}",
"collections.accounts.empty_description": "Tilføj op til {count} konti, du følger",
"collections.accounts.empty_title": "Denne samling er tom",
"collections.by_account": "af {account_handle}",
"collections.collection_description": "Beskrivelse",
"collections.collection_language": "Sprog",
"collections.collection_language_none": "Intet",
+2 -1
View File
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, one {# Konto} other {# Konten}}",
"collections.accounts.empty_description": "Füge bis zu {count} Konten, denen du folgst, hinzu",
"collections.accounts.empty_title": "Diese Sammlung ist leer",
"collections.by_account": "von {account_handle}",
"collections.collection_description": "Beschreibung",
"collections.collection_language": "Sprache",
"collections.collection_language_none": "Nicht festgelegt",
@@ -687,7 +688,7 @@
"followers.hide_other_followers": "Dieses Profil möchte die weiteren Follower geheim halten",
"followers.title": "Folgt {name}",
"following.hide_other_following": "Dieses Profil möchte die gefolgten Profile geheim halten",
"following.title": "Gefolgt von {name}",
"following.title": "Von {name} gefolgt",
"footer.about": "Über",
"footer.about_mastodon": "Über Mastodon",
"footer.about_server": "Über {domain}",
+1
View File
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, one {# λογαριασμός} other {# λογαριασμοί}}",
"collections.accounts.empty_description": "Προσθέστε μέχρι και {count} λογαριασμούς που ακολουθείτε",
"collections.accounts.empty_title": "Αυτή η συλλογή είναι κενή",
"collections.by_account": "από {account_handle}",
"collections.collection_description": "Περιγραφή",
"collections.collection_language": "Γλώσσα",
"collections.collection_language_none": "Καμία",
+6 -5
View File
@@ -173,7 +173,7 @@
"account_edit.field_edit_modal.discard_confirm": "Discard",
"account_edit.field_edit_modal.discard_message": "You have unsaved changes. Are you sure you want to discard them?",
"account_edit.field_edit_modal.edit_title": "Edit custom field",
"account_edit.field_edit_modal.limit_warning": "Recommended character limit exceeded. Mobile users might not see your field in full.",
"account_edit.field_edit_modal.length_warning": "Recommended character limit exceeded. Mobile users might not see your field in full.",
"account_edit.field_edit_modal.link_emoji_warning": "We recommend against the use of custom emoji in combination with urls. Custom fields containing both will display as text only instead of as a link, in order to prevent user confusion.",
"account_edit.field_edit_modal.name_hint": "E.g. “Personal website”",
"account_edit.field_edit_modal.name_label": "Label",
@@ -378,14 +378,13 @@
"collections.description_length_hint": "100 characters limit",
"collections.detail.accept_inclusion": "Okay",
"collections.detail.accounts_heading": "Accounts",
"collections.detail.author_added_you": "{author} added you to this collection",
"collections.detail.curated_by_author": "Curated by {author}",
"collections.detail.curated_by_you": "Curated by you",
"collections.detail.loading": "Loading collection…",
"collections.detail.other_accounts_in_collection": "Others in this collection:",
"collections.detail.other_accounts_count": "{count, plural, one {# other account} other {# other accounts}}",
"collections.detail.revoke_inclusion": "Remove me",
"collections.detail.sensitive_content": "Sensitive content",
"collections.detail.sensitive_note": "This collection contains accounts and content that may be sensitive to some users.",
"collections.detail.share": "Share this collection",
"collections.detail.you_were_added_to_this_collection": "You were added to this collection",
"collections.edit_details": "Edit details",
"collections.error_loading_collections": "There was an error when trying to load your collections.",
"collections.hints.accounts_counter": "{count} / {max} accounts",
@@ -541,6 +540,7 @@
"content_warning.hide": "Hide post",
"content_warning.show": "Show anyway",
"content_warning.show_more": "Show more",
"content_warning.show_short": "Show",
"conversation.delete": "Delete conversation",
"conversation.mark_as_read": "Mark as read",
"conversation.open": "View conversation",
@@ -700,6 +700,7 @@
"footer.source_code": "View source code",
"footer.status": "Status",
"footer.terms_of_service": "Terms of service",
"form_error.blank": "Field cannot be blank.",
"form_field.optional": "(optional)",
"generic.saved": "Saved",
"getting_started.heading": "Getting started",
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, one {# hora} other {# horas}}",
"collections.accounts.empty_description": "Agregá hasta {count} cuentas que seguís",
"collections.accounts.empty_title": "Esta colección está vacía",
"collections.by_account": "por {account_handle}",
"collections.collection_description": "Descripción",
"collections.collection_language": "Idioma",
"collections.collection_language_none": "Ninguno",
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural,one {# cuenta} other {# cuentas}}",
"collections.accounts.empty_description": "Añade hasta {count} cuentas que sigues",
"collections.accounts.empty_title": "Esta colección está vacía",
"collections.by_account": "de {account_handle}",
"collections.collection_description": "Descripción",
"collections.collection_language": "Idioma",
"collections.collection_language_none": "Ninguno",
+2 -1
View File
@@ -171,7 +171,7 @@
"account_edit.field_delete_modal.title": "¿Borrar campo personalizado?",
"account_edit.field_edit_modal.add_title": "Añadir campo personalizado",
"account_edit.field_edit_modal.discard_confirm": "Descartar",
"account_edit.field_edit_modal.discard_message": "Tiene cambios no guardados. ¿Seguro que quieres descartarlos?",
"account_edit.field_edit_modal.discard_message": "Tienes cambios no guardados. ¿Seguro que quieres descartarlos?",
"account_edit.field_edit_modal.edit_title": "Editar campo personalizado",
"account_edit.field_edit_modal.limit_warning": "Se ha superado el límite recomendado de caracteres. Es posible que los usuarios móviles no vean el campo completo.",
"account_edit.field_edit_modal.link_emoji_warning": "Recomendamos no usar emojis personalizados combinados con enlaces. Los campos personalizados que contengan ambos solo se mostrarán como texto en vez de un enlace, para evitar confusiones.",
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, one {# cuenta} other {# cuentas}}",
"collections.accounts.empty_description": "Añade hasta {count} cuentas que sigas",
"collections.accounts.empty_title": "Esta colección está vacía",
"collections.by_account": "de {account_handle}",
"collections.collection_description": "Descripción",
"collections.collection_language": "Idioma",
"collections.collection_language_none": "Ninguno",
+1
View File
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, one {# conta} other {# contas}}",
"collections.accounts.empty_description": "Engade ata {count} contas que segues",
"collections.accounts.empty_title": "A colección está baleira",
"collections.by_account": "de {account_handle}",
"collections.collection_description": "Descrición",
"collections.collection_language": "Idioma",
"collections.collection_language_none": "Ningún",
+1
View File
@@ -141,6 +141,7 @@
"account.unmute": "הפסקת השתקת @{name}",
"account.unmute_notifications_short": "הפעלת הודעות",
"account.unmute_short": "ביטול השתקה",
"account_edit.bio.add_label": "הוסיפו ביוגרפיה",
"account_edit.bio.edit_label": "עריכת ביוגרפיה",
"account_edit.bio.placeholder": "הוסיפו הצגה קצרה כדי לעזור לאחרים לזהות אותך.",
"account_edit.bio.title": "ביוגרפיה",
+2
View File
@@ -141,6 +141,7 @@
"account.unmute": "@{name} némításának feloldása",
"account.unmute_notifications_short": "Értesítések némításának feloldása",
"account.unmute_short": "Némitás feloldása",
"account_edit.bio.add_label": "Bemutatkozás hozzáadása",
"account_edit.bio.edit_label": "Bemutatkozás szerkesztése",
"account_edit.bio.placeholder": "Adj meg egy rövid bemutatkozást, hogy mások könnyebben megtaláljanak.",
"account_edit.bio.title": "Bemutatkozás",
@@ -358,6 +359,7 @@
"collections.account_count": "{count, plural, one {# fiók} other {# fiók}}",
"collections.accounts.empty_description": "Adj hozzá legfeljebb {count} követett fiókot",
"collections.accounts.empty_title": "Ez a gyűjtemény üres",
"collections.by_account": "szerző: {account_handle}",
"collections.collection_description": "Leírás",
"collections.collection_language": "Nyelv",
"collections.collection_language_none": "Egyik sem",
+1
View File
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, one {# aðgangur} other {# aðgangar}}",
"collections.accounts.empty_description": "Bættu við allt að {count} aðgöngum sem þú fylgist með",
"collections.accounts.empty_title": "Þetta safn er tómt",
"collections.by_account": "frá {account_handle}",
"collections.collection_description": "Lýsing",
"collections.collection_language": "Tungumál",
"collections.collection_language_none": "Ekkert",
+1
View File
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, one {# account} other {# account}}",
"collections.accounts.empty_description": "Aggiungi fino a {count} account che segui",
"collections.accounts.empty_title": "Questa collezione è vuota",
"collections.by_account": "di {account_handle}",
"collections.collection_description": "Descrizione",
"collections.collection_language": "Lingua",
"collections.collection_language_none": "Nessuna",
+6 -4
View File
@@ -73,7 +73,7 @@
"account.go_to_profile": "Ga naar profiel",
"account.hide_reblogs": "Boosts van @{name} verbergen",
"account.in_memoriam": "In memoriam.",
"account.joined_short": "Geregistreerd op",
"account.joined_short": "Geregistreerd",
"account.languages": "Getoonde talen wijzigen",
"account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}",
"account.locked_info": "De privacystatus van dit account is ingesteld op vergrendeld. De eigenaar beoordeelt handmatig wie diegene kan volgen.",
@@ -141,6 +141,7 @@
"account.unmute": "@{name} niet langer negeren",
"account.unmute_notifications_short": "Meldingen niet langer negeren",
"account.unmute_short": "Niet langer negeren",
"account_edit.bio.add_label": "Biografie toevoegen",
"account_edit.bio.edit_label": "Biografie bewerken",
"account_edit.bio.placeholder": "Vertel iets over jezelf, zodat anderen inzicht krijgen in wat voor persoon je bent.",
"account_edit.bio.title": "Biografie",
@@ -170,9 +171,9 @@
"account_edit.field_delete_modal.title": "Aangepast veld verwijderen?",
"account_edit.field_edit_modal.add_title": "Aangepast veld toevoegen",
"account_edit.field_edit_modal.discard_confirm": "Weggooien",
"account_edit.field_edit_modal.discard_message": "U heeft niet-opgeslagen wijzigingen. Weet u zeker dat u ze wilt weggooien?",
"account_edit.field_edit_modal.discard_message": "Je hebt niet-opgeslagen wijzigingen. Weet je zeker dat je ze wilt weggooien?",
"account_edit.field_edit_modal.edit_title": "Aangepast veld bewerken",
"account_edit.field_edit_modal.limit_warning": "Aanbevolen tekenlimiet overschreden. Mobiele gebruikers zien mogelijk uw veld niet volledig.",
"account_edit.field_edit_modal.limit_warning": "Aanbevolen tekenlimiet overschreden. Mobiele gebruikers zien mogelijk je veld niet volledig.",
"account_edit.field_edit_modal.link_emoji_warning": "We raden aan om geen lokale emoji in combinatie met URL's te gebruiken. Aangepaste velden die beide bevatten worden alleen als tekst weergegeven, in plaats van als een link. Dit om verwarring voor de gebruiker te voorkomen.",
"account_edit.field_edit_modal.name_hint": "Bijv. \"Persoonlijke website\"",
"account_edit.field_edit_modal.name_label": "Label",
@@ -358,6 +359,7 @@
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
"collections.accounts.empty_description": "Tot {count} accounts die je volgt toevoegen",
"collections.accounts.empty_title": "Deze verzameling is leeg",
"collections.by_account": "door {account_handle}",
"collections.collection_description": "Omschrijving",
"collections.collection_language": "Taal",
"collections.collection_language_none": "Geen",
@@ -638,7 +640,7 @@
"featured_carousel.header": "{count, plural, one {Vastgezet bericht} other {Vastgezette berichten}}",
"featured_carousel.slide": "Bericht {current, number} van {max, number}",
"featured_tags.more_items": "+{count}",
"featured_tags.suggestions": "De laatste tijd heb over {items} gepost. Deze toevoegen als uitgelichte hashtags?",
"featured_tags.suggestions": "Je hebt de afgelopen periode over {items} bericht. Deze toevoegen als uitgelichte hashtags?",
"featured_tags.suggestions.add": "Toevoegen",
"featured_tags.suggestions.added": "Je kunt je uitgelichte hashtags beheren onder <link>Profiel bewerken > Uitgelichte hashtags</link>.",
"featured_tags.suggestions.dismiss": "Nee, bedankt",
@@ -141,6 +141,7 @@
"account.unmute": "Dessilenciar @{name}",
"account.unmute_notifications_short": "Ativar som de notificações",
"account.unmute_short": "Desativar silêncio",
"account_edit.bio.add_label": "Adicionar biografia",
"account_edit.bio.edit_label": "Editar Biografia",
"account_edit.bio.placeholder": "Insira uma breve introdução para ajudar os outros a lhe identificar.",
"account_edit.bio.title": "Bio",
@@ -194,6 +195,15 @@
"account_edit.image_alt_modal.text_hint": "Texto alternativo ajuda leitores de tela a entender seu conteúdo.",
"account_edit.image_alt_modal.text_label": "Texto alternativo",
"account_edit.image_delete_modal.confirm": "Tem certeza de que deseja excluir esta imagem? Esta ação não pode ser desfeita.",
"account_edit.image_delete_modal.delete_button": "Deletar",
"account_edit.image_delete_modal.title": "Deletar imagem?",
"account_edit.image_edit.add_button": "Adicionar imagem",
"account_edit.image_edit.alt_add_button": "Adicionar texto alternativo",
"account_edit.image_edit.alt_edit_button": "Editar texto alternativo",
"account_edit.image_edit.remove_button": "Remover imagem",
"account_edit.image_edit.replace_button": "Substituir imagem",
"account_edit.item_list.delete": "Deletar {name}",
"account_edit.item_list.edit": "Editar {name}",
"account_edit.name_modal.add_title": "Inserir nome de exibição",
"account_edit.name_modal.edit_title": "Editar nome de exibição",
"account_edit.profile_tab.button_label": "Personalizar",
@@ -208,17 +218,33 @@
"account_edit.profile_tab.subtitle": "Personalizar as abas em seu perfil e o que elas exibem.",
"account_edit.profile_tab.title": "Configurações da aba de perfil",
"account_edit.save": "Salvar",
"account_edit.upload_modal.back": "Voltar",
"account_edit.upload_modal.done": "Concluído",
"account_edit.upload_modal.next": "Próximo",
"account_edit.upload_modal.step_crop.zoom": "Aproximar",
"account_edit.upload_modal.step_upload.button": "Procurar arquivos",
"account_edit.upload_modal.step_upload.dragging": "Solte para enviar",
"account_edit.upload_modal.step_upload.header": "Escolha uma imagem",
"account_edit.upload_modal.step_upload.hint": "WEBP, formatos GIF ou JPG, até {limit}MB.{br} imagem será redimensionada para {width}x{height}px.",
"account_edit.upload_modal.title_add.avatar": "Adicionar foto de perfil",
"account_edit.upload_modal.title_add.header": "Adicionar foto de capa",
"account_edit.upload_modal.title_replace.avatar": "Substituir foto de perfil",
"account_edit.upload_modal.title_replace.header": "Substituir foto de capa",
"account_edit.verified_modal.details": "Dê credibilidade ao seu perfil do Mastodon, verificando links para sites pessoais. Veja como funciona:",
"account_edit.verified_modal.invisible_link.details": "Adicione o link para seu HTML do site. A parte importante é rel=\"eu\" onde previne falsificação de identidade em sites com conteúdo gerado por usuários. Você pode até mesmo usar um link tag no HTML da página ao invés de {tag}, mas o HTML deve ser acessível sem a execução do JavaScript.",
"account_edit.verified_modal.invisible_link.summary": "Como posso tornar o link invisível?",
"account_edit.verified_modal.step1.header": "Copie o código HTML abaixo e cole no cabeçalho do seu site",
"account_edit.verified_modal.step2.details": "Se já adicionou seu site como um campo personalizado, deverá excluí-lo e adicioná-lo novamente para acionar a verificação.",
"account_edit.verified_modal.step2.header": "Adicione seu site como um campo personalizado",
"account_edit.verified_modal.title": "Como adicionar um link verificado",
"account_edit_tags.add_tag": "Adicionar #{tagName}",
"account_edit_tags.column_title": "Editar Tags",
"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.max_tags_reached": "Você atingiu o número máximo de hashtags em destaque.",
"account_edit_tags.search_placeholder": "Insira uma hashtag…",
"account_edit_tags.suggestions": "Sugestões:",
"account_edit_tags.tag_status_count": "{count, plural, one {# publicação} other {# publicações}}",
"account_list.total": "{total, plural,one {#conta}other {#contas}}",
"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",
@@ -333,7 +359,10 @@
"collections.account_count": "{count, plural, one {# conta} other {# conta}}",
"collections.accounts.empty_description": "Adicionar até {count} contas que você segue",
"collections.accounts.empty_title": "Esta coleção está vazia",
"collections.by_account": "por {account_handle}",
"collections.collection_description": "Descrição",
"collections.collection_language": "Língua",
"collections.collection_language_none": "Nenhum",
"collections.collection_name": "Nome",
"collections.collection_topic": "Tópico",
"collections.confirm_account_removal": "Tem certeza que deseja remover esta conta da sua coleção?",
@@ -377,6 +406,7 @@
"collections.search_accounts_max_reached": "Você acrescentou o numero máximo de contas",
"collections.sensitive": "Sensível",
"collections.topic_hint": "Adicione uma hashtag que ajude os outros a entender o tópico principal desta coleção.",
"collections.topic_special_chars_hint": "Caracteres especiais serão removidos ao salvar",
"collections.view_collection": "Ver coleção",
"collections.view_other_collections_by_user": "Ver outras coleções deste usuário",
"collections.visibility_public": "Público",
@@ -610,6 +640,10 @@
"featured_carousel.header": "{count, plural, one {Publicação fixada} other {Publicações fixadas}}",
"featured_carousel.slide": "Publicação {current, number} de {max, number}",
"featured_tags.more_items": "+{count}",
"featured_tags.suggestions": "Anteriormente você postou sobre {items}. Adicionar estas como hashtags em destaque?",
"featured_tags.suggestions.add": "Adicionar",
"featured_tags.suggestions.added": "Gerencie suas hashtags a qualquer momento sob <link>Editar Perfil > Hashtags em destaque</link>.",
"featured_tags.suggestions.dismiss": "Não, obrigado",
"filter_modal.added.context_mismatch_explanation": "Esta categoria de filtro não se aplica ao contexto no qual você acessou esta publicação. Se quiser que a publicação seja filtrada nesse contexto também, você terá que editar o filtro.",
"filter_modal.added.context_mismatch_title": "Incompatibilidade de contexto!",
"filter_modal.added.expired_explanation": "Esta categoria de filtro expirou, você precisará alterar a data de expiração para aplicar.",
@@ -652,7 +686,9 @@
"follow_suggestions.who_to_follow": "Quem seguir",
"followed_tags": "Hashtags seguidas",
"followers.hide_other_followers": "Este usuário escolheu não deixar visíveis seus seguidores",
"followers.title": "Seguinte {name}",
"following.hide_other_following": "Este usuário escolheu não deixar visíveis aqueles a quem segue",
"following.title": "Seguido por {name}",
"footer.about": "Sobre",
"footer.about_mastodon": "Sobre o mastodon",
"footer.about_server": "Sobre {domain}",
@@ -969,12 +1005,14 @@
"notifications_permission_banner.title": "Nunca perca nada",
"onboarding.follows.back": "Voltar",
"onboarding.follows.empty": "Infelizmente, não é possível mostrar resultados agora. Você pode tentar usar a busca ou navegar na página de exploração para encontrar pessoas para seguir, ou tentar novamente mais tarde.",
"onboarding.follows.next": "Próximo: Configure seu perfil",
"onboarding.follows.search": "Buscar",
"onboarding.follows.title": "Comece seguindo pessoas",
"onboarding.profile.discoverable": "Tornar meu perfil descobrível",
"onboarding.profile.discoverable_hint": "Quando você aceita a capacidade de descoberta no Mastodon, suas postagens podem aparecer nos resultados de busca e nas tendências, e seu perfil pode ser sugerido a pessoas com interesses similares aos seus.",
"onboarding.profile.display_name": "Nome de exibição",
"onboarding.profile.display_name_hint": "Seu nome completo ou apelido…",
"onboarding.profile.finish": "Finalizar",
"onboarding.profile.note": "Biografia",
"onboarding.profile.note_hint": "Você pode @mencionar outras pessoas ou usar #hashtags…",
"onboarding.profile.title": "Configuração do perfil",
@@ -1123,6 +1161,7 @@
"sign_in_banner.mastodon_is": "O Mastodon é a melhor maneira de acompanhar o que está acontecendo.",
"sign_in_banner.sign_in": "Entrar",
"sign_in_banner.sso_redirect": "Entrar ou Registrar-se",
"skip_links.hotkey": "<span>Tecla de atalho</span>{hotkey}",
"skip_links.skip_to_content": "Conteúdo principal",
"skip_links.skip_to_navigation": "Navegação principal",
"status.admin_account": "Abrir interface de moderação para @{name}",
+1
View File
@@ -354,6 +354,7 @@
"collection.share_template_own": "Shihni koleksionin tim të ri: {link}",
"collections.account_count": "{count, plural, one {# llogari} other {# llogari}}",
"collections.accounts.empty_title": "Ky koleksion është i zbrazët",
"collections.by_account": "nga {account_handle}",
"collections.collection_description": "Përshkrim",
"collections.collection_language": "Gjuhë",
"collections.collection_language_none": "Asnjë",
@@ -8,10 +8,14 @@
"about.domain_blocks.silenced.title": "Ograničen",
"about.domain_blocks.suspended.explanation": "Podaci sa ovog servera neće se obrađivati, čuvati niti razmenjivati, što će onemogućiti bilo kakvu interakciju ili komunikaciju sa korisnicima sa ovog servera.",
"about.domain_blocks.suspended.title": "Suspendovan",
"about.language_label": "Jezik",
"about.not_available": "Ove informacije nisu dostupne na ovom serveru.",
"about.powered_by": "Decentralizovana društvena mreža koju pokreće {mastodon}",
"about.rules": "Pravila servera",
"account.activity": "Aktivnosti",
"account.add_or_remove_from_list": "Dodaj ili ukloni sa lista",
"account.badges.admin": "Admin",
"account.badges.blocked": "Blokiran",
"account.badges.bot": "Automatizovano",
"account.badges.group": "Grupa",
"account.block": "Blokiraj @{name}",
@@ -23,18 +27,31 @@
"account.direct": "Privatno pomeni @{name}",
"account.disable_notifications": "Zaustavi obaveštavanje za objave korisnika @{name}",
"account.edit_profile": "Uredi profil",
"account.edit_profile_short": "Uredi",
"account.enable_notifications": "Obavesti me kada @{name} objavi",
"account.endorse": "Istakni na profilu",
"account.featured.accounts": "Profili",
"account.featured_tags.last_status_at": "Poslednja objava {date}",
"account.featured_tags.last_status_never": "Nema objava",
"account.filters.all": "Sve aktivnosti",
"account.filters.posts_only": "Objave",
"account.filters.posts_replies": "Objave i odgovori",
"account.filters.replies_toggle": "Prikaži odgovore",
"account.follow": "Prati",
"account.follow_back": "Uzvrati praćenje",
"account.follow_back_short": "Uzvrati praćenje",
"account.follow_request": "Zahtevaj praćenje",
"account.follow_request_cancel": "Poništi zahtev",
"account.follow_request_cancel_short": "Poništi",
"account.follow_request_short": "Zahtevaj",
"account.followers": "Pratioci",
"account.followers.empty": "Još uvek niko ne prati ovog korisnika.",
"account.followers_counter": "{count, plural, one {{counter} pratilac} few {{counter} pratioca} other {{counter} pratilaca}}",
"account.followers_you_know_counter": "{counter} koje ti poznaješ",
"account.following": "Prati",
"account.following_counter": "{count, plural, one {{counter} prati} few {{counter} prati} other {{counter} prati}}",
"account.follows.empty": "Ovaj korisnik još uvek nikog ne prati.",
"account.follows_you": "Prati te",
"account.go_to_profile": "Idi na profil",
"account.hide_reblogs": "Sakrij podržavanja @{name}",
"account.in_memoriam": "U znak sećanja na.",
@@ -44,6 +61,12 @@
"account.locked_info": "Status privatnosti ovog naloga je podešen na „zaključano”. Vlasnik ručno pregleda ko ga može pratiti.",
"account.media": "Multimedija",
"account.mention": "Pomeni korisnika @{name}",
"account.menu.add_to_list": "Dodaj u listu",
"account.menu.block": "Blokiraj nalog",
"account.menu.block_domain": "Blokiraj {domain}",
"account.menu.copy": "Kopiraj link",
"account.menu.remove_follower": "Ukloni pratioca",
"account.menu.report": "Prijavi nalog",
"account.moved_to": "Korisnik {name} je naznačio da je njegov novi nalog sada:",
"account.mute": "Ignoriši korisnika @{name}",
"account.mute_notifications_short": "Isključi obaveštenja",
+1
View File
@@ -272,6 +272,7 @@
"closed_registrations_modal.preamble": "Mastodon är decentraliserat så oavsett var du skapar ditt konto kommer du att kunna följa och interagera med någon på denna server. Du kan också köra din egen server!",
"closed_registrations_modal.title": "Registrera sig på Mastodon",
"collections.accounts.empty_description": "Lägg till upp till {count} konton som du följer",
"collections.by_account": "av {account_handle}",
"collections.collection_language": "Språk",
"collections.collection_language_none": "Inga",
"collections.create_a_collection_hint": "Skapa en samling för att rekommendera eller dela dina favoritkonton med andra.",
+11 -10
View File
@@ -79,8 +79,8 @@
"account.locked_info": "Đây là tài khoản riêng tư. Chủ tài khoản tự mình xét duyệt các yêu cầu theo dõi.",
"account.media": "Phương tiện",
"account.mention": "Nhắc đến @{name}",
"account.menu.add_to_list": "Thêm vào danh sách…",
"account.menu.block": "Chặn người này",
"account.menu.add_to_list": "Thêm vào danh sách",
"account.menu.block": "Chặn tài khoản",
"account.menu.block_domain": "Chặn {domain}",
"account.menu.copied": "Đã sao chép liên kết tài khoản vào clipboard",
"account.menu.copy": "Sao chép liên kết",
@@ -143,7 +143,7 @@
"account.unmute_short": "Bỏ phớt lờ",
"account_edit.bio.add_label": "Thêm giới thiệu",
"account_edit.bio.edit_label": "Sửa giới thiệu",
"account_edit.bio.placeholder": "Thêm một dòng giới thiệu để giúp mọi người nhận ra bạn.",
"account_edit.bio.placeholder": "Giúp mọi người nhận ra bạn là ai.",
"account_edit.bio.title": "Giới thiệu",
"account_edit.bio_modal.add_title": "Thêm giới thiệu",
"account_edit.bio_modal.edit_title": "Sửa giới thiệu",
@@ -159,7 +159,7 @@
"account_edit.custom_fields.verified_hint": "Làm thế nào để thêm liên kết xác minh?",
"account_edit.display_name.add_label": "Thêm tên gọi",
"account_edit.display_name.edit_label": "Sửa tên gọi",
"account_edit.display_name.placeholder": "Tên gọi là tên hiển thị trên hồ sơ của bạn, cũng như bảng tin.",
"account_edit.display_name.placeholder": "Tên gọi là tên hiển thị trên hồ sơ của bạn và bảng tin.",
"account_edit.display_name.title": "Tên gọi",
"account_edit.featured_hashtags.edit_label": "Thêm hashtag",
"account_edit.featured_hashtags.placeholder": "Giúp mọi người nhận diện và truy cập nhanh những chủ đề mà bạn thích.",
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, other {# tài khoản}}",
"collections.accounts.empty_description": "Thêm tối đa {count} tài khoản mà bạn theo dõi",
"collections.accounts.empty_title": "Gói khởi đầu này trống",
"collections.by_account": "bởi {account_handle}",
"collections.collection_description": "Mô tả",
"collections.collection_language": "Ngôn ngữ",
"collections.collection_language_none": "Không",
@@ -460,7 +461,7 @@
"compose_form.direct_message_warning_learn_more": "Tìm hiểu thêm",
"compose_form.encryption_warning": "Các tút trên Mastodon không được mã hóa đầu cuối. Không chia sẻ bất kỳ thông tin nhạy cảm nào qua Mastodon.",
"compose_form.hashtag_warning": "Tút này sẽ không xuất hiện công khai. Chỉ những tút công khai mới có thể được tìm kiếm thông qua hashtag.",
"compose_form.lock_disclaimer": "Tài khoản của bạn không {locked}. Bất cứ ai cũng có thể theo dõi và xem tút chỉ dành cho người theo dõi bạn.",
"compose_form.lock_disclaimer": "Tài khoản của bạn không {locked}. Bất kỳ ai cũng có thể theo dõi và xem tút chỉ dành cho người theo dõi bạn.",
"compose_form.lock_disclaimer.lock": "khóa",
"compose_form.placeholder": "Bạn đang nghĩ gì?",
"compose_form.poll.duration": "Hết hạn sau",
@@ -604,7 +605,7 @@
"empty_column.account_suspended": "Tài khoản vô hiệu hóa",
"empty_column.account_timeline": "Chưa có tút nào!",
"empty_column.account_unavailable": "Tài khoản bị đình chỉ",
"empty_column.blocks": "Bạn chưa chặn bất cứ ai.",
"empty_column.blocks": "Bạn chưa chặn ai.",
"empty_column.bookmarked_statuses": "Bạn chưa lưu tút nào. Nếu có, nó sẽ hiển thị ở đây.",
"empty_column.community": "Máy chủ của bạn chưa có tút nào công khai. Bạn hãy thử viết gì đó đi!",
"empty_column.direct": "Bạn chưa có tin nhắn riêng nào. Khi bạn gửi hoặc nhận một tin nhắn riêng, nó sẽ xuất hiện ở đây.",
@@ -641,7 +642,7 @@
"featured_tags.more_items": "+{count}",
"featured_tags.suggestions": "Gần đây bạn đã đăng về {items}. Thêm cái này vào hashtag thường dùng?",
"featured_tags.suggestions.add": "Thêm",
"featured_tags.suggestions.added": "Quản lý hashtag bạn thường dùng vào bất cứ lúc nào tại <link>Sửa hồ sơ > Hashtag thường dùng</link>.",
"featured_tags.suggestions.added": "Quản lý hashtag bạn thường dùng vào bất kỳ lúc nào tại <link>Sửa hồ sơ > Hashtag thường dùng</link>.",
"featured_tags.suggestions.dismiss": "Không, cảm ơn",
"filter_modal.added.context_mismatch_explanation": "Danh mục bộ lọc này không áp dụng cho ngữ cảnh mà bạn đã truy cập tút này. Nếu bạn muốn tút cũng được lọc trong ngữ cảnh này, bạn sẽ phải chỉnh sửa bộ lọc.",
"filter_modal.added.context_mismatch_title": "Bối cảnh không phù hợp!",
@@ -1035,13 +1036,13 @@
"privacy.direct.short": "Nhắn riêng",
"privacy.private.long": "Chỉ người theo dõi",
"privacy.private.short": "Người theo dõi",
"privacy.public.long": "Bất cứ ai",
"privacy.public.long": "Bất kỳ ai",
"privacy.public.short": "Công khai",
"privacy.quote.anyone": "{visibility}, cho phép trích dẫn",
"privacy.quote.disabled": "{visibility}, tắt trích dẫn",
"privacy.quote.limited": "{visibility}, hạn chế trích dẫn",
"privacy.unlisted.additional": "Công khai, nhưng tút sẽ không hiện trong bảng tin, hashtag, khám phá hoặc tìm kiếm Mastodon, kể cả trong cài đặt tài khoản bạn chọn cho phép.",
"privacy.unlisted.long": "Ẩn khỏi kết quả tìm kiếm, xu hướng và dòng thời gian công khai của Mastodon",
"privacy.unlisted.long": "Ẩn khỏi kết quả tìm kiếm, xu hướng và bảng tin công khai",
"privacy.unlisted.short": "Hạn chế",
"privacy_policy.last_updated": "Cập nhật lần cuối {date}",
"privacy_policy.title": "Chính sách bảo mật",
@@ -1321,6 +1322,6 @@
"visibility_modal.quote_followers": "Chỉ người theo dõi",
"visibility_modal.quote_label": "Ai có thể trích dẫn",
"visibility_modal.quote_nobody": "Chỉ tôi",
"visibility_modal.quote_public": "Bất cứ ai",
"visibility_modal.quote_public": "Bất kỳ ai",
"visibility_modal.save": "Lưu"
}
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, other {# 个账号}}",
"collections.accounts.empty_description": "添加你关注的账号,最多 {count} 个",
"collections.accounts.empty_title": "收藏列表为空",
"collections.by_account": "由 {account_handle}",
"collections.collection_description": "说明",
"collections.collection_language": "语言",
"collections.collection_language_none": "无",
@@ -359,6 +359,7 @@
"collections.account_count": "{count, plural, other {# 個帳號}}",
"collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號",
"collections.accounts.empty_title": "此收藏名單是空的",
"collections.by_account": "來自 {account_handle}",
"collections.collection_description": "說明",
"collections.collection_language": "語言",
"collections.collection_language_none": "無",