Merge commit '0b66e744263a4af1f14d03886ea2a9da4ca156db' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2026-02-17 12:11:33 +01:00
71 changed files with 1083 additions and 172 deletions

View File

@@ -0,0 +1,86 @@
# frozen_string_literal: true
class Api::V1::DonationCampaignsController < Api::BaseController
before_action :require_user!
STOPLIGHT_COOL_OFF_TIME = 60
STOPLIGHT_FAILURE_THRESHOLD = 10
def index
return head 204 if api_url.blank?
json = from_cache
return render json: json if json.present?
campaign = fetch_campaign
return head 204 if campaign.nil?
save_to_cache!(campaign)
render json: campaign
end
private
def api_url
Rails.configuration.x.donation_campaigns.api_url
end
def seed
@seed ||= Random.new(current_account.id).rand(100)
end
def from_cache
key = Rails.cache.read(request_key, raw: true)
return if key.blank?
campaign = Rails.cache.read("donation_campaign:#{key}", raw: true)
Oj.load(campaign) if campaign.present?
end
def save_to_cache!(campaign)
return if campaign.blank?
Rails.cache.write_multi(
{
request_key => campaign_key(campaign),
"donation_campaign:#{campaign_key(campaign)}" => Oj.dump(campaign),
},
expires_in: 1.hour,
raw: true
)
end
def fetch_campaign
stoplight_wrapper.run do
url = Addressable::URI.parse(api_url)
url.query_values = { platform: 'web', seed: seed, locale: locale, environment: Rails.configuration.x.donation_campaigns.environment }.compact
Request.new(:get, url.to_s).perform do |res|
return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200
end
end
rescue *Mastodon::HTTP_CONNECTION_ERRORS, Oj::ParseError
nil
end
def stoplight_wrapper
Stoplight(
'donation_campaigns',
cool_off_time: STOPLIGHT_COOL_OFF_TIME,
threshold: STOPLIGHT_FAILURE_THRESHOLD
)
end
def request_key
"donation_campaign_request:#{seed}:#{locale}"
end
def campaign_key(campaign)
"#{campaign['id']}:#{campaign['locale']}"
end
def locale
I18n.locale.to_s
end
end

View File

@@ -11,6 +11,7 @@ import type {
ApiCreateCollectionPayload,
ApiUpdateCollectionPayload,
ApiCollectionsJSON,
WrappedCollectionAccountItem,
} from '../api_types/collections';
export const apiCreateCollection = (collection: ApiCreateCollectionPayload) =>
@@ -37,3 +38,14 @@ export const apiGetAccountCollections = (accountId: string) =>
apiRequestGet<ApiCollectionsJSON>(
`v1_alpha/accounts/${accountId}/collections`,
);
export const apiAddCollectionItem = (collectionId: string, accountId: string) =>
apiRequestPost<WrappedCollectionAccountItem>(
`v1_alpha/collections/${collectionId}/items`,
{ account_id: accountId },
);
export const apiRemoveCollectionItem = (collectionId: string, itemId: string) =>
apiRequestDelete<WrappedCollectionAccountItem>(
`v1_alpha/collections/${collectionId}/items/${itemId}`,
);

View File

@@ -53,6 +53,7 @@ export interface ApiCollectionWithAccountsJSON extends ApiWrappedCollectionJSON
* Nested account item
*/
interface CollectionAccountItem {
id: string;
account_id?: string; // Only present when state is 'accepted' (or the collection is your own)
state: 'pending' | 'accepted' | 'rejected' | 'revoked';
position: number;

View File

@@ -73,6 +73,7 @@ interface AccountProps {
defaultAction?: 'block' | 'mute';
withBio?: boolean;
withMenu?: boolean;
children?: React.ReactNode;
}
export const Account: React.FC<AccountProps> = ({
@@ -83,6 +84,7 @@ export const Account: React.FC<AccountProps> = ({
defaultAction,
withBio,
withMenu = true,
children,
}) => {
const intl = useIntl();
const { signedIn } = useIdentity();
@@ -353,6 +355,8 @@ export const Account: React.FC<AccountProps> = ({
{button}
</div>
)}
{children}
</div>
</div>
);

View File

@@ -25,14 +25,17 @@
.popover {
z-index: 9999;
box-sizing: border-box;
max-height: max(200px, 30dvh);
padding: 4px;
border-radius: 4px;
color: var(--color-text-primary);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-primary);
box-shadow: var(--dropdown-shadow);
// backdrop-filter: $backdrop-blur-filter;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-gutter: stable;
overscroll-behavior-y: contain;
}
.menuItem {
@@ -47,7 +50,7 @@
cursor: pointer;
user-select: none;
&[aria-selected='true'] {
&[data-highlighted='true'] {
color: var(--color-text-on-brand-base);
background: var(--color-bg-brand-base);

View File

@@ -18,24 +18,30 @@ import { FormFieldWrapper } from './form_field_wrapper';
import type { CommonFieldWrapperProps } from './form_field_wrapper';
import { TextInput } from './text_input_field';
interface Item {
interface ComboboxItem {
id: string;
}
export interface ComboboxItemState {
isSelected: boolean;
isDisabled: boolean;
}
interface ComboboxProps<
T extends Item,
T extends ComboboxItem,
> extends ComponentPropsWithoutRef<'input'> {
value: string;
onChange: React.ChangeEventHandler<HTMLInputElement>;
isLoading?: boolean;
items: T[];
getItemId: (item: T) => string;
getIsItemSelected?: (item: T) => boolean;
getIsItemDisabled?: (item: T) => boolean;
renderItem: (item: T) => React.ReactElement;
renderItem: (item: T, state: ComboboxItemState) => React.ReactElement;
onSelectItem: (item: T) => void;
}
interface Props<T extends Item>
interface Props<T extends ComboboxItem>
extends ComboboxProps<T>, CommonFieldWrapperProps {}
/**
@@ -43,7 +49,7 @@ interface Props<T extends Item>
* from a large list of options by searching or filtering.
*/
export const ComboboxFieldWithRef = <T extends Item>(
export const ComboboxFieldWithRef = <T extends ComboboxItem>(
{ id, label, hint, hasError, required, ...otherProps }: Props<T>,
ref: React.ForwardedRef<HTMLInputElement>,
) => (
@@ -61,7 +67,7 @@ export const ComboboxFieldWithRef = <T extends Item>(
// Using a type assertion to maintain the full type signature of ComboboxWithRef
// (including its generic type) after wrapping it with `forwardRef`.
export const ComboboxField = forwardRef(ComboboxFieldWithRef) as {
<T extends Item>(
<T extends ComboboxItem>(
props: Props<T> & { ref?: React.ForwardedRef<HTMLInputElement> },
): ReturnType<typeof ComboboxFieldWithRef>;
displayName: string;
@@ -69,13 +75,15 @@ export const ComboboxField = forwardRef(ComboboxFieldWithRef) as {
ComboboxField.displayName = 'ComboboxField';
const ComboboxWithRef = <T extends Item>(
const ComboboxWithRef = <T extends ComboboxItem>(
{
value,
isLoading = false,
items,
getItemId,
getIsItemDisabled,
getIsItemSelected,
disabled,
renderItem,
onSelectItem,
onChange,
@@ -88,6 +96,7 @@ const ComboboxWithRef = <T extends Item>(
const intl = useIntl();
const wrapperRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement | null>();
const popoverRef = useRef<HTMLDivElement>(null);
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(
null,
@@ -101,11 +110,13 @@ const ComboboxWithRef = <T extends Item>(
});
const showStatusMessageInMenu =
!!statusMessage && value.length > 0 && items.length === 0;
const hasMenuContent = items.length > 0 || showStatusMessageInMenu;
const hasMenuContent =
!disabled && (items.length > 0 || showStatusMessageInMenu);
const isMenuOpen = shouldMenuOpen && hasMenuContent;
const openMenu = useCallback(() => {
setShouldMenuOpen(true);
inputRef.current?.focus();
}, []);
const closeMenu = useCallback(() => {
@@ -118,6 +129,18 @@ const ComboboxWithRef = <T extends Item>(
setHighlightedItemId(firstItemId);
}, [getItemId, items]);
const highlightItem = useCallback((id: string | null) => {
setHighlightedItemId(id);
if (id) {
const itemElement = popoverRef.current?.querySelector<HTMLLIElement>(
`[data-item-id='${id}']`,
);
if (itemElement && popoverRef.current) {
scrollItemIntoView(itemElement, popoverRef.current);
}
}
}, []);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e);
@@ -127,14 +150,14 @@ const ComboboxWithRef = <T extends Item>(
[onChange, resetHighlight],
);
const handleHighlightItem = useCallback(
const handleItemMouseEnter = useCallback(
(e: React.MouseEvent<HTMLLIElement>) => {
const { itemId } = e.currentTarget.dataset;
if (itemId) {
setHighlightedItemId(itemId);
highlightItem(itemId);
}
},
[],
[highlightItem],
);
const selectItem = useCallback(
@@ -175,10 +198,10 @@ const ComboboxWithRef = <T extends Item>(
// If no item is highlighted yet, highlight the first or last
if (direction > 0) {
const firstItem = items.at(0);
setHighlightedItemId(firstItem ? getItemId(firstItem) : null);
highlightItem(firstItem ? getItemId(firstItem) : null);
} else {
const lastItem = items.at(-1);
setHighlightedItemId(lastItem ? getItemId(lastItem) : null);
highlightItem(lastItem ? getItemId(lastItem) : null);
}
} else {
// If there is a highlighted item, select the next or previous item
@@ -191,12 +214,12 @@ const ComboboxWithRef = <T extends Item>(
}
const newHighlightedItem = items[newIndex];
setHighlightedItemId(
highlightItem(
newHighlightedItem ? getItemId(newHighlightedItem) : null,
);
}
},
[getItemId, highlightedItemId, items],
[getItemId, highlightItem, highlightedItemId, items],
);
useOnClickOutside(wrapperRef, closeMenu);
@@ -231,7 +254,6 @@ const ComboboxWithRef = <T extends Item>(
if (isMenuOpen) {
e.preventDefault();
selectHighlightedItem();
closeMenu();
}
}
if (e.key === 'Escape') {
@@ -271,9 +293,10 @@ const ComboboxWithRef = <T extends Item>(
<TextInput
role='combobox'
{...otherProps}
disabled={disabled}
aria-controls={listId}
aria-expanded={isMenuOpen ? 'true' : 'false'}
aria-haspopup='true'
aria-haspopup='listbox'
aria-activedescendant={
isMenuOpen && highlightedItemId ? highlightedItemId : undefined
}
@@ -311,11 +334,11 @@ const ComboboxWithRef = <T extends Item>(
{isMenuOpen && statusMessage}
</span>
<Overlay
flip
show={isMenuOpen}
offset={[0, 1]}
placement='bottom-start'
onHide={closeMenu}
ref={popoverRef}
target={inputRef as React.RefObject<HTMLInputElement>}
container={wrapperRef}
popperConfig={{
@@ -331,19 +354,30 @@ const ComboboxWithRef = <T extends Item>(
{items.map((item) => {
const id = getItemId(item);
const isDisabled = getIsItemDisabled?.(item);
const isHighlighted = id === highlightedItemId;
// If `getIsItemSelected` is defined, we assume 'multi-select'
// behaviour and don't set `aria-selected` based on highlight,
// but based on selected item state.
const isSelected = getIsItemSelected
? getIsItemSelected(item)
: isHighlighted;
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<li
key={id}
role='option'
className={classes.menuItem}
aria-selected={id === highlightedItemId}
data-highlighted={isHighlighted}
aria-selected={isSelected}
aria-disabled={isDisabled}
data-item-id={id}
onMouseEnter={handleHighlightItem}
onMouseEnter={handleItemMouseEnter}
onClick={handleSelectItem}
>
{renderItem(item)}
{renderItem(item, {
isSelected,
isDisabled: isDisabled ?? false,
})}
</li>
);
})}
@@ -359,7 +393,7 @@ const ComboboxWithRef = <T extends Item>(
// Using a type assertion to maintain the full type signature of ComboboxWithRef
// (including its generic type) after wrapping it with `forwardRef`.
export const Combobox = forwardRef(ComboboxWithRef) as {
<T extends Item>(
<T extends ComboboxItem>(
props: ComboboxProps<T> & { ref?: React.ForwardedRef<HTMLInputElement> },
): ReturnType<typeof ComboboxWithRef>;
displayName: string;
@@ -406,3 +440,23 @@ function useGetA11yStatusMessage({
}
return '';
}
const SCROLL_MARGIN = 6;
function scrollItemIntoView(item: HTMLElement, scrollParent: HTMLElement) {
const itemTopEdge = item.offsetTop;
const itemBottomEdge = itemTopEdge + item.offsetHeight;
// If item is above scroll area, scroll up
if (itemTopEdge < scrollParent.scrollTop) {
scrollParent.scrollTop = itemTopEdge - SCROLL_MARGIN;
}
// If item is below scroll area, scroll down
else if (
itemBottomEdge >
scrollParent.scrollTop + scrollParent.offsetHeight
) {
scrollParent.scrollTop =
itemBottomEdge - scrollParent.offsetHeight + SCROLL_MARGIN;
}
}

View File

@@ -3,7 +3,11 @@ 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 { ComboboxField, Combobox } from './combobox_field';
export {
ComboboxField,
Combobox,
type ComboboxItemState,
} from './combobox_field';
export { RadioButtonField, RadioButton } from './radio_button_field';
export { ToggleField, Toggle } from './toggle_field';
export { SelectField, Select } from './select_field';

View File

@@ -1,67 +1,363 @@
import { useCallback } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { useHistory, useLocation } from 'react-router-dom';
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Account } from 'mastodon/components/account';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import { FormStack } from 'mastodon/components/form_fields';
import { Callout } from 'mastodon/components/callout';
import { DisplayName } from 'mastodon/components/display_name';
import { EmptyState } from 'mastodon/components/empty_state';
import { FormStack, ComboboxField } from 'mastodon/components/form_fields';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import ScrollableList from 'mastodon/components/scrollable_list';
import { useSearchAccounts } from 'mastodon/features/lists/use_search_accounts';
import {
addCollectionItem,
removeCollectionItem,
} from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import type { TempCollectionState } from './state';
import { getInitialState } from './state';
import { getCollectionEditorState } from './state';
import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header';
const MIN_ACCOUNT_COUNT = 1;
const MAX_ACCOUNT_COUNT = 25;
const AddedAccountItem: React.FC<{
accountId: string;
isRemovable: boolean;
onRemove: (id: string) => void;
}> = ({ accountId, isRemovable, onRemove }) => {
const intl = useIntl();
const handleRemoveAccount = useCallback(() => {
onRemove(accountId);
}, [accountId, onRemove]);
return (
<Account minimal key={accountId} id={accountId}>
{isRemovable && (
<IconButton
title={intl.formatMessage({
id: 'collections.remove_account',
defaultMessage: 'Remove this account',
})}
icon='remove'
iconComponent={CancelIcon}
onClick={handleRemoveAccount}
/>
)}
</Account>
);
};
interface SuggestionItem {
id: string;
isSelected: boolean;
}
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id, isSelected }) => {
const account = useAppSelector((state) => state.accounts.get(id));
if (!account) return null;
return (
<>
<Avatar account={account} />
<DisplayName account={account} />
{isSelected && (
<Icon
id='checked'
icon={CheckIcon}
className={classes.selectedSuggestionIcon}
/>
)}
</>
);
};
const renderAccountItem = (item: SuggestionItem) => (
<SuggestedAccountItem id={item.id} isSelected={item.isSelected} />
);
const getItemId = (item: SuggestionItem) => item.id;
const getIsItemSelected = (item: SuggestionItem) => item.isSelected;
export const CollectionAccounts: React.FC<{
collection?: ApiCollectionJSON | null;
}> = ({ collection }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const history = useHistory();
const location = useLocation<TempCollectionState>();
const { id, initialItemIds } = getCollectionEditorState(
collection,
location.state,
);
const isEditMode = !!id;
const collectionItems = collection?.items;
const { id } = getInitialState(collection, location.state);
const [searchValue, setSearchValue] = useState('');
// This state is only used when creating a new collection.
// In edit mode, the collection will be updated instantly
const [addedAccountIds, setAccountIds] = useState(initialItemIds);
const accountIds = useMemo(
() =>
isEditMode
? (collectionItems
?.map((item) => item.account_id)
.filter((id): id is string => !!id) ?? [])
: addedAccountIds,
[isEditMode, collectionItems, addedAccountIds],
);
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
const hasMinAccounts = accountIds.length === MIN_ACCOUNT_COUNT;
const hasTooFewAccounts = accountIds.length < MIN_ACCOUNT_COUNT;
const canSubmit = !hasTooFewAccounts;
const {
accountIds: suggestedAccountIds,
isLoading: isLoadingSuggestions,
searchAccounts,
} = useSearchAccounts();
const suggestedItems = suggestedAccountIds.map((id) => ({
id,
isSelected: accountIds.includes(id),
}));
const handleSearchValueChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
searchAccounts(e.target.value);
},
[searchAccounts],
);
const handleSearchKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
}
},
[],
);
const toggleAccountItem = useCallback((item: SuggestionItem) => {
setAccountIds((ids) =>
ids.includes(item.id)
? ids.filter((id) => id !== item.id)
: [...ids, item.id],
);
}, []);
const instantRemoveAccountItem = useCallback(
(accountId: string) => {
const itemId = collectionItems?.find(
(item) => item.account_id === accountId,
)?.id;
if (itemId && id) {
if (
window.confirm(
intl.formatMessage({
id: 'collections.confirm_account_removal',
defaultMessage:
'Are you sure you want to remove this account from this collection?',
}),
)
) {
void dispatch(removeCollectionItem({ collectionId: id, itemId }));
}
}
},
[collectionItems, dispatch, id, intl],
);
const instantToggleAccountItem = useCallback(
(item: SuggestionItem) => {
if (accountIds.includes(item.id)) {
instantRemoveAccountItem(item.id);
} else {
if (id) {
void dispatch(
addCollectionItem({ collectionId: id, accountId: item.id }),
);
}
}
},
[accountIds, dispatch, id, instantRemoveAccountItem],
);
const handleRemoveAccountItem = useCallback(
(accountId: string) => {
if (isEditMode) {
instantRemoveAccountItem(accountId);
} else {
setAccountIds((ids) => ids.filter((id) => id !== accountId));
}
},
[isEditMode, instantRemoveAccountItem],
);
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (!canSubmit) {
return;
}
if (!id) {
history.push(`/collections/new/details`);
history.push(`/collections/new/details`, {
account_ids: accountIds,
});
}
},
[id, history],
[canSubmit, id, history, accountIds],
);
return (
<FormStack as='form' onSubmit={handleSubmit}>
{!id && (
<WizardStepHeader
step={1}
title={
<form onSubmit={handleSubmit} className={classes.form}>
<FormStack className={classes.formFieldStack}>
{!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.'
/>
}
/>
)}
<ComboboxField
label={
<FormattedMessage
id='collections.create.accounts_title'
defaultMessage='Who will you feature in this collection?'
id='collections.search_accounts_label'
defaultMessage='Search for accounts to add…'
/>
}
description={
<FormattedMessage
id='collections.create.accounts_subtitle'
defaultMessage='Only accounts you follow who have opted into discovery can be added.'
/>
hint={
hasMaxAccounts ? (
<FormattedMessage
id='collections.search_accounts_max_reached'
defaultMessage='You have added the maximum number of accounts'
/>
) : undefined
}
value={hasMaxAccounts ? '' : searchValue}
onChange={handleSearchValueChange}
onKeyDown={handleSearchKeyDown}
disabled={hasMaxAccounts}
isLoading={isLoadingSuggestions}
items={suggestedItems}
getItemId={getItemId}
getIsItemSelected={getIsItemSelected}
renderItem={renderAccountItem}
onSelectItem={
isEditMode ? instantToggleAccountItem : toggleAccountItem
}
/>
)}
<div className='actions'>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
{hasMinAccounts && (
<Callout>
<FormattedMessage
id='collections.continue'
defaultMessage='Continue'
id='collections.hints.can_not_remove_more_accounts'
defaultMessage='Collections must contain at least {count, plural, one {# account} other {# accounts}}. Removing more accounts is not possible.'
values={{ count: MIN_ACCOUNT_COUNT }}
/>
</Callout>
)}
<div className={classes.scrollableWrapper}>
<ScrollableList
scrollKey='collection-items'
className={classes.scrollableInner}
emptyMessage={
<EmptyState
title={
<FormattedMessage
id='collections.accounts.empty_title'
defaultMessage='This collection is empty'
/>
}
message={
<FormattedMessage
id='collections.accounts.empty_description'
defaultMessage='Add up to {count} accounts you follow'
values={{
count: MAX_ACCOUNT_COUNT,
}}
/>
}
/>
}
// TODO: Re-add `bindToDocument={!multiColumn}`
>
{accountIds.map((accountId) => (
<AddedAccountItem
key={accountId}
accountId={accountId}
isRemovable={!isEditMode || !hasMinAccounts}
onRemove={handleRemoveAccountItem}
/>
))}
</ScrollableList>
</div>
</FormStack>
{!isEditMode && (
<div className={classes.stickyFooter}>
{hasTooFewAccounts ? (
<Callout icon={false} className={classes.submitDisabledCallout}>
<FormattedMessage
id='collections.hints.add_more_accounts'
defaultMessage='Add at least {count, plural, one {# account} other {# accounts}} to continue'
values={{ count: MIN_ACCOUNT_COUNT }}
/>
</Callout>
) : (
<div className={classes.actionWrapper}>
<FormattedMessage
id='collections.hints.accounts_counter'
defaultMessage='{count} / {max} accounts'
values={{ count: accountIds.length, max: MAX_ACCOUNT_COUNT }}
>
{(text) => (
<div className={classes.itemCountReadout}>{text}</div>
)}
</FormattedMessage>
{canSubmit && (
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage
id='collections.continue'
defaultMessage='Continue'
/>
)}
</Button>
)}
</div>
)}
</Button>
</div>
</FormStack>
</div>
)}
</form>
);
};

View File

@@ -16,7 +16,8 @@ import { updateCollection } from 'mastodon/reducers/slices/collections';
import { useAppDispatch } from 'mastodon/store';
import type { TempCollectionState } from './state';
import { getInitialState } from './state';
import { getCollectionEditorState } from './state';
import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header';
export const CollectionDetails: React.FC<{
@@ -26,10 +27,8 @@ export const CollectionDetails: React.FC<{
const history = useHistory();
const location = useLocation<TempCollectionState>();
const { id, initialName, initialDescription, initialTopic } = getInitialState(
collection,
location.state,
);
const { id, initialName, initialDescription, initialTopic, initialItemIds } =
getCollectionEditorState(collection, location.state);
const [name, setName] = useState(initialName);
const [description, setDescription] = useState(initialDescription);
@@ -76,13 +75,14 @@ export const CollectionDetails: React.FC<{
name,
description,
tag_name: topic || null,
account_ids: initialItemIds,
};
history.replace('/collections/new', payload);
history.push('/collections/new/settings', payload);
}
},
[id, dispatch, name, description, topic, history],
[id, name, description, topic, dispatch, history, initialItemIds],
);
return (
@@ -155,7 +155,7 @@ export const CollectionDetails: React.FC<{
maxLength={40}
/>
<div className='actions'>
<div className={classes.actionWrapper}>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />

View File

@@ -25,7 +25,8 @@ import {
import { useAppDispatch } from 'mastodon/store';
import type { TempCollectionState } from './state';
import { getInitialState } from './state';
import { getCollectionEditorState } from './state';
import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header';
export const CollectionSettings: React.FC<{
@@ -35,8 +36,8 @@ export const CollectionSettings: React.FC<{
const history = useHistory();
const location = useLocation<TempCollectionState>();
const { id, initialDiscoverable, initialSensitive, ...temporaryState } =
getInitialState(collection, location.state);
const { id, initialDiscoverable, initialSensitive, ...editorState } =
getCollectionEditorState(collection, location.state);
const [discoverable, setDiscoverable] = useState(initialDiscoverable);
const [sensitive, setSensitive] = useState(initialSensitive);
@@ -71,13 +72,14 @@ export const CollectionSettings: React.FC<{
});
} else {
const payload: ApiCreateCollectionPayload = {
name: temporaryState.initialName,
description: temporaryState.initialDescription,
name: editorState.initialName,
description: editorState.initialDescription,
discoverable,
sensitive,
account_ids: editorState.initialItemIds,
};
if (temporaryState.initialTopic) {
payload.tag_name = temporaryState.initialTopic;
if (editorState.initialTopic) {
payload.tag_name = editorState.initialTopic;
}
void dispatch(
@@ -94,7 +96,7 @@ export const CollectionSettings: React.FC<{
});
}
},
[id, discoverable, sensitive, dispatch, history, temporaryState],
[id, discoverable, sensitive, dispatch, history, editorState],
);
return (
@@ -180,7 +182,7 @@ export const CollectionSettings: React.FC<{
/>
</Fieldset>
<div className='actions'>
<div className={classes.actionWrapper}>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />

View File

@@ -15,7 +15,7 @@ export type TempCollectionState =
* Resolve initial editor state. Temporary location state
* trumps stored data, otherwise initial values are returned.
*/
export function getInitialState(
export function getCollectionEditorState(
collection: ApiCollectionJSON | null | undefined,
locationState: TempCollectionState,
) {
@@ -27,10 +27,19 @@ export function getInitialState(
language = '',
discoverable = true,
sensitive = false,
items,
} = collection ?? {};
const collectionItemIds =
items?.map((item) => item.account_id).filter(onlyExistingIds) ?? [];
const initialItemIds = (
locationState?.account_ids ?? collectionItemIds
).filter(onlyExistingIds);
return {
id,
initialItemIds,
initialName: locationState?.name ?? name,
initialDescription: locationState?.description ?? description,
initialTopic: locationState?.tag_name ?? tag?.name ?? '',
@@ -39,3 +48,5 @@ export function getInitialState(
initialSensitive: locationState?.sensitive ?? sensitive,
};
}
const onlyExistingIds = (id?: string): id is string => !!id;

View File

@@ -13,3 +13,76 @@
font-size: 15px;
margin-top: 8px;
}
/* Make form stretch full height of the column */
.form {
--bottom-spacing: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100%;
padding-bottom: var(--bottom-spacing);
@media (width < 760px) {
--bottom-spacing: var(--mobile-bottom-nav-height);
}
}
.selectedSuggestionIcon {
box-sizing: border-box;
width: 18px;
height: 18px;
margin-left: auto;
padding: 2px;
border-radius: 100%;
color: var(--color-text-on-brand-base);
background: var(--color-bg-brand-base);
[data-highlighted='true'] & {
color: var(--color-bg-brand-base);
background: var(--color-text-on-brand-base);
}
}
.formFieldStack {
flex-grow: 1;
}
.scrollableWrapper {
display: flex;
flex: 1;
margin-inline: -8px;
}
.scrollableInner {
margin-inline: -8px;
}
.submitDisabledCallout {
align-self: center;
}
.stickyFooter {
position: sticky;
bottom: var(--bottom-spacing);
padding: 16px;
background-image: linear-gradient(
to bottom,
transparent,
var(--color-bg-primary) 32px
);
}
.itemCountReadout {
text-align: center;
}
.actionWrapper {
display: flex;
flex-direction: column;
width: min-content;
min-width: 240px;
margin-inline: auto;
gap: 8px;
}

View File

@@ -165,10 +165,11 @@ const ListMembers: React.FC<{
const [mode, setMode] = useState<Mode>('remove');
const {
accountIds: searchAccountIds = [],
accountIds: searchAccountIds,
isLoading: loadingSearchResults,
searchAccounts: handleSearch,
} = useSearchAccounts({
resetOnInputClear: false,
onSettled: (value) => {
if (value.trim().length === 0) {
setSearching(false);

View File

@@ -8,13 +8,15 @@ import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { useAppDispatch } from 'mastodon/store';
export function useSearchAccounts({
resetOnInputClear = true,
onSettled,
}: {
onSettled?: (value: string) => void;
resetOnInputClear?: boolean;
} = {}) {
const dispatch = useAppDispatch();
const [accountIds, setAccountIds] = useState<string[]>();
const [accountIds, setAccountIds] = useState<string[]>([]);
const [loadingState, setLoadingState] = useState<
'idle' | 'loading' | 'error'
>('idle');
@@ -29,6 +31,9 @@ export function useSearchAccounts({
if (value.trim().length === 0) {
onSettled?.('');
if (resetOnInputClear) {
setAccountIds([]);
}
return;
}

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon дэцэнтралізаваны, так што дзе б вы ні стварылі ўліковы запіс, вы зможаце падпісвацца і камунікаваць з кім хочаце на гэтым серверы. Вы нават можаце стварыць свой!",
"closed_registrations_modal.title": "Рэгістрацыя ў Mastodon",
"collections.account_count": "{count, plural,one {# уліковы запіс} few {# уліковыя запісы} other {# уліковых запісаў}}",
"collections.accounts.empty_description": "Дадайце да {count} уліковых запісаў, на якія Вы падпісаныя",
"collections.accounts.empty_title": "Гэтая калекцыя пустая",
"collections.collection_description": "Апісанне",
"collections.collection_name": "Назва",
"collections.collection_topic": "Тэма",
"collections.confirm_account_removal": "Упэўненыя, што хочаце прыбраць гэты ўліковы запіс з гэтай калекцыі?",
"collections.content_warning": "Папярэджанне аб змесціве",
"collections.continue": "Працягнуць",
"collections.create.accounts_subtitle": "Можна дадаць толькі ўліковыя запісы, на якія Вы падпісаныя і якія далі дазвол на тое, каб іх можна было знайсці.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Змяніць асноўныя звесткі",
"collections.edit_settings": "Змяніць налады",
"collections.error_loading_collections": "Адбылася памылка падчас загрузкі Вашых калекцый.",
"collections.hints.accounts_counter": "{count} / {max} уліковых запісаў",
"collections.hints.add_more_accounts": "Дадайце як мінімум {count, plural, one {# уліковы запіс} few{# ўліковыя запісы} other {# уліковых запісаў}}, каб працягнуць",
"collections.hints.can_not_remove_more_accounts": "У калекцыі мусіць быць як мінімум {count, plural, one {# уліковы запіс} few{# ўліковыя запісы} other {# уліковых запісаў}}. Немагчыма прыбраць больш уліковых запісаў.",
"collections.last_updated_at": "Апошняе абнаўленне: {date}",
"collections.manage_accounts": "Кіраванне ўліковымі запісамі",
"collections.manage_accounts_in_collection": "Кіраванне ўліковымі запісамі ў гэтай калекцыі",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Максімум 100 сімвалаў",
"collections.new_collection": "Новая калекцыя",
"collections.no_collections_yet": "Пакуль няма калекцый.",
"collections.remove_account": "Прыбраць гэты ўліковы запіс",
"collections.search_accounts_label": "Шукайце ўліковыя запісы, каб дадаць іх сюды…",
"collections.search_accounts_max_reached": "Вы дадалі максімальную колькасць уліковых запісаў",
"collections.topic_hint": "Дадайце хэштэг, які дапаможа іншым зразумець галоўную тэму гэтай калекцыі.",
"collections.view_collection": "Глядзець калекцыю",
"collections.visibility_public": "Публічная",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon er decentraliseret, så uanset hvor du opretter din konto, vil du være i stand til at følge og interagere med hvem som helst på denne server. Du kan endda selv være vært for den!",
"closed_registrations_modal.title": "Oprettelse på Mastodon",
"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.collection_description": "Beskrivelse",
"collections.collection_name": "Navn",
"collections.collection_topic": "Emne",
"collections.confirm_account_removal": "Er du sikker på, at du vil fjerne denne konto fra denne samling?",
"collections.content_warning": "Indholdsadvarsel",
"collections.continue": "Fortsæt",
"collections.create.accounts_subtitle": "Kun konti, du følger, og som har tilmeldt sig opdagelse, kan tilføjes.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Rediger grundlæggende oplysninger",
"collections.edit_settings": "Rediger indstillinger",
"collections.error_loading_collections": "Der opstod en fejl under indlæsning af dine samlinger.",
"collections.hints.accounts_counter": "{count} / {max} konti",
"collections.hints.add_more_accounts": "Tilføj mindst {count, plural, one {# konto} other {# konti}} for at fortsætte",
"collections.hints.can_not_remove_more_accounts": "Samlinger skal indeholde mindst {count, plural, one {# konto} other {# konti}}. Det er ikke muligt at fjerne flere konti.",
"collections.last_updated_at": "Senest opdateret: {date}",
"collections.manage_accounts": "Administrer konti",
"collections.manage_accounts_in_collection": "Administrer konti i denne samling",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Begrænset til 100 tegn",
"collections.new_collection": "Ny samling",
"collections.no_collections_yet": "Ingen samlinger endnu.",
"collections.remove_account": "Fjern denne konto",
"collections.search_accounts_label": "Søg efter konti for at tilføje…",
"collections.search_accounts_max_reached": "Du har tilføjet det maksimale antal konti",
"collections.topic_hint": "Tilføj et hashtag, der hjælper andre med at forstå det overordnede emne for denne samling.",
"collections.view_collection": "Vis samling",
"collections.visibility_public": "Offentlig",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon ist dezentralisiert, das heißt, unabhängig davon, wo du dein Konto erstellst, kannst du jedem Profil auf diesem Server folgen und mit ihm interagieren. Du kannst sogar deinen eigenen Mastodon-Server hosten!",
"closed_registrations_modal.title": "Bei Mastodon registrieren",
"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.collection_description": "Beschreibung",
"collections.collection_name": "Titel",
"collections.collection_topic": "Thema",
"collections.confirm_account_removal": "Möchtest du dieses Konto wirklich aus der Sammlung entfernen?",
"collections.content_warning": "Inhaltswarnung",
"collections.continue": "Fortfahren",
"collections.create.accounts_subtitle": "Du kannst nur Profile hinzufügen, denen du folgst und die das Hinzufügen gestatten.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Allgemeine Informationen bearbeiten",
"collections.edit_settings": "Einstellungen bearbeiten",
"collections.error_loading_collections": "Beim Laden deiner Sammlungen ist ein Fehler aufgetreten.",
"collections.hints.accounts_counter": "{count} / {max} Konten",
"collections.hints.add_more_accounts": "Füge mindestens {count, plural, one {# Konto} other {# Konten}} hinzu, um fortzufahren",
"collections.hints.can_not_remove_more_accounts": "Sammlungen müssen mindestens {count, plural, one {# Konto} other {# Konten}} enthalten. Weitere Konten zu entfernen, ist daher nicht erlaubt.",
"collections.last_updated_at": "Aktualisiert: {date}",
"collections.manage_accounts": "Profile verwalten",
"collections.manage_accounts_in_collection": "Profile in dieser Sammlung verwalten",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Maximal 100 Zeichen",
"collections.new_collection": "Neue Sammlung",
"collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.",
"collections.remove_account": "Dieses Konto entfernen",
"collections.search_accounts_label": "Konten suchen, um sie hinzuzufügen …",
"collections.search_accounts_max_reached": "Du hast die Höchstzahl an Konten hinzugefügt",
"collections.topic_hint": "Ein Hashtag für diese Sammlung kann anderen dabei helfen, dein Anliegen besser einordnen zu können.",
"collections.view_collection": "Sammlungen anzeigen",
"collections.visibility_public": "Öffentlich",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Το Mastodon είναι αποκεντρωμένο, οπότε ανεξάρτητα από το πού θα δημιουργήσεις τον λογαριασμό σου, μπορείς να ακολουθήσεις και να αλληλεπιδράσεις με οποιονδήποτε σε αυτόν τον διακομιστή. Μπορείς ακόμη και να κάνεις τον δικό σου!",
"closed_registrations_modal.title": "Εγγραφή στο Mastodon",
"collections.account_count": "{count, plural, one {# λογαριασμός} other {# λογαριασμοί}}",
"collections.accounts.empty_description": "Προσθέστε μέχρι και {count} λογαριασμούς που ακολουθείτε",
"collections.accounts.empty_title": "Αυτή η συλλογή είναι κενή",
"collections.collection_description": "Περιγραφή",
"collections.collection_name": "Όνομα",
"collections.collection_topic": "Θέμα",
"collections.confirm_account_removal": "Σίγουρα θέλετε να αφαιρέσετε αυτόν τον λογαριασμό από αυτή τη συλλογή;",
"collections.content_warning": "Προειδοποίηση περιεχομένου",
"collections.continue": "Συνέχεια",
"collections.create.accounts_subtitle": "Μόνο οι λογαριασμοί που ακολουθείτε που έχουν επιλέξει ανακάλυψη μπορούν να προστεθούν.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Επεξεργασία βασικών στοιχείων",
"collections.edit_settings": "Επεξεργασία ρυθμίσεων",
"collections.error_loading_collections": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια φόρτωσης των συλλογών σας.",
"collections.hints.accounts_counter": "{count} / {max} λογαριασμοί",
"collections.hints.add_more_accounts": "Προσθέστε τουλάχιστον {count, plural, one {# λογαριασμό} other {# λογαριασμούς}} για να συνεχίσετε",
"collections.hints.can_not_remove_more_accounts": "Οι συλλογές πρέπει να περιέχουν τουλάχιστον {count, plural, one {# λογαριασμό} other {# λογαριασμούς}}. Δεν είναι δυνατή η αφαίρεση περισσότερων λογαριασμών.",
"collections.last_updated_at": "Τελευταία ενημέρωση: {date}",
"collections.manage_accounts": "Διαχείριση λογαριασμών",
"collections.manage_accounts_in_collection": "Διαχείριση λογαριασμών σε αυτήν τη συλλογή",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Όριο 100 χαρακτήρων",
"collections.new_collection": "Νέα συλλογή",
"collections.no_collections_yet": "Καμία συλλογή ακόμη.",
"collections.remove_account": "Αφαίρεση λογαριασμού",
"collections.search_accounts_label": "Αναζήτηση λογαριασμών για προσθήκη…",
"collections.search_accounts_max_reached": "Έχετε προσθέσει τον μέγιστο αριθμό λογαριασμών",
"collections.topic_hint": "Προσθέστε μια ετικέτα που βοηθά άλλους να κατανοήσουν το κύριο θέμα αυτής της συλλογής.",
"collections.view_collection": "Προβολή συλλογής",
"collections.visibility_public": "Δημόσια",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon is decentralised, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
"closed_registrations_modal.title": "Signing up on Mastodon",
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
"collections.accounts.empty_description": "Add up to {count} accounts you follow",
"collections.accounts.empty_title": "This collection is empty",
"collections.collection_description": "Description",
"collections.collection_name": "Name",
"collections.collection_topic": "Topic",
"collections.confirm_account_removal": "Are you sure you want to remove this account from this collection?",
"collections.content_warning": "Content warning",
"collections.continue": "Continue",
"collections.create.accounts_subtitle": "Only accounts you follow who have opted into discovery can be added.",
@@ -261,6 +264,9 @@
"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.hints.accounts_counter": "{count} / {max} accounts",
"collections.hints.add_more_accounts": "Add at least {count, plural, one {# account} other {# accounts}} to continue",
"collections.hints.can_not_remove_more_accounts": "Collections must contain at least {count, plural, one {# account} other {# accounts}}. Removing more accounts is not possible.",
"collections.last_updated_at": "Last updated: {date}",
"collections.manage_accounts": "Manage accounts",
"collections.manage_accounts_in_collection": "Manage accounts in this collection",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "100 characters limit",
"collections.new_collection": "New collection",
"collections.no_collections_yet": "No collections yet.",
"collections.remove_account": "Remove this account",
"collections.search_accounts_label": "Search for accounts to add…",
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
"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",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
"closed_registrations_modal.title": "Signing up on Mastodon",
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
"collections.accounts.empty_description": "Add up to {count} accounts you follow",
"collections.accounts.empty_title": "This collection is empty",
"collections.collection_description": "Description",
"collections.collection_name": "Name",
"collections.collection_topic": "Topic",
"collections.confirm_account_removal": "Are you sure you want to remove this account from this collection?",
"collections.content_warning": "Content warning",
"collections.continue": "Continue",
"collections.create.accounts_subtitle": "Only accounts you follow who have opted into discovery can be added.",
@@ -261,6 +264,9 @@
"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.hints.accounts_counter": "{count} / {max} accounts",
"collections.hints.add_more_accounts": "Add at least {count, plural, one {# account} other {# accounts}} to continue",
"collections.hints.can_not_remove_more_accounts": "Collections must contain at least {count, plural, one {# account} other {# accounts}}. Removing more accounts is not possible.",
"collections.last_updated_at": "Last updated: {date}",
"collections.manage_accounts": "Manage accounts",
"collections.manage_accounts_in_collection": "Manage accounts in this collection",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "100 characters limit",
"collections.new_collection": "New collection",
"collections.no_collections_yet": "No collections yet.",
"collections.remove_account": "Remove this account",
"collections.search_accounts_label": "Search for accounts to add…",
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
"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",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon es descentralizado, por lo que no importa dónde creés tu cuenta, podrás seguir e interactuar con cualquier persona en este servidor. ¡Incluso podés montar tu propio servidor!",
"closed_registrations_modal.title": "Registrarse en Mastodon",
"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.collection_description": "Descripción",
"collections.collection_name": "Nombre",
"collections.collection_topic": "Tema",
"collections.confirm_account_removal": "¿Estás seguro de que querés eliminar esta cuenta de esta colección?",
"collections.content_warning": "Advertencia de contenido",
"collections.continue": "Continuar",
"collections.create.accounts_subtitle": "Solo las cuentas que seguís —las cuales optaron por ser descubiertas— pueden ser agregadas.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Editar detalles básicos",
"collections.edit_settings": "Editar configuración",
"collections.error_loading_collections": "Hubo un error al intentar cargar tus colecciones.",
"collections.hints.accounts_counter": "{count} / {max} cuentas",
"collections.hints.add_more_accounts": "Agregá, al menos, {count, plural, one {# cuenta} other {# cuentas}} para continuar",
"collections.hints.can_not_remove_more_accounts": "Las colecciones deben contener, al menos, {count, plural, one {# cuenta} other {# cuentas}}. No es posible eliminar más cuentas.",
"collections.last_updated_at": "Última actualización: {date}",
"collections.manage_accounts": "Administrar cuentas",
"collections.manage_accounts_in_collection": "Administrar cuentas en esta colección",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Límite de 100 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "No hay colecciones aún.",
"collections.remove_account": "Eliminar esta cuenta",
"collections.search_accounts_label": "Buscar cuentas para agregar…",
"collections.search_accounts_max_reached": "Agregaste el número máximo de cuentas",
"collections.topic_hint": "Agregá una etiqueta que ayude a otros usuarios a entender el tema principal de esta colección.",
"collections.view_collection": "Abrir colección",
"collections.visibility_public": "Pública",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon es descentralizado, por lo que no importa dónde crees tu cuenta, podrás seguir e interactuar con cualquier persona en este servidor. ¡Incluso puedes alojarlo tú mismo!",
"closed_registrations_modal.title": "Registrarse en Mastodon",
"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.collection_description": "Descripción",
"collections.collection_name": "Nombre",
"collections.collection_topic": "Tema",
"collections.confirm_account_removal": "¿Estás seguro/a de que quieres eliminar esta cuenta de esta colección?",
"collections.content_warning": "Advertencia de contenido",
"collections.continue": "Continuar",
"collections.create.accounts_subtitle": "Solo se pueden agregar cuentas que sigas y que hayan optado por aparecer en los resultados de búsqueda.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Editar detalles básicos",
"collections.edit_settings": "Editar configuración",
"collections.error_loading_collections": "Se produjo un error al intentar cargar tus colecciones.",
"collections.hints.accounts_counter": "{count} / {max} cuentas",
"collections.hints.add_more_accounts": "Añade al menos {count, plural,one {# cuenta} other {# cuentas}} para continuar",
"collections.hints.can_not_remove_more_accounts": "Las colecciones deben contener al menos {count, plural,one {# cuenta} other {# cuentas}}. No es posible eliminar más cuentas.",
"collections.last_updated_at": "Última actualización: {date}",
"collections.manage_accounts": "Administrar cuentas",
"collections.manage_accounts_in_collection": "Administrar cuentas en esta colección",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Limitado a 100 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "No hay colecciones todavía.",
"collections.remove_account": "Eliminar esta cuenta",
"collections.search_accounts_label": "Buscar cuentas para añadir…",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
"collections.topic_hint": "Agrega una etiqueta que ayude a los demás a comprender el tema principal de esta colección.",
"collections.view_collection": "Ver colección",
"collections.visibility_public": "Pública",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon es descentralizado, por lo que no importa dónde crees tu cuenta, podrás seguir e interactuar con cualquier persona en este servidor. ¡Incluso puedes alojarlo tú mismo!",
"closed_registrations_modal.title": "Registrarse en Mastodon",
"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.collection_description": "Descripción",
"collections.collection_name": "Nombre",
"collections.collection_topic": "Tema",
"collections.confirm_account_removal": "¿Estás seguro de que quieres eliminar esta cuenta de la colección?",
"collections.content_warning": "Advertencia de contenido",
"collections.continue": "Continuar",
"collections.create.accounts_subtitle": "Solo pueden añadirse cuentas que sigues y que han activado el descubrimiento.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Editar datos básicos",
"collections.edit_settings": "Cambiar ajustes",
"collections.error_loading_collections": "Se ha producido un error al intentar cargar tus colecciones.",
"collections.hints.accounts_counter": "{count} / {max} cuentas",
"collections.hints.add_more_accounts": "¡Añade al menos {count, plural, one {# cuenta} other {# cuentas}} para continuar",
"collections.hints.can_not_remove_more_accounts": "Las colecciones deben contener al menos {count, plural, one {# cuenta} other {# cuentas}}. No es posible eliminar más cuentas.",
"collections.last_updated_at": "Última actualización: {date}",
"collections.manage_accounts": "Administrar cuentas",
"collections.manage_accounts_in_collection": "Administrar cuentas en esta colección",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Limitado a 100 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "Aún no hay colecciones.",
"collections.remove_account": "Borrar esta cuenta",
"collections.search_accounts_label": "Buscar cuentas para añadir…",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
"collections.topic_hint": "Añadir una etiqueta que ayude a otros a entender el tema principal de esta colección.",
"collections.view_collection": "Ver colección",
"collections.visibility_public": "Pública",
@@ -454,7 +463,7 @@
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares",
"empty_column.account_about.me": "Aún no has añadido ninguna información sobre ti.",
"empty_column.account_about.other": "{acct} aún no ha añadido ninguna información sobre sí mismo/a.",
"empty_column.account_about.other": "{acct} aún no ha añadido ninguna información sobre sí mismo/a/e.",
"empty_column.account_featured.me": "Aún no has destacado nada. ¿Sabías que puedes destacar las etiquetas que más usas e incluso las cuentas de tus amigos en tu perfil?",
"empty_column.account_featured.other": "{acct} aún no ha destacado nada. ¿Sabías que puedes destacar las etiquetas que más usas e incluso las cuentas de tus amigos en tu perfil?",
"empty_column.account_featured_other.unknown": "Esta cuenta aún no ha destacado nada.",

View File

@@ -13,6 +13,7 @@
"about.not_available": "See info ei ole selles serveris saadavaks tehtud.",
"about.powered_by": "Hajutatud sotsiaalmeedia, mille taga on {mastodon}",
"about.rules": "Serveri reeglid",
"account.about": "Teave",
"account.account_note_header": "Isiklik märge",
"account.activity": "Tegevus",
"account.add_note": "Lisa isiklik märge",
@@ -29,9 +30,9 @@
"account.block_short": "Blokeeri",
"account.blocked": "Blokeeritud",
"account.blocking": "Blokeeritud kasutaja",
"account.cancel_follow_request": "Võta jälgimistaotlus tagasi",
"account.cancel_follow_request": "Võta jälgimissoov tagasi",
"account.copy": "Kopeeri profiili link",
"account.direct": "Maini privaatselt @{name}",
"account.direct": "Maini privaatselt kasutajat @{name}",
"account.disable_notifications": "Ära teavita, kui @{name} postitab",
"account.domain_blocking": "Blokeeritud domeen",
"account.edit_note": "Muuda isiklikku märget",
@@ -66,9 +67,9 @@
"account.followers_you_know_counter": "{counter} kasutaja(t), keda sa tead",
"account.following": "Jälgib",
"account.following_counter": "{count, plural, one {{counter} jälgib} other {{counter} jälgib}}",
"account.follows.empty": "See kasutaja ei jälgi veel kedagi.",
"account.follows.empty": "See kasutaja ei jälgi veel mitte kedagi.",
"account.follows_you": "Jälgib sind",
"account.go_to_profile": "Mine profiilile",
"account.go_to_profile": "Vaata profiili",
"account.hide_reblogs": "Peida @{name} jagamised",
"account.in_memoriam": "In Memoriam.",
"account.joined_long": "Liitus {date}",
@@ -103,6 +104,13 @@
"account.muted": "Summutatud",
"account.muting": "Summutatud konto",
"account.mutual": "Te jälgite teineteist",
"account.name.help.domain": "{domain} on server, kus antud konto koos profiiliga asub ning kust postitused saavad alguse.",
"account.name.help.domain_self": "{domain} on server, kus sinu konto koos profiiliga asub ning kust sinu postitused saavad alguse.",
"account.name.help.footer": "Nii nagu võid saata teiste e-posti serverite kasutajatele e-kirju, saad ka suhelda teiste Mastodoni serverite kasutajatega - ja tegelikult ka muude sotsiaalvõrkude kasutajatega, kus on kasutusel sama lahendus, nagu Mastodon pruugib (ActivityPubi protokoll).",
"account.name.help.header": "Kasutajatunnus on nagu e-posti aadress",
"account.name.help.username": "{username} on selle konto kasutajanimi tema serveris. Kellelgi teisel mõnes muus serveris võib olla sama kasutajanimi.",
"account.name.help.username_self": "{username} on sinu kasutajanimi selles serveris. Kellelgi teisel mõnes muus serveris võib olla sama kasutajanimi.",
"account.name_info": "Mida see tähendab?",
"account.no_bio": "Kirjeldust pole lisatud.",
"account.node_modal.callout": "Isiklikud märked on nähtavad vaid sulle.",
"account.node_modal.edit_title": "Muuda isiklikku märget",
@@ -116,7 +124,7 @@
"account.posts": "Postitused",
"account.posts_with_replies": "Postitused ja vastused",
"account.remove_from_followers": "Eemalda {name} jälgijate seast",
"account.report": "Raporteeri @{name}",
"account.report": "Teata kasutajast {name}",
"account.requested_follow": "{name} on soovinud sinu jälgimist",
"account.requests_to_follow_you": "soovib sind jälgida",
"account.share": "Jaga @{name} profiili",
@@ -163,7 +171,7 @@
"annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis",
"annual_report.nav_item.badge": "Uus",
"annual_report.shared_page.donate": "Toeta rahaliselt",
"annual_report.shared_page.footer": "Loodud {heart} Mastodoni meeskonna poolt",
"annual_report.shared_page.footer": "Loodud suure {heart}-ga Mastodoni meeskonna poolt",
"annual_report.shared_page.footer_server_info": "{username} kasutab {domain}-i, üht paljudest kogukondadest, mis toimivad Mastodonil.",
"annual_report.summary.archetype.booster.desc_public": "{name} jätkas postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.",
"annual_report.summary.archetype.booster.desc_self": "Sa jätkasid postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.",
@@ -185,25 +193,25 @@
"annual_report.summary.archetype.reveal_description": "Täname, et oled Mastodoni liige! Aeg on välja selgitada, millist põhitüüpi sa {year}. kehastasid.",
"annual_report.summary.archetype.title_public": "Kasutaja {name} põhitüüp",
"annual_report.summary.archetype.title_self": "Sinu põhitüüp",
"annual_report.summary.close": "Sule",
"annual_report.summary.close": "Sulge",
"annual_report.summary.copy_link": "Kopeeri link",
"annual_report.summary.followers.new_followers": "{count, plural, one {uus jälgija} other {uut jälgijat}}",
"annual_report.summary.highlighted_post.boost_count": "Seda postitust on jagatud {count, plural, one {ühe korra} other {# korda}}.",
"annual_report.summary.highlighted_post.favourite_count": "Seda postitust on lemmikuks märgitud {count, plural, one {ühe korra} other {# korda}}.",
"annual_report.summary.highlighted_post.reply_count": "See postitus on saanud {count, plural, one {ühe vastuse} other {# vastust}}.",
"annual_report.summary.highlighted_post.title": "Kõige populaarsemad postitused",
"annual_report.summary.most_used_app.most_used_app": "enim kasutatud äpp",
"annual_report.summary.most_used_app.most_used_app": "enimkasutatud rakendus",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "enim kasutatud teemaviide",
"annual_report.summary.most_used_hashtag.used_count": "Sa lisasid selle teemaviite {count, plural, one {ühele postitusele} other {#-le postitusele}}.",
"annual_report.summary.most_used_hashtag.used_count_public": "{name} kasutas seda silti {count, plural, one {ühes postituses} other {# postituses}}.",
"annual_report.summary.new_posts.new_posts": "uus postitus",
"annual_report.summary.most_used_hashtag.used_count_public": "{name} kasutas seda teemaviidet {count, plural, one {ühes postituses} other {#-s postituses}}.",
"annual_report.summary.new_posts.new_posts": "uued postitused",
"annual_report.summary.percentile.text": "<topLabel>See paneb su top</topLabel><percentage></percentage><bottomLabel> {domain} kasutajate hulka.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Vägev.",
"annual_report.summary.share_elsewhere": "Jaga mujal",
"annual_report.summary.share_message": "Minu arhetüüp on {archetype}!",
"annual_report.summary.share_on_mastodon": "Jaga Mastodonis",
"attachments_list.unprocessed": "(töötlemata)",
"audio.hide": "Peida audio",
"audio.hide": "Peida heliriba",
"block_modal.remote_users_caveat": "Serverile {domain} edastatakse palve otsust järgida. Ometi pole see tagatud, kuna mõned serverid võivad blokeeringuid käsitleda omal moel. Avalikud postitused võivad tuvastamata kasutajatele endiselt näha olla.",
"block_modal.show_less": "Kuva vähem",
"block_modal.show_more": "Kuva rohkem",
@@ -232,9 +240,10 @@
"carousel.slide": "Slaid {current, number} / {max, number}",
"closed_registrations.other_server_instructions": "Kuna Mastodon on detsentraliseeritud, võib konto teha teise serverisse ja sellegipoolest siinse kontoga suhelda.",
"closed_registrations_modal.description": "Praegu ei ole võimalik teha {domain} peale kontot, aga pea meeles, et sul ei pea olema just {domain} konto, et Mastodoni kasutada.",
"closed_registrations_modal.find_another_server": "Leia teine server",
"closed_registrations_modal.preamble": "Mastodon on detsentraliseeritud, mis tähendab, et konto võib luua ükskõik kuhu, kuid ikkagi saab jälgida ja suhelda igaühega sellel serveril. Võib isegi oma serveri püsti panna!",
"closed_registrations_modal.find_another_server": "Leia mõni muu server",
"closed_registrations_modal.preamble": "Mastodon on hajutatud võrk, mis tähendab, et konto võid luua ükskõik kuhu, kuid ikkagi saad jälgida ja suhelda igaühega selles serveris. Võid isegi oma serveri püsti panna!",
"closed_registrations_modal.title": "Mastodoni registreerumine",
"collections.account_count": "{count, plural, one {# kasutajakonto} other {# kasutajakontot}}",
"collections.collection_description": "Kirjeldus",
"collections.collection_name": "Nimi",
"collections.collection_topic": "Teema",
@@ -252,6 +261,7 @@
"collections.edit_details": "Muuda põhiandmeid",
"collections.edit_settings": "Muuda seadistusi",
"collections.error_loading_collections": "Sinu kogumike laadimisel tekkis viga.",
"collections.last_updated_at": "Viimati uuendatud: {date}",
"collections.manage_accounts": "Halda kasutajakontosid",
"collections.manage_accounts_in_collection": "Halda selle kogumiku kontosid",
"collections.mark_as_sensitive": "Märgi delikaatseks",
@@ -274,7 +284,7 @@
"column.create_list": "Loo loend",
"column.direct": "Privaatsed mainimised",
"column.directory": "Sirvi profiile",
"column.domain_blocks": "Peidetud domeenid",
"column.domain_blocks": "Blokeeritud domeenid",
"column.edit_list": "Muuda loendit",
"column.favourites": "Lemmikud",
"column.firehose": "Postitused reaalajas",
@@ -443,6 +453,8 @@
"emoji_button.search_results": "Otsitulemused",
"emoji_button.symbols": "Sümbolid",
"emoji_button.travel": "Reisimine & kohad",
"empty_column.account_about.me": "Sa pole enda kohta veel mitte mingit teavet lisanud.",
"empty_column.account_about.other": "{acct} pole enda kohta veel mitte mingit teavet lisanud.",
"empty_column.account_featured.me": "Sa pole veel midagi esile tõstnud. Kas sa teadsid, et oma profiilis saad esile tõsta enamkasutatavaid teemaviiteid või sõbra kasutajakontot?",
"empty_column.account_featured.other": "{acct} pole veel midagi esile tõstnud. Kas sa teadsid, et oma profiilis saad esile tõsta enamkasutatavaid teemaviiteid või sõbra kasutajakontot?",
"empty_column.account_featured_other.unknown": "See kasutajakonto pole veel midagi esile tõstnud.",
@@ -526,6 +538,8 @@
"follow_suggestions.view_all": "Vaata kõiki",
"follow_suggestions.who_to_follow": "Keda jälgida",
"followed_tags": "Jälgitavad teemaviited",
"followers.hide_other_followers": "See kasutaja eelistab mitte avaldada oma teisi jälgijaid",
"following.hide_other_following": "See kasutaja eelistab mitte avaldada oma teisi jälgitavaid",
"footer.about": "Teave",
"footer.about_mastodon": "Mastodoni kohta",
"footer.about_server": "{domain} kohta",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon on hajautettu, joten riippumatta siitä, missä luot tilisi, voit seurata ja olla vuorovaikutuksessa kenen tahansa kanssa tällä palvelimella. Voit jopa isännöidä palvelinta!",
"closed_registrations_modal.title": "Rekisteröityminen Mastodoniin",
"collections.account_count": "{count, plural, one {# tili} other {# tiliä}}",
"collections.accounts.empty_description": "Lisää enintään {count} seuraamaasi tiliä",
"collections.accounts.empty_title": "Tämä kokoelma on tyhjä",
"collections.collection_description": "Kuvaus",
"collections.collection_name": "Nimi",
"collections.collection_topic": "Aihe",
"collections.confirm_account_removal": "Haluatko varmasti poistaa tämän tilin tästä kokoelmasta?",
"collections.content_warning": "Sisältövaroitus",
"collections.continue": "Jatka",
"collections.create.accounts_subtitle": "Lisätä voi vain tilejä, joita seuraat ja jotka ovat valinneet tulla löydetyiksi.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Muokkaa perustietoja",
"collections.edit_settings": "Muokkaa asetuksia",
"collections.error_loading_collections": "Kokoelmien latauksessa tapahtui virhe.",
"collections.hints.accounts_counter": "{count} / {max} tiliä",
"collections.hints.add_more_accounts": "Jatka lisäämällä vähintään {count, plural, one {# tili} other {# tiliä}}",
"collections.hints.can_not_remove_more_accounts": "Kokoelmien on sisällettävä vähintään {count, plural, one {# tili} other {# tiliä}}. Enempää tilejä ei ole mahdollista poistaa.",
"collections.last_updated_at": "Päivitetty viimeksi {date}",
"collections.manage_accounts": "Hallitse tilejä",
"collections.manage_accounts_in_collection": "Hallitse tässä kokoelmassa olevia tilejä",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "100 merkin rajoitus",
"collections.new_collection": "Uusi kokoelma",
"collections.no_collections_yet": "Ei vielä kokoelmia.",
"collections.remove_account": "Poista tämä tili",
"collections.search_accounts_label": "Hae lisättäviä tilejä…",
"collections.search_accounts_max_reached": "Olet lisännyt enimmäismäärän tilejä",
"collections.topic_hint": "Lisää aihetunniste, joka auttaa muita ymmärtämään tämän kokoelman pääaiheen.",
"collections.view_collection": "Näytä kokoelma",
"collections.visibility_public": "Julkinen",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon er desentraliserað, so óansæð hvar tú stovnar tína kontu, so ber til hjá tær at fylgja og virka saman við einum og hvørjum á hesum ambætaranum. Tað ber enntá til at hýsa tí sjálvi!",
"closed_registrations_modal.title": "At stovna kontu á Mastodon",
"collections.account_count": "{count, plural, one {# konta} other {# kontur}}",
"collections.accounts.empty_description": "Legg afturat upp til {count} kontur, sum tú fylgir",
"collections.accounts.empty_title": "Hetta savnið er tómt",
"collections.collection_description": "Lýsing",
"collections.collection_name": "Navn",
"collections.collection_topic": "Evni",
"collections.confirm_account_removal": "Er tú vís/ur í, at tú vilt strika hesa kontuna frá hesum savninum?",
"collections.content_warning": "Innihaldsávaring",
"collections.continue": "Halt fram",
"collections.create.accounts_subtitle": "Einans kontur, sum tú fylgir og sum hava játtað at blíva uppdagaðar, kunnu leggjast afturat.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Rætta grundleggjandi smálutir",
"collections.edit_settings": "Rætta stillingar",
"collections.error_loading_collections": "Ein feilur hendi, tá tú royndi at finna fram søvnini hjá tær.",
"collections.hints.accounts_counter": "{count} / {max} kontur",
"collections.hints.add_more_accounts": "Legg minst {count, plural, one {# kontu} other {# kontur}} afturat fyri at halda fram",
"collections.hints.can_not_remove_more_accounts": "Søvn mugu innihalda minst {count, plural, one {# kontu} other {# kontur}}. Ikki møguligt at strika fleiri kontur.",
"collections.last_updated_at": "Seinast dagført: {date}",
"collections.manage_accounts": "Umsit kontur",
"collections.manage_accounts_in_collection": "Umsit kontur í hesum savninum",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Í mesta lagi 100 tekn",
"collections.new_collection": "Nýtt savn",
"collections.no_collections_yet": "Eingi søvn enn.",
"collections.remove_account": "Strika hesa kontuna",
"collections.search_accounts_label": "Leita eftir kontum at leggja afturat…",
"collections.search_accounts_max_reached": "Tú hevur lagt afturat mesta talið av kontum",
"collections.topic_hint": "Legg afturat eitt frámerki, sum hjálpir øðrum at skilja høvuðevnið í hesum savninum.",
"collections.view_collection": "Vís savn",
"collections.visibility_public": "Alment",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon est décentralisé, donc peu importe où vous créez votre compte, vous serez en mesure de suivre et d'interagir avec quiconque sur ce serveur. Vous pouvez même l'héberger vous-même!",
"closed_registrations_modal.title": "S'inscrire sur Mastodon",
"collections.account_count": "{count, plural, one {# compte} other {# comptes}}",
"collections.accounts.empty_description": "Ajouter jusqu'à {count} comptes que vous suivez",
"collections.accounts.empty_title": "Cette collection est vide",
"collections.collection_description": "Description",
"collections.collection_name": "Nom",
"collections.collection_topic": "Sujet",
"collections.confirm_account_removal": "Voulez-vous vraiment supprimer ce compte de la collection?",
"collections.content_warning": "Avertissement au public",
"collections.continue": "Continuer",
"collections.create.accounts_subtitle": "Seuls les comptes que vous suivez et qui ont autorisé leur découverte peuvent être ajoutés.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Modifier les informations générales",
"collections.edit_settings": "Modifier les paramètres",
"collections.error_loading_collections": "Une erreur s'est produite durant le chargement de vos collections.",
"collections.hints.accounts_counter": "{count} / {max} comptes",
"collections.hints.add_more_accounts": "Ajouter au moins {count, plural, one {# compte} other {# comptes}} pour continuer",
"collections.hints.can_not_remove_more_accounts": "Les collections doivent contenir au moins {count, plural, one {# compte} other {# comptes}}. Il n'est pas possible de supprimer plus de comptes.",
"collections.last_updated_at": "Dernière mise à jour : {date}",
"collections.manage_accounts": "Gérer les comptes",
"collections.manage_accounts_in_collection": "Gérer les comptes de cette collection",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Maximum 100 caractères",
"collections.new_collection": "Nouvelle collection",
"collections.no_collections_yet": "Aucune collection pour le moment.",
"collections.remove_account": "Supprimer ce compte",
"collections.search_accounts_label": "Chercher des comptes à ajouter…",
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
"collections.topic_hint": "Ajouter un hashtag pour aider les autres personnes à comprendre le sujet de la collection.",
"collections.view_collection": "Voir la collection",
"collections.visibility_public": "Publique",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon est décentralisé : peu importe où vous créez votre compte, vous serez en mesure de suivre et d'interagir avec quiconque sur ce serveur. Vous pouvez même l'héberger !",
"closed_registrations_modal.title": "Inscription sur Mastodon",
"collections.account_count": "{count, plural, one {# compte} other {# comptes}}",
"collections.accounts.empty_description": "Ajouter jusqu'à {count} comptes que vous suivez",
"collections.accounts.empty_title": "Cette collection est vide",
"collections.collection_description": "Description",
"collections.collection_name": "Nom",
"collections.collection_topic": "Sujet",
"collections.confirm_account_removal": "Voulez-vous vraiment supprimer ce compte de la collection?",
"collections.content_warning": "Avertissement au public",
"collections.continue": "Continuer",
"collections.create.accounts_subtitle": "Seuls les comptes que vous suivez et qui ont autorisé leur découverte peuvent être ajoutés.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Modifier les informations générales",
"collections.edit_settings": "Modifier les paramètres",
"collections.error_loading_collections": "Une erreur s'est produite durant le chargement de vos collections.",
"collections.hints.accounts_counter": "{count} / {max} comptes",
"collections.hints.add_more_accounts": "Ajouter au moins {count, plural, one {# compte} other {# comptes}} pour continuer",
"collections.hints.can_not_remove_more_accounts": "Les collections doivent contenir au moins {count, plural, one {# compte} other {# comptes}}. Il n'est pas possible de supprimer plus de comptes.",
"collections.last_updated_at": "Dernière mise à jour : {date}",
"collections.manage_accounts": "Gérer les comptes",
"collections.manage_accounts_in_collection": "Gérer les comptes de cette collection",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Maximum 100 caractères",
"collections.new_collection": "Nouvelle collection",
"collections.no_collections_yet": "Aucune collection pour le moment.",
"collections.remove_account": "Supprimer ce compte",
"collections.search_accounts_label": "Chercher des comptes à ajouter…",
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
"collections.topic_hint": "Ajouter un hashtag pour aider les autres personnes à comprendre le sujet de la collection.",
"collections.view_collection": "Voir la collection",
"collections.visibility_public": "Publique",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Ós rud é go bhfuil Mastodon díláraithe, is cuma cá háit a chruthaíonn tú do chuntas, beidh tú in ann idirghníomhú le haon duine ar an bhfreastalaí seo agus iad a leanúint. Is féidir fiú é a féin-óstáil!",
"closed_registrations_modal.title": "Cláraigh le Mastodon",
"collections.account_count": "{count, plural, one {# cuntas} two {# cuntais} few {# cuntais} many {# cuntais} other {# cuntais}}",
"collections.accounts.empty_description": "Cuir suas le {count} cuntas leis a leanann tú",
"collections.accounts.empty_title": "Tá an bailiúchán seo folamh",
"collections.collection_description": "Cur síos",
"collections.collection_name": "Ainm",
"collections.collection_topic": "Topaic",
"collections.confirm_account_removal": "An bhfuil tú cinnte gur mian leat an cuntas seo a bhaint den bhailiúchán seo?",
"collections.content_warning": "Rabhadh ábhair",
"collections.continue": "Lean ar aghaidh",
"collections.create.accounts_subtitle": "Ní féidir ach cuntais a leanann tú atá roghnaithe le fionnachtain a chur leis.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Cuir sonraí bunúsacha in eagar",
"collections.edit_settings": "Socruithe a chur in eagar",
"collections.error_loading_collections": "Tharla earráid agus iarracht á déanamh do bhailiúcháin a luchtú.",
"collections.hints.accounts_counter": "{count} / {max} cuntais",
"collections.hints.add_more_accounts": "Cuir ar a laghad {count, plural, one {# cuntas} two {# cuntais} few {# cuntais} many {# cuntais} other {# cuntais}} leis chun leanúint ar aghaidh",
"collections.hints.can_not_remove_more_accounts": "Ní mór go mbeadh ar a laghad {count, plural, one {# cuntas} two {# cuntais} few {# cuntais} many {# cuntais} other {# cuntais}} i mbailiúcháin. Ní féidir tuilleadh cuntas a bhaint.",
"collections.last_updated_at": "Nuashonraithe go deireanach: {date}",
"collections.manage_accounts": "Bainistigh cuntais",
"collections.manage_accounts_in_collection": "Bainistigh cuntais sa bhailiúchán seo",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Teorainn 100 carachtar",
"collections.new_collection": "Bailiúchán nua",
"collections.no_collections_yet": "Gan aon bhailiúcháin fós.",
"collections.remove_account": "Bain an cuntas seo",
"collections.search_accounts_label": "Cuardaigh cuntais le cur leis…",
"collections.search_accounts_max_reached": "Tá an líon uasta cuntas curtha leis agat",
"collections.topic_hint": "Cuir haischlib leis a chabhraíonn le daoine eile príomhábhar an bhailiúcháin seo a thuiscint.",
"collections.view_collection": "Féach ar bhailiúchán",
"collections.visibility_public": "Poiblí",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "A Mastodon decentralizált, így teljesen mindegy, hol hozod létre a fiókodat, követhetsz és kapcsolódhatsz bárkivel ezen a kiszolgálón is. Saját magad is üzemeltethetsz kiszolgálót!",
"closed_registrations_modal.title": "Regisztráció a Mastodonra",
"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.collection_description": "Leírás",
"collections.collection_name": "Név",
"collections.collection_topic": "Téma",
"collections.confirm_account_removal": "Biztos, hogy eltávolítod ezt a fiókot ebből a gyűjteményből?",
"collections.content_warning": "Tartalmi figyelmeztetés",
"collections.continue": "Folytatás",
"collections.create.accounts_subtitle": "Csak azok a követett fiókok adhatóak hozzá, melyek engedélyezték a felfedezést.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Alapvető részletek szerkesztése",
"collections.edit_settings": "Beállítások szerkesztése",
"collections.error_loading_collections": "Hiba történt a gyűjtemények betöltése során.",
"collections.hints.accounts_counter": "{count} / {max} fiók",
"collections.hints.add_more_accounts": "Adj hozzá legalább {count, plural, one {# fiókot} other {# fiókot}} a folytatáshoz",
"collections.hints.can_not_remove_more_accounts": "A gyűjteményeknek legalább {count, plural, one {# fiókot} other {# fiókot}} kell tartalmazniuk. Több fiók eltávolítása nem lehetséges.",
"collections.last_updated_at": "Utoljára frissítve: {date}",
"collections.manage_accounts": "Fiókok kezelése",
"collections.manage_accounts_in_collection": "Gyűjteményben szereplő fiókok kezelése",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "100 karakteres korlát",
"collections.new_collection": "Új gyűjtemény",
"collections.no_collections_yet": "Még nincsenek gyűjtemények.",
"collections.remove_account": "Fiók eltávolítása",
"collections.search_accounts_label": "Hozzáadandó fiókok keresése…",
"collections.search_accounts_max_reached": "Elérte a hozzáadott fiókok maximális számát",
"collections.topic_hint": "Egy hashtag hozzáadása segít másoknak abban, hogy megértsék a gyűjtemény fő témáját.",
"collections.view_collection": "Gyűjtemény megtekintése",
"collections.visibility_public": "Nyilvános",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon er ekki miðstýrt, svo það skiptir ekki máli hvar þú býrð til aðgang; þú munt get fylgt eftir og haft samskipti við hvern sem er á þessum þjóni. Þú getur jafnvel hýst þinn eigin Mastodon þjón!",
"closed_registrations_modal.title": "Að nýskrá sig á Mastodon",
"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.collection_description": "Lýsing",
"collections.collection_name": "Nafn",
"collections.collection_topic": "Umfjöllunarefni",
"collections.confirm_account_removal": "Ertu viss um að þú viljir fjarlægja þennan aðgang úr þessu safni?",
"collections.content_warning": "Viðvörun vegna efnis",
"collections.continue": "Halda áfram",
"collections.create.accounts_subtitle": "Einungis er hægt að bæta við notendum sem hafa samþykkt að vera með í opinberri birtingu.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Breyta grunnupplýsingum",
"collections.edit_settings": "Breyta stillingum",
"collections.error_loading_collections": "Villa kom upp þegar reynt var að hlaða inn söfnunum þínum.",
"collections.hints.accounts_counter": "{count} / {max} aðgangar",
"collections.hints.add_more_accounts": "Bættu við að minnsta kosti {count, plural, one {# aðgangi} other {# aðgöngum}} til að halda áfram",
"collections.hints.can_not_remove_more_accounts": "Söfn verða að innihalda að minnsta kosti {count, plural, one {# aðgang} other {# aðganga}}. Ekki er hægt að fjarlægja fleiri aðganga.",
"collections.last_updated_at": "Síðast uppfært: {date}",
"collections.manage_accounts": "Sýsla með notandaaðganga",
"collections.manage_accounts_in_collection": "Sýsla með notendaaðganga í þessu safni",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "100 stafa takmörk",
"collections.new_collection": "Nýtt safn",
"collections.no_collections_yet": "Engin söfn ennþá.",
"collections.remove_account": "Fjarlægja þennan aðgang",
"collections.search_accounts_label": "Leita að aðgöngum til að bæta við…",
"collections.search_accounts_max_reached": "Þú hefur þegar bætt við leyfilegum hámarksfjölda aðganga",
"collections.topic_hint": "Bættu við myllumerki sem hjálpar öðrum að skilja aðalefni þessa safns.",
"collections.view_collection": "Skoða safn",
"collections.visibility_public": "Opinbert",

View File

@@ -242,9 +242,11 @@
"closed_registrations_modal.preamble": "Mastodon-i është i decentralizuar, ndaj pavarësisht se ku krijoni llogarinë tuaj, do të jeni në gjendje të ndiqni dhe ndërveproni me këdo në këtë shërbyes. Mundeni madje edhe ta strehoni ju vetë!",
"closed_registrations_modal.title": "Po regjistroheni në Mastodon",
"collections.account_count": "{count, plural, one {# llogari} other {# llogari}}",
"collections.accounts.empty_title": "Ky koleksion është i zbrazët",
"collections.collection_description": "Përshkrim",
"collections.collection_name": "Emër",
"collections.collection_topic": "Temë",
"collections.confirm_account_removal": "Jeni i sigurt se doni të hiqet kjo llogari nga ky koleksion?",
"collections.content_warning": "Sinjalizim lënde",
"collections.continue": "Vazhdo",
"collections.create.accounts_subtitle": "Mund të shtohen vetëm llogari që ju ndiqni të cilat kanë zgjedhur të jenë të zbulueshme.",
@@ -259,6 +261,9 @@
"collections.edit_details": "Përpunoni hollësi bazë",
"collections.edit_settings": "Përpunoni rregullime",
"collections.error_loading_collections": "Pati një gabim teksa provohej të ngarkoheshin koleksionet tuaj.",
"collections.hints.accounts_counter": "{count} / {max} llogari",
"collections.hints.add_more_accounts": "Që të vazhdohet, shtoni të paktën {count, plural, one {# llogari} other {# llogari}}",
"collections.hints.can_not_remove_more_accounts": "Koleksionet duhet të përmbajnë të paktën {count, plural, one {# llogari} other {# llogari}}. Sështë e mundshme heqja e më tepër llogarive.",
"collections.last_updated_at": "Përditësuar së fundi më: {date}",
"collections.manage_accounts": "Administroni llogari",
"collections.manage_accounts_in_collection": "Administroni llogari në këtë koleksion",
@@ -267,6 +272,9 @@
"collections.name_length_hint": "Kufi prej 100 shenjash",
"collections.new_collection": "Koleksion i ri",
"collections.no_collections_yet": "Ende pa koleksione.",
"collections.remove_account": "Hiqe këtë llogari",
"collections.search_accounts_label": "Kërkoni për llogari për shtim…",
"collections.search_accounts_max_reached": "Keni shtuar numrin maksimum të llogarive",
"collections.topic_hint": "Shtoni një hashtag që ndihmon të tjerët të kuptojnë temën kryesore të këtij koleksion.",
"collections.view_collection": "Shiheni koleksionin",
"collections.visibility_public": "Publik",

View File

@@ -194,11 +194,16 @@
"closed_registrations_modal.find_another_server": "Hitta en annan server",
"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.create_a_collection_hint": "Skapa en samling för att rekommendera eller dela dina favoritkonton med andra.",
"collections.create_collection": "Skapa samling",
"collections.delete_collection": "Radera samling",
"collections.error_loading_collections": "Det uppstod ett fel när dina samlingar skulle laddas.",
"collections.hints.accounts_counter": "{count} / {max} konton",
"collections.no_collections_yet": "Inga samlingar än.",
"collections.remove_account": "Ta bort detta konto",
"collections.search_accounts_label": "Sök efter konton för att lägga till…",
"collections.search_accounts_max_reached": "Du har lagt till maximalt antal konton",
"collections.view_collection": "Visa samling",
"column.about": "Om",
"column.blocks": "Blockerade användare",

View File

@@ -13,6 +13,7 @@
"about.not_available": "Bu sunucuda bu bilgi kullanıma sunulmadı.",
"about.powered_by": "{mastodon} destekli merkeziyetsiz sosyal ağ",
"about.rules": "Sunucu kuralları",
"account.about": "Hakkında",
"account.account_note_header": "Kişisel not",
"account.activity": "Aktivite",
"account.add_note": "Kişisel bir not ekle",
@@ -452,6 +453,8 @@
"emoji_button.search_results": "Arama sonuçları",
"emoji_button.symbols": "Semboller",
"emoji_button.travel": "Seyahat ve Yerler",
"empty_column.account_about.me": "Henüz kendinle ilgili herhangi bir bilgi eklemedin.",
"empty_column.account_about.other": "{acct} henüz kendisiyle ilgili herhangi bir bilgi eklemedi.",
"empty_column.account_featured.me": "Henüz hiçbir şeyi öne çıkarmadınız. En çok kullandığınız etiketleri ve hatta arkadaşlarınızın hesaplarını profilinizde öne çıkarabileceğinizi biliyor muydunuz?",
"empty_column.account_featured.other": "{acct} henüz hiçbir şeyi öne çıkarmadı. En çok kullandığınız etiketleri ve hatta arkadaşlarınızın hesaplarını profilinizde öne çıkarabileceğinizi biliyor muydunuz?",
"empty_column.account_featured_other.unknown": "Bu hesap henüz hiçbir şeyi öne çıkarmadı.",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon liên hợp nên bất kể bạn tạo tài khoản ở đâu, bạn cũng sẽ có thể theo dõi và tương tác với tài khoản trên máy chủ này. Bạn thậm chí có thể tự mở máy chủ!",
"closed_registrations_modal.title": "Đăng ký Mastodon",
"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": "Collection này trống",
"collections.collection_description": "Mô tả",
"collections.collection_name": "Tên",
"collections.collection_topic": "Chủ đề",
"collections.confirm_account_removal": "Bạn có chắc muốn gỡ tài khoản này khỏi collection?",
"collections.content_warning": "Nội dung ẩn",
"collections.continue": "Tiếp tục",
"collections.create.accounts_subtitle": "Chỉ những tài khoản bạn theo dõi và đã chọn tham gia chương trình khám phá mới có thể được thêm vào.",
@@ -261,6 +264,9 @@
"collections.edit_details": "Sửa thông tin cơ bản",
"collections.edit_settings": "Sửa cài đặt",
"collections.error_loading_collections": "Đã xảy ra lỗi khi tải những collection của bạn.",
"collections.hints.accounts_counter": "{count} / {max} tài khoản",
"collections.hints.add_more_accounts": "Thêm tối thiểu {count, plural, other {# tài khoản}} để tiếp tục",
"collections.hints.can_not_remove_more_accounts": "Bộ sưu tập phải chứa tối thiểu {count, plural, other {# tài khoản}}. Không thể gỡ nữa.",
"collections.last_updated_at": "Lần cuối cập nhật: {date}",
"collections.manage_accounts": "Quản lý tài khoản",
"collections.manage_accounts_in_collection": "Quản lý tài khoản trong collection này",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "Giới hạn 100 ký tự",
"collections.new_collection": "Collection mới",
"collections.no_collections_yet": "Chưa có collection.",
"collections.remove_account": "Gỡ tài khoản này",
"collections.search_accounts_label": "Tìm tài khoản để thêm…",
"collections.search_accounts_max_reached": "Bạn đã đạt đến số lượng tài khoản tối đa",
"collections.topic_hint": "Thêm hashtag giúp người khác hiểu chủ đề chính của collection này.",
"collections.view_collection": "Xem collection",
"collections.visibility_public": "Công khai",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon 是去中心化的,所以无论在哪个实例创建账号,都可以关注本服务器上的账号并与之交流。 或者你还可以自己搭建实例!",
"closed_registrations_modal.title": "注册 Mastodon 账号",
"collections.account_count": "{count, plural, other {# 个账号}}",
"collections.accounts.empty_description": "添加你关注的账号,最多 {count} 个",
"collections.accounts.empty_title": "收藏列表为空",
"collections.collection_description": "说明",
"collections.collection_name": "名称",
"collections.collection_topic": "话题",
"collections.confirm_account_removal": "你确定要将从收藏列表中移除此账号吗?",
"collections.content_warning": "内容警告",
"collections.continue": "继续",
"collections.create.accounts_subtitle": "只有你关注的且已经主动加入发现功能的账号可以添加。",
@@ -261,6 +264,9 @@
"collections.edit_details": "编辑基本信息",
"collections.edit_settings": "编辑设置",
"collections.error_loading_collections": "加载你的收藏列表时发生错误。",
"collections.hints.accounts_counter": "{count} / {max} 个账号",
"collections.hints.add_more_accounts": "添加至少 {count, plural, other {# 个账号}}以继续",
"collections.hints.can_not_remove_more_accounts": "收藏列表必须包含至少 {count, plural, other {# 个账号}}。无法移除更多账号。",
"collections.last_updated_at": "最后更新:{date}",
"collections.manage_accounts": "管理账户",
"collections.manage_accounts_in_collection": "管理此收藏列表内的账户",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "100字限制",
"collections.new_collection": "新建收藏列表",
"collections.no_collections_yet": "尚无收藏列表。",
"collections.remove_account": "移除此账号",
"collections.search_accounts_label": "搜索要添加的账号…",
"collections.search_accounts_max_reached": "你添加的账号数量已达上限",
"collections.topic_hint": "添加话题标签,帮助他人了解此收藏列表的主题。",
"collections.view_collection": "查看收藏列表",
"collections.visibility_public": "公开",

View File

@@ -244,9 +244,12 @@
"closed_registrations_modal.preamble": "Mastodon 是去中心化的,所以無論您於哪個伺服器新增帳號,都可以與此伺服器上的任何人跟隨及互動。您甚至能自行架設自己的伺服器!",
"closed_registrations_modal.title": "註冊 Mastodon",
"collections.account_count": "{count, plural, other {# 個帳號}}",
"collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號",
"collections.accounts.empty_title": "此收藏是名單空的",
"collections.collection_description": "說明",
"collections.collection_name": "名稱",
"collections.collection_topic": "主題",
"collections.confirm_account_removal": "您是否確定要自此收藏名單中移除此帳號?",
"collections.content_warning": "內容警告",
"collections.continue": "繼續",
"collections.create.accounts_subtitle": "僅能加入您跟隨並選擇加入探索功能之帳號。",
@@ -261,6 +264,9 @@
"collections.edit_details": "編輯基本資料",
"collections.edit_settings": "編輯設定",
"collections.error_loading_collections": "讀取您的收藏名單時發生錯誤。",
"collections.hints.accounts_counter": "{count} / {max} 個帳號",
"collections.hints.add_more_accounts": "加入至少 {count, plural, other {# 個帳號}}以繼續",
"collections.hints.can_not_remove_more_accounts": "收藏名單必須至少包含 {count, plural, other {# 個帳號}}。無法移除更多帳號。",
"collections.last_updated_at": "最後更新:{date}",
"collections.manage_accounts": "管理帳號",
"collections.manage_accounts_in_collection": "管理此收藏名單之帳號",
@@ -269,6 +275,9 @@
"collections.name_length_hint": "100 字限制",
"collections.new_collection": "新增收藏名單",
"collections.no_collections_yet": "您沒有任何收藏名單。",
"collections.remove_account": "移除此帳號",
"collections.search_accounts_label": "搜尋帳號以加入...",
"collections.search_accounts_max_reached": "您新增之帳號數已達上限",
"collections.topic_hint": "新增主題標籤以協助其他人瞭解此收藏名單之主題。",
"collections.view_collection": "檢視收藏名單",
"collections.visibility_public": "公開",

View File

@@ -7,6 +7,8 @@ import {
apiUpdateCollection,
apiGetCollection,
apiDeleteCollection,
apiAddCollectionItem,
apiRemoveCollectionItem,
} from '@/mastodon/api/collections';
import type {
ApiCollectionJSON,
@@ -131,6 +133,32 @@ const collectionSlice = createSlice({
};
}
});
/**
* Adding an account to a collection
*/
builder.addCase(addCollectionItem.fulfilled, (state, action) => {
const { collection_item } = action.payload;
const { collectionId } = action.meta.arg;
state.collections[collectionId]?.items.push(collection_item);
});
/**
* Removing an account from a collection
*/
builder.addCase(removeCollectionItem.fulfilled, (state, action) => {
const { itemId, collectionId } = action.meta.arg;
const collection = state.collections[collectionId];
if (collection) {
collection.items = collection.items.filter(
(item) => item.id !== itemId,
);
}
});
},
});
@@ -169,6 +197,18 @@ export const deleteCollection = createDataLoadingThunk(
apiDeleteCollection(collectionId),
);
export const addCollectionItem = createDataLoadingThunk(
`${collectionSlice.name}/addCollectionItem`,
({ collectionId, accountId }: { collectionId: string; accountId: string }) =>
apiAddCollectionItem(collectionId, accountId),
);
export const removeCollectionItem = createDataLoadingThunk(
`${collectionSlice.name}/removeCollectionItem`,
({ collectionId, itemId }: { collectionId: string; itemId: string }) =>
apiRemoveCollectionItem(collectionId, itemId),
);
export const collections = collectionSlice.reducer;
/**

View File

@@ -41,7 +41,7 @@ class Tag < ApplicationRecord
HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}".freeze
HASHTAG_RE = %r{(?<![=/)\p{Alnum}])[#](#{HASHTAG_NAME_PAT})}
HASHTAG_RE = /(?<=^|\s)[#](#{HASHTAG_NAME_PAT})/
HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]\u0E47-\u0E4E#{HASHTAG_SEPARATORS}]/

View File

@@ -83,7 +83,10 @@ en:
access_denied: The resource owner or authorization server denied the request.
credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.
invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
invalid_code_challenge_method: The code challenge method must be S256, plain is unsupported.
invalid_code_challenge_method:
one: The code_challenge_method must be %{challenge_methods}.
other: The code_challenge_method must be one of %{challenge_methods}.
zero: The authorization server does not support PKCE as there are no accepted code_challenge_method values.
invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
invalid_redirect_uri: The redirect uri included is not valid.
invalid_request:

View File

@@ -1,7 +1,7 @@
---
et:
about:
about_mastodon_html: 'Tuleviku sotsiaalvõrgustik: Reklaamivaba, korporatiivse järelvalveta, eetiline kujundus ning detsentraliseeritus! Mastodonis omad sa enda andmeid ka päriselt!'
about_mastodon_html: 'Tuleviku sotsiaalvõrgustik: Reklaamivaba, korporatiivse jälitamiseta, eetiline kujundus ning hajutatus! Mastodonis omad sa enda andmeid ka päriselt!'
contact_missing: Määramata
contact_unavailable: Pole saadaval
hosted_on: Mastodoni teenus serveris %{domain}
@@ -18,7 +18,7 @@ et:
instance_actor_flash: See on serveri enda virtuaalne konto. See ei esinda ühtegi kindlat kasutajat, vaid seda kasutatakse födereerumisel. Seda kontot ei tohi kustutada.
last_active: viimati aktiivne
link_verified_on: Selle lingi autorsust kontrolliti %{date}
nothing_here: Siin pole midagi!
nothing_here: Siin pole mitte midagi!
pin_errors:
following: Pead olema juba selle kasutaja jälgija, keda soovitad
posts:
@@ -802,6 +802,7 @@ et:
view_devops_description: Lubab kasutajail ligipääsu Sidekiq ja pgHero töölaudadele
view_feeds: Vaata postituste ja teemade voogu reaalajas
view_feeds_description: Sõltumata serveri seadistustest luba kasutajatel vaadata postituste ja teemade voogu reaalajas
requires_2fa: Eeldab kaheastmelise autentimise kasutamist
title: Rollid
rules:
add_new: Lisa reegel
@@ -2022,6 +2023,8 @@ et:
past_preamble_html: Peale sinu viimast külastust oleme muutnud oma kasutustingimusi. Palun vaata muutunud tingimused üle.
review_link: Vaata üle kasutustingimused
title: "%{domain} saidi kasutustingimused muutuvad"
themes:
default: Mastodon
time:
formats:
default: "%d. %B, %Y. aastal, kell %H:%M"
@@ -2046,6 +2049,8 @@ et:
recovery_codes: Taastekoodide varundamine
recovery_codes_regenerated: Taastekoodid edukalt taasloodud
recovery_instructions_html: Kui telefon peaks kaotsi minema, on võimalik kontole sisenemisel kasutada ühte järgnevatest taastekoodidest. <strong>Hoia taastekoode turvaliselt</strong>. Näiteks võib neid prindituna hoida koos teiste tähtsate dokumentidega.
resume_app_authorization: Jätka rakenduse autentimist
role_requirement: "%{domain} teenus eeldab, et Mastodoni kasutamiseks lülitad sisse kaheastmelise autentimise."
webauthn: Turvavõtmed
user_mailer:
announcement_published:

View File

@@ -226,6 +226,7 @@ be:
email: Адрас электроннай пошты
expires_in: Заканчваецца пасля
fields: Метаданыя профілю
filter_action: Фільтраваць дзеянне
header: Загаловак
honeypot: "%{label} (не запаўняць)"
inbox_url: URL паштовай скрыні-рэтранслятара

View File

@@ -224,6 +224,7 @@ de:
email: E-Mail-Adresse
expires_in: Läuft ab
fields: Zusatzfelder
filter_action: Auswirkung
header: Titelbild
honeypot: "%{label} (nicht ausfüllen)"
inbox_url: URL des Relais-Posteingangs

View File

@@ -224,6 +224,7 @@ el:
email: Διεύθυνση email
expires_in: Λήξη μετά από
fields: Επιπλέον πεδία
filter_action: Ενέργεια φίλτρου
header: Εικόνα κεφαλίδας
honeypot: "%{label} (μη συμπληρώνετε)"
inbox_url: Το URL του inbox του ανταποκριτή (relay)

View File

@@ -224,6 +224,7 @@ en-GB:
email: Email address
expires_in: Expire after
fields: Profile metadata
filter_action: Filter action
header: Header
honeypot: "%{label} (do not fill in)"
inbox_url: URL of the relay inbox

View File

@@ -224,6 +224,7 @@ es-AR:
email: Dirección de correo electrónico
expires_in: Vence después de
fields: Campos extras
filter_action: Filtrar acción
header: Cabecera
honeypot: "%{label} (no rellenar)"
inbox_url: Dirección web de la bandeja de entrada del relé

View File

@@ -224,6 +224,7 @@ es-MX:
email: Dirección de correo electrónico
expires_in: Expirar tras
fields: Metadatos de perfil
filter_action: Filtrar acción
header: Imagen de encabezado
honeypot: "%{label} (no rellenar)"
inbox_url: URL de la entrada de relés

View File

@@ -224,6 +224,7 @@ es:
email: Dirección de correo electrónico
expires_in: Expirar tras
fields: Metadatos de perfil
filter_action: Acción de filtro
header: Imagen de encabezado
honeypot: "%{label} (no rellenar)"
inbox_url: URL de la entrada de relés

View File

@@ -164,6 +164,7 @@ et:
name: Rolli avalik nimi, kui roll on märgitud avalikuks kuvamiseks märgina
permissions_as_keys: Selle rolliga kasutajatel on ligipääs...
position: Kõrgem roll otsustab teatud olukordades konfliktide lahendamise. Teatud toiminguid saab teha ainult madalama prioriteediga rollidega
require_2fa: Selle rolliga kasutajad peavad sisse lülitama kaheastmelise autentimise
username_block:
allow_with_approval: Kohese liitumise asemel peavad vastavusekohased liitumised saama eeleeva heakskiidu
comparison: Kui lubad blokeerimise osalise vastavuse alusel, siis palun arvesta Scunthorpe'i probleemi tekkimise võimalusega
@@ -387,6 +388,7 @@ et:
name: Nimi
permissions_as_keys: Load
position: Positsioon
require_2fa: Eelda kaheastmelise autentimise kasutamist
username_block:
allow_with_approval: Luba kinnitamisega registreerimine
comparison: Võrdlemise meetod

View File

@@ -224,6 +224,7 @@ fi:
email: Sähköpostiosoite
expires_in: Vanhenee
fields: Lisäkentät
filter_action: Suodattimen toimi
header: Otsakekuva
honeypot: "%{label} (älä täytä)"
inbox_url: Välittäjän postilaatikon URL-osoite

View File

@@ -224,6 +224,7 @@ fo:
email: Teldubrævabústaður
expires_in: Endar aftan á
fields: Metadátur hjá vanganum
filter_action: Filtrera atgerð
header: Høvd
honeypot: "%{label} (ikki fylla út)"
inbox_url: URL'ur hjá innbakkanum hjá reiðlagnum

View File

@@ -224,6 +224,7 @@ fr-CA:
email: Adresse courriel
expires_in: Expire après
fields: Métadonnées du profil
filter_action: Action du filtre
header: Image den-tête
honeypot: "%{label} (ne pas remplir)"
inbox_url: URL de la boîte de relais

View File

@@ -224,6 +224,7 @@ fr:
email: Adresse de courriel
expires_in: Expire après
fields: Métadonnées du profil
filter_action: Action du filtre
header: Image den-tête
honeypot: "%{label} (ne pas remplir)"
inbox_url: URL de la boîte de relais

View File

@@ -227,6 +227,7 @@ ga:
email: Seoladh ríomhphoist
expires_in: In éag tar éis
fields: Réimsí breise
filter_action: Gníomh scagaire
header: Ceanntásc
honeypot: "%{label} (ná líon isteach)"
inbox_url: URL an bhosca isteach sealaíochta

View File

@@ -224,6 +224,7 @@ gl:
email: Enderezo de correo
expires_in: Caduca tras
fields: Metadatos do perfil
filter_action: Acción de filtrado
header: Cabeceira
honeypot: "%{label} (non completar)"
inbox_url: URL da caixa de entrada do repetidor

View File

@@ -226,6 +226,7 @@ he:
email: כתובת דוא"ל
expires_in: תפוגה לאחר
fields: מטא-נתונים על הפרופיל
filter_action: פעולות סינון
header: תמונת נושא
honeypot: "%{label} (לא למלא)"
inbox_url: קישורית לתיבת ממסר

View File

@@ -224,6 +224,7 @@ is:
email: Tölvupóstfang
expires_in: Rennur út eftir
fields: Lýsigögn notandasniðs
filter_action: Aðgerð síu
header: Síðuhaus
honeypot: "%{label} (ekki fylla út)"
inbox_url: URL-slóð á innhólf endurvarpa

View File

@@ -224,6 +224,7 @@ it:
email: Indirizzo email
expires_in: Scade dopo
fields: Metadati del profilo
filter_action: Azione del filtro
header: Intestazione
honeypot: "%{label} (non compilare)"
inbox_url: URL della inbox del ripetitore

View File

@@ -223,6 +223,7 @@ sq:
email: Adresë email
expires_in: Skadon pas
fields: Tejtëdhëna profili
filter_action: Veprim filtri
header: Krye
honeypot: "%{label} (mos plotësoni gjë këtu)"
inbox_url: URL e Të marrëve të relesë

View File

@@ -224,6 +224,7 @@ tr:
email: E-posta adresi
expires_in: Bitiş tarihi
fields: Profil meta verisi
filter_action: Eylemi filtrele
header: Kapak resmi
honeypot: "%{label} (doldurmayın)"
inbox_url: Aktarıcı gelen kutusunun URL'si

View File

@@ -223,6 +223,7 @@ vi:
email: Địa chỉ email
expires_in: Hết hạn sau
fields: Metadata
filter_action: Lọc hành động
header: Ảnh bìa
honeypot: "%{label} (đừng điền vào)"
inbox_url: Hộp thư relay

View File

@@ -223,6 +223,7 @@ zh-CN:
email: 邮箱地址
expires_in: 失效时间
fields: 个人资料附加信息
filter_action: 过滤器操作
header: 封面图
honeypot: "%{label} (请勿填写)"
inbox_url: 中继站收件箱的 URL

View File

@@ -223,6 +223,7 @@ zh-TW:
email: 電子郵件地址
expires_in: 失效時間
fields: 額外欄位
filter_action: 過濾器動作
header: 封面圖片
honeypot: "%{label} (請勿填寫)"
inbox_url: 中繼收件匣 URL

View File

@@ -4,6 +4,9 @@ shared:
limited_federation_mode: <%= (ENV.fetch('LIMITED_FEDERATION_MODE', nil) || ENV.fetch('WHITELIST_MODE', nil)) == 'true' %>
self_destruct_value: <%= ENV.fetch('SELF_DESTRUCT', nil)&.to_json %>
software_update_url: <%= ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check')&.to_json %>
donation_campaigns:
api_url: <%= ENV.fetch('DONATION_CAMPAIGNS_URL', nil)&.to_json %>
environment: <%= ENV.fetch('DONATION_CAMPAIGNS_ENVIRONMENT', nil)&.to_json %>
source:
base_url: <%= ENV.fetch('SOURCE_BASE_URL', nil)&.to_json %>
repository: <%= ENV.fetch('GITHUB_REPOSITORY', 'glitch-soc/mastodon') %>

View File

@@ -75,6 +75,7 @@ namespace :api, format: false do
resources :suggestions, only: [:index, :destroy]
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
resources :preferences, only: [:index]
resources :donation_campaigns, only: [:index]
resources :annual_reports, only: [:index, :show] do
member do

7
dist/nginx.conf vendored
View File

@@ -82,31 +82,37 @@ server {
location ^~ /avatars/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
# try_files $uri @mastodon;
}
location ^~ /emoji/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
# try_files $uri @mastodon;
}
location ^~ /headers/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
# try_files $uri @mastodon;
}
location ^~ /ocr/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
# try_files $uri @mastodon;
}
location ^~ /packs/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
# try_files $uri @mastodon;
}
location ^~ /sounds/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
# try_files $uri @mastodon;
}
location ^~ /system/ {
@@ -114,6 +120,7 @@ server {
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy "default-src 'none'; form-action 'none'";
# try_files $uri @mastodon;
}
location ^~ /api/v1/streaming {

View File

@@ -85,6 +85,10 @@ RSpec.describe Tag do
expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)?foo=#Lawsuit')).to be_nil
end
it 'does not match URLs with hashtag-like anchors after a dot' do
expect(subject.match('https://en.wikipedia.org/wiki/Google_LLC_v._Oracle_America,_Inc.#Decision')).to be_nil
end
it 'matches #' do
expect(subject.match('this is #').to_s).to eq '#'
end

View File

@@ -0,0 +1,105 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Donation campaigns' do
include_context 'with API authentication'
describe 'GET /api/v1/donation_campaigns' do
context 'when not authenticated' do
it 'returns http unprocessable entity' do
get '/api/v1/donation_campaigns'
expect(response)
.to have_http_status(422)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'when no donation campaign API is set up' do
it 'returns http empty' do
get '/api/v1/donation_campaigns', headers: headers
expect(response)
.to have_http_status(204)
end
end
context 'when a donation campaign API is set up' do
let(:api_url) { 'https://example.org/donations' }
let(:seed) { Random.new(user.account_id).rand(100) }
around do |example|
original = Rails.configuration.x.donation_campaigns.api_url
Rails.configuration.x.donation_campaigns.api_url = api_url
example.run
Rails.configuration.x.donation_campaigns.api_url = original
end
context 'when the donation campaign API does not return a campaign' do
before do
stub_request(:get, "#{api_url}?platform=web&seed=#{seed}&locale=en").to_return(status: 204)
end
it 'returns http empty' do
get '/api/v1/donation_campaigns', headers: headers
expect(response)
.to have_http_status(204)
end
end
context 'when the donation campaign API returns a campaign' do
let(:campaign_json) do
{
'id' => 'campaign-1',
'banner_message' => 'Hi',
'banner_button_text' => 'Donate!',
'donation_message' => 'Hi!',
'donation_button_text' => 'Money',
'donation_success_post' => 'Success post',
'amounts' => {
'one_time' => {
'EUR' => [1, 2, 3],
'USD' => [4, 5, 6],
},
'monthly' => {
'EUR' => [1],
'USD' => [2],
},
},
'default_currency' => 'EUR',
'donation_url' => 'https://sponsor.joinmastodon.org/donate/new',
'locale' => 'en',
}
end
before do
stub_request(:get, "#{api_url}?platform=web&seed=#{seed}&locale=en").to_return(body: Oj.dump(campaign_json), status: 200)
end
it 'returns the expected campaign' do
get '/api/v1/donation_campaigns', headers: headers
expect(response)
.to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body)
.to match(campaign_json)
expect(Rails.cache.read("donation_campaign_request:#{seed}:en", raw: true))
.to eq 'campaign-1:en'
expect(Oj.load(Rails.cache.read('donation_campaign:campaign-1:en', raw: true)))
.to match(campaign_json)
end
end
end
end
end

View File

@@ -98,28 +98,27 @@ RSpec.describe 'Using OAuth from an external app' do
context 'when using plain code challenge method' do
let(:pkce_code_challenge_method) { 'plain' }
it 'does not include the PKCE values in the response' do
it 'shows an error message and does not include the PKCE values or authorize button' do
subject
expect(page).to have_no_css('.oauth-prompt input[name=code_challenge]')
expect(page).to have_no_css('.oauth-prompt input[name=code_challenge_method]')
end
it 'does not include the authorize button' do
subject
expect(page).to have_no_css('.oauth-prompt button[type="submit"]')
end
it 'includes an error message' do
subject
expect(page)
.to have_no_css('.oauth-prompt input[name=code_challenge]')
.and have_no_css('.oauth-prompt input[name=code_challenge_method]')
.and have_no_css('.oauth-prompt button[type="submit"]')
within '.form-container .flash-message' do
# FIXME: Replace with doorkeeper.errors.messages.invalid_code_challenge_method.one for Doorkeeper > 5.8.0
# see: https://github.com/doorkeeper-gem/doorkeeper/pull/1747
expect(page).to have_content(I18n.t('doorkeeper.errors.messages.invalid_code_challenge_method'))
expect(page)
.to have_content(doorkeeper_invalid_code_message)
end
end
def doorkeeper_invalid_code_message
I18n.t(
'doorkeeper.errors.messages.invalid_code_challenge_method',
challenge_methods: Doorkeeper.configuration.pkce_code_challenge_methods.join(', '),
count: Doorkeeper.configuration.pkce_code_challenge_methods.length
)
end
end
context 'when the user has yet to enable TOTP' do

View File

@@ -3,76 +3,44 @@
require 'rails_helper'
RSpec.describe FollowLimitValidator do
describe '#validate' do
context 'with a nil account' do
it 'does not add validation errors to base' do
follow = Fabricate.build(:follow, account: nil)
subject { Fabricate.build(:follow) }
follow.valid?
context 'with a nil account' do
it { is_expected.to allow_values(nil).for(:account).against(:base) }
end
expect(follow.errors[:base]).to be_empty
end
context 'with a non-local account' do
let(:account) { Account.new(domain: 'host.example') }
it { is_expected.to allow_values(account).for(:account).against(:base) }
end
context 'with a local account' do
let(:account) { Account.new }
context 'when the followers count is under the limit' do
before { account.following_count = described_class::LIMIT - 100 }
it { is_expected.to allow_values(account).for(:account).against(:base) }
end
context 'with a non-local account' do
it 'does not add validation errors to base' do
follow = Fabricate.build(:follow, account: Account.new(domain: 'host.example'))
context 'when the following count is over the limit' do
before { account.following_count = described_class::LIMIT + 100 }
follow.valid?
context 'when the followers count is low' do
before { account.followers_count = 10 }
expect(follow.errors[:base]).to be_empty
end
end
it { is_expected.to_not allow_values(account).for(:account).against(:base).with_message(limit_reached_message) }
context 'with a local account' do
let(:account) { Account.new }
context 'when the followers count is under the limit' do
before do
allow(account).to receive(:following_count).and_return(described_class::LIMIT - 100)
end
it 'does not add validation errors to base' do
follow = Fabricate.build(:follow, account: account)
follow.valid?
expect(follow.errors[:base]).to be_empty
def limit_reached_message
I18n.t('users.follow_limit_reached', limit: described_class::LIMIT)
end
end
context 'when the following count is over the limit' do
before do
allow(account).to receive(:following_count).and_return(described_class::LIMIT + 100)
end
context 'when the followers count is high' do
before { account.followers_count = 100_000 }
context 'when the followers count is low' do
before do
allow(account).to receive(:followers_count).and_return(10)
end
it 'adds validation errors to base' do
follow = Fabricate.build(:follow, account: account)
follow.valid?
expect(follow.errors[:base]).to include(I18n.t('users.follow_limit_reached', limit: described_class::LIMIT))
end
end
context 'when the followers count is high' do
before do
allow(account).to receive(:followers_count).and_return(100_000)
end
it 'does not add validation errors to base' do
follow = Fabricate.build(:follow, account: account)
follow.valid?
expect(follow.errors[:base]).to be_empty
end
end
it { is_expected.to allow_values(account).for(:account).against(:base) }
end
end
end

View File

@@ -6607,9 +6607,9 @@ __metadata:
linkType: hard
"dotenv@npm:^17.0.0":
version: 17.2.4
resolution: "dotenv@npm:17.2.4"
checksum: 10c0/901aeee9cb40860291bdb452f6ca66aada78438331a026b0bd86fd41b94a79752dbc6a8971f4f26e6cafef11b4a27bb12ea99c0cbe7dfa61791f194727f799e5
version: 17.3.1
resolution: "dotenv@npm:17.3.1"
checksum: 10c0/c78e0c2d5a549c751e544cc60e2b95e7cb67e0c551f42e094d161c6b297aa44b630a3c2dcacf5569e529a6c2a6b84e2ab9be8d37b299d425df5a18b81ce4a35f
languageName: node
linkType: hard