Merge commit '8ebe2e673e2fd175140df7275eb362c8eecfec31' into glitch-soc/merge-upstream
This commit is contained in:
@@ -70,7 +70,8 @@ type CommonPayloadFields = Pick<
|
||||
ApiCollectionJSON,
|
||||
'name' | 'description' | 'sensitive' | 'discoverable'
|
||||
> & {
|
||||
tag_name?: string;
|
||||
tag_name?: string | null;
|
||||
language?: ApiCollectionJSON['language'];
|
||||
};
|
||||
|
||||
export interface ApiUpdateCollectionPayload extends Partial<CommonPayloadFields> {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export { FormStack } from './form_stack';
|
||||
export { Fieldset } from './fieldset';
|
||||
export { TextInputField, TextInput } from './text_input_field';
|
||||
export { TextAreaField, TextArea } from './text_area_field';
|
||||
export { CheckboxField, Checkbox } from './checkbox_field';
|
||||
export { RadioButtonField, RadioButton } from './radio_button_field';
|
||||
export { ToggleField, Toggle } from './toggle_field';
|
||||
export { SelectField, Select } from './select_field';
|
||||
|
||||
@@ -3,14 +3,12 @@ import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||
import { FilterWarning } from 'mastodon/components/filter_warning';
|
||||
@@ -26,16 +24,12 @@ import { MediaGallery, Video, Audio } from '../features/ui/util/async-components
|
||||
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
|
||||
import { displayMedia } from '../initial_state';
|
||||
|
||||
import { Avatar } from './avatar';
|
||||
import { AvatarOverlay } from './avatar_overlay';
|
||||
import { StatusHeader } from './status/header'
|
||||
import { LinkedDisplayName } from './display_name';
|
||||
import { getHashtagBarForStatus } from './hashtag_bar';
|
||||
import { RelativeTimestamp } from './relative_timestamp';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import StatusContent from './status_content';
|
||||
import { StatusThreadLabel } from './status_thread_label';
|
||||
import { VisibilityIcon } from './visibility_icon';
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
@@ -112,7 +106,6 @@ class Status extends ImmutablePureComponent {
|
||||
onToggleCollapsed: PropTypes.func,
|
||||
onTranslate: PropTypes.func,
|
||||
onInteractionModal: PropTypes.func,
|
||||
onQuoteCancel: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
@@ -129,6 +122,7 @@ class Status extends ImmutablePureComponent {
|
||||
avatarSize: PropTypes.number,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
unfocusable: PropTypes.bool,
|
||||
headerRenderFn: PropTypes.func,
|
||||
pictureInPicture: ImmutablePropTypes.contains({
|
||||
inUse: PropTypes.bool,
|
||||
available: PropTypes.bool,
|
||||
@@ -146,7 +140,6 @@ class Status extends ImmutablePureComponent {
|
||||
'hidden',
|
||||
'unread',
|
||||
'pictureInPicture',
|
||||
'onQuoteCancel',
|
||||
];
|
||||
|
||||
state = {
|
||||
@@ -364,10 +357,6 @@ class Status extends ImmutablePureComponent {
|
||||
this.setState(state => ({ ...state, showDespiteFilter: !state.showDespiteFilter }));
|
||||
};
|
||||
|
||||
handleQuoteCancel = () => {
|
||||
this.props.onQuoteCancel?.();
|
||||
}
|
||||
|
||||
_properStatus () {
|
||||
const { status } = this.props;
|
||||
|
||||
@@ -383,7 +372,24 @@ class Status extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, hidden, featured, unfocusable, unread, showThread, showActions = true, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
|
||||
const {
|
||||
intl,
|
||||
hidden,
|
||||
featured,
|
||||
unfocusable,
|
||||
unread,
|
||||
showThread,
|
||||
showActions = true,
|
||||
isQuotedPost = false,
|
||||
scrollKey,
|
||||
pictureInPicture,
|
||||
previousId,
|
||||
nextInReplyToId,
|
||||
rootId,
|
||||
skipPrepend,
|
||||
avatarSize = 46,
|
||||
children,
|
||||
} = this.props;
|
||||
|
||||
let { status, account, ...other } = this.props;
|
||||
|
||||
@@ -405,7 +411,7 @@ class Status extends ImmutablePureComponent {
|
||||
onTranslate: this.handleTranslate,
|
||||
};
|
||||
|
||||
let media, statusAvatar, prepend, rebloggedByText;
|
||||
let media, prepend, rebloggedByText;
|
||||
|
||||
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
||||
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
|
||||
@@ -547,13 +553,19 @@ class Status extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (account === undefined || account === null) {
|
||||
statusAvatar = <Avatar account={status.get('account')} size={avatarSize} />;
|
||||
} else {
|
||||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
|
||||
const header = this.props.headerRenderFn
|
||||
? this.props.headerRenderFn({ status, account, avatarSize, messages, onHeaderClick: this.handleHeaderClick })
|
||||
: (
|
||||
<StatusHeader
|
||||
status={status}
|
||||
account={account}
|
||||
avatarSize={avatarSize}
|
||||
onHeaderClick={this.handleHeaderClick}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Hotkeys handlers={handlers} focusable={!unfocusable}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted || unfocusable ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader({intl, status, rebloggedByText, isQuote: isQuotedPost})} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||
@@ -575,28 +587,7 @@ class Status extends ImmutablePureComponent {
|
||||
>
|
||||
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||
|
||||
<div onClick={this.handleHeaderClick} onAuxClick={this.handleHeaderClick} className='status__info'>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time'>
|
||||
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||
</Link>
|
||||
|
||||
<LinkedDisplayName displayProps={{account: status.get('account')}} className='status__display-name'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
</LinkedDisplayName>
|
||||
|
||||
{isQuotedPost && !!this.props.onQuoteCancel && (
|
||||
<IconButton
|
||||
onClick={this.handleQuoteCancel}
|
||||
className='status__quote-cancel'
|
||||
title={intl.formatMessage(messages.quote_cancel)}
|
||||
icon="cancel-fill"
|
||||
iconComponent={CancelFillIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{header}
|
||||
|
||||
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { FC, HTMLAttributes, MouseEventHandler, ReactNode } from 'react';
|
||||
|
||||
import { defineMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { isStatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type { Status } from '@/mastodon/models/status';
|
||||
|
||||
import { Avatar } from '../avatar';
|
||||
import { AvatarOverlay } from '../avatar_overlay';
|
||||
import type { DisplayNameProps } from '../display_name';
|
||||
import { LinkedDisplayName } from '../display_name';
|
||||
import { RelativeTimestamp } from '../relative_timestamp';
|
||||
import { VisibilityIcon } from '../visibility_icon';
|
||||
|
||||
export interface StatusHeaderProps {
|
||||
status: Status;
|
||||
account?: Account;
|
||||
avatarSize?: number;
|
||||
children?: ReactNode;
|
||||
wrapperProps?: HTMLAttributes<HTMLDivElement>;
|
||||
displayNameProps?: DisplayNameProps;
|
||||
onHeaderClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export type StatusHeaderRenderFn = (args: StatusHeaderProps) => ReactNode;
|
||||
|
||||
export const StatusHeader: FC<StatusHeaderProps> = ({
|
||||
status,
|
||||
account,
|
||||
children,
|
||||
avatarSize = 48,
|
||||
wrapperProps,
|
||||
onHeaderClick,
|
||||
}) => {
|
||||
const statusAccount = status.get('account') as Account | undefined;
|
||||
const editedAt = status.get('edited_at') as string;
|
||||
|
||||
return (
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
<div
|
||||
onClick={onHeaderClick}
|
||||
onAuxClick={onHeaderClick}
|
||||
{...wrapperProps}
|
||||
className='status__info'
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
>
|
||||
<Link
|
||||
to={`/@${statusAccount?.acct}/${status.get('id') as string}`}
|
||||
className='status__relative-time'
|
||||
>
|
||||
<StatusVisibility visibility={status.get('visibility')} />
|
||||
<RelativeTimestamp timestamp={status.get('created_at') as string} />
|
||||
{editedAt && <StatusEditedAt editedAt={editedAt} />}
|
||||
</Link>
|
||||
|
||||
<StatusDisplayName
|
||||
statusAccount={statusAccount}
|
||||
friendAccount={account}
|
||||
avatarSize={avatarSize}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusVisibility: FC<{ visibility: unknown }> = ({
|
||||
visibility,
|
||||
}) => {
|
||||
if (typeof visibility !== 'string' || !isStatusVisibility(visibility)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className='status__visibility-icon'>
|
||||
<VisibilityIcon visibility={visibility} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const editMessage = defineMessage({
|
||||
id: 'status.edited',
|
||||
defaultMessage: 'Edited {date}',
|
||||
});
|
||||
|
||||
export const StatusEditedAt: FC<{ editedAt: string }> = ({ editedAt }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<abbr
|
||||
title={intl.formatMessage(editMessage, {
|
||||
date: intl.formatDate(editedAt, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{' '}
|
||||
*
|
||||
</abbr>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusDisplayName: FC<{
|
||||
statusAccount?: Account;
|
||||
friendAccount?: Account;
|
||||
avatarSize: number;
|
||||
}> = ({ statusAccount, friendAccount, avatarSize }) => {
|
||||
const AccountComponent = friendAccount ? AvatarOverlay : Avatar;
|
||||
return (
|
||||
<LinkedDisplayName
|
||||
displayProps={{ account: statusAccount }}
|
||||
className='status__display-name'
|
||||
>
|
||||
<div className='status__avatar'>
|
||||
<AccountComponent
|
||||
account={statusAccount}
|
||||
friend={friendAccount}
|
||||
size={avatarSize}
|
||||
/>
|
||||
</div>
|
||||
</LinkedDisplayName>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||
import { fetchRelationships } from 'mastodon/actions/accounts';
|
||||
import { revealAccount } from 'mastodon/actions/accounts_typed';
|
||||
import { fetchStatus } from 'mastodon/actions/statuses';
|
||||
@@ -18,6 +19,9 @@ import type { RootState } from 'mastodon/store';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { Button } from './button';
|
||||
import { IconButton } from './icon_button';
|
||||
import type { StatusHeaderRenderFn } from './status/header';
|
||||
import { StatusHeader } from './status/header';
|
||||
|
||||
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
|
||||
|
||||
@@ -147,6 +151,11 @@ interface QuotedStatusProps {
|
||||
onQuoteCancel?: () => void; // Used for composer.
|
||||
}
|
||||
|
||||
const quoteCancelMessage = defineMessage({
|
||||
id: 'status.quote.cancel',
|
||||
defaultMessage: 'Cancel quote',
|
||||
});
|
||||
|
||||
export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
quote,
|
||||
contextType,
|
||||
@@ -213,6 +222,22 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
if (accountId && hiddenAccount) dispatch(fetchRelationships([accountId]));
|
||||
}, [accountId, hiddenAccount, dispatch]);
|
||||
|
||||
const intl = useIntl();
|
||||
const headerRenderFn: StatusHeaderRenderFn = useCallback(
|
||||
(props) => (
|
||||
<StatusHeader {...props}>
|
||||
<IconButton
|
||||
onClick={onQuoteCancel}
|
||||
className='status__quote-cancel'
|
||||
title={intl.formatMessage(quoteCancelMessage)}
|
||||
icon='cancel-fill'
|
||||
iconComponent={CancelFillIcon}
|
||||
/>
|
||||
</StatusHeader>
|
||||
),
|
||||
[intl, onQuoteCancel],
|
||||
);
|
||||
|
||||
const isFilteredAndHidden = loadingState === 'filtered';
|
||||
|
||||
let quoteError: React.ReactNode = null;
|
||||
@@ -314,7 +339,7 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
id={quotedStatusId}
|
||||
contextType={contextType}
|
||||
avatarSize={32}
|
||||
onQuoteCancel={onQuoteCancel}
|
||||
headerRenderFn={headerRenderFn}
|
||||
>
|
||||
{canRenderChildQuote && (
|
||||
<QuotedStatus
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
ApiCreateCollectionPayload,
|
||||
ApiUpdateCollectionPayload,
|
||||
} from 'mastodon/api_types/collections';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import {
|
||||
CheckboxField,
|
||||
FormStack,
|
||||
TextAreaField,
|
||||
} from 'mastodon/components/form_fields';
|
||||
import { TextInputField } from 'mastodon/components/form_fields/text_input_field';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import {
|
||||
createCollection,
|
||||
fetchCollection,
|
||||
updateCollection,
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
edit: { id: 'column.edit_collection', defaultMessage: 'Edit collection' },
|
||||
create: {
|
||||
id: 'column.create_collection',
|
||||
defaultMessage: 'Create collection',
|
||||
},
|
||||
});
|
||||
|
||||
const CollectionSettings: React.FC<{
|
||||
collection?: ApiCollectionJSON | null;
|
||||
}> = ({ collection }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
id,
|
||||
name: initialName = '',
|
||||
description: initialDescription = '',
|
||||
tag,
|
||||
discoverable: initialDiscoverable = true,
|
||||
sensitive: initialSensitive = false,
|
||||
} = collection ?? {};
|
||||
|
||||
const [name, setName] = useState(initialName);
|
||||
const [description, setDescription] = useState(initialDescription);
|
||||
const [topic, setTopic] = useState(tag?.name ?? '');
|
||||
const [discoverable] = useState(initialDiscoverable);
|
||||
const [sensitive, setSensitive] = useState(initialSensitive);
|
||||
|
||||
const handleNameChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDescriptionChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDescription(event.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTopicChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTopic(event.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSensitiveChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSensitive(event.target.checked);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (id) {
|
||||
const payload: ApiUpdateCollectionPayload = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
tag_name: topic,
|
||||
discoverable,
|
||||
sensitive,
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
});
|
||||
} else {
|
||||
const payload: ApiCreateCollectionPayload = {
|
||||
name,
|
||||
description,
|
||||
discoverable,
|
||||
sensitive,
|
||||
};
|
||||
if (topic) {
|
||||
payload.tag_name = topic;
|
||||
}
|
||||
|
||||
void dispatch(
|
||||
createCollection({
|
||||
payload,
|
||||
}),
|
||||
).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
history.replace(
|
||||
`/collections/${result.payload.collection.id}/edit`,
|
||||
);
|
||||
history.push(`/collections`);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[id, dispatch, name, description, topic, discoverable, sensitive, history],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormStack as='form' onSubmit={handleSubmit}>
|
||||
<TextInputField
|
||||
required
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.name_length_hint'
|
||||
defaultMessage='40 characters limit'
|
||||
/>
|
||||
}
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
maxLength={40}
|
||||
/>
|
||||
|
||||
<TextAreaField
|
||||
required
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_description'
|
||||
defaultMessage='Description'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.description_length_hint'
|
||||
defaultMessage='100 characters limit'
|
||||
/>
|
||||
}
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
maxLength={100}
|
||||
/>
|
||||
|
||||
<TextInputField
|
||||
required={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_topic'
|
||||
defaultMessage='Topic'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.topic_hint'
|
||||
defaultMessage='Add a hashtag that helps others understand the main topic of this collection.'
|
||||
/>
|
||||
}
|
||||
value={topic}
|
||||
onChange={handleTopicChange}
|
||||
maxLength={40}
|
||||
/>
|
||||
|
||||
<CheckboxField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.mark_as_sensitive'
|
||||
defaultMessage='Mark as sensitive'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.mark_as_sensitive_hint'
|
||||
defaultMessage="Hides the collection's description and accounts behind a content warning. The collection name will still be visible."
|
||||
/>
|
||||
}
|
||||
checked={sensitive}
|
||||
onChange={handleSensitiveChange}
|
||||
/>
|
||||
|
||||
<div className='actions'>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage id='lists.create' defaultMessage='Create' />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</FormStack>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionEditorPage: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const collection = useAppSelector((state) =>
|
||||
id ? state.collections.collections[id] : undefined,
|
||||
);
|
||||
const isEditMode = !!id;
|
||||
const isLoading = isEditMode && !collection;
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
void dispatch(fetchCollection({ collectionId: id }));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
const pageTitle = intl.formatMessage(id ? messages.edit : messages.create);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={pageTitle}>
|
||||
<ColumnHeader
|
||||
title={pageTitle}
|
||||
icon='list-ul'
|
||||
iconComponent={ListAltIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<div className='scrollable'>
|
||||
{isLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<CollectionSettings collection={collection} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { FormStack } from 'mastodon/components/form_fields';
|
||||
|
||||
import type { TempCollectionState } from './state';
|
||||
import { getInitialState } from './state';
|
||||
import { WizardStepHeader } from './wizard_step_header';
|
||||
|
||||
export const CollectionAccounts: React.FC<{
|
||||
collection?: ApiCollectionJSON | null;
|
||||
}> = ({ collection }) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation<TempCollectionState>();
|
||||
|
||||
const { id } = getInitialState(collection, location.state);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!id) {
|
||||
history.push(`/collections/new/details`);
|
||||
}
|
||||
},
|
||||
[id, history],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormStack as='form' onSubmit={handleSubmit}>
|
||||
{!id && (
|
||||
<WizardStepHeader
|
||||
step={1}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.create.accounts_title'
|
||||
defaultMessage='Who will you feature in this collection?'
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id='collections.create.accounts_subtitle'
|
||||
defaultMessage='Only accounts you follow who have opted into discovery can be added.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className='actions'>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collections.continue'
|
||||
defaultMessage='Continue'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</FormStack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
ApiCreateCollectionPayload,
|
||||
ApiUpdateCollectionPayload,
|
||||
} from 'mastodon/api_types/collections';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { FormStack, TextAreaField } from 'mastodon/components/form_fields';
|
||||
import { TextInputField } from 'mastodon/components/form_fields/text_input_field';
|
||||
import { updateCollection } from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import type { TempCollectionState } from './state';
|
||||
import { getInitialState } from './state';
|
||||
import { WizardStepHeader } from './wizard_step_header';
|
||||
|
||||
export const CollectionDetails: React.FC<{
|
||||
collection?: ApiCollectionJSON | null;
|
||||
}> = ({ collection }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const location = useLocation<TempCollectionState>();
|
||||
|
||||
const { id, initialName, initialDescription, initialTopic } = getInitialState(
|
||||
collection,
|
||||
location.state,
|
||||
);
|
||||
|
||||
const [name, setName] = useState(initialName);
|
||||
const [description, setDescription] = useState(initialDescription);
|
||||
const [topic, setTopic] = useState(initialTopic);
|
||||
|
||||
const handleNameChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDescriptionChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDescription(event.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTopicChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTopic(event.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (id) {
|
||||
const payload: ApiUpdateCollectionPayload = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
tag_name: topic || null,
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
});
|
||||
} else {
|
||||
const payload: Partial<ApiCreateCollectionPayload> = {
|
||||
name,
|
||||
description,
|
||||
tag_name: topic || null,
|
||||
};
|
||||
|
||||
history.replace('/collections/new', payload);
|
||||
history.push('/collections/new/settings', payload);
|
||||
}
|
||||
},
|
||||
[id, dispatch, name, description, topic, history],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormStack as='form' onSubmit={handleSubmit}>
|
||||
{!id && (
|
||||
<WizardStepHeader
|
||||
step={2}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.create.basic_details_title'
|
||||
defaultMessage='Basic details'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TextInputField
|
||||
required
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.name_length_hint'
|
||||
defaultMessage='40 characters limit'
|
||||
/>
|
||||
}
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
maxLength={40}
|
||||
/>
|
||||
|
||||
<TextAreaField
|
||||
required
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_description'
|
||||
defaultMessage='Description'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.description_length_hint'
|
||||
defaultMessage='100 characters limit'
|
||||
/>
|
||||
}
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
maxLength={100}
|
||||
/>
|
||||
|
||||
<TextInputField
|
||||
required={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_topic'
|
||||
defaultMessage='Topic'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.topic_hint'
|
||||
defaultMessage='Add a hashtag that helps others understand the main topic of this collection.'
|
||||
/>
|
||||
}
|
||||
value={topic}
|
||||
onChange={handleTopicChange}
|
||||
maxLength={40}
|
||||
/>
|
||||
|
||||
<div className='actions'>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collections.continue'
|
||||
defaultMessage='Continue'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</FormStack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import {
|
||||
Switch,
|
||||
Route,
|
||||
useParams,
|
||||
useRouteMatch,
|
||||
matchPath,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { fetchCollection } from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { CollectionAccounts } from './accounts';
|
||||
import { CollectionDetails } from './details';
|
||||
import { CollectionSettings } from './settings';
|
||||
|
||||
export const messages = defineMessages({
|
||||
create: {
|
||||
id: 'collections.create_collection',
|
||||
defaultMessage: 'Create collection',
|
||||
},
|
||||
newCollection: {
|
||||
id: 'collections.new_collection',
|
||||
defaultMessage: 'New collection',
|
||||
},
|
||||
editDetails: {
|
||||
id: 'collections.edit_details',
|
||||
defaultMessage: 'Edit basic details',
|
||||
},
|
||||
manageAccounts: {
|
||||
id: 'collections.manage_accounts',
|
||||
defaultMessage: 'Manage accounts',
|
||||
},
|
||||
manageAccountsLong: {
|
||||
id: 'collections.manage_accounts_in_collection',
|
||||
defaultMessage: 'Manage accounts in this collection',
|
||||
},
|
||||
editSettings: {
|
||||
id: 'collections.edit_settings',
|
||||
defaultMessage: 'Edit settings',
|
||||
},
|
||||
});
|
||||
|
||||
function usePageTitle(id: string | undefined) {
|
||||
const { path } = useRouteMatch();
|
||||
const location = useLocation();
|
||||
|
||||
if (!id) {
|
||||
return messages.newCollection;
|
||||
}
|
||||
|
||||
if (matchPath(location.pathname, { path, exact: true })) {
|
||||
return messages.manageAccounts;
|
||||
} else if (matchPath(location.pathname, { path: `${path}/details` })) {
|
||||
return messages.editDetails;
|
||||
} else if (matchPath(location.pathname, { path: `${path}/settings` })) {
|
||||
return messages.editSettings;
|
||||
} else {
|
||||
throw new Error('No page title defined for route');
|
||||
}
|
||||
}
|
||||
|
||||
export const CollectionEditorPage: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const { path } = useRouteMatch();
|
||||
const collection = useAppSelector((state) =>
|
||||
id ? state.collections.collections[id] : undefined,
|
||||
);
|
||||
const isEditMode = !!id;
|
||||
const isLoading = isEditMode && !collection;
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
void dispatch(fetchCollection({ collectionId: id }));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
const pageTitle = intl.formatMessage(usePageTitle(id));
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={pageTitle}>
|
||||
<ColumnHeader
|
||||
title={pageTitle}
|
||||
icon='list-ul'
|
||||
iconComponent={ListAltIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<div className='scrollable'>
|
||||
{isLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={path}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
render={() => <CollectionAccounts collection={collection} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${path}/details`}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
render={() => <CollectionDetails collection={collection} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${path}/settings`}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
render={() => <CollectionSettings collection={collection} />}
|
||||
/>
|
||||
</Switch>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
ApiCreateCollectionPayload,
|
||||
ApiUpdateCollectionPayload,
|
||||
} from 'mastodon/api_types/collections';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import {
|
||||
Fieldset,
|
||||
FormStack,
|
||||
CheckboxField,
|
||||
RadioButtonField,
|
||||
} from 'mastodon/components/form_fields';
|
||||
import {
|
||||
createCollection,
|
||||
updateCollection,
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import type { TempCollectionState } from './state';
|
||||
import { getInitialState } from './state';
|
||||
import { WizardStepHeader } from './wizard_step_header';
|
||||
|
||||
export const CollectionSettings: React.FC<{
|
||||
collection?: ApiCollectionJSON | null;
|
||||
}> = ({ collection }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const location = useLocation<TempCollectionState>();
|
||||
|
||||
const { id, initialDiscoverable, initialSensitive, ...temporaryState } =
|
||||
getInitialState(collection, location.state);
|
||||
|
||||
const [discoverable, setDiscoverable] = useState(initialDiscoverable);
|
||||
const [sensitive, setSensitive] = useState(initialSensitive);
|
||||
|
||||
const handleDiscoverableChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDiscoverable(event.target.value === 'public');
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSensitiveChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSensitive(event.target.checked);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (id) {
|
||||
const payload: ApiUpdateCollectionPayload = {
|
||||
id,
|
||||
discoverable,
|
||||
sensitive,
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
});
|
||||
} else {
|
||||
const payload: ApiCreateCollectionPayload = {
|
||||
name: temporaryState.initialName,
|
||||
description: temporaryState.initialDescription,
|
||||
discoverable,
|
||||
sensitive,
|
||||
};
|
||||
if (temporaryState.initialTopic) {
|
||||
payload.tag_name = temporaryState.initialTopic;
|
||||
}
|
||||
|
||||
void dispatch(
|
||||
createCollection({
|
||||
payload,
|
||||
}),
|
||||
).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
history.replace(
|
||||
`/collections/${result.payload.collection.id}/edit/settings`,
|
||||
);
|
||||
history.push(`/collections`);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[id, discoverable, sensitive, dispatch, history, temporaryState],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormStack as='form' onSubmit={handleSubmit}>
|
||||
{!id && (
|
||||
<WizardStepHeader
|
||||
step={3}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.create.settings_title'
|
||||
defaultMessage='Settings'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Fieldset
|
||||
legend={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_title'
|
||||
defaultMessage='Visibility'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RadioButtonField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_public'
|
||||
defaultMessage='Public'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_public_hint'
|
||||
defaultMessage='Discoverable in search results and other areas where recommendations appear.'
|
||||
/>
|
||||
}
|
||||
value='public'
|
||||
checked={discoverable}
|
||||
onChange={handleDiscoverableChange}
|
||||
/>
|
||||
<RadioButtonField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_unlisted'
|
||||
defaultMessage='Unlisted'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_unlisted_hint'
|
||||
defaultMessage='Visible to anyone with a link. Hidden from search results and recommendations.'
|
||||
/>
|
||||
}
|
||||
value='unlisted'
|
||||
checked={!discoverable}
|
||||
onChange={handleDiscoverableChange}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset
|
||||
legend={
|
||||
<FormattedMessage
|
||||
id='collections.content_warning'
|
||||
defaultMessage='Content warning'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CheckboxField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.mark_as_sensitive'
|
||||
defaultMessage='Mark as sensitive'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.mark_as_sensitive_hint'
|
||||
defaultMessage="Hides the collection's description and accounts behind a content warning. The collection name will still be visible."
|
||||
/>
|
||||
}
|
||||
checked={sensitive}
|
||||
onChange={handleSensitiveChange}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
<div className='actions'>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collections.create_collection'
|
||||
defaultMessage='Create collection'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</FormStack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
ApiCreateCollectionPayload,
|
||||
} from '@/mastodon/api_types/collections';
|
||||
|
||||
/**
|
||||
* Temporary editor state across creation steps,
|
||||
* kept in location state
|
||||
*/
|
||||
export type TempCollectionState =
|
||||
| Partial<ApiCreateCollectionPayload>
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* Resolve initial editor state. Temporary location state
|
||||
* trumps stored data, otherwise initial values are returned.
|
||||
*/
|
||||
export function getInitialState(
|
||||
collection: ApiCollectionJSON | null | undefined,
|
||||
locationState: TempCollectionState,
|
||||
) {
|
||||
const {
|
||||
id,
|
||||
name = '',
|
||||
description = '',
|
||||
tag,
|
||||
language = '',
|
||||
discoverable = true,
|
||||
sensitive = false,
|
||||
} = collection ?? {};
|
||||
|
||||
return {
|
||||
id,
|
||||
initialName: locationState?.name ?? name,
|
||||
initialDescription: locationState?.description ?? description,
|
||||
initialTopic: locationState?.tag_name ?? tag?.name ?? '',
|
||||
initialLanguage: locationState?.language ?? language,
|
||||
initialDiscoverable: locationState?.discoverable ?? discoverable,
|
||||
initialSensitive: locationState?.sensitive ?? sensitive,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.step {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const WizardStepHeader: React.FC<{
|
||||
step: number;
|
||||
title: React.ReactElement;
|
||||
description?: React.ReactElement;
|
||||
}> = ({ step, title, description }) => {
|
||||
return (
|
||||
<header>
|
||||
<FormattedMessage
|
||||
id='collections.create.steps'
|
||||
defaultMessage='Step {step}/{total}'
|
||||
values={{ step, total: 3 }}
|
||||
>
|
||||
{(content) => <p className={classes.step}>{content}</p>}
|
||||
</FormattedMessage>
|
||||
<h2 className={classes.title}>{title}</h2>
|
||||
{!!description && <p className={classes.description}>{description}</p>}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -21,12 +21,10 @@ import {
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { messages as editorMessages } from './editor';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.collections', defaultMessage: 'My collections' },
|
||||
create: {
|
||||
id: 'collections.create_collection',
|
||||
defaultMessage: 'Create collection',
|
||||
},
|
||||
view: {
|
||||
id: 'collections.view_collection',
|
||||
defaultMessage: 'View collection',
|
||||
@@ -60,14 +58,35 @@ const ListItem: React.FC<{
|
||||
const menu = useMemo(
|
||||
() => [
|
||||
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
|
||||
{ text: intl.formatMessage(messages.delete), action: handleDeleteClick },
|
||||
null,
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.manageAccounts),
|
||||
to: `/collections/${id}/edit`,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.editDetails),
|
||||
to: `/collections/${id}/edit/details`,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.editSettings),
|
||||
to: `/collections/${id}/edit/settings`,
|
||||
},
|
||||
null,
|
||||
{
|
||||
text: intl.formatMessage(messages.delete),
|
||||
action: handleDeleteClick,
|
||||
dangerous: true,
|
||||
},
|
||||
],
|
||||
[intl, id, handleDeleteClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='lists__item'>
|
||||
<Link to={`/collections/${id}/edit`} className='lists__item__title'>
|
||||
<Link
|
||||
to={`/collections/${id}/edit/details`}
|
||||
className='lists__item__title'
|
||||
>
|
||||
<span>{name}</span>
|
||||
</Link>
|
||||
|
||||
@@ -132,8 +151,8 @@ export const Collections: React.FC<{
|
||||
<Link
|
||||
to='/collections/new'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.create)}
|
||||
aria-label={intl.formatMessage(messages.create)}
|
||||
title={intl.formatMessage(editorMessages.create)}
|
||||
aria-label={intl.formatMessage(editorMessages.create)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</Link>
|
||||
|
||||
@@ -231,10 +231,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path='/collections/new' component={CollectionsEditor} content={children} />
|
||||
}
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path='/collections/:id/edit' component={CollectionsEditor} content={children} />
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />
|
||||
}
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||
|
||||
@@ -17,8 +17,12 @@
|
||||
"account.activity": "Activitat",
|
||||
"account.add_note": "Afegeix una nota personal",
|
||||
"account.add_or_remove_from_list": "Afegeix o elimina de les llistes",
|
||||
"account.badges.admin": "Administrador",
|
||||
"account.badges.blocked": "Blocat",
|
||||
"account.badges.bot": "Automatitzat",
|
||||
"account.badges.domain_blocked": "Domini blocat",
|
||||
"account.badges.group": "Grup",
|
||||
"account.badges.muted": "Silenciat",
|
||||
"account.block": "Bloca @{name}",
|
||||
"account.block_domain": "Bloca el domini {domain}",
|
||||
"account.block_short": "Bloca",
|
||||
@@ -42,6 +46,8 @@
|
||||
"account.featured.hashtags": "Etiquetes",
|
||||
"account.featured_tags.last_status_at": "Darrer tut el {date}",
|
||||
"account.featured_tags.last_status_never": "No hi ha tuts",
|
||||
"account.fields.scroll_next": "Mostrar el següent",
|
||||
"account.fields.scroll_prev": "Mostrar l'anterior",
|
||||
"account.filters.all": "Tota l'activitat",
|
||||
"account.filters.boosts_toggle": "Mostra els impulsos",
|
||||
"account.filters.posts_boosts": "Publicacions i impulsos",
|
||||
@@ -73,6 +79,23 @@
|
||||
"account.locked_info": "L'estat de privacitat del compte està definit com a blocat. El propietari revisa manualment qui pot seguir-lo.",
|
||||
"account.media": "Contingut",
|
||||
"account.mention": "Menciona @{name}",
|
||||
"account.menu.add_to_list": "Afegir a la llista…",
|
||||
"account.menu.block": "Blocar el compte",
|
||||
"account.menu.block_domain": "Blocar {domain}",
|
||||
"account.menu.copied": "Compte copiat al porta-retalls",
|
||||
"account.menu.copy": "Copiar l'enllaç",
|
||||
"account.menu.direct": "Mencionar privadament",
|
||||
"account.menu.hide_reblogs": "Amagar impulsos en les línies de temps",
|
||||
"account.menu.mention": "Mencionar",
|
||||
"account.menu.mute": "Silenciar el compte",
|
||||
"account.menu.open_original_page": "Veure a {domain}",
|
||||
"account.menu.remove_follower": "Eliminar el seguidor",
|
||||
"account.menu.report": "Denunciar el compte",
|
||||
"account.menu.share": "Compartir…",
|
||||
"account.menu.show_reblogs": "Mostrar impulsos en les línies de temps",
|
||||
"account.menu.unblock": "Desblocar el compte",
|
||||
"account.menu.unblock_domain": "Desblocar {domain}",
|
||||
"account.menu.unmute": "Deixar de silenciar el compte",
|
||||
"account.moved_to": "{name} ha indicat que el seu nou compte és:",
|
||||
"account.mute": "Silencia @{name}",
|
||||
"account.mute_notifications_short": "Silencia les notificacions",
|
||||
@@ -185,21 +208,31 @@
|
||||
"closed_registrations_modal.find_another_server": "Troba un altre servidor",
|
||||
"closed_registrations_modal.preamble": "Mastodon és descentralitzat. Per tant, tinguis on tinguis el compte, seràs capaç de seguir i interactuar amb tothom des d'aquest servidor. Fins i tot pots tenir el compte en el teu propi servidor!",
|
||||
"closed_registrations_modal.title": "Registrant-se a Mastodon",
|
||||
"collections.collection_description": "Descripció",
|
||||
"collections.collection_name": "Nom",
|
||||
"collections.collection_topic": "Tema",
|
||||
"collections.create_a_collection_hint": "Creeu una coŀlecció per a recomanar o compartir amb d'altres els vostres comptes preferits.",
|
||||
"collections.create_collection": "Crea una coŀlecció",
|
||||
"collections.delete_collection": "Elimina la coŀlecció",
|
||||
"collections.description_length_hint": "Límit de 100 caràcters",
|
||||
"collections.error_loading_collections": "Hi ha hagut un error en carregar les vostres coŀleccions.",
|
||||
"collections.mark_as_sensitive": "Marcar com a sensible",
|
||||
"collections.mark_as_sensitive_hint": "Amaga la descripció i els comptes de la coŀlecció rere un avís de contingut. El nom de la coŀlecció serà encara visible.",
|
||||
"collections.name_length_hint": "Límit de 100 caràcters",
|
||||
"collections.no_collections_yet": "Encara no hi ha coŀleccions.",
|
||||
"collections.topic_hint": "Afegir una etiqueta que ajudi altres a entendre el tema principal de la coŀlecció.",
|
||||
"collections.view_collection": "Mostra la coŀlecció",
|
||||
"column.about": "Quant a",
|
||||
"column.blocks": "Usuaris blocats",
|
||||
"column.bookmarks": "Marcadors",
|
||||
"column.collections": "Les meves coŀleccions",
|
||||
"column.community": "Línia de temps local",
|
||||
"column.create_collection": "Crear una coŀlecció",
|
||||
"column.create_list": "Crea una llista",
|
||||
"column.direct": "Mencions privades",
|
||||
"column.directory": "Navega pels perfils",
|
||||
"column.domain_blocks": "Dominis blocats",
|
||||
"column.edit_collection": "Editar la col·lecció",
|
||||
"column.edit_list": "Edita la llista",
|
||||
"column.favourites": "Favorits",
|
||||
"column.firehose": "Tuts en directe",
|
||||
@@ -254,6 +287,9 @@
|
||||
"confirmations.delete.confirm": "Elimina",
|
||||
"confirmations.delete.message": "Segur que vols eliminar aquest tut?",
|
||||
"confirmations.delete.title": "Eliminar la publicació?",
|
||||
"confirmations.delete_collection.confirm": "Eliminar",
|
||||
"confirmations.delete_collection.message": "Aquesta acció no es pot desfer.",
|
||||
"confirmations.delete_collection.title": "Voleu eliminar «{name}»?",
|
||||
"confirmations.delete_list.confirm": "Elimina",
|
||||
"confirmations.delete_list.message": "Segur que vols suprimir permanentment aquesta llista?",
|
||||
"confirmations.delete_list.title": "Eliminar la llista?",
|
||||
|
||||
@@ -236,28 +236,43 @@
|
||||
"collections.collection_description": "Description",
|
||||
"collections.collection_name": "Name",
|
||||
"collections.collection_topic": "Topic",
|
||||
"collections.content_warning": "Content warning",
|
||||
"collections.continue": "Continue",
|
||||
"collections.create.accounts_subtitle": "Only accounts you follow who have opted into discovery can be added.",
|
||||
"collections.create.accounts_title": "Who will you feature in this collection?",
|
||||
"collections.create.basic_details_title": "Basic details",
|
||||
"collections.create.settings_title": "Settings",
|
||||
"collections.create.steps": "Step {step}/{total}",
|
||||
"collections.create_a_collection_hint": "Create a collection to recommend or share your favourite accounts with others.",
|
||||
"collections.create_collection": "Create collection",
|
||||
"collections.delete_collection": "Delete collection",
|
||||
"collections.description_length_hint": "100 characters limit",
|
||||
"collections.edit_details": "Edit basic details",
|
||||
"collections.edit_settings": "Edit settings",
|
||||
"collections.error_loading_collections": "There was an error when trying to load your collections.",
|
||||
"collections.manage_accounts": "Manage accounts",
|
||||
"collections.manage_accounts_in_collection": "Manage accounts in this collection",
|
||||
"collections.mark_as_sensitive": "Mark as sensitive",
|
||||
"collections.mark_as_sensitive_hint": "Hides the collection's description and accounts behind a content warning. The collection name will still be visible.",
|
||||
"collections.name_length_hint": "100 characters limit",
|
||||
"collections.new_collection": "New collection",
|
||||
"collections.no_collections_yet": "No collections yet.",
|
||||
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.",
|
||||
"collections.view_collection": "View collection",
|
||||
"collections.visibility_public": "Public",
|
||||
"collections.visibility_public_hint": "Discoverable in search results and other areas where recommendations appear.",
|
||||
"collections.visibility_title": "Visibility",
|
||||
"collections.visibility_unlisted": "Unlisted",
|
||||
"collections.visibility_unlisted_hint": "Visible to anyone with a link. Hidden from search results and recommendations.",
|
||||
"column.about": "About",
|
||||
"column.blocks": "Blocked users",
|
||||
"column.bookmarks": "Bookmarks",
|
||||
"column.collections": "My collections",
|
||||
"column.community": "Local timeline",
|
||||
"column.create_collection": "Create collection",
|
||||
"column.create_list": "Create list",
|
||||
"column.direct": "Private mentions",
|
||||
"column.directory": "Browse profiles",
|
||||
"column.domain_blocks": "Blocked domains",
|
||||
"column.edit_collection": "Edit collection",
|
||||
"column.edit_list": "Edit list",
|
||||
"column.favourites": "Favorites",
|
||||
"column.firehose": "Live feeds",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"account.account_note_header": "Henkilökohtainen muistiinpano",
|
||||
"account.activity": "Toiminta",
|
||||
"account.add_note": "Lisää henkilökohtainen muistiinpano",
|
||||
"account.add_or_remove_from_list": "Lisää tai poista listoista",
|
||||
"account.add_or_remove_from_list": "Lisää tai poista listoilta",
|
||||
"account.badges.admin": "Ylläpitäjä",
|
||||
"account.badges.blocked": "Estetty",
|
||||
"account.badges.bot": "Botti",
|
||||
@@ -95,7 +95,7 @@
|
||||
"account.menu.show_reblogs": "Näytä tehostukset aikajanalla",
|
||||
"account.menu.unblock": "Kumoa tilin esto",
|
||||
"account.menu.unblock_domain": "Kumoa palvelimen {domain} esto",
|
||||
"account.menu.unmute": "Poista tilin mykistys",
|
||||
"account.menu.unmute": "Kumoa tilin mykistys",
|
||||
"account.moved_to": "{name} on ilmoittanut uudeksi tilikseen",
|
||||
"account.mute": "Mykistä @{name}",
|
||||
"account.mute_notifications_short": "Mykistä ilmoitukset",
|
||||
@@ -128,9 +128,9 @@
|
||||
"account.unblock_short": "Kumoa esto",
|
||||
"account.unendorse": "Kumoa profiilissa esittely",
|
||||
"account.unfollow": "Älä seuraa",
|
||||
"account.unmute": "Poista käyttäjän @{name} mykistys",
|
||||
"account.unmute_notifications_short": "Poista ilmoitusten mykistys",
|
||||
"account.unmute_short": "Poista mykistys",
|
||||
"account.unmute": "Kumoa käyttäjän @{name} mykistys",
|
||||
"account.unmute_notifications_short": "Kumoa ilmoitusten mykistys",
|
||||
"account.unmute_short": "Kumoa mykistys",
|
||||
"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",
|
||||
@@ -501,7 +501,7 @@
|
||||
"follow_suggestions.popular_suggestion_longer": "Suosittu palvelimella {domain}",
|
||||
"follow_suggestions.similar_to_recently_followed_longer": "Samankaltainen kuin äskettäin seuraamasi profiilit",
|
||||
"follow_suggestions.view_all": "Näytä kaikki",
|
||||
"follow_suggestions.who_to_follow": "Ehdotuksia seurattavaksi",
|
||||
"follow_suggestions.who_to_follow": "Seurantaehdotuksia",
|
||||
"followed_tags": "Seurattavat aihetunnisteet",
|
||||
"footer.about": "Tietoja",
|
||||
"footer.about_mastodon": "Tietoja Mastodonista",
|
||||
@@ -1111,7 +1111,7 @@
|
||||
"video.play": "Toista",
|
||||
"video.skip_backward": "Siirry taaksepäin",
|
||||
"video.skip_forward": "Siirry eteenpäin",
|
||||
"video.unmute": "Poista mykistys",
|
||||
"video.unmute": "Kumoa mykistys",
|
||||
"video.volume_down": "Vähennä äänenvoimakkuutta",
|
||||
"video.volume_up": "Lisää äänenvoimakkuutta",
|
||||
"visibility_modal.button_title": "Aseta näkyvyys",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"account.follow": "Suivre",
|
||||
"account.follow_back": "Suivre en retour",
|
||||
"account.follow_back_short": "Suivre en retour",
|
||||
"account.follow_request": "Demande d’abonnement",
|
||||
"account.follow_request": "Demander à suivre",
|
||||
"account.follow_request_cancel": "Annuler la demande",
|
||||
"account.follow_request_cancel_short": "Annuler",
|
||||
"account.follow_request_short": "Demander à suivre",
|
||||
@@ -85,12 +85,23 @@
|
||||
"account.menu.copied": "Lien du compte copié dans le presse-papiers",
|
||||
"account.menu.copy": "Copier le lien",
|
||||
"account.menu.direct": "Mentionner en privé",
|
||||
"account.menu.hide_reblogs": "Masquer les partages dans le fil d’actualité",
|
||||
"account.menu.mention": "Mentionner",
|
||||
"account.menu.mute": "Masquer le compte",
|
||||
"account.menu.open_original_page": "Voir sur {domain}",
|
||||
"account.menu.remove_follower": "Supprimer des abonné·e·s",
|
||||
"account.menu.report": "Signaler le compte",
|
||||
"account.menu.share": "Partager…",
|
||||
"account.menu.show_reblogs": "Afficher les partages dans le fil d'actualité",
|
||||
"account.menu.unblock": "Débloquer le compte",
|
||||
"account.menu.unblock_domain": "Débloquer {domain}",
|
||||
"account.menu.unmute": "Ne plus masquer le compte",
|
||||
"account.moved_to": "{name} a indiqué que son nouveau compte est maintenant:",
|
||||
"account.mute": "Masquer @{name}",
|
||||
"account.mute_notifications_short": "Rendre les notifications muettes",
|
||||
"account.mute_short": "Rendre muet",
|
||||
"account.muted": "Masqué·e",
|
||||
"account.muting": "Sourdine",
|
||||
"account.muting": "Masqué",
|
||||
"account.mutual": "Vous vous suivez mutuellement",
|
||||
"account.no_bio": "Description manquante.",
|
||||
"account.node_modal.callout": "Les notes personnelles sont ne sont visibles que pour vous.",
|
||||
@@ -523,7 +534,7 @@
|
||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} message} other {{counter} messages}} aujourd’hui",
|
||||
"hashtag.feature": "Mettre en avant sur votre profil",
|
||||
"hashtag.follow": "Suivre ce hashtag",
|
||||
"hashtag.mute": "Mettre #{hashtag} en sourdine",
|
||||
"hashtag.mute": "Masquer #{hashtag}",
|
||||
"hashtag.unfeature": "Ne plus mettre en avant sur votre profil",
|
||||
"hashtag.unfollow": "Ne plus suivre ce hashtag",
|
||||
"hashtags.and_other": "…et {count, plural, other {# de plus}}",
|
||||
@@ -1008,7 +1019,7 @@
|
||||
"status.quote_error.filtered": "Caché par un de vos filtres",
|
||||
"status.quote_error.limited_account_hint.action": "Afficher quand même",
|
||||
"status.quote_error.limited_account_hint.title": "Ce compte a été masqué par la modération de {domain}.",
|
||||
"status.quote_error.muted_account_hint.title": "Ce message est masqué car vous avez masqué @{name}.",
|
||||
"status.quote_error.muted_account_hint.title": "Ce message est caché car vous avez masqué @{name}.",
|
||||
"status.quote_error.not_available": "Message indisponible",
|
||||
"status.quote_error.pending_approval": "Message en attente",
|
||||
"status.quote_error.pending_approval_popout.body": "Sur Mastodon, vous pouvez contrôler si quelqu'un peut vous citer. Ce message est en attente pendant que nous recevons l'approbation de l'auteur original.",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"account.follow": "Suivre",
|
||||
"account.follow_back": "Suivre en retour",
|
||||
"account.follow_back_short": "Suivre en retour",
|
||||
"account.follow_request": "Demande d’abonnement",
|
||||
"account.follow_request": "Demander à suivre",
|
||||
"account.follow_request_cancel": "Annuler la demande",
|
||||
"account.follow_request_cancel_short": "Annuler",
|
||||
"account.follow_request_short": "Demander à suivre",
|
||||
@@ -85,12 +85,23 @@
|
||||
"account.menu.copied": "Lien du compte copié dans le presse-papiers",
|
||||
"account.menu.copy": "Copier le lien",
|
||||
"account.menu.direct": "Mentionner en privé",
|
||||
"account.menu.hide_reblogs": "Masquer les partages dans le fil d’actualité",
|
||||
"account.menu.mention": "Mentionner",
|
||||
"account.menu.mute": "Masquer le compte",
|
||||
"account.menu.open_original_page": "Voir sur {domain}",
|
||||
"account.menu.remove_follower": "Supprimer des abonné·e·s",
|
||||
"account.menu.report": "Signaler le compte",
|
||||
"account.menu.share": "Partager…",
|
||||
"account.menu.show_reblogs": "Afficher les partages dans le fil d'actualité",
|
||||
"account.menu.unblock": "Débloquer le compte",
|
||||
"account.menu.unblock_domain": "Débloquer {domain}",
|
||||
"account.menu.unmute": "Ne plus masquer le compte",
|
||||
"account.moved_to": "{name} a indiqué que son nouveau compte est maintenant :",
|
||||
"account.mute": "Masquer @{name}",
|
||||
"account.mute_notifications_short": "Désactiver les notifications",
|
||||
"account.mute_short": "Mettre en sourdine",
|
||||
"account.mute_short": "Masquer",
|
||||
"account.muted": "Masqué·e",
|
||||
"account.muting": "Sourdine",
|
||||
"account.muting": "Masqué",
|
||||
"account.mutual": "Vous vous suivez mutuellement",
|
||||
"account.no_bio": "Aucune description fournie.",
|
||||
"account.node_modal.callout": "Les notes personnelles sont ne sont visibles que pour vous.",
|
||||
@@ -523,7 +534,7 @@
|
||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} message} other {{counter} messages}} aujourd’hui",
|
||||
"hashtag.feature": "Mettre en avant sur votre profil",
|
||||
"hashtag.follow": "Suivre le hashtag",
|
||||
"hashtag.mute": "Mettre #{hashtag} en sourdine",
|
||||
"hashtag.mute": "Masquer #{hashtag}",
|
||||
"hashtag.unfeature": "Ne plus mettre en avant sur votre profil",
|
||||
"hashtag.unfollow": "Ne plus suivre le hashtag",
|
||||
"hashtags.and_other": "…et {count, plural, other {# de plus}}",
|
||||
@@ -1008,7 +1019,7 @@
|
||||
"status.quote_error.filtered": "Caché par un de vos filtres",
|
||||
"status.quote_error.limited_account_hint.action": "Afficher quand même",
|
||||
"status.quote_error.limited_account_hint.title": "Ce compte a été masqué par la modération de {domain}.",
|
||||
"status.quote_error.muted_account_hint.title": "Ce message est masqué car vous avez masqué @{name}.",
|
||||
"status.quote_error.muted_account_hint.title": "Ce message est caché car vous avez masqué @{name}.",
|
||||
"status.quote_error.not_available": "Message indisponible",
|
||||
"status.quote_error.pending_approval": "Message en attente",
|
||||
"status.quote_error.pending_approval_popout.body": "Sur Mastodon, vous pouvez contrôler si quelqu'un peut vous citer. Ce message est en attente pendant que nous recevons l'approbation de l'auteur original.",
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
"account.featured.hashtags": "Haischlibeanna",
|
||||
"account.featured_tags.last_status_at": "Postáil is déanaí ar {date}",
|
||||
"account.featured_tags.last_status_never": "Gan aon phoist",
|
||||
"account.fields.scroll_next": "Taispeáin an chéad cheann eile",
|
||||
"account.fields.scroll_prev": "Taispeáin roimhe seo",
|
||||
"account.filters.all": "Gach gníomhaíocht",
|
||||
"account.filters.boosts_toggle": "Taispeáin borradh",
|
||||
"account.filters.posts_boosts": "Poist agus borradh",
|
||||
@@ -77,6 +79,23 @@
|
||||
"account.locked_info": "Tá an socrú príobháideachais don cuntas seo curtha go 'faoi ghlas'. Déanann an t-úinéir léirmheas ar cén daoine atá ceadaithe an cuntas leanúint.",
|
||||
"account.media": "Meáin",
|
||||
"account.mention": "Luaigh @{name}",
|
||||
"account.menu.add_to_list": "Cuir leis an liosta…",
|
||||
"account.menu.block": "Cuntas blocáilte",
|
||||
"account.menu.block_domain": "Blocáil {domain}",
|
||||
"account.menu.copied": "Cóipeáladh nasc an chuntais chuig an ghearrthaisce",
|
||||
"account.menu.copy": "Cóipeáil nasc",
|
||||
"account.menu.direct": "Luaigh go príobháideach",
|
||||
"account.menu.hide_reblogs": "Folaigh borradh sa líne ama",
|
||||
"account.menu.mention": "Luaigh",
|
||||
"account.menu.mute": "Balbhaigh cuntas",
|
||||
"account.menu.open_original_page": "Féach ar {domain}",
|
||||
"account.menu.remove_follower": "Bain leantóir",
|
||||
"account.menu.report": "Tuairiscigh cuntas",
|
||||
"account.menu.share": "Comhroinn…",
|
||||
"account.menu.show_reblogs": "Taispeáin borradh san amlíne",
|
||||
"account.menu.unblock": "Díbhlocáil cuntas",
|
||||
"account.menu.unblock_domain": "Díbhlocáil {domain}",
|
||||
"account.menu.unmute": "Díbholg an cuntas",
|
||||
"account.moved_to": "Tá tugtha le fios ag {name} gurb é an cuntas nua atá acu ná:",
|
||||
"account.mute": "Balbhaigh @{name}",
|
||||
"account.mute_notifications_short": "Balbhaigh fógraí",
|
||||
|
||||
@@ -411,7 +411,7 @@
|
||||
"keyboard_shortcuts.muted": "o lukin e lipu sina pi jan len",
|
||||
"keyboard_shortcuts.my_profile": "o lukin e lipu sina",
|
||||
"keyboard_shortcuts.open_media": "o lukin e sitelen",
|
||||
"keyboard_shortcuts.pinned": "o lukin pi lipu sina pi toki sewi",
|
||||
"keyboard_shortcuts.pinned": "o lukin pi toki sina sewi",
|
||||
"keyboard_shortcuts.reply": "o toki lon ijo ni",
|
||||
"keyboard_shortcuts.toggle_hidden": "o lukin ala lukin e toki len",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "o lukin ala lukin e sitelen",
|
||||
|
||||
Reference in New Issue
Block a user