Merge pull request #3484 from glitch-soc/glitch-soc/merge-upstream

Merge upstream changes up to b15d234ccb5d004180561c440dd45536d70e67b2
This commit is contained in:
Claire 2026-04-18 19:56:43 +02:00 committed by GitHub
commit 687a24dee8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
154 changed files with 2340 additions and 1889 deletions

2
.nvmrc
View File

@ -1 +1 @@
24.14
24.15

View File

@ -99,7 +99,7 @@ GEM
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1236.0)
aws-partitions (1.1238.0)
aws-sdk-core (3.244.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@ -156,7 +156,7 @@ GEM
playwright-ruby-client (>= 1.16.0)
case_transform (0.2)
activesupport
cbor (0.5.10.1)
cbor (0.5.10.2)
cgi (0.5.1)
charlock_holmes (0.7.9)
chewy (8.0.1)
@ -178,7 +178,7 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (1.21.1)
css_parser (2.0.0)
addressable
csv (3.3.5)
database_cleaner-active_record (2.2.2)
@ -214,7 +214,7 @@ GEM
dotenv (3.2.0)
drb (2.2.3)
dry-cli (1.4.1)
elastic-transport (8.4.1)
elastic-transport (8.5.1)
faraday (< 3)
multi_json
elasticsearch (8.19.3)
@ -226,14 +226,14 @@ GEM
elasticsearch-dsl (0.1.10)
email_validator (2.2.4)
activemodel
erb (6.0.2)
erb (6.0.3)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
excon (1.4.0)
excon (1.4.2)
logger
fabrication (3.0.0)
faker (3.6.1)
faker (3.7.1)
i18n (>= 1.8.11, < 2)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
@ -247,7 +247,7 @@ GEM
net-http (~> 0.5)
fast_blank (1.0.1)
fastimage (2.4.1)
ffi (1.17.3)
ffi (1.17.4)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
@ -292,9 +292,9 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.72.0)
haml_lint (0.73.0)
haml (>= 5.0)
parallel (~> 1.10)
parallel (>= 1.10)
rainbow
rubocop (>= 1.0)
sysexits (~> 1.1)
@ -314,7 +314,7 @@ GEM
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0)
http-cookie (1.1.0)
http-cookie (1.1.4)
domain_name (~> 0.5)
http-form_data (2.3.0)
http_accept_language (2.1.1)
@ -447,14 +447,14 @@ GEM
mime-types (3.7.0)
logger
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2026.0407)
mime-types-data (3.2026.0414)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (6.0.3)
minitest (6.0.4)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
multi_json (1.19.1)
multi_json (1.20.1)
mutex_m (0.3.0)
net-http (0.6.0)
uri
@ -589,8 +589,8 @@ GEM
ostruct (0.6.3)
ox (2.14.23)
bigdecimal (>= 3.0)
parallel (1.27.0)
parser (3.3.10.2)
parallel (1.28.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
parslet (2.0.0)
@ -605,7 +605,7 @@ GEM
mime-types (>= 3.0)
pp (0.6.3)
prettyprint
premailer (1.27.0)
premailer (1.29.0)
addressable
css_parser (>= 1.19.0)
htmlentities (>= 4.0.0)
@ -691,7 +691,7 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.1)
rake (13.4.1)
rdf (3.3.4)
bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5)
@ -712,7 +712,7 @@ GEM
redis-client (>= 0.22.0)
redis-client (0.28.0)
connection_pool
regexp_parser (2.11.3)
regexp_parser (2.12.0)
reline (0.6.3)
io-console (~> 0.5)
request_store (1.7.0)
@ -773,7 +773,7 @@ GEM
rubocop-capybara (2.22.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-i18n (3.2.3)
rubocop-i18n (3.3.0)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-performance (1.26.1)
@ -905,7 +905,7 @@ GEM
vite_rails (3.0.20)
railties (>= 5.1, < 9)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.9.3)
vite_ruby (3.10.2)
dry-cli (>= 0.7, < 2)
logger (~> 1.6)
mutex_m
@ -1100,4 +1100,4 @@ RUBY VERSION
ruby 4.0.2
BUNDLED WITH
4.0.8
4.0.10

View File

@ -7,7 +7,7 @@ module Settings
skip_before_action :require_functional!
before_action :require_challenge!, on: :create
before_action :require_challenge!
def create
@recovery_codes = current_user.generate_otp_backup_codes!

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module InvitesHelper
def invites_max_uses_options
[1, 5, 10, 25, 50, 100]
end
def invites_expires_options
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week]
end
end

View File

@ -23,6 +23,10 @@ module SettingsHelper
)
end
def user_settings_collection(value)
UserSettings.definition_for(value)&.in || []
end
def author_attribution_name(account)
return if account.nil?

View File

@ -35,3 +35,12 @@
text-wrap: pretty;
}
}
[data-color-scheme='dark'] .defaultImage {
--color-skin-1: #3a3a50;
--color-skin-2: #67678e;
--color-skin-3: #44445f;
--color-outline: #21212c;
--color-shadow: #181820;
--color-highlight: #b2b1c8;
}

View File

@ -29,12 +29,6 @@ export const Default: Story = {
},
};
export const WithoutMessage: Story = {
args: {
message: undefined,
},
};
export const WithAction: Story = {
args: {
...Default.args,
@ -42,3 +36,17 @@ export const WithAction: Story = {
children: <Button onClick={() => action('Refresh')}>Refresh</Button>,
},
};
export const WithoutImage: Story = {
args: {
...Default.args,
image: null,
},
};
export const WithoutMessage: Story = {
args: {
...Default.args,
message: undefined,
},
};

View File

@ -1,31 +1,39 @@
import { FormattedMessage } from 'react-intl';
import ElephantImage from '@/images/elephant_ui.svg?react';
import classes from './empty_state.module.scss';
const images = {
default: <ElephantImage className={classes.defaultImage} />,
};
/**
* Simple empty state component with a neutral default title and customisable message.
*
* Action buttons can be passed as `children`
* Action buttons can be passed as `children`.
*/
export const EmptyState: React.FC<{
image?: React.ReactNode;
image?: keyof typeof images | React.ReactElement | null;
title?: React.ReactNode;
message?: React.ReactNode;
children?: React.ReactNode;
}> = ({
image,
image = 'default',
title = (
<FormattedMessage id='empty_state.no_results' defaultMessage='No results' />
),
message,
children,
}) => {
const imageToRender = typeof image === 'string' ? images[image] : image;
return (
<div className={classes.wrapper}>
{(title || message || image) && (
{(title || message || imageToRender) && (
<div className={classes.content}>
{image}
{imageToRender}
{!!title && <h3>{title}</h3>}
{!!message && <p>{message}</p>}
</div>

View File

@ -25,7 +25,7 @@
.popover {
z-index: 9999;
box-sizing: border-box;
max-height: max(200px, 30dvh);
max-height: min(320px, 50dvh);
padding: 4px;
border-radius: 4px;
color: var(--color-text-primary);
@ -38,6 +38,18 @@
overscroll-behavior-y: contain;
}
.groupLabel {
padding-block: 12px 4px;
padding-inline: 12px;
font-size: 13px;
font-weight: bold;
text-transform: uppercase;
ul:first-child > & {
padding-top: 4px;
}
}
.menuItem {
display: flex;
align-items: center;
@ -51,13 +63,7 @@
user-select: none;
&[data-highlighted='true'] {
color: var(--color-text-on-brand-base);
background: var(--color-bg-brand-base);
&[aria-disabled='true'] {
color: var(--color-text-on-disabled);
background: var(--color-bg-disabled);
}
background: var(--color-bg-overlay-highlight);
}
&[aria-disabled='true'] {

View File

@ -4,40 +4,46 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { ComboboxField } from './combobox_field';
const ComboboxDemo: React.FC = () => {
interface Fruit {
id: string;
name: string;
type: 'citrus' | 'berryish' | 'seedy' | 'stony' | 'longish' | 'chonky';
disabled?: boolean;
}
const ComboboxDemo: React.FC<{ withGroups?: boolean }> = ({ withGroups }) => {
const [searchValue, setSearchValue] = useState('');
const items = [
{ id: '1', name: 'Apple' },
{ id: '2', name: 'Banana' },
{ id: '3', name: 'Cherry', disabled: true },
{ id: '4', name: 'Date' },
{ id: '5', name: 'Fig', disabled: true },
{ id: '6', name: 'Grape' },
{ id: '7', name: 'Honeydew' },
{ id: '8', name: 'Kiwi' },
{ id: '9', name: 'Lemon' },
{ id: '10', name: 'Mango' },
{ id: '11', name: 'Nectarine' },
{ id: '12', name: 'Orange' },
{ id: '13', name: 'Papaya' },
{ id: '14', name: 'Quince' },
{ id: '15', name: 'Raspberry' },
{ id: '16', name: 'Strawberry' },
{ id: '17', name: 'Tangerine' },
{ id: '19', name: 'Vanilla bean' },
{ id: '20', name: 'Watermelon' },
{ id: '22', name: 'Yellow Passion Fruit' },
{ id: '23', name: 'Zucchini' },
{ id: '24', name: 'Cantaloupe' },
{ id: '25', name: 'Blackberry' },
{ id: '26', name: 'Persimmon' },
{ id: '27', name: 'Lychee' },
{ id: '28', name: 'Dragon Fruit' },
{ id: '29', name: 'Passion Fruit' },
{ id: '30', name: 'Starfruit' },
const items: Fruit[] = [
{ id: '1', name: 'Apple', type: 'seedy' },
{ id: '2', name: 'Banana', type: 'longish' },
{ id: '3', name: 'Cherry', type: 'berryish', disabled: true },
{ id: '4', name: 'Date', type: 'stony' },
{ id: '5', name: 'Fig', type: 'seedy', disabled: true },
{ id: '6', name: 'Grape', type: 'berryish' },
{ id: '7', name: 'Honeydew', type: 'chonky' },
{ id: '8', name: 'Kiwi', type: 'seedy' },
{ id: '9', name: 'Lemon', type: 'citrus' },
{ id: '10', name: 'Mango', type: 'stony' },
{ id: '11', name: 'Nectarine', type: 'stony' },
{ id: '12', name: 'Orange', type: 'citrus' },
{ id: '13', name: 'Papaya', type: 'seedy' },
{ id: '14', name: 'Quince', type: 'seedy' },
{ id: '15', name: 'Raspberry', type: 'berryish' },
{ id: '16', name: 'Strawberry', type: 'berryish' },
{ id: '17', name: 'Tangerine', type: 'citrus' },
{ id: '19', name: 'Vanilla bean', type: 'longish' },
{ id: '20', name: 'Watermelon', type: 'chonky' },
{ id: '22', name: 'Yellow Passion Fruit', type: 'seedy' },
{ id: '23', name: 'Zucchini', type: 'longish' },
{ id: '24', name: 'Cantaloupe', type: 'chonky' },
{ id: '25', name: 'Blackberry', type: 'berryish' },
{ id: '26', name: 'Persimmon', type: 'seedy' },
{ id: '27', name: 'Lychee', type: 'berryish' },
{ id: '28', name: 'Dragon Fruit', type: 'seedy' },
{ id: '29', name: 'Passion Fruit', type: 'seedy' },
{ id: '30', name: 'Starfruit', type: 'seedy' },
];
type Fruit = (typeof items)[number];
const getItemId = useCallback((item: Fruit) => item.id, []);
const getIsItemDisabled = useCallback((item: Fruit) => !!item.disabled, []);
@ -66,12 +72,16 @@ const ComboboxDemo: React.FC = () => {
)
: items;
const groupedResults = withGroups
? Object.groupBy(results, (item) => item.type)
: results;
return (
<ComboboxField
label='Favourite fruit'
value={searchValue}
onChange={handleSearchValueChange}
items={results}
items={groupedResults}
getItemId={getItemId}
getIsItemDisabled={getIsItemDisabled}
onSelectItem={selectFruit}
@ -90,15 +100,23 @@ export default meta;
type Story = StoryObj<typeof meta>;
export const Example: Story = {
args: {
// Adding these types to keep TS happy, they're not passed on to `ComboboxDemo`
label: '',
value: '',
onChange: () => undefined,
items: [],
getItemId: () => '',
renderItem: () => <>Nothing</>,
onSelectItem: () => undefined,
},
// These args are just used to keep TS happy,
// they're not passed to `ComboboxDemo`
const dummyArgs = {
label: '',
value: '',
onChange: () => undefined,
items: [],
getItemId: () => '',
renderItem: () => <>Nothing</>,
onSelectItem: () => undefined,
};
export const Simple: Story = {
args: dummyArgs,
};
export const WithGroups: Story = {
render: () => <ComboboxDemo withGroups />,
args: dummyArgs,
};

View File

@ -1,4 +1,11 @@
import { forwardRef, useCallback, useId, useRef, useState } from 'react';
import {
forwardRef,
useCallback,
useId,
useMemo,
useRef,
useState,
} from 'react';
import { useIntl } from 'react-intl';
@ -28,10 +35,10 @@ export interface ComboboxItemState {
isDisabled: boolean;
}
interface ComboboxProps<T extends ComboboxItem> extends Omit<
TextInputProps,
'icon'
> {
interface ComboboxProps<
Item extends ComboboxItem,
GroupKey extends string,
> extends Omit<TextInputProps, 'icon'> {
/**
* The value of the combobox's text input
*/
@ -46,34 +53,45 @@ interface ComboboxProps<T extends ComboboxItem> extends Omit<
*/
isLoading?: boolean;
/**
* The set of options/suggestions that should be rendered in the dropdown menu.
* The set of options/suggestions that should be rendered in the dropdown menu,
* optionally separated into groups by providing an object
*/
items: T[];
items: Item[] | Partial<Record<GroupKey, Item[]>>;
/**
* A function that must return a unique id for each option passed via `items`
*/
getItemId?: (item: T) => string;
getItemId?: (item: Item) => string;
/**
* Providing this function turns the combobox into a multi-select box that assumes
* multiple options to be selectable. Single-selection is handled automatically.
*/
getIsItemSelected?: (item: T) => boolean;
getIsItemSelected?: (item: Item) => boolean;
/**
* Use this function to mark items as disabled, if needed
*/
getIsItemDisabled?: (item: T) => boolean;
getIsItemDisabled?: (item: Item) => boolean;
/**
* Customise the rendering of each option.
* The rendered content must not contain other interactive content!
*/
renderItem: (
item: T,
item: Item,
state: ComboboxItemState,
) => React.ReactElement | string;
/**
* Customise the rendering of group titles.
* The `titleId` must be attached to the element that provides the
* accessible name for the group.
* Return `null` to omit rendering the group title.
*/
renderGroupTitle?: (
groupKey: GroupKey,
titleId: string,
) => React.ReactElement | null;
/**
* The main selection handler, called when an option is selected or deselected.
*/
onSelectItem: (item: T) => void;
onSelectItem: (item: Item) => void;
/**
* Icon to be displayed in the text input
*/
@ -88,8 +106,8 @@ interface ComboboxProps<T extends ComboboxItem> extends Omit<
suppressMenu?: boolean;
}
interface Props<T extends ComboboxItem>
extends ComboboxProps<T>, CommonFieldWrapperProps {}
interface Props<Item extends ComboboxItem, GroupKey extends string>
extends ComboboxProps<Item, GroupKey>, CommonFieldWrapperProps {}
/**
* The combobox field allows users to select one or more items
@ -100,8 +118,11 @@ interface Props<T extends ComboboxItem>
* [research & implementations](https://sarahmhigley.com/writing/select-your-poison/).
*/
export const ComboboxFieldWithRef = <T extends ComboboxItem>(
{ id, label, hint, status, required, ...otherProps }: Props<T>,
export const ComboboxFieldWithRef = <
Item extends ComboboxItem,
GroupKey extends string,
>(
{ id, label, hint, status, required, ...otherProps }: Props<Item, GroupKey>,
ref: React.ForwardedRef<HTMLInputElement>,
) => (
<FormFieldWrapper
@ -118,15 +139,17 @@ export const ComboboxFieldWithRef = <T extends ComboboxItem>(
// 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 ComboboxItem>(
props: Props<T> & { ref?: React.ForwardedRef<HTMLInputElement> },
<Item extends ComboboxItem, GroupKey extends string>(
props: Props<Item, GroupKey> & {
ref?: React.ForwardedRef<HTMLInputElement>;
},
): ReturnType<typeof ComboboxFieldWithRef>;
displayName: string;
};
ComboboxField.displayName = 'ComboboxField';
const ComboboxWithRef = <T extends ComboboxItem>(
const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
{
value,
isLoading = false,
@ -135,6 +158,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
getIsItemDisabled,
getIsItemSelected,
disabled,
renderGroupTitle,
renderItem,
onSelectItem,
onChange,
@ -144,7 +168,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
icon = SearchIcon,
className,
...otherProps
}: ComboboxProps<T>,
}: ComboboxProps<Item, GroupKey>,
ref: React.ForwardedRef<HTMLInputElement>,
) => {
const intl = useIntl();
@ -157,15 +181,28 @@ const ComboboxWithRef = <T extends ComboboxItem>(
);
const [shouldMenuOpen, setShouldMenuOpen] = useState(false);
const hasGroups = !Array.isArray(items);
const flatItems = useMemo(
() =>
hasGroups
? (Object.values(items)
.flat()
.filter((i) => !!i) as Item[])
: items,
[hasGroups, items],
);
const statusMessage = useGetA11yStatusMessage({
value,
isLoading,
itemCount: items.length,
itemCount: flatItems.length,
});
const showStatusMessageInMenu =
!!statusMessage && value.length > 0 && items.length === 0;
!!statusMessage && value.length > 0 && flatItems.length === 0;
const hasMenuContent =
!disabled && !suppressMenu && (items.length > 0 || showStatusMessageInMenu);
!disabled &&
!suppressMenu &&
(flatItems.length > 0 || showStatusMessageInMenu);
const isMenuOpen = shouldMenuOpen && hasMenuContent;
const openMenu = useCallback(() => {
@ -178,10 +215,10 @@ const ComboboxWithRef = <T extends ComboboxItem>(
}, []);
const resetHighlight = useCallback(() => {
const firstItem = items[0];
const firstItem = flatItems[0];
const firstItemId = firstItem ? getItemId(firstItem) : null;
setHighlightedItemId(firstItemId);
}, [getItemId, items]);
}, [getItemId, flatItems]);
const highlightItem = useCallback((id: string | null) => {
setHighlightedItemId(id);
@ -216,7 +253,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
const selectItem = useCallback(
(itemId: string | null) => {
const item = items.find((item) => item.id === itemId);
const item = flatItems.find((item) => item.id === itemId);
if (item) {
const isDisabled = getIsItemDisabled?.(item) ?? false;
if (!isDisabled) {
@ -229,7 +266,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
}
inputRef.current?.focus();
},
[closeMenu, closeOnSelect, getIsItemDisabled, items, onSelectItem],
[closeMenu, closeOnSelect, getIsItemDisabled, flatItems, onSelectItem],
);
const handleSelectItem = useCallback(
@ -246,38 +283,38 @@ const ComboboxWithRef = <T extends ComboboxItem>(
const moveHighlight = useCallback(
(direction: number) => {
if (items.length === 0) {
if (flatItems.length === 0) {
return;
}
const highlightedItemIndex = items.findIndex(
const highlightedItemIndex = flatItems.findIndex(
(item) => getItemId(item) === highlightedItemId,
);
if (highlightedItemIndex === -1) {
// If no item is highlighted yet, highlight the first or last
if (direction > 0) {
const firstItem = items.at(0);
const firstItem = flatItems.at(0);
highlightItem(firstItem ? getItemId(firstItem) : null);
} else {
const lastItem = items.at(-1);
const lastItem = flatItems.at(-1);
highlightItem(lastItem ? getItemId(lastItem) : null);
}
} else {
// If there is a highlighted item, select the next or previous item
// and wrap around at the start or end:
let newIndex = highlightedItemIndex + direction;
if (newIndex >= items.length) {
if (newIndex >= flatItems.length) {
newIndex = 0;
} else if (newIndex < 0) {
newIndex = items.length - 1;
newIndex = flatItems.length - 1;
}
const newHighlightedItem = items[newIndex];
const newHighlightedItem = flatItems[newIndex];
highlightItem(
newHighlightedItem ? getItemId(newHighlightedItem) : null,
);
}
},
[getItemId, highlightItem, highlightedItemId, items],
[getItemId, highlightItem, highlightedItemId, flatItems],
);
useOnClickOutside(wrapperRef, closeMenu);
@ -331,6 +368,38 @@ const ComboboxWithRef = <T extends ComboboxItem>(
],
);
const renderItems = (items: 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}
data-highlighted={isHighlighted}
aria-selected={isSelected}
aria-disabled={isDisabled}
data-item-id={id}
onMouseEnter={handleItemMouseEnter}
onClick={handleSelectItem}
>
{renderItem(item, {
isSelected,
isDisabled: isDisabled ?? false,
})}
</li>
);
});
const mergeRefs = useCallback(
(element: HTMLInputElement | null) => {
inputRef.current = element;
@ -406,42 +475,50 @@ const ComboboxWithRef = <T extends ComboboxItem>(
>
{({ props, placement }) => (
<div {...props} className={classNames(classes.popover, placement)}>
{showStatusMessageInMenu ? (
<span className={classes.emptyMessage}>{statusMessage}</span>
) : (
<ul role='listbox' id={listId} tabIndex={-1}>
{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}
data-highlighted={isHighlighted}
aria-selected={isSelected}
aria-disabled={isDisabled}
data-item-id={id}
onMouseEnter={handleItemMouseEnter}
onClick={handleSelectItem}
>
{renderItem(item, {
isSelected,
isDisabled: isDisabled ?? false,
})}
</li>
);
})}
</ul>
)}
<StatusMessageWrapper
showStatus={showStatusMessageInMenu}
status={statusMessage}
>
{hasGroups ? (
<div role='listbox' id={listId} tabIndex={-1}>
{(Object.keys(items) as GroupKey[]).map((groupKey) => {
const groupItems = items[groupKey];
const groupTitleId = `${listId}-group-${groupKey}`;
const customGroupTitle = renderGroupTitle?.(
groupKey,
groupTitleId,
);
const hasTitle = customGroupTitle !== null;
if (!groupItems?.length) return null;
return (
<ul
key={groupKey}
role='group'
aria-labelledby={hasTitle ? groupTitleId : undefined}
>
{hasTitle && (
<li
role='presentation'
className={classes.groupLabel}
>
{customGroupTitle ?? (
<span id={groupTitleId}>{groupKey}</span>
)}
</li>
)}
{renderItems(groupItems)}
</ul>
);
})}
</div>
) : (
<ul role='listbox' id={listId} tabIndex={-1}>
{renderItems(items)}
</ul>
)}
</StatusMessageWrapper>
</div>
)}
</Overlay>
@ -452,14 +529,28 @@ const ComboboxWithRef = <T extends ComboboxItem>(
// 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 ComboboxItem>(
props: ComboboxProps<T> & { ref?: React.ForwardedRef<HTMLInputElement> },
<Item extends ComboboxItem, GroupKey extends string>(
props: ComboboxProps<Item, GroupKey> & {
ref?: React.ForwardedRef<HTMLInputElement>;
},
): ReturnType<typeof ComboboxWithRef>;
displayName: string;
};
Combobox.displayName = 'Combobox';
const StatusMessageWrapper: React.FC<{
showStatus: boolean;
status: string;
children: React.ReactNode;
}> = ({ showStatus, status, children }) => {
if (showStatus) {
return <span className={classes.emptyMessage}>{status}</span>;
}
return children;
};
function useGetA11yStatusMessage({
itemCount,
value,

View File

@ -14,8 +14,9 @@ interface WrapperProps extends Omit<
/**
* A basic list item component that can be used as a base for more bespoke list items.
*
* Depending on functionality, use `ListItemButton` or `ListItemLink` as a child of the
* wrapper component.
* Choose the child of the wrapper component based on needed interactivity:
* `ListItemContent` for a non-interactive item, `ListItemButton` or `ListItemLink`
* for interactive items.
*/
export const ListItemWrapper: React.FC<WrapperProps> = ({
icon,
@ -33,10 +34,31 @@ export const ListItemWrapper: React.FC<WrapperProps> = ({
);
};
interface LinkProps extends React.ComponentPropsWithoutRef<typeof Link> {
interface WithSubtitle {
subtitle?: React.ReactNode;
}
interface ContentProps
extends React.ComponentPropsWithoutRef<'h3'>, WithSubtitle {}
export const ListItemContent: React.FC<ContentProps> = ({
subtitle,
children,
...otherProps
}) => {
return (
<>
<h3 className={classes.title} {...otherProps}>
{children}
</h3>
{subtitle && <div className={classes.subtitle}>{subtitle}</div>}
</>
);
};
interface LinkProps
extends React.ComponentPropsWithoutRef<typeof Link>, WithSubtitle {}
export const ListItemLink: React.FC<LinkProps> = ({
subtitle,
children,
@ -44,20 +66,16 @@ export const ListItemLink: React.FC<LinkProps> = ({
...otherProps
}) => {
return (
<>
<h3 className={classes.title}>
<Link className={classNames(className, 'focusable')} {...otherProps}>
{children}
</Link>
</h3>
{subtitle && <div className={classes.subtitle}>{subtitle}</div>}
</>
<ListItemContent subtitle={subtitle}>
<Link className={classNames(className, 'focusable')} {...otherProps}>
{children}
</Link>
</ListItemContent>
);
};
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
subtitle?: React.ReactNode;
}
interface ButtonProps
extends React.ComponentPropsWithoutRef<'button'>, WithSubtitle {}
export const ListItemButton: React.FC<ButtonProps> = ({
subtitle,
@ -66,17 +84,14 @@ export const ListItemButton: React.FC<ButtonProps> = ({
...otherProps
}) => {
return (
<>
<h3 className={classes.title}>
<button
type='button'
className={classNames(className, 'focusable')}
{...otherProps}
>
{children}
</button>
</h3>
{subtitle && <div className={classes.subtitle}>{subtitle}</div>}
</>
<ListItemContent subtitle={subtitle}>
<button
type='button'
className={classNames(className, 'focusable')}
{...otherProps}
>
{children}
</button>
</ListItemContent>
);
};

View File

@ -7,18 +7,31 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac
import { Icon } from '../icon';
import { ListItemWrapper, ListItemButton, ListItemLink } from './index';
import {
ListItemWrapper,
ListItemContent,
ListItemButton,
ListItemLink,
} from './index';
const meta = {
title: 'Components/ListItem',
component: ListItemWrapper,
subcomponents: { ListItemButton, ListItemLink },
subcomponents: { ListItemContent, ListItemButton, ListItemLink },
} satisfies Meta<typeof ListItemWrapper>;
export default meta;
type Story = StoryObj<typeof meta>;
export const NonInteractive: Story = {
render: () => (
<ListItemWrapper icon={<Icon icon={VisibilityIcon} id='visibility' />}>
<ListItemContent>View more</ListItemContent>
</ListItemWrapper>
),
};
export const WithButton: Story = {
render: () => (
<ListItemWrapper

View File

@ -65,7 +65,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
const description = audio.getIn(['translation', 'description']) || audio.get('description');
return (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} key='audio'>
{Component => (
<Component
src={audio.get('url')}
@ -87,7 +87,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
const description = video.getIn(['translation', 'description']) || video.get('description');
return (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} key='video'>
{Component => (
<Component
preview={video.get('preview_url')}
@ -108,7 +108,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
);
} else {
return (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} key='gallery'>
{Component => (
<Component
media={mediaAttachments}

View File

@ -565,7 +565,7 @@ class Status extends ImmutablePureComponent {
);
} else if (['image', 'gifv', 'unknown'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
media.push(
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} key='gallery'>
{Component => (
<Component
media={attachments}
@ -590,7 +590,7 @@ class Status extends ImmutablePureComponent {
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media.push(
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} key='audio'>
{Component => (
<Component
src={attachment.get('url')}
@ -617,7 +617,7 @@ class Status extends ImmutablePureComponent {
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media.push(
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} key='video'>
{Component => (<Component
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}

View File

@ -1,8 +0,0 @@
[data-color-scheme='dark'] .image {
--color-skin-1: #3a3a50;
--color-skin-2: #67678e;
--color-skin-3: #44445f;
--color-outline: #21212c;
--color-shadow: #181820;
--color-highlight: #b2b1c8;
}

View File

@ -12,9 +12,6 @@ import { LimitedAccountHint } from '@/flavours/glitch/features/account_timeline/
import { areCollectionsEnabled } from '@/flavours/glitch/features/collections/utils';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { useAppDispatch } from '@/flavours/glitch/store';
import ElephantImage from '@/images/elephant_ui.svg?react';
import classes from './empty_message.module.scss';
interface EmptyMessageProps {
suspended: boolean;
@ -54,14 +51,11 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
const hasCollections = areCollectionsEnabled();
const image = <ElephantImage className={classes.image} />;
if (me === accountId) {
if (hasCollections) {
// Return only here to insert the "Create a collection" button as the action for the empty state.
return (
<EmptyState
image={image}
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
@ -140,5 +134,5 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
}
}
return <EmptyState title={title} message={message} image={image} />;
return <EmptyState title={title} message={message} />;
};

View File

@ -2,8 +2,8 @@ import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { matchPath } from 'react-router';
import { showAlert } from '@/flavours/glitch/actions/alerts';
import { initBlockModal } from '@/flavours/glitch/actions/blocks';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
@ -21,9 +21,17 @@ const messages = defineMessages({
id: 'collections.view_collection',
defaultMessage: 'View collection',
},
viewOtherCollections: {
id: 'collections.view_other_collections_by_user',
defaultMessage: 'View other collections by this user',
share: {
id: 'collections.share_short',
defaultMessage: 'Share',
},
copyLink: {
id: 'collections.copy_link',
defaultMessage: 'Copy link',
},
copyLinkConfirmation: {
id: 'collections.copy_link_confirmation',
defaultMessage: 'Copied collection link to clipboard',
},
delete: {
id: 'collections.delete_collection',
@ -33,6 +41,10 @@ const messages = defineMessages({
id: 'collections.report_collection',
defaultMessage: 'Report this collection',
},
blockOwner: {
id: 'collections.block_collection_owner',
defaultMessage: 'Block account',
},
revoke: {
id: 'collections.revoke_collection_inclusion',
defaultMessage: 'Remove myself from this collection',
@ -42,15 +54,29 @@ const messages = defineMessages({
export const CollectionMenu: React.FC<{
collection: ApiCollectionJSON;
context: 'list' | 'collection';
context: 'list' | 'notifications' | 'collection';
className?: string;
}> = ({ collection, context, className }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { id, name, account_id } = collection;
const isOwnCollection = account_id === me;
const { id, name, account_id, items } = collection;
const ownerAccount = useAccount(account_id);
const isOwnCollection = account_id === me;
const currentAccountInCollection = items.find(
(item) => item.account_id === me,
);
const openShareModal = useCallback(() => {
dispatch(
openModal({
modalType: 'SHARE_COLLECTION',
modalProps: {
collection,
},
}),
);
}, [collection, dispatch]);
const openDeleteConfirmation = useCallback(() => {
dispatch(
@ -75,9 +101,9 @@ export const CollectionMenu: React.FC<{
);
}, [collection, dispatch]);
const currentAccountInCollection = collection.items.find(
(item) => item.account_id === me,
);
const openBlockModal = useCallback(() => {
dispatch(initBlockModal(ownerAccount));
}, [ownerAccount, dispatch]);
const openRevokeConfirmation = useCallback(() => {
void dispatch(
@ -92,8 +118,28 @@ export const CollectionMenu: React.FC<{
}, [collection.id, currentAccountInCollection?.id, dispatch]);
const menu = useMemo(() => {
const viewCollectionItem: MenuItem = {
text: intl.formatMessage(messages.view),
to: `/collections/${id}`,
};
const shareItems: MenuItem[] = [
{
text: intl.formatMessage(messages.share),
action: openShareModal,
},
{
text: intl.formatMessage(messages.copyLink),
action: () => {
void navigator.clipboard.writeText(`/collections/${id}`);
dispatch(showAlert({ message: messages.copyLinkConfirmation }));
},
},
];
if (isOwnCollection) {
const commonItems: MenuItem[] = [
const ownerItems: MenuItem[] = [
...shareItems,
null,
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
@ -111,62 +157,51 @@ export const CollectionMenu: React.FC<{
];
if (context === 'list') {
return [
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
null,
...commonItems,
];
return [viewCollectionItem, ...ownerItems];
} else {
return commonItems;
return ownerItems;
}
} else {
const items: MenuItem[] = [];
const nonOwnerItems: MenuItem[] = [
viewCollectionItem,
...shareItems,
null,
];
if (ownerAccount) {
const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`;
// Don't show menu link to featured collections while on that very page
if (
!matchPath(location.pathname, {
path: featuredCollectionsPath,
exact: true,
})
) {
items.push(
...[
{
text: intl.formatMessage(messages.viewOtherCollections),
to: featuredCollectionsPath,
},
null,
],
);
}
}
if (currentAccountInCollection) {
items.push({
// Collection notifications already have a prominent 'Remove me' button
if (currentAccountInCollection && context !== 'notifications') {
nonOwnerItems.push({
text: intl.formatMessage(messages.revoke),
action: openRevokeConfirmation,
});
}
items.push({
nonOwnerItems.push({
text: intl.formatMessage(messages.report),
action: openReportModal,
});
return items;
if (currentAccountInCollection) {
nonOwnerItems.push({
text: intl.formatMessage(messages.blockOwner),
action: openBlockModal,
});
}
return nonOwnerItems;
}
}, [
isOwnCollection,
intl,
id,
openShareModal,
isOwnCollection,
dispatch,
openDeleteConfirmation,
context,
currentAccountInCollection,
openRevokeConfirmation,
ownerAccount,
openReportModal,
openBlockModal,
openRevokeConfirmation,
]);
return (

View File

@ -115,7 +115,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
);
const isCurrentUserInCollection = !isOwnCollection && currentUserIndex > -1;
const handleShare = useCallback(() => {
const openShareModal = useCallback(() => {
dispatch(
openModal({
modalType: 'SHARE_COLLECTION',
@ -132,9 +132,9 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
if (isNewCollection) {
// Replace with current pathname to clear `newCollection` state
history.replace(location.pathname);
handleShare();
openShareModal();
}
}, [history, handleShare, isNewCollection, location.pathname]);
}, [history, openShareModal, isNewCollection, location.pathname]);
return (
<header className={classes.header}>
@ -150,7 +150,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
icon='share-icon'
title={intl.formatMessage(messages.share)}
className={classes.iconButton}
onClick={handleShare}
onClick={openShareModal}
/>
<CollectionMenu
context='collection'

View File

@ -4,22 +4,22 @@ import { FormattedMessage, useIntl } from 'react-intl';
import { useHistory } 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 WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { showAlertForError } from 'flavours/glitch/actions/alerts';
import { openModal } from 'flavours/glitch/actions/modal';
import { apiFollowAccount } from 'flavours/glitch/api/accounts';
import type { ApiMutedAccountJSON } from 'flavours/glitch/api_types/accounts';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Account } from 'flavours/glitch/components/account';
import { AccountListItem } from 'flavours/glitch/components/account_list_item';
import { Avatar } from 'flavours/glitch/components/avatar';
import { Badge } from 'flavours/glitch/components/badge';
import { Button } from 'flavours/glitch/components/button';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { useAccountHandle } from 'flavours/glitch/components/display_name/default';
import { EmptyState } from 'flavours/glitch/components/empty_state';
import { FormStack, Combobox } from 'flavours/glitch/components/form_fields';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import {
FormStack,
ComboboxField,
} from 'flavours/glitch/components/form_fields';
import {
ListItemContent,
ListItemWrapper,
} from 'flavours/glitch/components/list_item';
import {
Article,
ItemList,
@ -27,110 +27,132 @@ import {
} from 'flavours/glitch/components/scrollable_list/components';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { useSearchAccounts } from 'flavours/glitch/hooks/useSearchAccounts';
import { me } from 'flavours/glitch/initial_state';
import { domain } from 'flavours/glitch/initial_state';
import {
addCollectionItem,
getCollectionItemIds,
removeCollectionItem,
updateCollectionEditorField,
} from 'flavours/glitch/reducers/slices/collections';
import { store, useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header';
import { WizardStepTitle } from './wizard_step_title';
const MAX_ACCOUNT_COUNT = 25;
function isOlderThanAWeek(date?: string): boolean {
if (!date) return false;
const targetDate = new Date(date);
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
return targetDate < sevenDaysAgo;
}
const AddedAccountItem: React.FC<{
accountId: string;
onRemove: (id: string) => void;
}> = ({ accountId, onRemove }) => {
const intl = useIntl();
const account = useAccount(accountId);
const handleRemoveAccount = useCallback(() => {
onRemove(accountId);
}, [accountId, onRemove]);
const lastStatusAt = account?.last_status_at;
const lastPostHint = useMemo(
() =>
(!lastStatusAt || isOlderThanAWeek(lastStatusAt)) && (
<Badge
label={
<FormattedMessage
id='collections.old_last_post_note'
defaultMessage='Last posted over a week ago'
/>
}
icon={<WarningIcon />}
className={classes.accountBadge}
const renderButton = useCallback(
() => (
<Button compact secondary onClick={handleRemoveAccount}>
<FormattedMessage
id='collections.remove_account'
defaultMessage='Remove'
/>
),
[lastStatusAt],
</Button>
),
[handleRemoveAccount],
);
return (
<Account
minimal
key={accountId}
id={accountId}
extraAccountInfo={lastPostHint}
>
<IconButton
title={intl.formatMessage({
id: 'collections.remove_account',
defaultMessage: 'Remove this account',
})}
icon='remove'
iconComponent={CancelIcon}
onClick={handleRemoveAccount}
/>
</Account>
);
return <AccountListItem accountId={accountId} renderButton={renderButton} />;
};
interface SuggestionItem {
id: string;
isSelected: boolean;
}
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id, isSelected }) => {
const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => {
const account = useAccount(id);
const handle = useAccountHandle(account, domain);
if (!account) return null;
return (
<>
<Avatar account={account} />
<DisplayName account={account} />
{isSelected && (
<Icon
id='checked'
icon={CheckIcon}
className={classes.selectedSuggestionIcon}
/>
)}
</>
<ListItemWrapper
className={classes.suggestion}
icon={<Avatar account={account} size={40} />}
>
<ListItemContent subtitle={handle}>
<DisplayName account={account} variant='simple' />
</ListItemContent>
</ListItemWrapper>
);
};
const renderAccountItem = (item: SuggestionItem) => (
<SuggestedAccountItem id={item.id} isSelected={item.isSelected} />
const renderAccountItem = (account: ApiMutedAccountJSON) => (
<SuggestedAccountItem id={account.id} />
);
const getItemId = (item: SuggestionItem) => item.id;
const getIsItemSelected = (item: SuggestionItem) => item.isSelected;
type GroupKey = 'available' | 'mustFollow' | 'disabled';
function groupSuggestions(accounts: ApiMutedAccountJSON[]) {
const { available, disabled } = Object.groupBy(accounts, (account) => {
if (getIsItemDisabled(account)) {
return 'disabled';
}
// if (account.locked && !relationship?.following) {
// return 'mustFollow';
// }
return 'available';
});
// Returning a new object ensures a fixed property order
return { available, disabled };
}
const renderGroupTitle = (groupKey: GroupKey, titleId: string) => {
if (groupKey === 'available') {
return null;
}
let title: React.ReactElement;
let description: React.ReactElement;
if (groupKey === 'mustFollow') {
title = (
<FormattedMessage
id='collections.suggestions.must_follow'
defaultMessage='Must follow first'
/>
);
description = (
<FormattedMessage
id='collections.suggestions.must_follow_desc'
defaultMessage='These accounts review all follow requests. Followers can add them to collections.'
/>
);
} else {
title = (
<FormattedMessage
id='collections.suggestions.can_not_add'
defaultMessage='Cant be added'
/>
);
description = (
<FormattedMessage
id='collections.suggestions.can_not_add_desc'
defaultMessage='These accounts may have opted out of discovery, or they might be on a server that doesnt support collections.'
/>
);
}
return (
<ListItemWrapper className={classes.suggestionGroup}>
<ListItemContent id={titleId} subtitle={description}>
{title}
</ListItemContent>
</ListItemWrapper>
);
};
const getItemId = (account: ApiMutedAccountJSON) => account.id;
// Disable accounts who can't be added to a collection
const getIsItemDisabled = (account: ApiMutedAccountJSON) =>
!['automatic', 'manual'].includes(account.feature_approval.current_user);
export const CollectionAccounts: React.FC<{
collection?: ApiCollectionJSON | null;
@ -139,9 +161,8 @@ export const CollectionAccounts: React.FC<{
const dispatch = useAppDispatch();
const history = useHistory();
const { id, items } = collection ?? {};
const { id, items: collectionItems } = collection ?? {};
const isEditMode = !!id;
const collectionItems = items;
const addedAccountIds = useAppSelector(
(state) => state.collections.editor.accountIds,
@ -157,24 +178,20 @@ export const CollectionAccounts: React.FC<{
const [searchValue, setSearchValue] = useState('');
const hasAccounts = accountIds.length > 0;
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
const {
accountIds: suggestedAccountIds,
accounts: suggestedAccounts,
isLoading: isLoadingSuggestions,
searchAccounts,
resetAccounts,
} = useSearchAccounts({
withRelationships: true,
filterResults: (account) =>
// Only suggest accounts who allow being featured/recommended
account.feature_approval.current_user === 'automatic',
// Don't suggest accounts that were already added
filterResults: (account) => !accountIds.includes(account.id),
});
const suggestedItems = suggestedAccountIds.map((id) => ({
id,
isSelected: accountIds.includes(id),
}));
const handleSearchValueChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
@ -192,43 +209,6 @@ export const CollectionAccounts: React.FC<{
[],
);
const relationships = useAppSelector((state) => state.relationships);
const confirmFollowStatus = useCallback(
(accountId: string, onFollowing: () => void) => {
const relationship = relationships.get(accountId);
if (!relationship) {
return;
}
if (
accountId === me ||
relationship.following ||
relationship.requested
) {
onFollowing();
} else {
dispatch(
openModal({
modalType: 'CONFIRM_FOLLOW_TO_COLLECTION',
modalProps: {
accountId,
onConfirm: () => {
apiFollowAccount(accountId)
.then(onFollowing)
.catch((err: unknown) => {
store.dispatch(showAlertForError(err));
});
},
},
}),
);
}
},
[dispatch, relationships],
);
const removeAccountItem = useCallback(
(accountId: string) => {
dispatch(
@ -242,28 +222,15 @@ export const CollectionAccounts: React.FC<{
);
const addAccountItem = useCallback(
(accountId: string) => {
confirmFollowStatus(accountId, () => {
dispatch(
updateCollectionEditorField({
field: 'accountIds',
value: [...accountIds, accountId],
}),
);
});
(item: ApiMutedAccountJSON) => {
dispatch(
updateCollectionEditorField({
field: 'accountIds',
value: [...accountIds, item.id],
}),
);
},
[accountIds, confirmFollowStatus, dispatch],
);
const toggleAccountItem = useCallback(
(item: SuggestionItem) => {
if (accountIds.includes(item.id)) {
removeAccountItem(item.id);
} else {
addAccountItem(item.id);
}
},
[accountIds, addAccountItem, removeAccountItem],
[accountIds, dispatch],
);
const instantRemoveAccountItem = useCallback(
@ -289,23 +256,14 @@ export const CollectionAccounts: React.FC<{
);
const instantAddAccountItem = useCallback(
(collectionId: string, accountId: string) => {
confirmFollowStatus(accountId, () => {
void dispatch(addCollectionItem({ collectionId, accountId }));
});
},
[confirmFollowStatus, dispatch],
);
const instantToggleAccountItem = useCallback(
(item: SuggestionItem) => {
if (accountIds.includes(item.id)) {
instantRemoveAccountItem(item.id);
} else if (id) {
instantAddAccountItem(id, item.id);
(item: ApiMutedAccountJSON) => {
if (id) {
void dispatch(
addCollectionItem({ collectionId: id, accountId: item.id }),
);
}
},
[accountIds, id, instantAddAccountItem, instantRemoveAccountItem],
[dispatch, id],
);
const handleRemoveAccountItem = useCallback(
@ -319,6 +277,20 @@ export const CollectionAccounts: React.FC<{
[isEditMode, instantRemoveAccountItem, removeAccountItem],
);
const handleSelectItem = useCallback(
(item: ApiMutedAccountJSON) => {
if (isEditMode) {
instantAddAccountItem(item);
} else {
addAccountItem(item);
}
setSearchValue('');
resetAccounts();
},
[addAccountItem, instantAddAccountItem, isEditMode, resetAccounts],
);
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
@ -333,117 +305,116 @@ export const CollectionAccounts: React.FC<{
);
const inputId = useId();
const inputLabel = intl.formatMessage({
id: 'collections.search_accounts_label',
defaultMessage: 'Search for accounts to add…',
});
const AccountsHeadingElement = id ? 'h2' : 'h3';
return (
<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.'
/>
}
/>
)}
<label htmlFor={inputId} className='sr-only'>
{inputLabel}
</label>
<Combobox
id={inputId}
placeholder={inputLabel}
value={hasMaxAccounts ? '' : searchValue}
onChange={handleSearchValueChange}
onKeyDown={handleSearchKeyDown}
disabled={hasMaxAccounts}
isLoading={isLoadingSuggestions}
items={suggestedItems}
getItemId={getItemId}
getIsItemSelected={getIsItemSelected}
renderItem={renderAccountItem}
onSelectItem={
isEditMode ? instantToggleAccountItem : toggleAccountItem
}
closeOnSelect={false}
/>
{hasMaxAccounts && (
<FormattedMessage
id='collections.search_accounts_max_reached'
defaultMessage='You have added the maximum number of accounts'
/>
)}
<Scrollable className={classes.scrollableWrapper}>
<ItemList
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,
}}
/>
}
/>
}
>
{accountIds.map((accountId, index) => (
<Article
key={accountId}
aria-posinset={index}
aria-setsize={accountIds.length}
>
<AddedAccountItem
accountId={accountId}
onRemove={handleRemoveAccountItem}
/>
</Article>
))}
</ItemList>
</Scrollable>
</FormStack>
{!isEditMode && (
<div className={classes.stickyFooter}>
<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>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<header className={classes.header}>
{!id && (
<WizardStepTitle
step={1}
title={
<FormattedMessage
id='collections.continue'
defaultMessage='Continue'
id='collections.create.accounts_title'
defaultMessage='Who will you feature in this collection?'
/>
)}
</Button>
</div>
}
/>
)}
<ComboboxField
id={inputId}
label={intl.formatMessage({
id: 'collections.search_accounts_label',
defaultMessage: 'Search for an account to add',
})}
value={hasMaxAccounts ? '' : searchValue}
onChange={handleSearchValueChange}
onKeyDown={handleSearchKeyDown}
disabled={hasMaxAccounts}
isLoading={isLoadingSuggestions}
items={groupSuggestions(suggestedAccounts)}
getItemId={getItemId}
getIsItemDisabled={getIsItemDisabled}
renderItem={renderAccountItem}
renderGroupTitle={renderGroupTitle}
onSelectItem={handleSelectItem}
status={
hasMaxAccounts
? {
variant: 'warning',
message: intl.formatMessage({
id: 'collections.search_accounts_max_reached',
defaultMessage:
'You have added the maximum number of accounts',
}),
}
: null
}
/>
</header>
<div>
{hasAccounts && (
<AccountsHeadingElement className={classes.listHeading}>
<FormattedMessage
id='collections.hints.accounts_counter'
defaultMessage='{count}/{max} accounts'
values={{ count: accountIds.length, max: MAX_ACCOUNT_COUNT }}
/>
</AccountsHeadingElement>
)}
<Scrollable className={classes.scrollableWrapper}>
<ItemList
emptyMessage={
<EmptyState
title={
<FormattedMessage
id='collections.accounts.empty_editor_title'
defaultMessage='No one is in this collection yet'
/>
}
message={
<FormattedMessage
id='collections.accounts.empty_description'
defaultMessage='Add up to {count} accounts'
values={{
count: MAX_ACCOUNT_COUNT,
}}
/>
}
/>
}
>
{accountIds.map((accountId, index) => (
<Article
key={accountId}
aria-posinset={index}
aria-setsize={accountIds.length}
>
<AddedAccountItem
accountId={accountId}
onRemove={handleRemoveAccountItem}
/>
</Article>
))}
</ItemList>
</Scrollable>
</div>
</FormStack>
{!isEditMode && hasAccounts && (
<div className={classes.stickyFooter}>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage
id='collections.continue'
defaultMessage='Continue'
/>
)}
</Button>
</div>
)}
</form>

View File

@ -36,7 +36,7 @@ import {
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header';
import { WizardStepTitle } from './wizard_step_title';
export const CollectionDetails: React.FC = () => {
const dispatch = useAppDispatch();
@ -152,7 +152,7 @@ export const CollectionDetails: React.FC = () => {
<form onSubmit={handleSubmit} className={classes.form}>
<FormStack className={classes.formFieldStack}>
{!id && (
<WizardStepHeader
<WizardStepTitle
step={2}
title={
<FormattedMessage
@ -276,18 +276,16 @@ export const CollectionDetails: React.FC = () => {
</FormStack>
<div className={classes.stickyFooter}>
<div className={classes.actionWrapper}>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage
id='collections.create_collection'
defaultMessage='Create collection'
/>
)}
</Button>
</div>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage
id='collections.create_collection'
defaultMessage='Create collection'
/>
)}
</Button>
</div>
</form>
);

View File

@ -44,7 +44,7 @@ export const messages = defineMessages({
},
});
function usePageTitle(id: string | undefined) {
function usePageTitle(id: string | null) {
const { path } = useRouteMatch();
const location = useLocation();
@ -66,7 +66,7 @@ export const CollectionEditorPage: React.FC<{
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { id } = useParams<{ id?: string }>();
const { id = null } = useParams<{ id?: string }>();
const { path } = useRouteMatch();
const collection = useAppSelector((state) =>
id ? state.collections.collections[id] : undefined,

View File

@ -1,10 +1,17 @@
.header {
display: flex;
flex-direction: column;
gap: 12px;
}
.step {
font-size: 13px;
color: var(--color-text-secondary);
}
.title {
font-size: 22px;
font-size: 17px;
font-weight: 500;
line-height: 1.2;
margin-top: 4px;
}
@ -14,79 +21,56 @@
margin-top: 8px;
}
/* Make form stretch full height of the column */
.listHeading {
margin-bottom: 8px;
font-size: 15px;
font-weight: 500;
line-height: 1.2;
}
/* Ensure sticky footer isn't covered by mobile bottom nav */
.form {
--bottom-spacing: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100%;
padding-bottom: var(--bottom-spacing);
padding-bottom: 0;
@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,
.scrollableInner {
margin-inline: -8px;
}
.submitDisabledCallout {
align-self: center;
}
.stickyFooter {
position: sticky;
bottom: var(--bottom-spacing);
margin-top: -16px;
padding: 16px;
background-image: linear-gradient(
to bottom,
transparent,
var(--color-bg-primary) 32px
var(--color-bg-primary) 24px
);
}
.itemCountReadout {
text-align: center;
.formFieldStack {
gap: 16px;
}
.actionWrapper {
display: flex;
flex-direction: column;
width: min-content;
min-width: 240px;
margin-inline: auto;
gap: 8px;
.scrollableWrapper {
margin-inline: -16px;
}
.accountBadge {
margin-inline-start: 56px;
.suggestion {
padding: 4px 0;
@container (width < 360px) {
margin-top: 4px;
margin-inline-start: 46px;
[aria-disabled='true'] & {
opacity: 0.6;
}
}
.suggestionGroup {
padding: 4px 0;
// Undo default group styles:
font-weight: 400;
text-transform: none;
}

View File

@ -1,23 +0,0 @@
import { FormattedMessage } from 'react-intl';
import classes from './styles.module.scss';
export const WizardStepHeader: React.FC<{
step: number;
title: React.ReactElement;
description?: React.ReactElement;
}> = ({ step, title, description }) => {
return (
<header>
<FormattedMessage
id='collections.create.steps'
defaultMessage='Step {step}/{total}'
values={{ step, total: 2 }}
>
{(content) => <p className={classes.step}>{content}</p>}
</FormattedMessage>
<h2 className={classes.title}>{title}</h2>
{!!description && <p className={classes.description}>{description}</p>}
</header>
);
};

View File

@ -0,0 +1,21 @@
import { FormattedMessage } from 'react-intl';
import classes from './styles.module.scss';
export const WizardStepTitle: React.FC<{
step: number;
title: React.ReactElement;
}> = ({ step, title }) => {
return (
<div>
<p className={classes.step}>
<FormattedMessage
id='collections.create.steps'
defaultMessage='Step {step}/{total}'
values={{ step, total: 2 }}
/>
</p>
<h2 className={classes.title}>{title}</h2>
</div>
);
};

View File

@ -164,7 +164,7 @@ const ListMembers: React.FC<{
const [mode, setMode] = useState<Mode>('remove');
const {
accountIds: searchAccountIds,
accounts: accountsFromSearch,
isLoading: loadingSearchResults,
searchAccounts: handleSearch,
} = useSearchAccounts({
@ -177,6 +177,7 @@ const ListMembers: React.FC<{
}
},
});
const accountIdsFromSearch = accountsFromSearch.map((item) => item.id);
useEffect(() => {
if (id) {
@ -220,7 +221,7 @@ const ListMembers: React.FC<{
let displayedAccountIds: string[];
if (mode === 'add' && searching) {
displayedAccountIds = searchAccountIds;
displayedAccountIds = accountIdsFromSearch;
} else {
displayedAccountIds = accountIds;
}

View File

@ -0,0 +1,17 @@
.actions {
display: flex;
align-items: center;
}
.menuButton {
box-sizing: content-box;
margin-inline-start: auto;
padding: 4px;
border-radius: 4px;
border: 1px solid var(--color-border-primary);
svg {
width: 20px;
height: 20px;
}
}

View File

@ -2,8 +2,12 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { DisplayNameSimple } from '@/flavours/glitch/components/display_name/simple';
import { Button } from '@/flavours/glitch/components/button';
import { LinkedDisplayName } from '@/flavours/glitch/components/display_name';
import { Icon } from '@/flavours/glitch/components/icon';
import { CollectionMenu } from '@/flavours/glitch/features/collections/components/collection_menu';
import { CollectionPreviewCard } from '@/flavours/glitch/features/collections/components/collection_preview_card';
import { useConfirmRevoke } from '@/flavours/glitch/features/collections/detail/revoke_collection_inclusion_modal';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?react';
import type {
@ -11,7 +15,7 @@ import type {
NotificationGroupCollectionUpdate,
} from 'flavours/glitch/models/notification_group';
import { CollectionPreviewCard } from '../../collections/components/collection_preview_card';
import classes from './notification_collection.module.scss';
export const NotificationCollection: React.FC<{
notification:
@ -21,6 +25,7 @@ export const NotificationCollection: React.FC<{
}> = ({ notification, unread }) => {
const { collection, type } = notification;
const collectionCreatorAccount = useAccount(collection.account_id);
const confirmRevoke = useConfirmRevoke(collection);
return (
<div
@ -43,7 +48,12 @@ export const NotificationCollection: React.FC<{
defaultMessage='{name} added you to a collection'
values={{
name: (
<DisplayNameSimple account={collectionCreatorAccount} />
<LinkedDisplayName
displayProps={{
variant: 'simple',
account: collectionCreatorAccount,
}}
/>
),
}}
/>
@ -54,7 +64,12 @@ export const NotificationCollection: React.FC<{
defaultMessage='{name} edited a collection youre in'
values={{
name: (
<DisplayNameSimple account={collectionCreatorAccount} />
<LinkedDisplayName
displayProps={{
variant: 'simple',
account: collectionCreatorAccount,
}}
/>
),
}}
/>
@ -63,6 +78,26 @@ export const NotificationCollection: React.FC<{
</div>
<CollectionPreviewCard collection={collection} />
<div className={classes.actions}>
<Button
compact
secondary
className='button--destructive'
onClick={confirmRevoke}
>
<FormattedMessage
id='collections.detail.revoke_inclusion'
defaultMessage='Remove me'
/>
</Button>
<CollectionMenu
context='notifications'
collection={collection}
className={classes.menuButton}
/>
</div>
</div>
</div>
);

View File

@ -1,43 +0,0 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
title: {
id: 'confirmations.follow_to_collection.title',
defaultMessage: 'Follow account?',
},
confirm: {
id: 'confirmations.follow_to_collection.confirm',
defaultMessage: 'Follow and add to collection',
},
});
export const ConfirmFollowToCollectionModal: React.FC<
{
accountId: string;
onConfirm: () => void;
} & BaseConfirmationModalProps
> = ({ accountId, onConfirm, onClose }) => {
const intl = useIntl();
const account = useAccount(accountId);
return (
<ConfirmationModal
title={intl.formatMessage(messages.title)}
message={
<FormattedMessage
id='confirmations.follow_to_collection.message'
defaultMessage='You need to be following {name} to add them to a collection.'
values={{ name: <strong>@{account?.acct}</strong> }}
/>
}
confirm={intl.formatMessage(messages.confirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@ -13,7 +13,6 @@ export { ConfirmUnblockModal } from './unblock';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list';
export { ConfirmFollowToCollectionModal } from './follow_to_collection';
export { ConfirmMissingAltTextModal } from './missing_alt_text';
export { ConfirmRevokeQuoteModal } from './revoke_quote';
export { QuietPostQuoteInfoModal } from './quiet_post_quote_info';

View File

@ -40,7 +40,6 @@ import {
ConfirmClearNotificationsModal,
ConfirmLogOutModal,
ConfirmFollowToListModal,
ConfirmFollowToCollectionModal,
ConfirmMissingAltTextModal,
ConfirmRevokeQuoteModal,
QuietPostQuoteInfoModal,
@ -75,7 +74,6 @@ export const MODAL_COMPONENTS = {
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_FOLLOW_TO_COLLECTION': () => Promise.resolve({ default: ConfirmFollowToCollectionModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),

View File

@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
@ -21,7 +21,7 @@ export function useSearchAccounts({
} = {}) {
const dispatch = useAppDispatch();
const [accountIds, setAccountIds] = useState<string[]>([]);
const [accounts, setAccounts] = useState<ApiAccountJSON[]>([]);
const [loadingState, setLoadingState] = useState<
'idle' | 'loading' | 'error'
>('idle');
@ -37,7 +37,7 @@ export function useSearchAccounts({
if (value.trim().length === 0) {
onSettled?.('');
if (resetOnInputClear) {
setAccountIds([]);
setAccounts([]);
}
return;
}
@ -60,7 +60,7 @@ export function useSearchAccounts({
if (withRelationships) {
dispatch(fetchRelationships(accountIds));
}
setAccountIds(accountIds);
setAccounts(accounts);
setLoadingState('idle');
onSettled?.(value);
})
@ -73,9 +73,14 @@ export function useSearchAccounts({
{ leading: true, trailing: true },
);
const resetAccounts = useCallback(() => {
setAccounts([]);
}, []);
return {
searchAccounts,
accountIds,
resetAccounts,
accounts,
isLoading: loadingState === 'loading',
isError: loadingState === 'error',
};

View File

@ -35,3 +35,12 @@
text-wrap: pretty;
}
}
[data-color-scheme='dark'] .defaultImage {
--color-skin-1: #3a3a50;
--color-skin-2: #67678e;
--color-skin-3: #44445f;
--color-outline: #21212c;
--color-shadow: #181820;
--color-highlight: #b2b1c8;
}

View File

@ -29,12 +29,6 @@ export const Default: Story = {
},
};
export const WithoutMessage: Story = {
args: {
message: undefined,
},
};
export const WithAction: Story = {
args: {
...Default.args,
@ -42,3 +36,17 @@ export const WithAction: Story = {
children: <Button onClick={() => action('Refresh')}>Refresh</Button>,
},
};
export const WithoutImage: Story = {
args: {
...Default.args,
image: null,
},
};
export const WithoutMessage: Story = {
args: {
...Default.args,
message: undefined,
},
};

View File

@ -1,31 +1,39 @@
import { FormattedMessage } from 'react-intl';
import ElephantImage from '@/images/elephant_ui.svg?react';
import classes from './empty_state.module.scss';
const images = {
default: <ElephantImage className={classes.defaultImage} />,
};
/**
* Simple empty state component with a neutral default title and customisable message.
*
* Action buttons can be passed as `children`
* Action buttons can be passed as `children`.
*/
export const EmptyState: React.FC<{
image?: React.ReactNode;
image?: keyof typeof images | React.ReactElement | null;
title?: React.ReactNode;
message?: React.ReactNode;
children?: React.ReactNode;
}> = ({
image,
image = 'default',
title = (
<FormattedMessage id='empty_state.no_results' defaultMessage='No results' />
),
message,
children,
}) => {
const imageToRender = typeof image === 'string' ? images[image] : image;
return (
<div className={classes.wrapper}>
{(title || message || image) && (
{(title || message || imageToRender) && (
<div className={classes.content}>
{image}
{imageToRender}
{!!title && <h3>{title}</h3>}
{!!message && <p>{message}</p>}
</div>

View File

@ -25,7 +25,7 @@
.popover {
z-index: 9999;
box-sizing: border-box;
max-height: max(200px, 30dvh);
max-height: min(320px, 50dvh);
padding: 4px;
border-radius: 4px;
color: var(--color-text-primary);
@ -38,6 +38,18 @@
overscroll-behavior-y: contain;
}
.groupLabel {
padding-block: 12px 4px;
padding-inline: 12px;
font-size: 13px;
font-weight: bold;
text-transform: uppercase;
ul:first-child > & {
padding-top: 4px;
}
}
.menuItem {
display: flex;
align-items: center;
@ -51,13 +63,7 @@
user-select: none;
&[data-highlighted='true'] {
color: var(--color-text-on-brand-base);
background: var(--color-bg-brand-base);
&[aria-disabled='true'] {
color: var(--color-text-on-disabled);
background: var(--color-bg-disabled);
}
background: var(--color-bg-overlay-highlight);
}
&[aria-disabled='true'] {

View File

@ -4,40 +4,46 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { ComboboxField } from './combobox_field';
const ComboboxDemo: React.FC = () => {
interface Fruit {
id: string;
name: string;
type: 'citrus' | 'berryish' | 'seedy' | 'stony' | 'longish' | 'chonky';
disabled?: boolean;
}
const ComboboxDemo: React.FC<{ withGroups?: boolean }> = ({ withGroups }) => {
const [searchValue, setSearchValue] = useState('');
const items = [
{ id: '1', name: 'Apple' },
{ id: '2', name: 'Banana' },
{ id: '3', name: 'Cherry', disabled: true },
{ id: '4', name: 'Date' },
{ id: '5', name: 'Fig', disabled: true },
{ id: '6', name: 'Grape' },
{ id: '7', name: 'Honeydew' },
{ id: '8', name: 'Kiwi' },
{ id: '9', name: 'Lemon' },
{ id: '10', name: 'Mango' },
{ id: '11', name: 'Nectarine' },
{ id: '12', name: 'Orange' },
{ id: '13', name: 'Papaya' },
{ id: '14', name: 'Quince' },
{ id: '15', name: 'Raspberry' },
{ id: '16', name: 'Strawberry' },
{ id: '17', name: 'Tangerine' },
{ id: '19', name: 'Vanilla bean' },
{ id: '20', name: 'Watermelon' },
{ id: '22', name: 'Yellow Passion Fruit' },
{ id: '23', name: 'Zucchini' },
{ id: '24', name: 'Cantaloupe' },
{ id: '25', name: 'Blackberry' },
{ id: '26', name: 'Persimmon' },
{ id: '27', name: 'Lychee' },
{ id: '28', name: 'Dragon Fruit' },
{ id: '29', name: 'Passion Fruit' },
{ id: '30', name: 'Starfruit' },
const items: Fruit[] = [
{ id: '1', name: 'Apple', type: 'seedy' },
{ id: '2', name: 'Banana', type: 'longish' },
{ id: '3', name: 'Cherry', type: 'berryish', disabled: true },
{ id: '4', name: 'Date', type: 'stony' },
{ id: '5', name: 'Fig', type: 'seedy', disabled: true },
{ id: '6', name: 'Grape', type: 'berryish' },
{ id: '7', name: 'Honeydew', type: 'chonky' },
{ id: '8', name: 'Kiwi', type: 'seedy' },
{ id: '9', name: 'Lemon', type: 'citrus' },
{ id: '10', name: 'Mango', type: 'stony' },
{ id: '11', name: 'Nectarine', type: 'stony' },
{ id: '12', name: 'Orange', type: 'citrus' },
{ id: '13', name: 'Papaya', type: 'seedy' },
{ id: '14', name: 'Quince', type: 'seedy' },
{ id: '15', name: 'Raspberry', type: 'berryish' },
{ id: '16', name: 'Strawberry', type: 'berryish' },
{ id: '17', name: 'Tangerine', type: 'citrus' },
{ id: '19', name: 'Vanilla bean', type: 'longish' },
{ id: '20', name: 'Watermelon', type: 'chonky' },
{ id: '22', name: 'Yellow Passion Fruit', type: 'seedy' },
{ id: '23', name: 'Zucchini', type: 'longish' },
{ id: '24', name: 'Cantaloupe', type: 'chonky' },
{ id: '25', name: 'Blackberry', type: 'berryish' },
{ id: '26', name: 'Persimmon', type: 'seedy' },
{ id: '27', name: 'Lychee', type: 'berryish' },
{ id: '28', name: 'Dragon Fruit', type: 'seedy' },
{ id: '29', name: 'Passion Fruit', type: 'seedy' },
{ id: '30', name: 'Starfruit', type: 'seedy' },
];
type Fruit = (typeof items)[number];
const getItemId = useCallback((item: Fruit) => item.id, []);
const getIsItemDisabled = useCallback((item: Fruit) => !!item.disabled, []);
@ -66,12 +72,16 @@ const ComboboxDemo: React.FC = () => {
)
: items;
const groupedResults = withGroups
? Object.groupBy(results, (item) => item.type)
: results;
return (
<ComboboxField
label='Favourite fruit'
value={searchValue}
onChange={handleSearchValueChange}
items={results}
items={groupedResults}
getItemId={getItemId}
getIsItemDisabled={getIsItemDisabled}
onSelectItem={selectFruit}
@ -90,15 +100,23 @@ export default meta;
type Story = StoryObj<typeof meta>;
export const Example: Story = {
args: {
// Adding these types to keep TS happy, they're not passed on to `ComboboxDemo`
label: '',
value: '',
onChange: () => undefined,
items: [],
getItemId: () => '',
renderItem: () => <>Nothing</>,
onSelectItem: () => undefined,
},
// These args are just used to keep TS happy,
// they're not passed to `ComboboxDemo`
const dummyArgs = {
label: '',
value: '',
onChange: () => undefined,
items: [],
getItemId: () => '',
renderItem: () => <>Nothing</>,
onSelectItem: () => undefined,
};
export const Simple: Story = {
args: dummyArgs,
};
export const WithGroups: Story = {
render: () => <ComboboxDemo withGroups />,
args: dummyArgs,
};

View File

@ -1,4 +1,11 @@
import { forwardRef, useCallback, useId, useRef, useState } from 'react';
import {
forwardRef,
useCallback,
useId,
useMemo,
useRef,
useState,
} from 'react';
import { useIntl } from 'react-intl';
@ -28,10 +35,10 @@ export interface ComboboxItemState {
isDisabled: boolean;
}
interface ComboboxProps<T extends ComboboxItem> extends Omit<
TextInputProps,
'icon'
> {
interface ComboboxProps<
Item extends ComboboxItem,
GroupKey extends string,
> extends Omit<TextInputProps, 'icon'> {
/**
* The value of the combobox's text input
*/
@ -46,34 +53,45 @@ interface ComboboxProps<T extends ComboboxItem> extends Omit<
*/
isLoading?: boolean;
/**
* The set of options/suggestions that should be rendered in the dropdown menu.
* The set of options/suggestions that should be rendered in the dropdown menu,
* optionally separated into groups by providing an object
*/
items: T[];
items: Item[] | Partial<Record<GroupKey, Item[]>>;
/**
* A function that must return a unique id for each option passed via `items`
*/
getItemId?: (item: T) => string;
getItemId?: (item: Item) => string;
/**
* Providing this function turns the combobox into a multi-select box that assumes
* multiple options to be selectable. Single-selection is handled automatically.
*/
getIsItemSelected?: (item: T) => boolean;
getIsItemSelected?: (item: Item) => boolean;
/**
* Use this function to mark items as disabled, if needed
*/
getIsItemDisabled?: (item: T) => boolean;
getIsItemDisabled?: (item: Item) => boolean;
/**
* Customise the rendering of each option.
* The rendered content must not contain other interactive content!
*/
renderItem: (
item: T,
item: Item,
state: ComboboxItemState,
) => React.ReactElement | string;
/**
* Customise the rendering of group titles.
* The `titleId` must be attached to the element that provides the
* accessible name for the group.
* Return `null` to omit rendering the group title.
*/
renderGroupTitle?: (
groupKey: GroupKey,
titleId: string,
) => React.ReactElement | null;
/**
* The main selection handler, called when an option is selected or deselected.
*/
onSelectItem: (item: T) => void;
onSelectItem: (item: Item) => void;
/**
* Icon to be displayed in the text input
*/
@ -88,8 +106,8 @@ interface ComboboxProps<T extends ComboboxItem> extends Omit<
suppressMenu?: boolean;
}
interface Props<T extends ComboboxItem>
extends ComboboxProps<T>, CommonFieldWrapperProps {}
interface Props<Item extends ComboboxItem, GroupKey extends string>
extends ComboboxProps<Item, GroupKey>, CommonFieldWrapperProps {}
/**
* The combobox field allows users to select one or more items
@ -100,8 +118,11 @@ interface Props<T extends ComboboxItem>
* [research & implementations](https://sarahmhigley.com/writing/select-your-poison/).
*/
export const ComboboxFieldWithRef = <T extends ComboboxItem>(
{ id, label, hint, status, required, ...otherProps }: Props<T>,
export const ComboboxFieldWithRef = <
Item extends ComboboxItem,
GroupKey extends string,
>(
{ id, label, hint, status, required, ...otherProps }: Props<Item, GroupKey>,
ref: React.ForwardedRef<HTMLInputElement>,
) => (
<FormFieldWrapper
@ -118,15 +139,17 @@ export const ComboboxFieldWithRef = <T extends ComboboxItem>(
// 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 ComboboxItem>(
props: Props<T> & { ref?: React.ForwardedRef<HTMLInputElement> },
<Item extends ComboboxItem, GroupKey extends string>(
props: Props<Item, GroupKey> & {
ref?: React.ForwardedRef<HTMLInputElement>;
},
): ReturnType<typeof ComboboxFieldWithRef>;
displayName: string;
};
ComboboxField.displayName = 'ComboboxField';
const ComboboxWithRef = <T extends ComboboxItem>(
const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
{
value,
isLoading = false,
@ -135,6 +158,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
getIsItemDisabled,
getIsItemSelected,
disabled,
renderGroupTitle,
renderItem,
onSelectItem,
onChange,
@ -144,7 +168,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
icon = SearchIcon,
className,
...otherProps
}: ComboboxProps<T>,
}: ComboboxProps<Item, GroupKey>,
ref: React.ForwardedRef<HTMLInputElement>,
) => {
const intl = useIntl();
@ -157,15 +181,28 @@ const ComboboxWithRef = <T extends ComboboxItem>(
);
const [shouldMenuOpen, setShouldMenuOpen] = useState(false);
const hasGroups = !Array.isArray(items);
const flatItems = useMemo(
() =>
hasGroups
? (Object.values(items)
.flat()
.filter((i) => !!i) as Item[])
: items,
[hasGroups, items],
);
const statusMessage = useGetA11yStatusMessage({
value,
isLoading,
itemCount: items.length,
itemCount: flatItems.length,
});
const showStatusMessageInMenu =
!!statusMessage && value.length > 0 && items.length === 0;
!!statusMessage && value.length > 0 && flatItems.length === 0;
const hasMenuContent =
!disabled && !suppressMenu && (items.length > 0 || showStatusMessageInMenu);
!disabled &&
!suppressMenu &&
(flatItems.length > 0 || showStatusMessageInMenu);
const isMenuOpen = shouldMenuOpen && hasMenuContent;
const openMenu = useCallback(() => {
@ -178,10 +215,10 @@ const ComboboxWithRef = <T extends ComboboxItem>(
}, []);
const resetHighlight = useCallback(() => {
const firstItem = items[0];
const firstItem = flatItems[0];
const firstItemId = firstItem ? getItemId(firstItem) : null;
setHighlightedItemId(firstItemId);
}, [getItemId, items]);
}, [getItemId, flatItems]);
const highlightItem = useCallback((id: string | null) => {
setHighlightedItemId(id);
@ -216,7 +253,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
const selectItem = useCallback(
(itemId: string | null) => {
const item = items.find((item) => item.id === itemId);
const item = flatItems.find((item) => item.id === itemId);
if (item) {
const isDisabled = getIsItemDisabled?.(item) ?? false;
if (!isDisabled) {
@ -229,7 +266,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
}
inputRef.current?.focus();
},
[closeMenu, closeOnSelect, getIsItemDisabled, items, onSelectItem],
[closeMenu, closeOnSelect, getIsItemDisabled, flatItems, onSelectItem],
);
const handleSelectItem = useCallback(
@ -246,38 +283,38 @@ const ComboboxWithRef = <T extends ComboboxItem>(
const moveHighlight = useCallback(
(direction: number) => {
if (items.length === 0) {
if (flatItems.length === 0) {
return;
}
const highlightedItemIndex = items.findIndex(
const highlightedItemIndex = flatItems.findIndex(
(item) => getItemId(item) === highlightedItemId,
);
if (highlightedItemIndex === -1) {
// If no item is highlighted yet, highlight the first or last
if (direction > 0) {
const firstItem = items.at(0);
const firstItem = flatItems.at(0);
highlightItem(firstItem ? getItemId(firstItem) : null);
} else {
const lastItem = items.at(-1);
const lastItem = flatItems.at(-1);
highlightItem(lastItem ? getItemId(lastItem) : null);
}
} else {
// If there is a highlighted item, select the next or previous item
// and wrap around at the start or end:
let newIndex = highlightedItemIndex + direction;
if (newIndex >= items.length) {
if (newIndex >= flatItems.length) {
newIndex = 0;
} else if (newIndex < 0) {
newIndex = items.length - 1;
newIndex = flatItems.length - 1;
}
const newHighlightedItem = items[newIndex];
const newHighlightedItem = flatItems[newIndex];
highlightItem(
newHighlightedItem ? getItemId(newHighlightedItem) : null,
);
}
},
[getItemId, highlightItem, highlightedItemId, items],
[getItemId, highlightItem, highlightedItemId, flatItems],
);
useOnClickOutside(wrapperRef, closeMenu);
@ -331,6 +368,38 @@ const ComboboxWithRef = <T extends ComboboxItem>(
],
);
const renderItems = (items: 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}
data-highlighted={isHighlighted}
aria-selected={isSelected}
aria-disabled={isDisabled}
data-item-id={id}
onMouseEnter={handleItemMouseEnter}
onClick={handleSelectItem}
>
{renderItem(item, {
isSelected,
isDisabled: isDisabled ?? false,
})}
</li>
);
});
const mergeRefs = useCallback(
(element: HTMLInputElement | null) => {
inputRef.current = element;
@ -406,42 +475,50 @@ const ComboboxWithRef = <T extends ComboboxItem>(
>
{({ props, placement }) => (
<div {...props} className={classNames(classes.popover, placement)}>
{showStatusMessageInMenu ? (
<span className={classes.emptyMessage}>{statusMessage}</span>
) : (
<ul role='listbox' id={listId} tabIndex={-1}>
{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}
data-highlighted={isHighlighted}
aria-selected={isSelected}
aria-disabled={isDisabled}
data-item-id={id}
onMouseEnter={handleItemMouseEnter}
onClick={handleSelectItem}
>
{renderItem(item, {
isSelected,
isDisabled: isDisabled ?? false,
})}
</li>
);
})}
</ul>
)}
<StatusMessageWrapper
showStatus={showStatusMessageInMenu}
status={statusMessage}
>
{hasGroups ? (
<div role='listbox' id={listId} tabIndex={-1}>
{(Object.keys(items) as GroupKey[]).map((groupKey) => {
const groupItems = items[groupKey];
const groupTitleId = `${listId}-group-${groupKey}`;
const customGroupTitle = renderGroupTitle?.(
groupKey,
groupTitleId,
);
const hasTitle = customGroupTitle !== null;
if (!groupItems?.length) return null;
return (
<ul
key={groupKey}
role='group'
aria-labelledby={hasTitle ? groupTitleId : undefined}
>
{hasTitle && (
<li
role='presentation'
className={classes.groupLabel}
>
{customGroupTitle ?? (
<span id={groupTitleId}>{groupKey}</span>
)}
</li>
)}
{renderItems(groupItems)}
</ul>
);
})}
</div>
) : (
<ul role='listbox' id={listId} tabIndex={-1}>
{renderItems(items)}
</ul>
)}
</StatusMessageWrapper>
</div>
)}
</Overlay>
@ -452,14 +529,28 @@ const ComboboxWithRef = <T extends ComboboxItem>(
// 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 ComboboxItem>(
props: ComboboxProps<T> & { ref?: React.ForwardedRef<HTMLInputElement> },
<Item extends ComboboxItem, GroupKey extends string>(
props: ComboboxProps<Item, GroupKey> & {
ref?: React.ForwardedRef<HTMLInputElement>;
},
): ReturnType<typeof ComboboxWithRef>;
displayName: string;
};
Combobox.displayName = 'Combobox';
const StatusMessageWrapper: React.FC<{
showStatus: boolean;
status: string;
children: React.ReactNode;
}> = ({ showStatus, status, children }) => {
if (showStatus) {
return <span className={classes.emptyMessage}>{status}</span>;
}
return children;
};
function useGetA11yStatusMessage({
itemCount,
value,

View File

@ -14,8 +14,9 @@ interface WrapperProps extends Omit<
/**
* A basic list item component that can be used as a base for more bespoke list items.
*
* Depending on functionality, use `ListItemButton` or `ListItemLink` as a child of the
* wrapper component.
* Choose the child of the wrapper component based on needed interactivity:
* `ListItemContent` for a non-interactive item, `ListItemButton` or `ListItemLink`
* for interactive items.
*/
export const ListItemWrapper: React.FC<WrapperProps> = ({
icon,
@ -33,10 +34,31 @@ export const ListItemWrapper: React.FC<WrapperProps> = ({
);
};
interface LinkProps extends React.ComponentPropsWithoutRef<typeof Link> {
interface WithSubtitle {
subtitle?: React.ReactNode;
}
interface ContentProps
extends React.ComponentPropsWithoutRef<'h3'>, WithSubtitle {}
export const ListItemContent: React.FC<ContentProps> = ({
subtitle,
children,
...otherProps
}) => {
return (
<>
<h3 className={classes.title} {...otherProps}>
{children}
</h3>
{subtitle && <div className={classes.subtitle}>{subtitle}</div>}
</>
);
};
interface LinkProps
extends React.ComponentPropsWithoutRef<typeof Link>, WithSubtitle {}
export const ListItemLink: React.FC<LinkProps> = ({
subtitle,
children,
@ -44,20 +66,16 @@ export const ListItemLink: React.FC<LinkProps> = ({
...otherProps
}) => {
return (
<>
<h3 className={classes.title}>
<Link className={classNames(className, 'focusable')} {...otherProps}>
{children}
</Link>
</h3>
{subtitle && <div className={classes.subtitle}>{subtitle}</div>}
</>
<ListItemContent subtitle={subtitle}>
<Link className={classNames(className, 'focusable')} {...otherProps}>
{children}
</Link>
</ListItemContent>
);
};
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
subtitle?: React.ReactNode;
}
interface ButtonProps
extends React.ComponentPropsWithoutRef<'button'>, WithSubtitle {}
export const ListItemButton: React.FC<ButtonProps> = ({
subtitle,
@ -66,17 +84,14 @@ export const ListItemButton: React.FC<ButtonProps> = ({
...otherProps
}) => {
return (
<>
<h3 className={classes.title}>
<button
type='button'
className={classNames(className, 'focusable')}
{...otherProps}
>
{children}
</button>
</h3>
{subtitle && <div className={classes.subtitle}>{subtitle}</div>}
</>
<ListItemContent subtitle={subtitle}>
<button
type='button'
className={classNames(className, 'focusable')}
{...otherProps}
>
{children}
</button>
</ListItemContent>
);
};

View File

@ -7,18 +7,31 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac
import { Icon } from '../icon';
import { ListItemWrapper, ListItemButton, ListItemLink } from './index';
import {
ListItemWrapper,
ListItemContent,
ListItemButton,
ListItemLink,
} from './index';
const meta = {
title: 'Components/ListItem',
component: ListItemWrapper,
subcomponents: { ListItemButton, ListItemLink },
subcomponents: { ListItemContent, ListItemButton, ListItemLink },
} satisfies Meta<typeof ListItemWrapper>;
export default meta;
type Story = StoryObj<typeof meta>;
export const NonInteractive: Story = {
render: () => (
<ListItemWrapper icon={<Icon icon={VisibilityIcon} id='visibility' />}>
<ListItemContent>View more</ListItemContent>
</ListItemWrapper>
),
};
export const WithButton: Story = {
render: () => (
<ListItemWrapper

View File

@ -65,7 +65,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
const description = audio.getIn(['translation', 'description']) || audio.get('description');
return (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} key='audio'>
{Component => (
<Component
src={audio.get('url')}
@ -87,7 +87,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
const description = video.getIn(['translation', 'description']) || video.get('description');
return (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} key='video'>
{Component => (
<Component
preview={video.get('preview_url')}
@ -108,7 +108,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
);
} else {
return (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} key='gallery'>
{Component => (
<Component
media={mediaAttachments}

View File

@ -477,7 +477,7 @@ class Status extends ImmutablePureComponent {
if (['image', 'gifv', 'unknown'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} key='gallery'>
{Component => (
<Component
media={status.get('media_attachments')}
@ -499,7 +499,7 @@ class Status extends ImmutablePureComponent {
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} key='audio'>
{Component => (
<Component
src={attachment.get('url')}
@ -525,7 +525,7 @@ class Status extends ImmutablePureComponent {
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} key='video'>
{Component => (
<Component
preview={attachment.get('preview_url')}

View File

@ -1,8 +0,0 @@
[data-color-scheme='dark'] .image {
--color-skin-1: #3a3a50;
--color-skin-2: #67678e;
--color-skin-3: #44445f;
--color-outline: #21212c;
--color-shadow: #181820;
--color-highlight: #b2b1c8;
}

View File

@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router';
import { Link } from 'react-router-dom';
import ElephantImage from '@/images/elephant_ui.svg?react';
import { openModal } from '@/mastodon/actions/modal';
import { Button } from '@/mastodon/components/button';
import { EmptyState } from '@/mastodon/components/empty_state';
@ -14,8 +13,6 @@ import { areCollectionsEnabled } from '@/mastodon/features/collections/utils';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import { useAppDispatch } from '@/mastodon/store';
import classes from './empty_message.module.scss';
interface EmptyMessageProps {
suspended: boolean;
hidden: boolean;
@ -54,14 +51,11 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
const hasCollections = areCollectionsEnabled();
const image = <ElephantImage className={classes.image} />;
if (me === accountId) {
if (hasCollections) {
// Return only here to insert the "Create a collection" button as the action for the empty state.
return (
<EmptyState
image={image}
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
@ -140,5 +134,5 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
}
}
return <EmptyState title={title} message={message} image={image} />;
return <EmptyState title={title} message={message} />;
};

View File

@ -2,8 +2,8 @@ import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { matchPath } from 'react-router';
import { showAlert } from '@/mastodon/actions/alerts';
import { initBlockModal } from '@/mastodon/actions/blocks';
import { useAccount } from '@/mastodon/hooks/useAccount';
import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
import { openModal } from 'mastodon/actions/modal';
@ -21,9 +21,17 @@ const messages = defineMessages({
id: 'collections.view_collection',
defaultMessage: 'View collection',
},
viewOtherCollections: {
id: 'collections.view_other_collections_by_user',
defaultMessage: 'View other collections by this user',
share: {
id: 'collections.share_short',
defaultMessage: 'Share',
},
copyLink: {
id: 'collections.copy_link',
defaultMessage: 'Copy link',
},
copyLinkConfirmation: {
id: 'collections.copy_link_confirmation',
defaultMessage: 'Copied collection link to clipboard',
},
delete: {
id: 'collections.delete_collection',
@ -33,6 +41,10 @@ const messages = defineMessages({
id: 'collections.report_collection',
defaultMessage: 'Report this collection',
},
blockOwner: {
id: 'collections.block_collection_owner',
defaultMessage: 'Block account',
},
revoke: {
id: 'collections.revoke_collection_inclusion',
defaultMessage: 'Remove myself from this collection',
@ -42,15 +54,29 @@ const messages = defineMessages({
export const CollectionMenu: React.FC<{
collection: ApiCollectionJSON;
context: 'list' | 'collection';
context: 'list' | 'notifications' | 'collection';
className?: string;
}> = ({ collection, context, className }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { id, name, account_id } = collection;
const isOwnCollection = account_id === me;
const { id, name, account_id, items } = collection;
const ownerAccount = useAccount(account_id);
const isOwnCollection = account_id === me;
const currentAccountInCollection = items.find(
(item) => item.account_id === me,
);
const openShareModal = useCallback(() => {
dispatch(
openModal({
modalType: 'SHARE_COLLECTION',
modalProps: {
collection,
},
}),
);
}, [collection, dispatch]);
const openDeleteConfirmation = useCallback(() => {
dispatch(
@ -75,9 +101,9 @@ export const CollectionMenu: React.FC<{
);
}, [collection, dispatch]);
const currentAccountInCollection = collection.items.find(
(item) => item.account_id === me,
);
const openBlockModal = useCallback(() => {
dispatch(initBlockModal(ownerAccount));
}, [ownerAccount, dispatch]);
const openRevokeConfirmation = useCallback(() => {
void dispatch(
@ -92,8 +118,28 @@ export const CollectionMenu: React.FC<{
}, [collection.id, currentAccountInCollection?.id, dispatch]);
const menu = useMemo(() => {
const viewCollectionItem: MenuItem = {
text: intl.formatMessage(messages.view),
to: `/collections/${id}`,
};
const shareItems: MenuItem[] = [
{
text: intl.formatMessage(messages.share),
action: openShareModal,
},
{
text: intl.formatMessage(messages.copyLink),
action: () => {
void navigator.clipboard.writeText(`/collections/${id}`);
dispatch(showAlert({ message: messages.copyLinkConfirmation }));
},
},
];
if (isOwnCollection) {
const commonItems: MenuItem[] = [
const ownerItems: MenuItem[] = [
...shareItems,
null,
{
text: intl.formatMessage(editorMessages.manageAccounts),
to: `/collections/${id}/edit`,
@ -111,62 +157,51 @@ export const CollectionMenu: React.FC<{
];
if (context === 'list') {
return [
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
null,
...commonItems,
];
return [viewCollectionItem, ...ownerItems];
} else {
return commonItems;
return ownerItems;
}
} else {
const items: MenuItem[] = [];
const nonOwnerItems: MenuItem[] = [
viewCollectionItem,
...shareItems,
null,
];
if (ownerAccount) {
const featuredCollectionsPath = `/@${ownerAccount.acct}/featured`;
// Don't show menu link to featured collections while on that very page
if (
!matchPath(location.pathname, {
path: featuredCollectionsPath,
exact: true,
})
) {
items.push(
...[
{
text: intl.formatMessage(messages.viewOtherCollections),
to: featuredCollectionsPath,
},
null,
],
);
}
}
if (currentAccountInCollection) {
items.push({
// Collection notifications already have a prominent 'Remove me' button
if (currentAccountInCollection && context !== 'notifications') {
nonOwnerItems.push({
text: intl.formatMessage(messages.revoke),
action: openRevokeConfirmation,
});
}
items.push({
nonOwnerItems.push({
text: intl.formatMessage(messages.report),
action: openReportModal,
});
return items;
if (currentAccountInCollection) {
nonOwnerItems.push({
text: intl.formatMessage(messages.blockOwner),
action: openBlockModal,
});
}
return nonOwnerItems;
}
}, [
isOwnCollection,
intl,
id,
openShareModal,
isOwnCollection,
dispatch,
openDeleteConfirmation,
context,
currentAccountInCollection,
openRevokeConfirmation,
ownerAccount,
openReportModal,
openBlockModal,
openRevokeConfirmation,
]);
return (

View File

@ -115,7 +115,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
);
const isCurrentUserInCollection = !isOwnCollection && currentUserIndex > -1;
const handleShare = useCallback(() => {
const openShareModal = useCallback(() => {
dispatch(
openModal({
modalType: 'SHARE_COLLECTION',
@ -132,9 +132,9 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
if (isNewCollection) {
// Replace with current pathname to clear `newCollection` state
history.replace(location.pathname);
handleShare();
openShareModal();
}
}, [history, handleShare, isNewCollection, location.pathname]);
}, [history, openShareModal, isNewCollection, location.pathname]);
return (
<header className={classes.header}>
@ -150,7 +150,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
icon='share-icon'
title={intl.formatMessage(messages.share)}
className={classes.iconButton}
onClick={handleShare}
onClick={openShareModal}
/>
<CollectionMenu
context='collection'

View File

@ -4,22 +4,19 @@ import { FormattedMessage, useIntl } from 'react-intl';
import { useHistory } 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 WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { showAlertForError } from 'mastodon/actions/alerts';
import { openModal } from 'mastodon/actions/modal';
import { apiFollowAccount } from 'mastodon/api/accounts';
import type { ApiMutedAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Account } from 'mastodon/components/account';
import { AccountListItem } from 'mastodon/components/account_list_item';
import { Avatar } from 'mastodon/components/avatar';
import { Badge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
import { useAccountHandle } from 'mastodon/components/display_name/default';
import { EmptyState } from 'mastodon/components/empty_state';
import { FormStack, Combobox } from 'mastodon/components/form_fields';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { FormStack, ComboboxField } from 'mastodon/components/form_fields';
import {
ListItemContent,
ListItemWrapper,
} from 'mastodon/components/list_item';
import {
Article,
ItemList,
@ -27,110 +24,132 @@ import {
} from 'mastodon/components/scrollable_list/components';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts';
import { me } from 'mastodon/initial_state';
import { domain } from 'mastodon/initial_state';
import {
addCollectionItem,
getCollectionItemIds,
removeCollectionItem,
updateCollectionEditorField,
} from 'mastodon/reducers/slices/collections';
import { store, useAppDispatch, useAppSelector } from 'mastodon/store';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header';
import { WizardStepTitle } from './wizard_step_title';
const MAX_ACCOUNT_COUNT = 25;
function isOlderThanAWeek(date?: string): boolean {
if (!date) return false;
const targetDate = new Date(date);
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
return targetDate < sevenDaysAgo;
}
const AddedAccountItem: React.FC<{
accountId: string;
onRemove: (id: string) => void;
}> = ({ accountId, onRemove }) => {
const intl = useIntl();
const account = useAccount(accountId);
const handleRemoveAccount = useCallback(() => {
onRemove(accountId);
}, [accountId, onRemove]);
const lastStatusAt = account?.last_status_at;
const lastPostHint = useMemo(
() =>
(!lastStatusAt || isOlderThanAWeek(lastStatusAt)) && (
<Badge
label={
<FormattedMessage
id='collections.old_last_post_note'
defaultMessage='Last posted over a week ago'
/>
}
icon={<WarningIcon />}
className={classes.accountBadge}
const renderButton = useCallback(
() => (
<Button compact secondary onClick={handleRemoveAccount}>
<FormattedMessage
id='collections.remove_account'
defaultMessage='Remove'
/>
),
[lastStatusAt],
</Button>
),
[handleRemoveAccount],
);
return (
<Account
minimal
key={accountId}
id={accountId}
extraAccountInfo={lastPostHint}
>
<IconButton
title={intl.formatMessage({
id: 'collections.remove_account',
defaultMessage: 'Remove this account',
})}
icon='remove'
iconComponent={CancelIcon}
onClick={handleRemoveAccount}
/>
</Account>
);
return <AccountListItem accountId={accountId} renderButton={renderButton} />;
};
interface SuggestionItem {
id: string;
isSelected: boolean;
}
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id, isSelected }) => {
const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => {
const account = useAccount(id);
const handle = useAccountHandle(account, domain);
if (!account) return null;
return (
<>
<Avatar account={account} />
<DisplayName account={account} />
{isSelected && (
<Icon
id='checked'
icon={CheckIcon}
className={classes.selectedSuggestionIcon}
/>
)}
</>
<ListItemWrapper
className={classes.suggestion}
icon={<Avatar account={account} size={40} />}
>
<ListItemContent subtitle={handle}>
<DisplayName account={account} variant='simple' />
</ListItemContent>
</ListItemWrapper>
);
};
const renderAccountItem = (item: SuggestionItem) => (
<SuggestedAccountItem id={item.id} isSelected={item.isSelected} />
const renderAccountItem = (account: ApiMutedAccountJSON) => (
<SuggestedAccountItem id={account.id} />
);
const getItemId = (item: SuggestionItem) => item.id;
const getIsItemSelected = (item: SuggestionItem) => item.isSelected;
type GroupKey = 'available' | 'mustFollow' | 'disabled';
function groupSuggestions(accounts: ApiMutedAccountJSON[]) {
const { available, disabled } = Object.groupBy(accounts, (account) => {
if (getIsItemDisabled(account)) {
return 'disabled';
}
// if (account.locked && !relationship?.following) {
// return 'mustFollow';
// }
return 'available';
});
// Returning a new object ensures a fixed property order
return { available, disabled };
}
const renderGroupTitle = (groupKey: GroupKey, titleId: string) => {
if (groupKey === 'available') {
return null;
}
let title: React.ReactElement;
let description: React.ReactElement;
if (groupKey === 'mustFollow') {
title = (
<FormattedMessage
id='collections.suggestions.must_follow'
defaultMessage='Must follow first'
/>
);
description = (
<FormattedMessage
id='collections.suggestions.must_follow_desc'
defaultMessage='These accounts review all follow requests. Followers can add them to collections.'
/>
);
} else {
title = (
<FormattedMessage
id='collections.suggestions.can_not_add'
defaultMessage='Cant be added'
/>
);
description = (
<FormattedMessage
id='collections.suggestions.can_not_add_desc'
defaultMessage='These accounts may have opted out of discovery, or they might be on a server that doesnt support collections.'
/>
);
}
return (
<ListItemWrapper className={classes.suggestionGroup}>
<ListItemContent id={titleId} subtitle={description}>
{title}
</ListItemContent>
</ListItemWrapper>
);
};
const getItemId = (account: ApiMutedAccountJSON) => account.id;
// Disable accounts who can't be added to a collection
const getIsItemDisabled = (account: ApiMutedAccountJSON) =>
!['automatic', 'manual'].includes(account.feature_approval.current_user);
export const CollectionAccounts: React.FC<{
collection?: ApiCollectionJSON | null;
@ -139,9 +158,8 @@ export const CollectionAccounts: React.FC<{
const dispatch = useAppDispatch();
const history = useHistory();
const { id, items } = collection ?? {};
const { id, items: collectionItems } = collection ?? {};
const isEditMode = !!id;
const collectionItems = items;
const addedAccountIds = useAppSelector(
(state) => state.collections.editor.accountIds,
@ -157,24 +175,20 @@ export const CollectionAccounts: React.FC<{
const [searchValue, setSearchValue] = useState('');
const hasAccounts = accountIds.length > 0;
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
const {
accountIds: suggestedAccountIds,
accounts: suggestedAccounts,
isLoading: isLoadingSuggestions,
searchAccounts,
resetAccounts,
} = useSearchAccounts({
withRelationships: true,
filterResults: (account) =>
// Only suggest accounts who allow being featured/recommended
account.feature_approval.current_user === 'automatic',
// Don't suggest accounts that were already added
filterResults: (account) => !accountIds.includes(account.id),
});
const suggestedItems = suggestedAccountIds.map((id) => ({
id,
isSelected: accountIds.includes(id),
}));
const handleSearchValueChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
@ -192,43 +206,6 @@ export const CollectionAccounts: React.FC<{
[],
);
const relationships = useAppSelector((state) => state.relationships);
const confirmFollowStatus = useCallback(
(accountId: string, onFollowing: () => void) => {
const relationship = relationships.get(accountId);
if (!relationship) {
return;
}
if (
accountId === me ||
relationship.following ||
relationship.requested
) {
onFollowing();
} else {
dispatch(
openModal({
modalType: 'CONFIRM_FOLLOW_TO_COLLECTION',
modalProps: {
accountId,
onConfirm: () => {
apiFollowAccount(accountId)
.then(onFollowing)
.catch((err: unknown) => {
store.dispatch(showAlertForError(err));
});
},
},
}),
);
}
},
[dispatch, relationships],
);
const removeAccountItem = useCallback(
(accountId: string) => {
dispatch(
@ -242,28 +219,15 @@ export const CollectionAccounts: React.FC<{
);
const addAccountItem = useCallback(
(accountId: string) => {
confirmFollowStatus(accountId, () => {
dispatch(
updateCollectionEditorField({
field: 'accountIds',
value: [...accountIds, accountId],
}),
);
});
(item: ApiMutedAccountJSON) => {
dispatch(
updateCollectionEditorField({
field: 'accountIds',
value: [...accountIds, item.id],
}),
);
},
[accountIds, confirmFollowStatus, dispatch],
);
const toggleAccountItem = useCallback(
(item: SuggestionItem) => {
if (accountIds.includes(item.id)) {
removeAccountItem(item.id);
} else {
addAccountItem(item.id);
}
},
[accountIds, addAccountItem, removeAccountItem],
[accountIds, dispatch],
);
const instantRemoveAccountItem = useCallback(
@ -289,23 +253,14 @@ export const CollectionAccounts: React.FC<{
);
const instantAddAccountItem = useCallback(
(collectionId: string, accountId: string) => {
confirmFollowStatus(accountId, () => {
void dispatch(addCollectionItem({ collectionId, accountId }));
});
},
[confirmFollowStatus, dispatch],
);
const instantToggleAccountItem = useCallback(
(item: SuggestionItem) => {
if (accountIds.includes(item.id)) {
instantRemoveAccountItem(item.id);
} else if (id) {
instantAddAccountItem(id, item.id);
(item: ApiMutedAccountJSON) => {
if (id) {
void dispatch(
addCollectionItem({ collectionId: id, accountId: item.id }),
);
}
},
[accountIds, id, instantAddAccountItem, instantRemoveAccountItem],
[dispatch, id],
);
const handleRemoveAccountItem = useCallback(
@ -319,6 +274,20 @@ export const CollectionAccounts: React.FC<{
[isEditMode, instantRemoveAccountItem, removeAccountItem],
);
const handleSelectItem = useCallback(
(item: ApiMutedAccountJSON) => {
if (isEditMode) {
instantAddAccountItem(item);
} else {
addAccountItem(item);
}
setSearchValue('');
resetAccounts();
},
[addAccountItem, instantAddAccountItem, isEditMode, resetAccounts],
);
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
@ -333,117 +302,116 @@ export const CollectionAccounts: React.FC<{
);
const inputId = useId();
const inputLabel = intl.formatMessage({
id: 'collections.search_accounts_label',
defaultMessage: 'Search for accounts to add…',
});
const AccountsHeadingElement = id ? 'h2' : 'h3';
return (
<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.'
/>
}
/>
)}
<label htmlFor={inputId} className='sr-only'>
{inputLabel}
</label>
<Combobox
id={inputId}
placeholder={inputLabel}
value={hasMaxAccounts ? '' : searchValue}
onChange={handleSearchValueChange}
onKeyDown={handleSearchKeyDown}
disabled={hasMaxAccounts}
isLoading={isLoadingSuggestions}
items={suggestedItems}
getItemId={getItemId}
getIsItemSelected={getIsItemSelected}
renderItem={renderAccountItem}
onSelectItem={
isEditMode ? instantToggleAccountItem : toggleAccountItem
}
closeOnSelect={false}
/>
{hasMaxAccounts && (
<FormattedMessage
id='collections.search_accounts_max_reached'
defaultMessage='You have added the maximum number of accounts'
/>
)}
<Scrollable className={classes.scrollableWrapper}>
<ItemList
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,
}}
/>
}
/>
}
>
{accountIds.map((accountId, index) => (
<Article
key={accountId}
aria-posinset={index}
aria-setsize={accountIds.length}
>
<AddedAccountItem
accountId={accountId}
onRemove={handleRemoveAccountItem}
/>
</Article>
))}
</ItemList>
</Scrollable>
</FormStack>
{!isEditMode && (
<div className={classes.stickyFooter}>
<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>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<header className={classes.header}>
{!id && (
<WizardStepTitle
step={1}
title={
<FormattedMessage
id='collections.continue'
defaultMessage='Continue'
id='collections.create.accounts_title'
defaultMessage='Who will you feature in this collection?'
/>
)}
</Button>
</div>
}
/>
)}
<ComboboxField
id={inputId}
label={intl.formatMessage({
id: 'collections.search_accounts_label',
defaultMessage: 'Search for an account to add',
})}
value={hasMaxAccounts ? '' : searchValue}
onChange={handleSearchValueChange}
onKeyDown={handleSearchKeyDown}
disabled={hasMaxAccounts}
isLoading={isLoadingSuggestions}
items={groupSuggestions(suggestedAccounts)}
getItemId={getItemId}
getIsItemDisabled={getIsItemDisabled}
renderItem={renderAccountItem}
renderGroupTitle={renderGroupTitle}
onSelectItem={handleSelectItem}
status={
hasMaxAccounts
? {
variant: 'warning',
message: intl.formatMessage({
id: 'collections.search_accounts_max_reached',
defaultMessage:
'You have added the maximum number of accounts',
}),
}
: null
}
/>
</header>
<div>
{hasAccounts && (
<AccountsHeadingElement className={classes.listHeading}>
<FormattedMessage
id='collections.hints.accounts_counter'
defaultMessage='{count}/{max} accounts'
values={{ count: accountIds.length, max: MAX_ACCOUNT_COUNT }}
/>
</AccountsHeadingElement>
)}
<Scrollable className={classes.scrollableWrapper}>
<ItemList
emptyMessage={
<EmptyState
title={
<FormattedMessage
id='collections.accounts.empty_editor_title'
defaultMessage='No one is in this collection yet'
/>
}
message={
<FormattedMessage
id='collections.accounts.empty_description'
defaultMessage='Add up to {count} accounts'
values={{
count: MAX_ACCOUNT_COUNT,
}}
/>
}
/>
}
>
{accountIds.map((accountId, index) => (
<Article
key={accountId}
aria-posinset={index}
aria-setsize={accountIds.length}
>
<AddedAccountItem
accountId={accountId}
onRemove={handleRemoveAccountItem}
/>
</Article>
))}
</ItemList>
</Scrollable>
</div>
</FormStack>
{!isEditMode && hasAccounts && (
<div className={classes.stickyFooter}>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage
id='collections.continue'
defaultMessage='Continue'
/>
)}
</Button>
</div>
)}
</form>

View File

@ -36,7 +36,7 @@ import {
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import classes from './styles.module.scss';
import { WizardStepHeader } from './wizard_step_header';
import { WizardStepTitle } from './wizard_step_title';
export const CollectionDetails: React.FC = () => {
const dispatch = useAppDispatch();
@ -152,7 +152,7 @@ export const CollectionDetails: React.FC = () => {
<form onSubmit={handleSubmit} className={classes.form}>
<FormStack className={classes.formFieldStack}>
{!id && (
<WizardStepHeader
<WizardStepTitle
step={2}
title={
<FormattedMessage
@ -276,18 +276,16 @@ export const CollectionDetails: React.FC = () => {
</FormStack>
<div className={classes.stickyFooter}>
<div className={classes.actionWrapper}>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage
id='collections.create_collection'
defaultMessage='Create collection'
/>
)}
</Button>
</div>
<Button type='submit'>
{id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage
id='collections.create_collection'
defaultMessage='Create collection'
/>
)}
</Button>
</div>
</form>
);

View File

@ -44,7 +44,7 @@ export const messages = defineMessages({
},
});
function usePageTitle(id: string | undefined) {
function usePageTitle(id: string | null) {
const { path } = useRouteMatch();
const location = useLocation();
@ -66,7 +66,7 @@ export const CollectionEditorPage: React.FC<{
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { id } = useParams<{ id?: string }>();
const { id = null } = useParams<{ id?: string }>();
const { path } = useRouteMatch();
const collection = useAppSelector((state) =>
id ? state.collections.collections[id] : undefined,

View File

@ -1,10 +1,17 @@
.header {
display: flex;
flex-direction: column;
gap: 12px;
}
.step {
font-size: 13px;
color: var(--color-text-secondary);
}
.title {
font-size: 22px;
font-size: 17px;
font-weight: 500;
line-height: 1.2;
margin-top: 4px;
}
@ -14,79 +21,56 @@
margin-top: 8px;
}
/* Make form stretch full height of the column */
.listHeading {
margin-bottom: 8px;
font-size: 15px;
font-weight: 500;
line-height: 1.2;
}
/* Ensure sticky footer isn't covered by mobile bottom nav */
.form {
--bottom-spacing: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100%;
padding-bottom: var(--bottom-spacing);
padding-bottom: 0;
@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,
.scrollableInner {
margin-inline: -8px;
}
.submitDisabledCallout {
align-self: center;
}
.stickyFooter {
position: sticky;
bottom: var(--bottom-spacing);
margin-top: -16px;
padding: 16px;
background-image: linear-gradient(
to bottom,
transparent,
var(--color-bg-primary) 32px
var(--color-bg-primary) 24px
);
}
.itemCountReadout {
text-align: center;
.formFieldStack {
gap: 16px;
}
.actionWrapper {
display: flex;
flex-direction: column;
width: min-content;
min-width: 240px;
margin-inline: auto;
gap: 8px;
.scrollableWrapper {
margin-inline: -16px;
}
.accountBadge {
margin-inline-start: 56px;
.suggestion {
padding: 4px 0;
@container (width < 360px) {
margin-top: 4px;
margin-inline-start: 46px;
[aria-disabled='true'] & {
opacity: 0.6;
}
}
.suggestionGroup {
padding: 4px 0;
// Undo default group styles:
font-weight: 400;
text-transform: none;
}

View File

@ -1,23 +0,0 @@
import { FormattedMessage } from 'react-intl';
import classes from './styles.module.scss';
export const WizardStepHeader: React.FC<{
step: number;
title: React.ReactElement;
description?: React.ReactElement;
}> = ({ step, title, description }) => {
return (
<header>
<FormattedMessage
id='collections.create.steps'
defaultMessage='Step {step}/{total}'
values={{ step, total: 2 }}
>
{(content) => <p className={classes.step}>{content}</p>}
</FormattedMessage>
<h2 className={classes.title}>{title}</h2>
{!!description && <p className={classes.description}>{description}</p>}
</header>
);
};

View File

@ -0,0 +1,21 @@
import { FormattedMessage } from 'react-intl';
import classes from './styles.module.scss';
export const WizardStepTitle: React.FC<{
step: number;
title: React.ReactElement;
}> = ({ step, title }) => {
return (
<div>
<p className={classes.step}>
<FormattedMessage
id='collections.create.steps'
defaultMessage='Step {step}/{total}'
values={{ step, total: 2 }}
/>
</p>
<h2 className={classes.title}>{title}</h2>
</div>
);
};

View File

@ -164,7 +164,7 @@ const ListMembers: React.FC<{
const [mode, setMode] = useState<Mode>('remove');
const {
accountIds: searchAccountIds,
accounts: accountsFromSearch,
isLoading: loadingSearchResults,
searchAccounts: handleSearch,
} = useSearchAccounts({
@ -177,6 +177,7 @@ const ListMembers: React.FC<{
}
},
});
const accountIdsFromSearch = accountsFromSearch.map((item) => item.id);
useEffect(() => {
if (id) {
@ -220,7 +221,7 @@ const ListMembers: React.FC<{
let displayedAccountIds: string[];
if (mode === 'add' && searching) {
displayedAccountIds = searchAccountIds;
displayedAccountIds = accountIdsFromSearch;
} else {
displayedAccountIds = accountIds;
}

View File

@ -0,0 +1,17 @@
.actions {
display: flex;
align-items: center;
}
.menuButton {
box-sizing: content-box;
margin-inline-start: auto;
padding: 4px;
border-radius: 4px;
border: 1px solid var(--color-border-primary);
svg {
width: 20px;
height: 20px;
}
}

View File

@ -2,8 +2,12 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { DisplayNameSimple } from '@/mastodon/components/display_name/simple';
import { Button } from '@/mastodon/components/button';
import { LinkedDisplayName } from '@/mastodon/components/display_name';
import { Icon } from '@/mastodon/components/icon';
import { CollectionMenu } from '@/mastodon/features/collections/components/collection_menu';
import { CollectionPreviewCard } from '@/mastodon/features/collections/components/collection_preview_card';
import { useConfirmRevoke } from '@/mastodon/features/collections/detail/revoke_collection_inclusion_modal';
import { useAccount } from '@/mastodon/hooks/useAccount';
import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?react';
import type {
@ -11,7 +15,7 @@ import type {
NotificationGroupCollectionUpdate,
} from 'mastodon/models/notification_group';
import { CollectionPreviewCard } from '../../collections/components/collection_preview_card';
import classes from './notification_collection.module.scss';
export const NotificationCollection: React.FC<{
notification:
@ -21,6 +25,7 @@ export const NotificationCollection: React.FC<{
}> = ({ notification, unread }) => {
const { collection, type } = notification;
const collectionCreatorAccount = useAccount(collection.account_id);
const confirmRevoke = useConfirmRevoke(collection);
return (
<div
@ -43,7 +48,12 @@ export const NotificationCollection: React.FC<{
defaultMessage='{name} added you to a collection'
values={{
name: (
<DisplayNameSimple account={collectionCreatorAccount} />
<LinkedDisplayName
displayProps={{
variant: 'simple',
account: collectionCreatorAccount,
}}
/>
),
}}
/>
@ -54,7 +64,12 @@ export const NotificationCollection: React.FC<{
defaultMessage='{name} edited a collection youre in'
values={{
name: (
<DisplayNameSimple account={collectionCreatorAccount} />
<LinkedDisplayName
displayProps={{
variant: 'simple',
account: collectionCreatorAccount,
}}
/>
),
}}
/>
@ -63,6 +78,26 @@ export const NotificationCollection: React.FC<{
</div>
<CollectionPreviewCard collection={collection} />
<div className={classes.actions}>
<Button
compact
secondary
className='button--destructive'
onClick={confirmRevoke}
>
<FormattedMessage
id='collections.detail.revoke_inclusion'
defaultMessage='Remove me'
/>
</Button>
<CollectionMenu
context='notifications'
collection={collection}
className={classes.menuButton}
/>
</div>
</div>
</div>
);

View File

@ -1,43 +0,0 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAccount } from 'mastodon/hooks/useAccount';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
title: {
id: 'confirmations.follow_to_collection.title',
defaultMessage: 'Follow account?',
},
confirm: {
id: 'confirmations.follow_to_collection.confirm',
defaultMessage: 'Follow and add to collection',
},
});
export const ConfirmFollowToCollectionModal: React.FC<
{
accountId: string;
onConfirm: () => void;
} & BaseConfirmationModalProps
> = ({ accountId, onConfirm, onClose }) => {
const intl = useIntl();
const account = useAccount(accountId);
return (
<ConfirmationModal
title={intl.formatMessage(messages.title)}
message={
<FormattedMessage
id='confirmations.follow_to_collection.message'
defaultMessage='You need to be following {name} to add them to a collection.'
values={{ name: <strong>@{account?.acct}</strong> }}
/>
}
confirm={intl.formatMessage(messages.confirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@ -13,7 +13,6 @@ export { ConfirmUnblockModal } from './unblock';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list';
export { ConfirmFollowToCollectionModal } from './follow_to_collection';
export { ConfirmMissingAltTextModal } from './missing_alt_text';
export { ConfirmRevokeQuoteModal } from './revoke_quote';
export { QuietPostQuoteInfoModal } from './quiet_post_quote_info';

View File

@ -39,7 +39,6 @@ import {
ConfirmClearNotificationsModal,
ConfirmLogOutModal,
ConfirmFollowToListModal,
ConfirmFollowToCollectionModal,
ConfirmMissingAltTextModal,
ConfirmRevokeQuoteModal,
QuietPostQuoteInfoModal,
@ -69,7 +68,6 @@ export const MODAL_COMPONENTS = {
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_FOLLOW_TO_COLLECTION': () => Promise.resolve({ default: ConfirmFollowToCollectionModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),

View File

@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
@ -21,7 +21,7 @@ export function useSearchAccounts({
} = {}) {
const dispatch = useAppDispatch();
const [accountIds, setAccountIds] = useState<string[]>([]);
const [accounts, setAccounts] = useState<ApiAccountJSON[]>([]);
const [loadingState, setLoadingState] = useState<
'idle' | 'loading' | 'error'
>('idle');
@ -37,7 +37,7 @@ export function useSearchAccounts({
if (value.trim().length === 0) {
onSettled?.('');
if (resetOnInputClear) {
setAccountIds([]);
setAccounts([]);
}
return;
}
@ -60,7 +60,7 @@ export function useSearchAccounts({
if (withRelationships) {
dispatch(fetchRelationships(accountIds));
}
setAccountIds(accountIds);
setAccounts(accounts);
setLoadingState('idle');
onSettled?.(value);
})
@ -73,9 +73,14 @@ export function useSearchAccounts({
{ leading: true, trailing: true },
);
const resetAccounts = useCallback(() => {
setAccounts([]);
}, []);
return {
searchAccounts,
accountIds,
resetAccounts,
accounts,
isLoading: loadingState === 'loading',
isError: loadingState === 'error',
};

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Глядзі, якая класная калекцыя: {link}",
"collection.share_template_own": "Глядзі, у мяне новая калекцыя: {link}",
"collections.account_count": "{count, plural,one {# уліковы запіс} few {# уліковыя запісы} other {# уліковых запісаў}}",
"collections.accounts.empty_description": "Дадайце да {count} уліковых запісаў, на якія Вы падпісаныя",
"collections.accounts.empty_description": "Дадайце да {count} уліковых запісаў",
"collections.accounts.empty_editor_title": "У гэтай калекцыі пакуль нікога няма",
"collections.accounts.empty_title": "Гэтая калекцыя пустая",
"collections.block_collection_owner": "Заблакіраваць профіль",
"collections.by_account": "ад {account_handle}",
"collections.collection_description": "Апісанне",
"collections.collection_language": "Мова",
@ -370,7 +372,8 @@
"collections.confirm_account_removal": "Упэўненыя, што хочаце прыбраць гэты ўліковы запіс з гэтай калекцыі?",
"collections.content_warning": "Папярэджанне аб змесціве",
"collections.continue": "Працягнуць",
"collections.create.accounts_subtitle": "Можна дадаць толькі ўліковыя запісы, на якія Вы падпісаныя і якія далі дазвол на тое, каб іх можна было знайсці.",
"collections.copy_link": "Капіяваць спасылку",
"collections.copy_link_confirmation": "Калекцыя скапіяваная ў буфер абмену",
"collections.create.accounts_title": "Каго Вы ўключыце ў гэтую калекцыю?",
"collections.create.basic_details_title": "Асноўныя звесткі",
"collections.create.steps": "Крок {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Адбылася памылка падчас загрузкі Вашых калекцый.",
"collections.hidden_accounts_description": "Вы заблакіравалі або ігнаруеце {count, plural, one {гэтага карыстальніка} other {гэтых карыстальнікаў}}",
"collections.hidden_accounts_link": "{count, plural, one {# схаваны ўліковы запіс} few {# схаваныя ўліковыя запісы} other {# схаваных уліковых запісаў}}",
"collections.hints.accounts_counter": "{count} / {max} уліковых запісаў",
"collections.hints.accounts_counter": "{count}/{max} уліковых запісаў",
"collections.last_updated_at": "Апошняе абнаўленне: {date}",
"collections.manage_accounts": "Кіраванне ўліковымі запісамі",
"collections.mark_as_sensitive": "Пазначыць як адчувальную",
@ -398,17 +401,19 @@
"collections.name_length_hint": "Максімум 40 сімвалаў",
"collections.new_collection": "Новая калекцыя",
"collections.no_collections_yet": "Пакуль няма калекцый.",
"collections.old_last_post_note": "Апошні допіс быў больш за тыдзень таму",
"collections.remove_account": "Прыбраць гэты ўліковы запіс",
"collections.remove_account": "Выдаліць",
"collections.report_collection": "Паскардзіцца на гэту калекцыю",
"collections.revoke_collection_inclusion": "Прыбраць сябе з гэтай калекцыі",
"collections.revoke_inclusion.confirmation": "Вас прыбралі з \"{collection}\"",
"collections.revoke_inclusion.error": "Адбылася памылка, калі ласка, спрабуйце яшчэ раз пазней.",
"collections.search_accounts_label": "Шукайце ўліковыя запісы, каб дадаць іх сюды…",
"collections.search_accounts_label": "Адшукайце ўліковы запіс, каб дадаць яго сюды",
"collections.search_accounts_max_reached": "Вы дадалі максімальную колькасць уліковых запісаў",
"collections.sensitive": "Адчувальная",
"collections.share_short": "Абагуліць",
"collections.topic_hint": "Дадайце хэштэг, які дапаможа іншым зразумець галоўную тэму гэтай калекцыі.",
"collections.topic_special_chars_hint": "Спецыяльныя сімвалы будуць прыбраныя пры захаванні",
"collections.unlisted_collections_description": "Яны не паказваюцца ў Вашым профілі іншым карыстальнікам. Іх можа пабачыць любы, хто мае спасылку на іх.",
"collections.unlisted_collections_with_count": "Схаваныя калекцыі ({count})",
"collections.view_collection": "Глядзець калекцыю",
"collections.view_other_collections_by_user": "Паглядзець іншыя калекцыі гэтага карыстальніка",
"collections.visibility_public": "Публічная",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Скасаваць чарнавік?",
"confirmations.discard_edit_media.confirm": "Адмяніць",
"confirmations.discard_edit_media.message": "У вас ёсць незахаваныя змены ў апісанні або прэв'ю, усе роўна скасаваць іх?",
"confirmations.follow_to_collection.confirm": "Падпісацца і дадаць у калекцыю",
"confirmations.follow_to_collection.message": "Вам трэба падпісацца на {name}, каб дадаць яго/яе ў калекцыю.",
"confirmations.follow_to_collection.title": "Падпісацца на ўліковы запіс?",
"confirmations.follow_to_list.confirm": "Падпісацца і дадаць у спіс",
"confirmations.follow_to_list.message": "Вам трэба падпісацца на {name}, каб дадаць яго/яе ў спіс.",
"confirmations.follow_to_list.title": "Падпісацца на карыстальніка?",

View File

@ -252,8 +252,6 @@
"collections.name_length_hint": "Limit 40 znaků",
"collections.new_collection": "Nová sbírka",
"collections.no_collections_yet": "Ještě nemáte žádné sbírky.",
"collections.remove_account": "Odstranit tento účet",
"collections.search_accounts_label": "Hledat účty pro přidání…",
"collections.search_accounts_max_reached": "Přidali jste maximální počet účtů",
"collections.sensitive": "Citlivé",
"collections.topic_hint": "Přidat štítek, který pomůže ostatním pochopit hlavní téma této kolekce.",

View File

@ -329,7 +329,6 @@
"collection.share_template_other": "Edrychwch ar y casgliad trawiadol hwn: {link}",
"collection.share_template_own": "Edrychwch ar fy nghasgliad newydd: {link}",
"collections.account_count": "{count, plural, one {# cyfrif} other {# cyfrif}}",
"collections.accounts.empty_description": "Ychwanegwch hyd at {count} cyfrif rydych chi'n eu dilyn",
"collections.accounts.empty_title": "Mae'r casgliad hwn yn wag",
"collections.collection_description": "Disgrifiad",
"collections.collection_name": "Enw",
@ -337,7 +336,6 @@
"collections.confirm_account_removal": "Ydych chi'n siŵr eich bod chi eisiau tynnu'r cyfrif hwn o'r casgliad hwn?",
"collections.content_warning": "Rhybudd cynnwys",
"collections.continue": "Parhau",
"collections.create.accounts_subtitle": "Dim ond cyfrifon rydych chi'n eu dilyn sydd wedi dewis cael eu darganfod y mae modd eu hychwanegu.",
"collections.create.accounts_title": "Pwy fyddwch chi'n ei gynnwys yn y casgliad hwn?",
"collections.create.basic_details_title": "Manylion sylfaenol",
"collections.create.steps": "Cam {step}/{total}",
@ -352,7 +350,6 @@
"collections.detail.share": "Rhannu'r casgliad hwn",
"collections.edit_details": "Golygu manylion",
"collections.error_loading_collections": "Bu gwall wrth geisio llwytho eich casgliadau.",
"collections.hints.accounts_counter": "{count} / {max} cyfrif",
"collections.last_updated_at": "Diweddarwyd ddiwethaf: {date}",
"collections.manage_accounts": "Rheoli cyfrifon",
"collections.mark_as_sensitive": "Marcio fel sensitif",
@ -360,13 +357,10 @@
"collections.name_length_hint": "Terfyn o 40 nod",
"collections.new_collection": "Casgliad newydd",
"collections.no_collections_yet": "Dim casgliadau eto.",
"collections.old_last_post_note": "Postiwyd ddiwethaf dros wythnos yn ôl",
"collections.remove_account": "Dileu'r cyfrif hwn",
"collections.report_collection": "Adroddwch am y casgliad hwn",
"collections.revoke_collection_inclusion": "Tynnu fy hun o'r casgliad hwn",
"collections.revoke_inclusion.confirmation": "Rydych chi wedi cael eich tynnu o \"{collection}\"",
"collections.revoke_inclusion.error": "Bu gwall, ceisiwch eto yn nes ymlaen.",
"collections.search_accounts_label": "Chwiliwch am gyfrifon i'w hychwanegu…",
"collections.search_accounts_max_reached": "Rydych chi wedi ychwanegu'r nifer mwyaf o gyfrifon",
"collections.sensitive": "Sensitif",
"collections.topic_hint": "Ychwanegwch hashnod sy'n helpu eraill i ddeall prif bwnc y casgliad hwn.",
@ -460,9 +454,6 @@
"confirmations.discard_draft.post.title": "Dileu drafft eich postiad?",
"confirmations.discard_edit_media.confirm": "Dileu",
"confirmations.discard_edit_media.message": "Mae gennych newidiadau heb eu cadw i'r disgrifiad cyfryngau neu'r rhagolwg - eu dileu beth bynnag?",
"confirmations.follow_to_collection.confirm": "Dilyn ac ychwanegu at y casgliad",
"confirmations.follow_to_collection.message": "Mae angen i chi fod yn dilyn {name} i'w hychwanegu at gasgliad.",
"confirmations.follow_to_collection.title": "Dilyn cyfrif?",
"confirmations.follow_to_list.confirm": "Dilyn ac ychwanegu at y rhestr",
"confirmations.follow_to_list.message": "Mae angen i chi fod yn dilyn {name} i'w ychwanegu at restr.",
"confirmations.follow_to_list.title": "Dilyn defnyddiwr?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Tjek denne seje samling: {link}",
"collection.share_template_own": "Tjek min nye samling: {link}",
"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_description": "Tilføj op til {count} konti",
"collections.accounts.empty_editor_title": "Ingen er i denne samling endnu",
"collections.accounts.empty_title": "Denne samling er tom",
"collections.block_collection_owner": "Blokér konto",
"collections.by_account": "af {account_handle}",
"collections.collection_description": "Beskrivelse",
"collections.collection_language": "Sprog",
@ -370,7 +372,8 @@
"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.",
"collections.copy_link": "Kopiér link",
"collections.copy_link_confirmation": "Link til samling kopieret til udklipsholderen",
"collections.create.accounts_title": "Hvem vil du fremhæve i denne samling?",
"collections.create.basic_details_title": "Grundlæggende oplysninger",
"collections.create.steps": "Trin {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Der opstod en fejl under indlæsning af dine samlinger.",
"collections.hidden_accounts_description": "Du har blokeret eller skjult {count, plural, one {denne bruger} other {disse brugere}}",
"collections.hidden_accounts_link": "{count, plural, one {# skjult konto} other {# skjulte konti}}",
"collections.hints.accounts_counter": "{count} / {max} konti",
"collections.hints.accounts_counter": "{count}/{max} konti",
"collections.last_updated_at": "Senest opdateret: {date}",
"collections.manage_accounts": "Administrer konti",
"collections.mark_as_sensitive": "Markér som sensitiv",
@ -398,17 +401,19 @@
"collections.name_length_hint": "Begrænset til 40 tegn",
"collections.new_collection": "Ny samling",
"collections.no_collections_yet": "Ingen samlinger endnu.",
"collections.old_last_post_note": "Seneste indlæg er fra over en uge siden",
"collections.remove_account": "Fjern denne konto",
"collections.remove_account": "Fjern",
"collections.report_collection": "Anmeld denne samling",
"collections.revoke_collection_inclusion": "Fjern mig selv fra denne samling",
"collections.revoke_inclusion.confirmation": "Du er blevet fjernet fra \"{collection}\"",
"collections.revoke_inclusion.error": "Der opstod en fejl, prøv igen senere.",
"collections.search_accounts_label": "Søg efter konti for at tilføje",
"collections.search_accounts_label": "Søg efter en konto at tilføje",
"collections.search_accounts_max_reached": "Du har tilføjet det maksimale antal konti",
"collections.sensitive": "Sensitivt",
"collections.share_short": "Del",
"collections.topic_hint": "Tilføj et hashtag, der hjælper andre med at forstå det overordnede emne for denne samling.",
"collections.topic_special_chars_hint": "Specialtegn fjernes, når der gemmes",
"collections.unlisted_collections_description": "Disse vises ikke på din profil for andre. Alle, der har linket, kan finde dem.",
"collections.unlisted_collections_with_count": "Ikke-listede samlinger ({count})",
"collections.view_collection": "Vis samling",
"collections.view_other_collections_by_user": "Se andre samlinger af denne bruger",
"collections.visibility_public": "Offentlig",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Kassér dit indlægsudkast?",
"confirmations.discard_edit_media.confirm": "Kassér",
"confirmations.discard_edit_media.message": "Der er ugemte ændringer i mediebeskrivelsen eller forhåndsvisningen, kassér dem alligevel?",
"confirmations.follow_to_collection.confirm": "Følg og føj til samling",
"confirmations.follow_to_collection.message": "Du skal følge {name} for at føje vedkommende til en samling.",
"confirmations.follow_to_collection.title": "Følg konto?",
"confirmations.follow_to_list.confirm": "Følg og føj til liste",
"confirmations.follow_to_list.message": "Du skal følge {name} for at føje vedkommende til en liste.",
"confirmations.follow_to_list.title": "Følg bruger?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Seht euch diese coole Sammlung an: {link}",
"collection.share_template_own": "Seht euch meine neue Sammlung an: {link}",
"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_description": "Füge bis zu {count} Konten hinzu",
"collections.accounts.empty_editor_title": "Noch befindet sich niemand in dieser Sammlung",
"collections.accounts.empty_title": "Diese Sammlung ist leer",
"collections.block_collection_owner": "Konto blockieren",
"collections.by_account": "von {account_handle}",
"collections.collection_description": "Beschreibung",
"collections.collection_language": "Sprache",
@ -370,7 +372,8 @@
"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.",
"collections.copy_link": "Link kopieren",
"collections.copy_link_confirmation": "Link zur Sammlung in die Zwischenablage kopiert",
"collections.create.accounts_title": "Wen möchtest du in dieser Sammlung präsentieren?",
"collections.create.basic_details_title": "Allgemeine Informationen",
"collections.create.steps": "Schritt {step}/{total}",
@ -398,17 +401,19 @@
"collections.name_length_hint": "Maximal 40 Zeichen",
"collections.new_collection": "Neue Sammlung",
"collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.",
"collections.old_last_post_note": "Neuester Beitrag mehr als eine Woche alt",
"collections.remove_account": "Dieses Konto entfernen",
"collections.remove_account": "Entfernen",
"collections.report_collection": "Sammlung melden",
"collections.revoke_collection_inclusion": "Mich aus dieser Sammlung entfernen",
"collections.revoke_inclusion.confirmation": "Du wurdest aus „{collection}“ entfernt",
"collections.revoke_inclusion.error": "Es ist ein Fehler aufgetreten. Bitte versuche es später erneut.",
"collections.search_accounts_label": "Suche nach Konten, um sie hinzuzufügen …",
"collections.search_accounts_label": "Suche nach einem Konto, um es hinzuzufügen",
"collections.search_accounts_max_reached": "Du hast die Höchstzahl an Konten hinzugefügt",
"collections.sensitive": "Inhaltswarnung",
"collections.share_short": "Teilen",
"collections.topic_hint": "Ein Hashtag für diese Sammlung kann anderen dabei helfen, dein Anliegen besser einordnen zu können.",
"collections.topic_special_chars_hint": "Sonderzeichen werden beim Speichern entfernt",
"collections.unlisted_collections_description": "Die Sammlungen sind für andere nicht auf deinem Profil sichtbar. Ein direkter Zugriff ist per Link möglich.",
"collections.unlisted_collections_with_count": "Nicht öffentliche Sammlungen ({count})",
"collections.view_collection": "Sammlungen anzeigen",
"collections.view_other_collections_by_user": "Andere Sammlungen dieses Kontos ansehen",
"collections.visibility_public": "Öffentlich",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Entwurf verwerfen?",
"confirmations.discard_edit_media.confirm": "Verwerfen",
"confirmations.discard_edit_media.message": "Du hast Änderungen an der Medienbeschreibung oder -vorschau vorgenommen, die noch nicht gespeichert sind. Trotzdem verwerfen?",
"confirmations.follow_to_collection.confirm": "Folgen und zur Sammlung hinzufügen",
"confirmations.follow_to_collection.message": "Du musst {name} folgen, um das Profil zu einer Sammlung hinzufügen zu können.",
"confirmations.follow_to_collection.title": "Konto folgen?",
"confirmations.follow_to_list.confirm": "Folgen und zur Liste hinzufügen",
"confirmations.follow_to_list.message": "Du musst {name} folgen, um das Profil zu einer Liste hinzufügen zu können.",
"confirmations.follow_to_list.title": "Profil folgen?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Δείτε αυτή την ωραία συλλογή: {link}",
"collection.share_template_own": "Δείτε τη νέα μου συλλογή: {link}",
"collections.account_count": "{count, plural, one {# λογαριασμός} other {# λογαριασμοί}}",
"collections.accounts.empty_description": "Προσθέστε μέχρι και {count} λογαριασμούς που ακολουθείτε",
"collections.accounts.empty_description": "Προσθέστε μέχρι και {count} λογαριασμούς",
"collections.accounts.empty_editor_title": "Κανείς δεν είναι ακόμη σε αυτήν τη συλλογή",
"collections.accounts.empty_title": "Αυτή η συλλογή είναι κενή",
"collections.block_collection_owner": "Αποκλεισμός λογαριασμού",
"collections.by_account": "από {account_handle}",
"collections.collection_description": "Περιγραφή",
"collections.collection_language": "Γλώσσα",
@ -370,7 +372,8 @@
"collections.confirm_account_removal": "Σίγουρα θέλετε να αφαιρέσετε αυτόν τον λογαριασμό από αυτή τη συλλογή;",
"collections.content_warning": "Προειδοποίηση περιεχομένου",
"collections.continue": "Συνέχεια",
"collections.create.accounts_subtitle": "Μόνο οι λογαριασμοί που ακολουθείτε που έχουν επιλέξει ανακάλυψη μπορούν να προστεθούν.",
"collections.copy_link": "Αντιγραφή συνδέσμου",
"collections.copy_link_confirmation": "Αντιγράφηκε ο σύνδεσμος συλλογής στο πρόχειρο",
"collections.create.accounts_title": "Ποιον θα αναδείξετε σε αυτήν τη συλλογή;",
"collections.create.basic_details_title": "Βασικά στοιχεία",
"collections.create.steps": "Βήμα {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Παρουσιάστηκε σφάλμα κατά την προσπάθεια φόρτωσης των συλλογών σας.",
"collections.hidden_accounts_description": "Έχετε αποκλείσει ή κάνει σίγαση {count, plural, one {αυτόν τον χρήστη} other {αυτούς τους χρήστες}}",
"collections.hidden_accounts_link": "{count, plural, one {# κρυμμένος λογαριασμός} other {# κρυμμένοι λογαριασμοί}}",
"collections.hints.accounts_counter": "{count} / {max} λογαριασμοί",
"collections.hints.accounts_counter": "{count}/{max} λογαριασμοί",
"collections.last_updated_at": "Τελευταία ενημέρωση: {date}",
"collections.manage_accounts": "Διαχείριση λογαριασμών",
"collections.mark_as_sensitive": "Σήμανση ως ευαίσθητο",
@ -398,17 +401,19 @@
"collections.name_length_hint": "Όριο 40 χαρακτήρων",
"collections.new_collection": "Νέα συλλογή",
"collections.no_collections_yet": "Καμία συλλογή ακόμη.",
"collections.old_last_post_note": "Τελευταία ανάρτηση πριν από μια εβδομάδα",
"collections.remove_account": "Αφαίρεση λογαριασμού",
"collections.remove_account": "Αφαίρεση",
"collections.report_collection": "Αναφορά αυτής της συλλογής",
"collections.revoke_collection_inclusion": "Αφαίρεσε τον εαυτό μου από αυτήν τη συλλογή",
"collections.revoke_inclusion.confirmation": "Έχεις αφαιρεθεί από τη συλλογή \"{collection}\"",
"collections.revoke_inclusion.error": "Υπήρξε ένα σφάλμα, παρακαλούμε προσπαθήστε ξανά αργότερα.",
"collections.search_accounts_label": "Αναζήτηση λογαριασμών για προσθήκη…",
"collections.search_accounts_label": "Αναζήτηση λογαριασμού για προσθήκη",
"collections.search_accounts_max_reached": "Έχετε προσθέσει τον μέγιστο αριθμό λογαριασμών",
"collections.sensitive": "Ευαίσθητο",
"collections.share_short": "Κοινοποίηση",
"collections.topic_hint": "Προσθέστε μια ετικέτα που βοηθά άλλους να κατανοήσουν το κύριο θέμα αυτής της συλλογής.",
"collections.topic_special_chars_hint": "Οι ειδικοί χαρακτήρες θα αφαιρεθούν κατά την αποθήκευση",
"collections.unlisted_collections_description": "Αυτές δεν εμφανίζονται στο προφίλ σας σε άλλους. Οποιοσδήποτε με τον σύνδεσμο μπορεί να τις ανακαλύψει.",
"collections.unlisted_collections_with_count": "Μη καταχωρισμένες συλλογές ({count})",
"collections.view_collection": "Προβολή συλλογής",
"collections.view_other_collections_by_user": "Δείτε άλλες συλλογές από αυτόν τον χρήστη",
"collections.visibility_public": "Δημόσια",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Απόρριψη της πρόχειρης ανάρτησης σας;",
"confirmations.discard_edit_media.confirm": "Απόρριψη",
"confirmations.discard_edit_media.message": "Έχεις μη αποθηκευμένες αλλαγές στην περιγραφή πολυμέσων ή στην προεπισκόπηση, απόρριψη ούτως ή άλλως;",
"confirmations.follow_to_collection.confirm": "Ακολουθήστε και προσθέστε στη συλλογή",
"confirmations.follow_to_collection.message": "Πρέπει να ακολουθήσετε τον χρήστη {name} για να τους προσθέσετε σε μια συλλογή.",
"confirmations.follow_to_collection.title": "Ακολουθήστε τον λογαριασμό;",
"confirmations.follow_to_list.confirm": "Ακολούθησε και πρόσθεσε στη λίστα",
"confirmations.follow_to_list.message": "Πρέπει να ακολουθήσεις τον χρήστη {name} για να τον προσθέσεις σε μια λίστα.",
"confirmations.follow_to_list.title": "Ακολούθηση χρήστη;",

View File

@ -357,7 +357,6 @@
"collection.share_template_other": "Check out this cool collection: {link}",
"collection.share_template_own": "Check out my new collection: {link}",
"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.by_account": "Delete",
"collections.collection_description": "Description",
@ -368,7 +367,6 @@
"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.",
"collections.create.accounts_title": "Who will you feature in this collection?",
"collections.create.basic_details_title": "Basic details",
"collections.create.steps": "Step {step}/{total}",
@ -386,7 +384,6 @@
"collections.detail.you_are_in_this_collection": "You're featured in this collection",
"collections.edit_details": "Edit details",
"collections.error_loading_collections": "There was an error when trying to load your collections.",
"collections.hints.accounts_counter": "{count} / {max} accounts",
"collections.last_updated_at": "Last updated: {date}",
"collections.manage_accounts": "Manage accounts",
"collections.mark_as_sensitive": "Mark as sensitive",
@ -394,13 +391,10 @@
"collections.name_length_hint": "40 characters limit",
"collections.new_collection": "New collection",
"collections.no_collections_yet": "No collections yet.",
"collections.old_last_post_note": "Last posted over a week ago",
"collections.remove_account": "Remove this account",
"collections.report_collection": "Report this collection",
"collections.revoke_collection_inclusion": "Remove myself from this collection",
"collections.revoke_inclusion.confirmation": "You've been removed from \"{collection}\"",
"collections.revoke_inclusion.error": "There was an error, please try again later.",
"collections.search_accounts_label": "Search for accounts to add…",
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
"collections.sensitive": "Sensitive",
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.",
@ -496,9 +490,6 @@
"confirmations.discard_draft.post.title": "Discard your draft post?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.follow_to_collection.confirm": "Follow and add to collection",
"confirmations.follow_to_collection.message": "You need to be following {name} to add them to a collection.",
"confirmations.follow_to_collection.title": "Follow account?",
"confirmations.follow_to_list.confirm": "Follow and add to list",
"confirmations.follow_to_list.message": "You need to be following {name} to add them to a list.",
"confirmations.follow_to_list.title": "Follow user?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Check out this cool collection: {link}",
"collection.share_template_own": "Check out my new collection: {link}",
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
"collections.accounts.empty_description": "Add up to {count} accounts you follow",
"collections.accounts.empty_description": "Add up to {count} accounts",
"collections.accounts.empty_editor_title": "No one is in this collection yet",
"collections.accounts.empty_title": "This collection is empty",
"collections.block_collection_owner": "Block account",
"collections.by_account": "by {account_handle}",
"collections.collection_description": "Description",
"collections.collection_language": "Language",
@ -370,7 +372,8 @@
"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.",
"collections.copy_link": "Copy link",
"collections.copy_link_confirmation": "Copied collection link to clipboard",
"collections.create.accounts_title": "Who will you feature in this collection?",
"collections.create.basic_details_title": "Basic details",
"collections.create.steps": "Step {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "There was an error when trying to load your collections.",
"collections.hidden_accounts_description": "Youve blocked or muted {count, plural, one {this user} other {these users}}",
"collections.hidden_accounts_link": "{count, plural, one {# hidden account} other {# hidden accounts}}",
"collections.hints.accounts_counter": "{count} / {max} accounts",
"collections.hints.accounts_counter": "{count}/{max} accounts",
"collections.last_updated_at": "Last updated: {date}",
"collections.manage_accounts": "Manage accounts",
"collections.mark_as_sensitive": "Mark as sensitive",
@ -398,21 +401,24 @@
"collections.name_length_hint": "40 characters limit",
"collections.new_collection": "New collection",
"collections.no_collections_yet": "No collections yet.",
"collections.old_last_post_note": "Last posted over a week ago",
"collections.remove_account": "Remove this account",
"collections.remove_account": "Remove",
"collections.report_collection": "Report this collection",
"collections.revoke_collection_inclusion": "Remove myself from this collection",
"collections.revoke_inclusion.confirmation": "You've been removed from \"{collection}\"",
"collections.revoke_inclusion.error": "There was an error, please try again later.",
"collections.search_accounts_label": "Search for accounts to add",
"collections.search_accounts_label": "Search for an account to add",
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
"collections.sensitive": "Sensitive",
"collections.share_short": "Share",
"collections.suggestions.can_not_add": "Cant be added",
"collections.suggestions.can_not_add_desc": "These accounts may have opted out of discovery, or they might be on a server that doesnt support collections.",
"collections.suggestions.must_follow": "Must follow first",
"collections.suggestions.must_follow_desc": "These accounts review all follow requests. Followers can add them to collections.",
"collections.topic_hint": "Add a hashtag that helps others understand the main topic of this collection.",
"collections.topic_special_chars_hint": "Special characters will be removed when saving",
"collections.unlisted_collections_description": "These dont appear on your profile to others. Anyone with the link can discover them.",
"collections.unlisted_collections_with_count": "Unlisted collections ({count})",
"collections.view_collection": "View collection",
"collections.view_other_collections_by_user": "View other collections by this user",
"collections.visibility_public": "Public",
"collections.visibility_public_hint": "Discoverable in search results and other areas where recommendations appear.",
"collections.visibility_title": "Visibility",
@ -502,9 +508,6 @@
"confirmations.discard_draft.post.title": "Discard your draft post?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.follow_to_collection.confirm": "Follow and add to collection",
"confirmations.follow_to_collection.message": "You need to be following {name} to add them to a collection.",
"confirmations.follow_to_collection.title": "Follow account?",
"confirmations.follow_to_list.confirm": "Follow and add to list",
"confirmations.follow_to_list.message": "You need to be following {name} to add them to a list.",
"confirmations.follow_to_list.title": "Follow user?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "¡Mirá qué copada está esta colección! {link}",
"collection.share_template_own": "Mirá mi nueva colección: {link}",
"collections.account_count": "{count, plural, one {# hora} other {# horas}}",
"collections.accounts.empty_description": "Agregá hasta {count} cuentas que seguís",
"collections.accounts.empty_description": "Agregá hasta {count} cuentas",
"collections.accounts.empty_editor_title": "Todavía no hay nadie en esta colección",
"collections.accounts.empty_title": "Esta colección está vacía",
"collections.block_collection_owner": "Bloquear cuenta",
"collections.by_account": "por {account_handle}",
"collections.collection_description": "Descripción",
"collections.collection_language": "Idioma",
@ -370,7 +372,8 @@
"collections.confirm_account_removal": "¿De verdad 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.",
"collections.copy_link": "Copiar enlace",
"collections.copy_link_confirmation": "Enlace a la colección copiado al portapapeles",
"collections.create.accounts_title": "¿A quién vas a destacar en esta colección?",
"collections.create.basic_details_title": "Detalles básicos",
"collections.create.steps": "Paso {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Hubo un error al intentar cargar tus colecciones.",
"collections.hidden_accounts_description": "Bloqueaste o silenciaste a {count, plural, one {este usuario} other {estos usuarios}}",
"collections.hidden_accounts_link": "{count, plural, one {# cuenta oculta} other {# cuentas ocultas}}",
"collections.hints.accounts_counter": "{count} / {max} cuentas",
"collections.hints.accounts_counter": "{count}/{max} cuentas",
"collections.last_updated_at": "Última actualización: {date}",
"collections.manage_accounts": "Administrar cuentas",
"collections.mark_as_sensitive": "Marcar como sensible",
@ -398,17 +401,19 @@
"collections.name_length_hint": "Límite de 40 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "No hay colecciones aún.",
"collections.old_last_post_note": "Último mensaje hace más de una semana",
"collections.remove_account": "Eliminar esta cuenta",
"collections.remove_account": "Quitar",
"collections.report_collection": "Denunciar esta colección",
"collections.revoke_collection_inclusion": "Quitarme de esta colección",
"collections.revoke_inclusion.confirmation": "Saliste de «{collection}»",
"collections.revoke_inclusion.error": "Hubo un error; por favor, intentalo de nuevo más tarde.",
"collections.search_accounts_label": "Buscar cuentas para agregar",
"collections.search_accounts_label": "Buscar una cuenta para agregar",
"collections.search_accounts_max_reached": "Agregaste el número máximo de cuentas",
"collections.sensitive": "Sensible",
"collections.share_short": "Compartir",
"collections.topic_hint": "Agregá una etiqueta que ayude a otros usuarios a entender el tema principal de esta colección.",
"collections.topic_special_chars_hint": "Los símbolos se eliminarán al guardar",
"collections.unlisted_collections_description": "Estas colecciones no aparecen en tu perfil para ser mostradas a otras cuentas. Cualquier persona que tenga el enlace puede descubrirlas.",
"collections.unlisted_collections_with_count": "Colecciones no listadas ({count})",
"collections.view_collection": "Abrir colección",
"collections.view_other_collections_by_user": "Ver otras colecciones de este usuario",
"collections.visibility_public": "Pública",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "¿Descartar tu borrador?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tenés cambios sin guardar en la descripción de medios o en la vista previa, ¿querés descartarlos de todos modos?",
"confirmations.follow_to_collection.confirm": "Seguir y agregar a la colección",
"confirmations.follow_to_collection.message": "Necesitás seguir a {name} para agregarle a una colección.",
"confirmations.follow_to_collection.title": "¿Seguir cuenta?",
"confirmations.follow_to_list.confirm": "Seguir y agregar a la lista",
"confirmations.follow_to_list.message": "Necesitás seguir a {name} para agregarle a una lista.",
"confirmations.follow_to_list.title": "¿Querés seguirle?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Echa un vistazo a esta increíble colección: {link}",
"collection.share_template_own": "Echa un vistazo a mi nueva colección: {link}",
"collections.account_count": "{count, plural,one {# cuenta} other {# cuentas}}",
"collections.accounts.empty_description": "Añade hasta {count} cuentas que sigues",
"collections.accounts.empty_description": "Añade hasta {count} cuentas",
"collections.accounts.empty_editor_title": "No hay nadie en esta colección todavía",
"collections.accounts.empty_title": "Esta colección está vacía",
"collections.block_collection_owner": "Bloquer cuenta",
"collections.by_account": "de {account_handle}",
"collections.collection_description": "Descripción",
"collections.collection_language": "Idioma",
@ -370,7 +372,8 @@
"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 añadir cuentas que sigas y que hayan optado por aparecer en los resultados de búsqueda.",
"collections.copy_link": "Copiar enlace",
"collections.copy_link_confirmation": "Se ha copiado el enlace de la colección al portapapeles",
"collections.create.accounts_title": "¿A quién incluirás en esta colección?",
"collections.create.basic_details_title": "Detalles básicos",
"collections.create.steps": "Paso {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Se produjo un error al intentar cargar tus colecciones.",
"collections.hidden_accounts_description": "Has bloqueado o silenciado {count, plural, one {a este usuario} other {a estos usuarios}}",
"collections.hidden_accounts_link": "{count, plural, one {# cuenta oculta} other {# cuentas ocultas}}",
"collections.hints.accounts_counter": "{count} / {max} cuentas",
"collections.hints.accounts_counter": "{count}/{max} cuentas",
"collections.last_updated_at": "Última actualización: {date}",
"collections.manage_accounts": "Administrar cuentas",
"collections.mark_as_sensitive": "Marcar como sensible",
@ -398,17 +401,19 @@
"collections.name_length_hint": "Limitado a 40 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "No hay colecciones todavía.",
"collections.old_last_post_note": "Última publicación hace más de una semana",
"collections.remove_account": "Eliminar esta cuenta",
"collections.remove_account": "Eliminar",
"collections.report_collection": "Reportar esta colección",
"collections.revoke_collection_inclusion": "Excluirme de esta colección",
"collections.revoke_inclusion.confirmation": "Has sido excluido de \"{collection}\"",
"collections.revoke_inclusion.error": "Se ha producido un error, por favor, inténtalo de nuevo más tarde.",
"collections.search_accounts_label": "Buscar cuentas para añadir",
"collections.search_accounts_label": "Buscar una cuenta para añadir",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
"collections.sensitive": "Sensible",
"collections.share_short": "Compartir",
"collections.topic_hint": "Añade una etiqueta que ayude a los demás a comprender el tema principal de esta colección.",
"collections.topic_special_chars_hint": "Los caracteres especiales se eliminarán al guardar",
"collections.unlisted_collections_description": "No aparecen en tu perfil. Cualquier persona que tenga el enlace puede descubrirlas.",
"collections.unlisted_collections_with_count": "Colecciones no listadas ({count})",
"collections.view_collection": "Ver colección",
"collections.view_other_collections_by_user": "Ver otras colecciones de este usuario",
"collections.visibility_public": "Pública",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "¿Deseas descartar tu borrador?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tienes cambios sin guardar en la descripción o vista previa del archivo, ¿deseas descartarlos de cualquier manera?",
"confirmations.follow_to_collection.confirm": "Seguir y añadir a la colección",
"confirmations.follow_to_collection.message": "Debes seguir a {name} para añadirlo a una colección.",
"confirmations.follow_to_collection.title": "¿Seguir cuenta?",
"confirmations.follow_to_list.confirm": "Seguir y agregar a la lista",
"confirmations.follow_to_list.message": "Tienes que seguir a {name} para añadirlo a una lista.",
"confirmations.follow_to_list.title": "¿Seguir a usuario?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Echa un vistazo a esta fantástica colección: {link}",
"collection.share_template_own": "Echa un vistazo a mi nueva colección: {link}",
"collections.account_count": "{count, plural, one {# cuenta} other {# cuentas}}",
"collections.accounts.empty_description": "Añade hasta {count} cuentas que sigas",
"collections.accounts.empty_description": "Añade hasta {count} cuentas",
"collections.accounts.empty_editor_title": "No hay nadie en esta colección todavía",
"collections.accounts.empty_title": "Esta colección está vacía",
"collections.block_collection_owner": "Bloquear cuenta",
"collections.by_account": "de {account_handle}",
"collections.collection_description": "Descripción",
"collections.collection_language": "Idioma",
@ -370,7 +372,8 @@
"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.",
"collections.copy_link": "Copiar enlace",
"collections.copy_link_confirmation": "Enlace a la colección copiado al portapapeles",
"collections.create.accounts_title": "¿A quién vas a destacar en esta colección?",
"collections.create.basic_details_title": "Datos básicos",
"collections.create.steps": "Paso {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Se ha producido un error al intentar cargar tus colecciones.",
"collections.hidden_accounts_description": "Has bloqueado o silenciado {count, plural,one {a este usuario}other {a estos usuarios}}",
"collections.hidden_accounts_link": "{count, plural, one {# cuenta oculta} other {# cuentas ocultas}}",
"collections.hints.accounts_counter": "{count} / {max} cuentas",
"collections.hints.accounts_counter": "{count}/{max} cuentas",
"collections.last_updated_at": "Última actualización: {date}",
"collections.manage_accounts": "Administrar cuentas",
"collections.mark_as_sensitive": "Marcar como sensible",
@ -398,17 +401,19 @@
"collections.name_length_hint": "Límite de 40 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "Aún no hay colecciones.",
"collections.old_last_post_note": "Última publicación hace más de una semana",
"collections.remove_account": "Quitar esta cuenta",
"collections.remove_account": "Eliminar",
"collections.report_collection": "Informar de esta colección",
"collections.revoke_collection_inclusion": "Sácame de esta colección",
"collections.revoke_inclusion.confirmation": "Has salido de \"{collection}\"",
"collections.revoke_inclusion.error": "Se ha producido un error, inténtalo de nuevo más tarde.",
"collections.search_accounts_label": "Buscar cuentas para añadir",
"collections.search_accounts_label": "Buscar una cuenta para añadir",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
"collections.sensitive": "Sensible",
"collections.share_short": "Compartir",
"collections.topic_hint": "Añadir una etiqueta que ayude a otros a entender el tema principal de esta colección.",
"collections.topic_special_chars_hint": "Los caracteres especiales se eliminarán al guardar",
"collections.unlisted_collections_description": "No aparecen en tu perfil. Cualquiera que tenga un enlace puede descubrirlas.",
"collections.unlisted_collections_with_count": "Colecciones no listadas ({count})",
"collections.view_collection": "Ver colección",
"collections.view_other_collections_by_user": "Ver otras colecciones de este usuario",
"collections.visibility_public": "Pública",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "¿Descartar tu borrador?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tienes cambios sin guardar en la descripción o vista previa del archivo audiovisual, ¿descartarlos de todos modos?",
"confirmations.follow_to_collection.confirm": "Seguir y añadir a la colección",
"confirmations.follow_to_collection.message": "Debes seguir a {name} para añadirlo a una colección.",
"confirmations.follow_to_collection.title": "¿Seguir cuenta?",
"confirmations.follow_to_list.confirm": "Seguir y añadir a la lista",
"confirmations.follow_to_list.message": "Necesitas seguir a {name} para agregarlo a una lista.",
"confirmations.follow_to_list.title": "¿Seguir usuario?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Vaata seda lahedat kogumikku: {link}",
"collection.share_template_own": "Vaata mu uut kogumikku: {link}",
"collections.account_count": "{count, plural, one {# kasutajakonto} other {# kasutajakontot}}",
"collections.accounts.empty_description": "Lisa kuni {count} kontot, mida jälgid",
"collections.accounts.empty_description": "Lisa kuni {count} kontot",
"collections.accounts.empty_editor_title": "Selles kogus pole veel kedagi",
"collections.accounts.empty_title": "See kogumik on tühi",
"collections.block_collection_owner": "Konto blokeerimine",
"collections.by_account": "{account_handle} poolt",
"collections.collection_description": "Kirjeldus",
"collections.collection_language": "Keel",
@ -370,7 +372,8 @@
"collections.confirm_account_removal": "Kas oled kindel, et soovid selle konto kogumikust eemaldada?",
"collections.content_warning": "Sisuhoiatus",
"collections.continue": "Jätka",
"collections.create.accounts_subtitle": "Lisada saad vaid kasutajakontosid, keda sa jälgid ja kes on sellise leiatvuse lubanud.",
"collections.copy_link": "Kopeeri link",
"collections.copy_link_confirmation": "Kogu link kopeeriti lõikelauale",
"collections.create.accounts_title": "Kes saavad olema selles kogumikus?",
"collections.create.basic_details_title": "Põhiandmed",
"collections.create.steps": "Samm {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Sinu kogumike laadimisel tekkis viga.",
"collections.hidden_accounts_description": "Oled blokeerinud või vaigistanud {count, plural, one {selle kasutaja} other {need kasutajad}}",
"collections.hidden_accounts_link": "{count, plural, one {# peidetud konto} other {# peidetud kontot}}",
"collections.hints.accounts_counter": "{count} / {max} kontot",
"collections.hints.accounts_counter": "{count}/{max} kontot",
"collections.last_updated_at": "Viimati uuendatud: {date}",
"collections.manage_accounts": "Halda kasutajakontosid",
"collections.mark_as_sensitive": "Märgi delikaatseks",
@ -398,17 +401,19 @@
"collections.name_length_hint": "40 märgi piir",
"collections.new_collection": "Uus kogumik",
"collections.no_collections_yet": "Kogumikke veel pole.",
"collections.old_last_post_note": "Viimati postitanud üle nädala tagasi",
"collections.remove_account": "Eemalda see konto",
"collections.remove_account": "Eemalda",
"collections.report_collection": "Raporteeri sellest kogumikust",
"collections.revoke_collection_inclusion": "Eemalda mind sellest kogumikust",
"collections.revoke_inclusion.confirmation": "Oled eemaldatud \"{collection}\"-st",
"collections.revoke_inclusion.error": "Oli viga, palun proovi hiljem uuesti.",
"collections.search_accounts_label": "Lisatavate kontode otsimine…",
"collections.search_accounts_label": "Otsi kontot, mida lisada",
"collections.search_accounts_max_reached": "Oled lisanud maksimumarv kontosid",
"collections.sensitive": "Tundlik",
"collections.share_short": "Jaga",
"collections.topic_hint": "Lisa teemaviide, mis aitab teistel kasutajatel mõista selle kogumiku põhisisu.",
"collections.topic_special_chars_hint": "Erimärgid eemaldatakse salvestamisel",
"collections.unlisted_collections_description": "Need pole sinu profiilis teiste kasutajate jaoks nähtavad. Neid saavad vaadata kõik, kellel on link.",
"collections.unlisted_collections_with_count": "Loetlemata kogud ({count})",
"collections.view_collection": "Vaata kogumikku",
"collections.view_other_collections_by_user": "Vaata selle kasuta ja teisi kogumikke",
"collections.visibility_public": "Avalik",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Kas loobud postituse kavandist?",
"confirmations.discard_edit_media.confirm": "Hülga",
"confirmations.discard_edit_media.message": "Sul on salvestamata muudatusi meediakirjelduses või eelvaates, kas hülgad need?",
"confirmations.follow_to_collection.confirm": "Jälgi ja lisa kogumikku",
"confirmations.follow_to_collection.message": "Pead jälgima {name}, enne kui neid kogumikku lisada saad.",
"confirmations.follow_to_collection.title": "Jälgida kontot?",
"confirmations.follow_to_list.confirm": "Jälgi ja lisa loetellu",
"confirmations.follow_to_list.message": "Pead jälgima kasutajat {name}, et lisada teda loetellu.",
"confirmations.follow_to_list.title": "Jälgida kasutajat?",
@ -1029,12 +1031,14 @@
"notifications_permission_banner.title": "Ära jää millestki ilma",
"onboarding.follows.back": "Tagasi",
"onboarding.follows.empty": "Kahjuks ei saa hetkel tulemusi näidata. Proovi kasutada otsingut või lehitse uurimise lehte, et leida inimesi, keda jälgida, või proovi hiljem uuesti.",
"onboarding.follows.next": "Järgmine: Seadista oma profiil",
"onboarding.follows.search": "Otsi",
"onboarding.follows.title": "Jälgi inimesi, et alustada",
"onboarding.profile.discoverable": "Muuda mu profiil avastatavaks",
"onboarding.profile.discoverable_hint": "Kui nõustud enda avastamisega Mastodonis, võivad sinu postitused ilmuda otsingutulemustes ja trendides ning sinu profiili võidakse soovitada sinuga sarnaste huvidega inimestele.",
"onboarding.profile.display_name": "Näidatav nimi",
"onboarding.profile.display_name_hint": "Su täisnimi või naljanimi…",
"onboarding.profile.finish": "Valmis",
"onboarding.profile.note": "Elulugu",
"onboarding.profile.note_hint": "Saad @mainida teisi kasutajaid või lisada #teemaviiteid…",
"onboarding.profile.title": "Profiili seadistamine",
@ -1106,6 +1110,7 @@
"report.category.title_account": "kontoga",
"report.category.title_status": "postitusega",
"report.close": "Valmis",
"report.collection_comment": "Miks soovid sellest kogust raporteerida?",
"report.comment.title": "Kas arvad, et on veel midagi, mida me peaks teadma?",
"report.forward": "Edasta ka {target} domeeni",
"report.forward_hint": "See kasutaja on teisest serverist. Kas saadan anonümiseeritud koopia sellest teatest sinna ka?",
@ -1127,6 +1132,8 @@
"report.rules.title": "Milliseid reegleid rikutakse?",
"report.statuses.subtitle": "Vali kõik, mis sobivad",
"report.statuses.title": "Kas on olemas postitusi, mis on sellele teavitusele tõenduseks?",
"report.submission_error": "Raportit ei saanud saata",
"report.submission_error_details": "Palun kontrolli internetiühendust ja proovi hiljem uuesti.",
"report.submit": "Esita",
"report.target": "Teatamine {target} kohta",
"report.thanks.take_action": "Need on su võimalused määrata, mida Mastodonis näed:",
@ -1180,6 +1187,8 @@
"sign_in_banner.mastodon_is": "Mastodon on parim viis olemaks kursis sellega, mis toimub.",
"sign_in_banner.sign_in": "Logi sisse",
"sign_in_banner.sso_redirect": "Sisene või registreeru",
"skip_links.hotkey": "<span>Kiirklahv</span> {hotkey}",
"skip_links.skip_to_content": "Hüppa põhisisuni",
"status.admin_account": "Ava @{name} moderaatorivaates",
"status.admin_domain": "Ava {domain} modeereerimisliides",
"status.admin_status": "Ava postitus moderaatorivaates",

View File

@ -81,7 +81,7 @@
"account.menu.add_to_list": "Lisää listaan…",
"account.menu.block": "Estä tili",
"account.menu.block_domain": "Estä {domain}",
"account.menu.copied": "Kopioitu tilin linkki leikepöydälle",
"account.menu.copied": "Tlin linkki kopioitu leikepöydälle",
"account.menu.copy": "Kopioi linkki",
"account.menu.direct": "Mainitse yksityisesti",
"account.menu.hide_reblogs": "Piilota tehostukset aikajanalta",
@ -359,8 +359,10 @@
"collection.share_template_other": "Katso tämä siisti kokoelma: {link}",
"collection.share_template_own": "Katso uusi kokoelmani: {link}",
"collections.account_count": "{count, plural, one {# tili} other {# tiliä}}",
"collections.accounts.empty_description": "Lisää enintään {count} seuraamaasi tiliä",
"collections.accounts.empty_description": "Lisää enintään {count} tiliä",
"collections.accounts.empty_editor_title": "Kukaan ei ole vielä tässä kokoelmassa",
"collections.accounts.empty_title": "Tämä kokoelma on tyhjä",
"collections.block_collection_owner": "Estä tili",
"collections.by_account": "koonnut {account_handle}",
"collections.collection_description": "Kuvaus",
"collections.collection_language": "Kieli",
@ -370,7 +372,8 @@
"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.",
"collections.copy_link": "Kopioi linkki",
"collections.copy_link_confirmation": "Kokoelman linkki kopioitu leikepöydälle",
"collections.create.accounts_title": "Keitä esittelet tässä kokoelmassa?",
"collections.create.basic_details_title": "Perustiedot",
"collections.create.steps": "Vaihe {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Kokoelmien latauksessa tapahtui virhe.",
"collections.hidden_accounts_description": "Olet estänyt tai mykistänyt {count, plural, one {tämän käyttäjän} other {nämä käyttäjät}}",
"collections.hidden_accounts_link": "{count, plural, one {# piilotettu tili} other {# piilotettua tiliä}}",
"collections.hints.accounts_counter": "{count} / {max} tiliä",
"collections.hints.accounts_counter": "{count}/{max} tiliä",
"collections.last_updated_at": "Päivitetty viimeksi {date}",
"collections.manage_accounts": "Hallitse tilejä",
"collections.mark_as_sensitive": "Merkitse arkaluonteiseksi",
@ -398,17 +401,19 @@
"collections.name_length_hint": "40 merkin rajoitus",
"collections.new_collection": "Uusi kokoelma",
"collections.no_collections_yet": "Ei vielä kokoelmia.",
"collections.old_last_post_note": "Julkaissut viimeksi yli viikko sitten",
"collections.remove_account": "Poista tämä tili",
"collections.remove_account": "Poista",
"collections.report_collection": "Raportoi tämä kokoelma",
"collections.revoke_collection_inclusion": "Poista itseni tästä kokoelmasta",
"collections.revoke_inclusion.confirmation": "Sinut on poistettu kokoelmasta ”{collection}”",
"collections.revoke_inclusion.error": "Tapahtui virhe yritä myöhemmin uudelleen.",
"collections.search_accounts_label": "Hae lisättäviä tilejä…",
"collections.search_accounts_label": "Hae lisättävää tiliä",
"collections.search_accounts_max_reached": "Olet lisännyt enimmäismäärän tilejä",
"collections.sensitive": "Arkaluonteinen",
"collections.share_short": "Jaa",
"collections.topic_hint": "Lisää aihetunniste, joka auttaa muita ymmärtämään tämän kokoelman pääaiheen.",
"collections.topic_special_chars_hint": "Erikoismerkit poistetaan tallennettaessa",
"collections.unlisted_collections_description": "Nämä eivät näy muille profiilissasi. Kuka tahansa, jolla on linkki, voi löytää ne.",
"collections.unlisted_collections_with_count": "Listaamattomat kokoelmat ({count})",
"collections.view_collection": "Näytä kokoelma",
"collections.view_other_collections_by_user": "Näytä muut tämän käyttäjän kokoelmat",
"collections.visibility_public": "Julkinen",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Hylätäänkö luonnosjulkaisusi?",
"confirmations.discard_edit_media.confirm": "Hylkää",
"confirmations.discard_edit_media.message": "Sinulla on tallentamattomia muutoksia median kuvaukseen tai esikatseluun. Hylätäänkö ne silti?",
"confirmations.follow_to_collection.confirm": "Seuraa ja lisää kokoelmaan",
"confirmations.follow_to_collection.message": "Sinun on seurattava tiliä {name}, jotta voit lisätä sen kokoelmaan.",
"confirmations.follow_to_collection.title": "Seurataanko tiliä?",
"confirmations.follow_to_list.confirm": "Seuraa ja lisää listaan",
"confirmations.follow_to_list.message": "Sinun on seurattava käyttäjää {name}, jotta voit lisätä hänet listaan.",
"confirmations.follow_to_list.title": "Seurataanko käyttäjää?",

View File

@ -278,7 +278,6 @@
"collection.share_template_other": "Hygg at hesum kula savninum: {link}",
"collection.share_template_own": "Hygg at mínum nýggja savni: {link}",
"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",
@ -286,7 +285,6 @@
"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.",
"collections.create.accounts_title": "Hvønn vil tú framhevja í hesum savninum?",
"collections.create.basic_details_title": "Grundleggjandi smálutir",
"collections.create.steps": "Stig {step}/{total}",
@ -299,7 +297,6 @@
"collections.detail.share": "Deil hetta savnið",
"collections.edit_details": "Rætta smálutir",
"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.last_updated_at": "Seinast dagført: {date}",
"collections.manage_accounts": "Umsit kontur",
"collections.mark_as_sensitive": "Merk sum viðkvæmt",
@ -307,10 +304,7 @@
"collections.name_length_hint": "Í mesta lagi 40 tekn",
"collections.new_collection": "Nýtt savn",
"collections.no_collections_yet": "Eingi søvn enn.",
"collections.old_last_post_note": "Postaði seinast fyri meira enn einari viku síðani",
"collections.remove_account": "Strika hesa kontuna",
"collections.report_collection": "Melda hetta savnið",
"collections.search_accounts_label": "Leita eftir kontum at leggja afturat…",
"collections.search_accounts_max_reached": "Tú hevur lagt afturat mesta talið av kontum",
"collections.sensitive": "Viðkvæmt",
"collections.topic_hint": "Legg afturat eitt frámerki, sum hjálpir øðrum at skilja høvuðevnið í hesum savninum.",
@ -403,9 +397,6 @@
"confirmations.discard_draft.post.title": "Vraka kladdupostin?",
"confirmations.discard_edit_media.confirm": "Vraka",
"confirmations.discard_edit_media.message": "Tú hevur broytingar í miðlalýsingini ella undansýningini, sum ikki eru goymdar. Vilt tú kortini vraka?",
"confirmations.follow_to_collection.confirm": "Fylg og legg afturat savni",
"confirmations.follow_to_collection.message": "Tú mást fylgja {name} fyri at leggja tey afturat einum savni.",
"confirmations.follow_to_collection.title": "Fylg kontu?",
"confirmations.follow_to_list.confirm": "Fylg og legg afturat lista",
"confirmations.follow_to_list.message": "Tú mást fylgja {name} fyri at leggja tey afturat einum lista.",
"confirmations.follow_to_list.title": "Fylg brúkara?",

View File

@ -106,6 +106,7 @@
"account.name.copy": "Copier lidentifiant",
"account.name.help.domain": "{domain} est le serveur qui héberge le profil et les messages du compte.",
"account.name.help.domain_self": "{domain} est le serveur qui héberge votre profil et vos messages.",
"account.name.help.footer": "Tout comme vous pouvez envoyer des courriels à des personnes utilisant différents fournisseurs de messagerie, vous pouvez interagir avec des personnes sur d'autres serveurs Mastodon, ainsi qu'avec n'importe qui utilisant une application sociale compatible avec Mastodon.",
"account.name.help.header": "Un identifiant est comme une adresse de courriel",
"account.name.help.username": "{username} est le nom d'utilisateur·ice de ce compte sur son serveur. Quelqu'un sur un autre serveur peut avoir le même nom.",
"account.name.help.username_self": "{username} est votre nom d'utilisateur·ice sur ce serveur. Quelqu'un sur un autre serveur peut avoir le même nom.",
@ -358,8 +359,8 @@
"collection.share_template_other": "Découvrez cette collection incroyable : {link}",
"collection.share_template_own": "Découvrez ma nouvelle collection : {link}",
"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.block_collection_owner": "Bloquer le compte",
"collections.by_account": "par {account_handle}",
"collections.collection_description": "Description",
"collections.collection_language": "Langue",
@ -369,7 +370,8 @@
"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.",
"collections.copy_link": "Copier le lien",
"collections.copy_link_confirmation": "Lien de la collection copié dans le presse-papiers",
"collections.create.accounts_title": "Qui voulez-vous mettre en avant dans cette collection?",
"collections.create.basic_details_title": "Informations générales",
"collections.create.steps": "Étape {step}/{total}",
@ -389,7 +391,6 @@
"collections.error_loading_collections": "Une erreur s'est produite durant le chargement de vos collections.",
"collections.hidden_accounts_description": "Vous avez bloqué ou masqué {count, plural, one {ce compte} other {ces comptes}}",
"collections.hidden_accounts_link": "{count, plural, one {# compte caché} other {# comptes cachés}}",
"collections.hints.accounts_counter": "{count} / {max} comptes",
"collections.last_updated_at": "Dernière mise à jour : {date}",
"collections.manage_accounts": "Gérer les comptes",
"collections.mark_as_sensitive": "Marquer comme sensible",
@ -397,17 +398,17 @@
"collections.name_length_hint": "Maximum 40 caractères",
"collections.new_collection": "Nouvelle collection",
"collections.no_collections_yet": "Aucune collection pour le moment.",
"collections.old_last_post_note": "Dernier message il y a plus d'une semaine",
"collections.remove_account": "Supprimer ce compte",
"collections.report_collection": "Signaler cette collection",
"collections.revoke_collection_inclusion": "Me retirer de cette collection",
"collections.revoke_inclusion.confirmation": "Vous avez été retiré·e de « {collection} »",
"collections.revoke_inclusion.error": "Une erreur s'est produite, veuillez réessayer plus tard.",
"collections.search_accounts_label": "Chercher des comptes à ajouter…",
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
"collections.sensitive": "Sensible",
"collections.share_short": "Partager",
"collections.topic_hint": "Ajouter un hashtag pour aider les autres personnes à comprendre le sujet de la collection.",
"collections.topic_special_chars_hint": "Les caractères spéciaux seront supprimés lors de l'enregistrement",
"collections.unlisted_collections_description": "Celles-ci n'apparaissent pas sur votre profil. N'importe qui ayant leur lien peut les découvrir.",
"collections.unlisted_collections_with_count": "Collections non listées ({count})",
"collections.view_collection": "Voir la collection",
"collections.view_other_collections_by_user": "Voir les autres collections par ce compte",
"collections.visibility_public": "Publique",
@ -499,9 +500,6 @@
"confirmations.discard_draft.post.title": "Abandonner votre brouillon ?",
"confirmations.discard_edit_media.confirm": "Rejeter",
"confirmations.discard_edit_media.message": "Vous avez des modifications non enregistrées de la description ou de l'aperçu du média, voulez-vous quand même les supprimer?",
"confirmations.follow_to_collection.confirm": "Suivre et ajouter à la collection",
"confirmations.follow_to_collection.message": "Vous devez suivre {name} pour l'ajouter à une collection.",
"confirmations.follow_to_collection.title": "Suivre le compte ?",
"confirmations.follow_to_list.confirm": "Suivre et ajouter à la liste",
"confirmations.follow_to_list.message": "Vous devez suivre {name} pour l'ajouter à une liste.",
"confirmations.follow_to_list.title": "Suivre l'utilisateur·rice ?",

View File

@ -106,6 +106,7 @@
"account.name.copy": "Copier lidentifiant",
"account.name.help.domain": "{domain} est le serveur qui héberge le profil et les messages du compte.",
"account.name.help.domain_self": "{domain} est le serveur qui héberge votre profil et vos messages.",
"account.name.help.footer": "Tout comme vous pouvez envoyer des courriels à des personnes utilisant différents fournisseurs de messagerie, vous pouvez interagir avec des personnes sur d'autres serveurs Mastodon, ainsi qu'avec n'importe qui utilisant une application sociale compatible avec Mastodon.",
"account.name.help.header": "Un identifiant est comme une adresse de courriel",
"account.name.help.username": "{username} est le nom d'utilisateur·ice de ce compte sur son serveur. Quelqu'un sur un autre serveur peut avoir le même nom.",
"account.name.help.username_self": "{username} est votre nom d'utilisateur·ice sur ce serveur. Quelqu'un sur un autre serveur peut avoir le même nom.",
@ -358,8 +359,8 @@
"collection.share_template_other": "Découvrez cette collection incroyable : {link}",
"collection.share_template_own": "Découvrez ma nouvelle collection : {link}",
"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.block_collection_owner": "Bloquer le compte",
"collections.by_account": "par {account_handle}",
"collections.collection_description": "Description",
"collections.collection_language": "Langue",
@ -369,7 +370,8 @@
"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.",
"collections.copy_link": "Copier le lien",
"collections.copy_link_confirmation": "Lien de la collection copié dans le presse-papiers",
"collections.create.accounts_title": "Qui voulez-vous mettre en avant dans cette collection?",
"collections.create.basic_details_title": "Informations générales",
"collections.create.steps": "Étape {step}/{total}",
@ -389,7 +391,6 @@
"collections.error_loading_collections": "Une erreur s'est produite durant le chargement de vos collections.",
"collections.hidden_accounts_description": "Vous avez bloqué ou masqué {count, plural, one {ce compte} other {ces comptes}}",
"collections.hidden_accounts_link": "{count, plural, one {# compte caché} other {# comptes cachés}}",
"collections.hints.accounts_counter": "{count} / {max} comptes",
"collections.last_updated_at": "Dernière mise à jour : {date}",
"collections.manage_accounts": "Gérer les comptes",
"collections.mark_as_sensitive": "Marquer comme sensible",
@ -397,17 +398,17 @@
"collections.name_length_hint": "Maximum 40 caractères",
"collections.new_collection": "Nouvelle collection",
"collections.no_collections_yet": "Aucune collection pour le moment.",
"collections.old_last_post_note": "Dernier message il y a plus d'une semaine",
"collections.remove_account": "Supprimer ce compte",
"collections.report_collection": "Signaler cette collection",
"collections.revoke_collection_inclusion": "Me retirer de cette collection",
"collections.revoke_inclusion.confirmation": "Vous avez été retiré·e de « {collection} »",
"collections.revoke_inclusion.error": "Une erreur s'est produite, veuillez réessayer plus tard.",
"collections.search_accounts_label": "Chercher des comptes à ajouter…",
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
"collections.sensitive": "Sensible",
"collections.share_short": "Partager",
"collections.topic_hint": "Ajouter un hashtag pour aider les autres personnes à comprendre le sujet de la collection.",
"collections.topic_special_chars_hint": "Les caractères spéciaux seront supprimés lors de l'enregistrement",
"collections.unlisted_collections_description": "Celles-ci n'apparaissent pas sur votre profil. N'importe qui ayant leur lien peut les découvrir.",
"collections.unlisted_collections_with_count": "Collections non listées ({count})",
"collections.view_collection": "Voir la collection",
"collections.view_other_collections_by_user": "Voir les autres collections par ce compte",
"collections.visibility_public": "Publique",
@ -499,9 +500,6 @@
"confirmations.discard_draft.post.title": "Abandonner votre brouillon ?",
"confirmations.discard_edit_media.confirm": "Supprimer",
"confirmations.discard_edit_media.message": "Vous avez des modifications non enregistrées de la description ou de l'aperçu du média. Voulez-vous les supprimer?",
"confirmations.follow_to_collection.confirm": "Suivre et ajouter à la collection",
"confirmations.follow_to_collection.message": "Vous devez suivre {name} pour l'ajouter à une collection.",
"confirmations.follow_to_collection.title": "Suivre le compte ?",
"confirmations.follow_to_list.confirm": "Suivre et ajouter à la liste",
"confirmations.follow_to_list.message": "Vous devez suivre {name} pour l'ajouter à une liste.",
"confirmations.follow_to_list.title": "Suivre l'utilisateur·rice ?",

View File

@ -106,6 +106,7 @@
"account.name.copy": "Láimhseáil chóipeála",
"account.name.help.domain": "Is é {domain} an freastalaí a óstálann próifíl agus poist an úsáideora.",
"account.name.help.domain_self": "Is é {domain} an freastalaí a óstálann do phróifíl agus do phoist.",
"account.name.help.footer": "Díreach mar is féidir leat ríomhphoist a sheoladh chuig daoine ag baint úsáide as soláthraithe ríomhphoist éagsúla, is féidir leat idirghníomhú le daoine ar fhreastalaithe Mastodon eile, agus le duine ar bith ar aipeanna sóisialta eile atá comhoiriúnach le Mastodon.",
"account.name.help.header": "Is cosúil le seoladh ríomhphoist é láimhseáil",
"account.name.help.username": "Is é {username} ainm úsáideora an chuntais seo ar a bhfreastalaí. Dfhéadfadh an t-ainm úsáideora céanna a bheith ag duine éigin ar fhreastalaí eile.",
"account.name.help.username_self": "Is é {username} d'ainm úsáideora ar an bhfreastalaí seo. D'fhéadfadh an t-ainm úsáideora céanna a bheith ag duine éigin ar fhreastalaí eile.",
@ -358,8 +359,8 @@
"collection.share_template_other": "Féach ar an mbailiúchán fionnuar seo: {link}",
"collection.share_template_own": "Féach ar mo bhailiúchán nua: {link}",
"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.block_collection_owner": "Cuntas blocáilte",
"collections.by_account": "le {account_handle}",
"collections.collection_description": "Cur síos",
"collections.collection_language": "Teanga",
@ -369,7 +370,8 @@
"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.",
"collections.copy_link": "Cóipeáil nasc",
"collections.copy_link_confirmation": "Cóipeáladh nasc an bhailiúcháin chuig an ghearrthaisce",
"collections.create.accounts_title": "Cé a bheidh le feiceáil agat sa bhailiúchán seo?",
"collections.create.basic_details_title": "Sonraí bunúsacha",
"collections.create.steps": "Céim {step}/{total}",
@ -389,7 +391,6 @@
"collections.error_loading_collections": "Tharla earráid agus iarracht á déanamh do bhailiúcháin a luchtú.",
"collections.hidden_accounts_description": "Tá bac nó múchadh déanta agat ar {count, plural, one {an t-úsáideoir seo} two {na húsáideoirí seo} few {na húsáideoirí seo} many {na húsáideoirí seo} other {na húsáideoirí seo}}",
"collections.hidden_accounts_link": "{count, plural, one {# cuntas folaithe} two {# cuntais folaithe} few {# cuntais folaithe} many {# cuntais folaithe} other {# cuntais folaithe}}",
"collections.hints.accounts_counter": "{count} / {max} cuntais",
"collections.last_updated_at": "Nuashonraithe go deireanach: {date}",
"collections.manage_accounts": "Bainistigh cuntais",
"collections.mark_as_sensitive": "Marcáil mar íogair",
@ -397,17 +398,17 @@
"collections.name_length_hint": "Teorainn 40 carachtar",
"collections.new_collection": "Bailiúchán nua",
"collections.no_collections_yet": "Gan aon bhailiúcháin fós.",
"collections.old_last_post_note": "Postáilte go deireanach breis agus seachtain ó shin",
"collections.remove_account": "Bain an cuntas seo",
"collections.report_collection": "Tuairiscigh an bailiúchán seo",
"collections.revoke_collection_inclusion": "Bain mé féin as an mbailiúchán seo",
"collections.revoke_inclusion.confirmation": "Baineadh as \"{collection}\" thú",
"collections.revoke_inclusion.error": "Tharla earráid, déan iarracht arís ar ball.",
"collections.search_accounts_label": "Cuardaigh cuntais le cur leis…",
"collections.search_accounts_max_reached": "Tá an líon uasta cuntas curtha leis agat",
"collections.sensitive": "Íogair",
"collections.share_short": "Comhroinn",
"collections.topic_hint": "Cuir haischlib leis a chabhraíonn le daoine eile príomhábhar an bhailiúcháin seo a thuiscint.",
"collections.topic_special_chars_hint": "Bainfear carachtair speisialta agus tú ag sábháil",
"collections.unlisted_collections_description": "Ní fheictear iad seo ar do phróifíl do dhaoine eile. Is féidir le duine ar bith a bhfuil an nasc aige iad a fháil amach.",
"collections.unlisted_collections_with_count": "Bailiúcháin neamhliostaithe ({count})",
"collections.view_collection": "Féach ar bhailiúchán",
"collections.view_other_collections_by_user": "Féach ar bhailiúcháin eile ón úsáideoir seo",
"collections.visibility_public": "Poiblí",
@ -499,9 +500,6 @@
"confirmations.discard_draft.post.title": "An bhfuil tú ag iarraidh do dhréachtphost a chaitheamh amach?",
"confirmations.discard_edit_media.confirm": "Faigh réidh de",
"confirmations.discard_edit_media.message": "Tá athruithe neamhshlánaithe don tuarascáil gné nó réamhamharc agat, faigh réidh dóibh ar aon nós?",
"confirmations.follow_to_collection.confirm": "Lean agus cuir leis an mbailiúchán",
"confirmations.follow_to_collection.message": "Ní mór duit a bheith ag leanúint {name} le go gcuirfidh tú iad le bailiúchán.",
"confirmations.follow_to_collection.title": "Lean an cuntas?",
"confirmations.follow_to_list.confirm": "Lean agus cuir leis an liosta",
"confirmations.follow_to_list.message": "Ní mór duit {name} a leanúint chun iad a chur le liosta.",
"confirmations.follow_to_list.title": "Lean an t-úsáideoir?",

View File

@ -356,7 +356,6 @@
"collection.share_template_other": "Thoir sùil air an deagh-chruinneachadh seo: {link}",
"collection.share_template_own": "Thoir sùil air a chruinneachadh ùr agam: {link}",
"collections.account_count": "{count, plural, one {# chunntas} two {# chunntas} few {# cunntasan} other {# cunntas}}",
"collections.accounts.empty_description": "Cuir ris suas ri {count} cunntas(an) a tha thu a leantainn",
"collections.accounts.empty_title": "Tha an an cruinneachadh seo falamh",
"collections.by_account": "le {account_handle}",
"collections.collection_description": "Tuairisgeul",
@ -367,7 +366,6 @@
"collections.confirm_account_removal": "A bheil thu cinnteach gu bheil thu airson an cunntas seo a thoirt air falbh on chruinneachadh seo?",
"collections.content_warning": "Rabhadh susbainte",
"collections.continue": "Lean air adhart",
"collections.create.accounts_subtitle": "Chan urrainn dhut cur ris ach cunntasan a leanas tu s a ghabh ri rùrachadh.",
"collections.create.accounts_title": "Cò bhrosnaicheas tu sa chruinneachadh seo?",
"collections.create.basic_details_title": "Bun-fhiosrachadh",
"collections.create.steps": "Ceum {step}/{total}",
@ -385,7 +383,6 @@
"collections.detail.you_are_in_this_collection": "Thathar do bhrosnachadh sa chruinneachadh seo",
"collections.edit_details": "Deasaich am fiosrachadh",
"collections.error_loading_collections": "Thachair mearachd nuair a dhfheuch sinn ris a chruinneachaidhean agad a luchdadh.",
"collections.hints.accounts_counter": "{count} / {max} cunntas(an)",
"collections.last_updated_at": "An tùrachadh mu dheireadh: {date}",
"collections.manage_accounts": "Stiùirich na cunntasan",
"collections.mark_as_sensitive": "Cuir comharra gu bheil e frionasach",
@ -393,13 +390,10 @@
"collections.name_length_hint": "Crìoch de 40 caractar",
"collections.new_collection": "Cruinneachadh ùr",
"collections.no_collections_yet": "Chan eil cruinneachadh agad fhathast.",
"collections.old_last_post_note": "Tha am post mu dheireadh còrr is seachdain air ais",
"collections.remove_account": "Thoir air falbh an cunntas seo",
"collections.report_collection": "Dèan gearan mun chruinneachadh seo",
"collections.revoke_collection_inclusion": "Thoir mi fhìn air falbh on chruinneachadh seo",
"collections.revoke_inclusion.confirmation": "Chaidh do thoirt air falbh o “{collection}”",
"collections.revoke_inclusion.error": "Thachair mearachd. Feuch ris a-rithist an ceann greis.",
"collections.search_accounts_label": "Lorg cunntasan gus an cur ris…",
"collections.search_accounts_max_reached": "Chuir thu na tha ceadaichte de chunntasan ris",
"collections.sensitive": "Frionasach",
"collections.topic_hint": "Cuir taga hais ris a chuidicheas càch le tuigse prìomh-chuspair a chruinneachaidh seo.",
@ -493,9 +487,6 @@
"confirmations.discard_draft.post.title": "A bheil thu airson dreachd a phuist agad a thilgeil air falbh?",
"confirmations.discard_edit_media.confirm": "Tilg air falbh",
"confirmations.discard_edit_media.message": "Tha atharraichean gun sàbhaladh agad ann an tuairisgeul no ro-shealladh a mheadhain, a bheil thu airson an tilgeil air falbh co-dhiù?",
"confirmations.follow_to_collection.confirm": "Lean s cuir ri cruinneachadh",
"confirmations.follow_to_collection.message": "Feumaidh tu {name} a leantainn mus cuir thu ri cruinneachadh iad.",
"confirmations.follow_to_collection.title": "A bheil thu airson an cunntas a leantainn?",
"confirmations.follow_to_list.confirm": "Lean s cuir ris an liosta",
"confirmations.follow_to_list.message": "Feumaidh tu {name} a leantainn ron chur ri liosta.",
"confirmations.follow_to_list.title": "A bheil thu airson an cleachdaiche a leantainn?",

View File

@ -106,6 +106,7 @@
"account.name.copy": "Copiar identificador",
"account.name.help.domain": "{domain} é o servidor que aloxa o perfil e as publicacións da usuaria.",
"account.name.help.domain_self": "{domain} é o servidor que aloxa o teu perfil e publicacións.",
"account.name.help.footer": "Do mesmo xeito que podes enviar un correo electrónico a unha persoa que utiliza outro provedor, podes relacionarte con persoas doutros servidores Mastodon, e con calquera das outras aplicacións sociais compatibles con Mastodon.",
"account.name.help.header": "Un alcume é o como o enderezo de correo",
"account.name.help.username": "{username} é o nome de usuaria da conta no seu servidor. Alguén noutro servidor podería ter o mesmo nome de usuaria.",
"account.name.help.username_self": "{username} é o teu nome de usuaria nete servidor. Alguén noutro sevidor podería ter o mesmo nome de usuaria.",
@ -358,8 +359,10 @@
"collection.share_template_other": "Mira que colección máis boa: {link}",
"collection.share_template_own": "Mira a miña nova colección: {link}",
"collections.account_count": "{count, plural, one {# conta} other {# contas}}",
"collections.accounts.empty_description": "Engade ata {count} contas que segues",
"collections.accounts.empty_description": "Engade ate {count} contas",
"collections.accounts.empty_editor_title": "Aínda non hai ninguén nesta colección",
"collections.accounts.empty_title": "A colección está baleira",
"collections.block_collection_owner": "Bloquear conta",
"collections.by_account": "de {account_handle}",
"collections.collection_description": "Descrición",
"collections.collection_language": "Idioma",
@ -369,7 +372,8 @@
"collections.confirm_account_removal": "Tes certeza de querer retirar esta conta desta colección?",
"collections.content_warning": "Aviso sobre o contido",
"collections.continue": "Continuar",
"collections.create.accounts_subtitle": "Só se poden engadir contas que segues e que optaron por ser incluídas en descubrir.",
"collections.copy_link": "Copiar ligazón",
"collections.copy_link_confirmation": "Copiouse ao portapapeis a ligazón á colección",
"collections.create.accounts_title": "A quen queres incluír nesta colección?",
"collections.create.basic_details_title": "Detalles básicos",
"collections.create.steps": "Paso {step}/{total}",
@ -389,7 +393,7 @@
"collections.error_loading_collections": "Houbo un erro ao intentar cargar as túas coleccións.",
"collections.hidden_accounts_description": "Bloqueaches ou silenciaches {count, plural, one {esta conta} other {estas contas}}",
"collections.hidden_accounts_link": "{count, plural, one {# conta oculta} other {# contas ocultas}}",
"collections.hints.accounts_counter": "{count} / {max} contas",
"collections.hints.accounts_counter": "{count}/{max} contas",
"collections.last_updated_at": "Última actualización: {date}",
"collections.manage_accounts": "Xestionar contas",
"collections.mark_as_sensitive": "Marcar como sensible",
@ -397,17 +401,19 @@
"collections.name_length_hint": "Límite de 40 caracteres",
"collections.new_collection": "Nova colección",
"collections.no_collections_yet": "Aínda non tes coleccións.",
"collections.old_last_post_note": "Hai máis dunha semana da última publicación",
"collections.remove_account": "Retirar esta conta",
"collections.remove_account": "Retirar",
"collections.report_collection": "Denunciar esta colección",
"collections.revoke_collection_inclusion": "Sácame desta colección",
"collections.revoke_inclusion.confirmation": "Quitámoste da colección \"{collection}\"",
"collections.revoke_inclusion.error": "Algo fallou, inténtao outra vez máis tarde.",
"collections.search_accounts_label": "Buscar contas para engadir…",
"collections.search_accounts_label": "Busca contas para engadilas",
"collections.search_accounts_max_reached": "Acadaches o máximo de contas permitidas",
"collections.sensitive": "Sensible",
"collections.share_short": "Compartir",
"collections.topic_hint": "Engadir un cancelo para que axudar a que outras persoas coñezan a temática desta colección.",
"collections.topic_special_chars_hint": "Vanse eliminar os caracteres especiais ao gardar",
"collections.unlisted_collections_description": "Estas non se mostran no teu perfil, pero calquera que coñeza a ligazón pode velas.",
"collections.unlisted_collections_with_count": "Coleccións fóra do perfil ({count})",
"collections.view_collection": "Ver colección",
"collections.view_other_collections_by_user": "Ver outras coleccións desta usuaria",
"collections.visibility_public": "Pública",
@ -499,9 +505,6 @@
"confirmations.discard_draft.post.title": "Desbotar o borrador?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tes cambios sen gardar para a vista previa ou descrición do multimedia, descartamos os cambios?",
"confirmations.follow_to_collection.confirm": "Seguir e engadir á colección",
"confirmations.follow_to_collection.message": "Tes que seguir a {name} para poder engadila a unha colección.",
"confirmations.follow_to_collection.title": "Seguir a conta?",
"confirmations.follow_to_list.confirm": "Seguir e engadir á lista",
"confirmations.follow_to_list.message": "Tes que seguir a {name} para poder engadila a unha lista.",
"confirmations.follow_to_list.title": "Seguir á usuaria?",
@ -1036,7 +1039,7 @@
"onboarding.profile.display_name": "Nome público",
"onboarding.profile.display_name_hint": "O teu nome completo ou un nome divertido…",
"onboarding.profile.finish": "Finalizar",
"onboarding.profile.note": "Acerca de ti",
"onboarding.profile.note": "Sobre ti",
"onboarding.profile.note_hint": "Podes @mencionar a outras persoas ou usar #cancelos…",
"onboarding.profile.title": "Configuración do perfil",
"onboarding.profile.upload_avatar": "Subir imaxe do perfil",

View File

@ -359,8 +359,8 @@
"collection.share_template_other": "הציצו על האוסף המעניין הזה: {link}",
"collection.share_template_own": "הציצו על האוסף החדש שלי: {link}",
"collections.account_count": "{count, plural, one {חשבון אחד} other {# חשבונות}}",
"collections.accounts.empty_description": "להוסיף עד ל־{count} חשבונות שאתם עוקבים אחריהם",
"collections.accounts.empty_title": "האוסף הזה ריק",
"collections.block_collection_owner": "חסימת חשבון",
"collections.by_account": "מאת {account_handle}",
"collections.collection_description": "תיאור",
"collections.collection_language": "שפה",
@ -370,7 +370,8 @@
"collections.confirm_account_removal": "בוודאות להסיר חשבון זה מהאוסף?",
"collections.content_warning": "אזהרת תוכן",
"collections.continue": "המשך",
"collections.create.accounts_subtitle": "רק חשבונות נעקבים שבחרו להופיע ב\"תגליות\" ניתנים להוספה.",
"collections.copy_link": "להעתקת הקישור",
"collections.copy_link_confirmation": "קישור לחשבון הועתק לקליפבורד",
"collections.create.accounts_title": "את מי תבליטו באוסף זה?",
"collections.create.basic_details_title": "פרטים בסיסיים",
"collections.create.steps": "צעד {step} מתוך {total}",
@ -390,7 +391,6 @@
"collections.error_loading_collections": "חלה שגיאה בנסיון לטעון את אוספיך.",
"collections.hidden_accounts_description": "חסמת או השתקת {count, plural,one {המשתמש.ת}other {המשתמשים}} האלו",
"collections.hidden_accounts_link": "{count, plural, one {חשבון מוסתר אחד} other {# חשבונות מוסתרים}}",
"collections.hints.accounts_counter": "{count} \\ {max} חשבונות",
"collections.last_updated_at": "עדכון אחרון: {date}",
"collections.manage_accounts": "ניהול חשבונות",
"collections.mark_as_sensitive": "מסומנים כרגישים",
@ -398,17 +398,17 @@
"collections.name_length_hint": "מגבלה של 40 תווים",
"collections.new_collection": "אוסף חדש",
"collections.no_collections_yet": "עוד אין אוספים.",
"collections.old_last_post_note": "פרסמו לאחרונה לפני יותר משבוע",
"collections.remove_account": "הסר חשבון זה",
"collections.report_collection": "דיווח על אוסף זה",
"collections.revoke_collection_inclusion": "הסירוני מאוסף זה",
"collections.revoke_inclusion.confirmation": "הוסרת מֿ\"{collection}\"",
"collections.revoke_inclusion.error": "הייתה שגיאה. נסו שוב מאוחר יותר.",
"collections.search_accounts_label": "לחפש חשבונות להוספה…",
"collections.search_accounts_max_reached": "הגעת למספר החשבונות המירבי",
"collections.sensitive": "רגיש",
"collections.share_short": "שיתוף",
"collections.topic_hint": "הוספת תגית שמסייעת לאחרים להבין את הנושא הראשי של האוסף.",
"collections.topic_special_chars_hint": "תווים מיוחדים יוסרו בעת השמירה",
"collections.unlisted_collections_description": "אוספים אלו לא מוצגים לאחרים בפרופיל שלך. למי שיקבל את הקישור תהיה גישה לגלות את התכנים.",
"collections.unlisted_collections_with_count": "אוספים שאינם לתצוגה ({count})",
"collections.view_collection": "צפיה באוסף",
"collections.view_other_collections_by_user": "צפייה באוספים אחרים של משתמש.ת אלו",
"collections.visibility_public": "פומבי",
@ -500,9 +500,6 @@
"confirmations.discard_draft.post.title": "לוותר על הטיוטא?",
"confirmations.discard_edit_media.confirm": "השלך",
"confirmations.discard_edit_media.message": "יש לך שינויים לא שמורים לתיאור המדיה. להשליך אותם בכל זאת?",
"confirmations.follow_to_collection.confirm": "עקיבה והוספה לאוסף",
"confirmations.follow_to_collection.message": "כדי להכניס את {name} לאוסף, ראשית יש לעקוב אחריהם.",
"confirmations.follow_to_collection.title": "לעקוב אחר החשבון?",
"confirmations.follow_to_list.confirm": "עקיבה והוספה לרשימה",
"confirmations.follow_to_list.message": "כדי להכניס את {name} לרשימה, ראשית יש לעקוב אחריהם.",
"confirmations.follow_to_list.title": "לעקוב אחר המשתמש.ת?",

View File

@ -358,7 +358,6 @@
"collection.share_template_other": "Nézd meg ezt a gyűjteményt: {link}",
"collection.share_template_own": "Nézd meg az új gyűjteményemet: {link}",
"collections.account_count": "{count, plural, one {# fiók} other {# fiók}}",
"collections.accounts.empty_description": "Adj hozzá legfeljebb {count} követett fiókot",
"collections.accounts.empty_title": "Ez a gyűjtemény üres",
"collections.by_account": "szerző: {account_handle}",
"collections.collection_description": "Leírás",
@ -369,7 +368,6 @@
"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.",
"collections.create.accounts_title": "Kit emelsz ki ebben a gyűjteményben?",
"collections.create.basic_details_title": "Alapvető részletek",
"collections.create.steps": "{step}. lépés/{total}",
@ -389,7 +387,6 @@
"collections.error_loading_collections": "Hiba történt a gyűjtemények betöltése során.",
"collections.hidden_accounts_description": "Blokkoltad vagy letiltottad {count, plural, one {ezt a felhasználót} other {ezeket a felhasználókat}}",
"collections.hidden_accounts_link": "{count, plural, one {# rejtett fiók} other {# rejtett fiók}}",
"collections.hints.accounts_counter": "{count} / {max} fiók",
"collections.last_updated_at": "Utoljára frissítve: {date}",
"collections.manage_accounts": "Fiókok kezelése",
"collections.mark_as_sensitive": "Megjelelölés érzénykenként",
@ -397,13 +394,10 @@
"collections.name_length_hint": "40 karakteres korlát",
"collections.new_collection": "Új gyűjtemény",
"collections.no_collections_yet": "Még nincsenek gyűjtemények.",
"collections.old_last_post_note": "Egy hete osztott meg legutóbb",
"collections.remove_account": "Fiók eltávolítása",
"collections.report_collection": "Gyűjtemény jelentése",
"collections.revoke_collection_inclusion": "Saját magam eltávolítása ebből a gyűjteményből",
"collections.revoke_inclusion.confirmation": "El lettél távolítva innen: „{collection}”",
"collections.revoke_inclusion.error": "Hiba történt, próbáld újra később.",
"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.sensitive": "Érzékeny",
"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.",
@ -499,9 +493,6 @@
"confirmations.discard_draft.post.title": "Elveted a piszkozatot?",
"confirmations.discard_edit_media.confirm": "Elvetés",
"confirmations.discard_edit_media.message": "Mentetlen változtatásaid vannak a média leírásában vagy előnézetében, mindenképp elveted?",
"confirmations.follow_to_collection.confirm": "Követés, és gyűjteményhez adás",
"confirmations.follow_to_collection.message": "Követned kell {name} felhasználót, hogy hozzáadhasd egy gyűjteményhez.",
"confirmations.follow_to_collection.title": "Fiók követése?",
"confirmations.follow_to_list.confirm": "Követés, és hozzáadás a listához",
"confirmations.follow_to_list.message": "Követned kell {name} felhasználót, hogy hozzáadhasd a listához.",
"confirmations.follow_to_list.title": "Felhasználó követése?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Kíktu á þetta áhugaverða safn: {link}",
"collection.share_template_own": "Kíktu á nýja safnið mitt: {link}",
"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_description": "Bættu við allt að {count} aðgöngum",
"collections.accounts.empty_editor_title": "Enginn er enn í þessu safni",
"collections.accounts.empty_title": "Þetta safn er tómt",
"collections.block_collection_owner": "Útiloka notandaaðgang",
"collections.by_account": "frá {account_handle}",
"collections.collection_description": "Lýsing",
"collections.collection_language": "Tungumál",
@ -370,7 +372,8 @@
"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.",
"collections.copy_link": "Afrita tengil",
"collections.copy_link_confirmation": "Afritaði tengil á safn á klippispjald",
"collections.create.accounts_title": "Hvern vilt þú gera áberandi í þessu safni?",
"collections.create.basic_details_title": "Grunnupplýsingar",
"collections.create.steps": "Skref {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Villa kom upp þegar reynt var að hlaða inn söfnunum þínum.",
"collections.hidden_accounts_description": "Þú hefur lokað á eða þaggað niður í {count, plural, one {þessum notanda} other {þessum notendum}}",
"collections.hidden_accounts_link": "{count, plural, one {# falinn aðgangur} other {# faldir aðgangar}}",
"collections.hints.accounts_counter": "{count} / {max} aðgangar",
"collections.hints.accounts_counter": "{count}/{max} aðgöngum",
"collections.last_updated_at": "Síðast uppfært: {date}",
"collections.manage_accounts": "Sýsla með notandaaðganga",
"collections.mark_as_sensitive": "Merkja sem viðkvæmt",
@ -398,17 +401,19 @@
"collections.name_length_hint": "40 stafa takmörk",
"collections.new_collection": "Nýtt safn",
"collections.no_collections_yet": "Engin söfn ennþá.",
"collections.old_last_post_note": "Birti síðast fyrir meira en viku síðan",
"collections.remove_account": "Fjarlægja þennan aðgang",
"collections.remove_account": "Fjarlægja",
"collections.report_collection": "Kæra þetta safn",
"collections.revoke_collection_inclusion": "Fjarlægja mig úr þessu safni",
"collections.revoke_inclusion.confirmation": "Þú varst fjarlægð/ur úr \"{collection}\"",
"collections.revoke_inclusion.error": "Upp kom villa, reyndu aftur síðar.",
"collections.search_accounts_label": "Leita að aðgöngum til að bæta við…",
"collections.search_accounts_label": "Leitaðu að aðgangi til að bæta við",
"collections.search_accounts_max_reached": "Þú hefur þegar bætt við leyfilegum hámarksfjölda aðganga",
"collections.sensitive": "Viðkvæmt",
"collections.share_short": "Deila",
"collections.topic_hint": "Bættu við myllumerki sem hjálpar öðrum að skilja aðalefni þessa safns.",
"collections.topic_special_chars_hint": "Sérstafir verða fjarlægðir við vistun",
"collections.unlisted_collections_description": "Þessi birtast ekki öðrum á notandasíðunni þinni. Þeir sem eru með tengil á þau geta séð þau.",
"collections.unlisted_collections_with_count": "Óskráð söfn ({count})",
"collections.view_collection": "Skoða safn",
"collections.view_other_collections_by_user": "Skoða önnur söfn frá þessum notanda",
"collections.visibility_public": "Opinbert",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Henda drögum að færslunni þinni?",
"confirmations.discard_edit_media.confirm": "Henda",
"confirmations.discard_edit_media.message": "Þú ert með óvistaðar breytingar á lýsingu myndefnis eða forskoðunar, henda þeim samt?",
"confirmations.follow_to_collection.confirm": "Fylgjast með og bæta í safn",
"confirmations.follow_to_collection.message": "Þú þarft að fylgjast með {name} til að geta bætt viðkomandi í safn.",
"confirmations.follow_to_collection.title": "Fylgjast með notandaaðgangnum?",
"confirmations.follow_to_list.confirm": "Fylgjast með og bæta á lista",
"confirmations.follow_to_list.message": "Þú þarft að fylgjast með {name} til að bæta viðkomandi á lista.",
"confirmations.follow_to_list.title": "Fylgjast með notanda?",

View File

@ -359,8 +359,8 @@
"collection.share_template_other": "Dai un'occhiata a questa fantastica collezione: {link}",
"collection.share_template_own": "Dai un'occhiata alla mia collezione: {link}",
"collections.account_count": "{count, plural, one {# account} other {# account}}",
"collections.accounts.empty_description": "Aggiungi fino a {count} account che segui",
"collections.accounts.empty_title": "Questa collezione è vuota",
"collections.block_collection_owner": "Blocca l'account",
"collections.by_account": "di {account_handle}",
"collections.collection_description": "Descrizione",
"collections.collection_language": "Lingua",
@ -370,7 +370,8 @@
"collections.confirm_account_removal": "Si è sicuri di voler rimuovere questo account da questa collezione?",
"collections.content_warning": "Avviso sul contenuto",
"collections.continue": "Continua",
"collections.create.accounts_subtitle": "Possono essere aggiunti solo gli account che segui e che hanno aderito alla funzione di scoperta.",
"collections.copy_link": "Copia il сollegamento",
"collections.copy_link_confirmation": "Collegamento della collezione copiato negli appunti",
"collections.create.accounts_title": "Chi includerai in questa collezione?",
"collections.create.basic_details_title": "Dettagli di base",
"collections.create.steps": "Step {step}/{total}",
@ -390,7 +391,6 @@
"collections.error_loading_collections": "Si è verificato un errore durante il tentativo di caricare le tue collezioni.",
"collections.hidden_accounts_description": "Hai bloccato o silenziato {count, plural, one {questo utente} other {questi utenti}}",
"collections.hidden_accounts_link": "{count, plural, one {# account nascosto} other {# account nascosti}}",
"collections.hints.accounts_counter": "{count} / {max} account",
"collections.last_updated_at": "Ultimo aggiornamento: {date}",
"collections.manage_accounts": "Gestisci account",
"collections.mark_as_sensitive": "Segna come sensibile",
@ -398,17 +398,17 @@
"collections.name_length_hint": "Limite di 40 caratteri",
"collections.new_collection": "Nuova collezione",
"collections.no_collections_yet": "Nessuna collezione ancora.",
"collections.old_last_post_note": "Ultimo post più di una settimana fa",
"collections.remove_account": "Rimuovi questo account",
"collections.report_collection": "Segnala questa collezione",
"collections.revoke_collection_inclusion": "Rimuovimi da questa collezione",
"collections.revoke_inclusion.confirmation": "Sei stato/a rimosso/a da \"{collection}\"",
"collections.revoke_inclusion.error": "Si è verificato un errore, si prega di riprovare più tardi.",
"collections.search_accounts_label": "Cerca account da aggiungere…",
"collections.search_accounts_max_reached": "Hai aggiunto il numero massimo di account",
"collections.sensitive": "Sensibile",
"collections.share_short": "Condividi",
"collections.topic_hint": "Aggiungi un hashtag che aiuti gli altri a comprendere l'argomento principale di questa collezione.",
"collections.topic_special_chars_hint": "I caratteri speciali verranno rimossi durante il salvataggio",
"collections.unlisted_collections_description": "Queste collezioni non sono visibili agli altri sul tuo profilo. Chiunque abbia i link può trovarle.",
"collections.unlisted_collections_with_count": "Collezioni non elencate ({count})",
"collections.view_collection": "Visualizza la collezione",
"collections.view_other_collections_by_user": "Visualizza altre collezioni da questo utente",
"collections.visibility_public": "Pubblica",
@ -500,9 +500,6 @@
"confirmations.discard_draft.post.title": "Scartare la tua bozza del post?",
"confirmations.discard_edit_media.confirm": "Scarta",
"confirmations.discard_edit_media.message": "Hai delle modifiche non salvate alla descrizione o anteprima del media, scartarle comunque?",
"confirmations.follow_to_collection.confirm": "Segui e aggiungi alla collezione",
"confirmations.follow_to_collection.message": "Devi seguire {name} per aggiungerlo/a ad una collezione.",
"confirmations.follow_to_collection.title": "Seguire l'account?",
"confirmations.follow_to_list.confirm": "Segui e aggiungi alla lista",
"confirmations.follow_to_list.message": "Devi seguire {name} per aggiungerli a una lista.",
"confirmations.follow_to_list.title": "Seguire l'utente?",

View File

@ -191,7 +191,6 @@
"collection.share_modal.title_new": "新しいコレクションを共有しよう!",
"collection.share_template_other": "この素敵なコレクションを見てみてください: {link}",
"collection.share_template_own": "私の新しいコレクションを見てみてください: {link}",
"collections.accounts.empty_description": "フォローしている {count} アカウントまで追加できます",
"collections.accounts.empty_title": "このコレクションは空です",
"collections.by_account": "{account_handle} による",
"collections.collection_description": "詳細",
@ -201,7 +200,6 @@
"collections.confirm_account_removal": "このコレクションからこのアカウントを削除してもよろしいですか?",
"collections.content_warning": "閲覧注意",
"collections.continue": "次に進む",
"collections.create.accounts_subtitle": "あなたがフォローしていてディスカバリー機能にオプトインしたアカウントのみを追加することができます。",
"collections.create.accounts_title": "このコレクションで誰に注目しますか?",
"collections.create.basic_details_title": "基本情報",
"collections.create.steps": "ステップ {step}/{total}",
@ -214,15 +212,12 @@
"collections.detail.loading": "コレクションを読み込み中…",
"collections.detail.share": "コレクションを共有",
"collections.detail.you_are_in_this_collection": "あなたはこのコレクションで紹介されています",
"collections.hints.accounts_counter": "{count} / {max} アカウント",
"collections.mark_as_sensitive": "閲覧注意としてマーク",
"collections.mark_as_sensitive_hint": "コレクションの説明とアカウントを閲覧注意で隠します。コレクション名は表示されます。",
"collections.name_length_hint": "40 文字まで",
"collections.new_collection": "新規のコレクション",
"collections.no_collections_yet": "コレクションはまだありません。",
"collections.remove_account": "このアカウントを削除する",
"collections.report_collection": "このコレクションを通報する",
"collections.search_accounts_label": "追加するアカウントを探す…",
"collections.topic_hint": "ハッシュタグを追加して、このコレクションの主なトピックを知ってもらいましょう。",
"collections.visibility_public": "公開",
"collections.visibility_public_hint": "検索結果やその他おすすめに表示されて、見つけてもらうことができます。",

View File

@ -259,15 +259,12 @@
"collections.detail.sensitive_content": "Agbur amḥulfu",
"collections.detail.share": "Zuzer talkensit-a",
"collections.edit_details": "Ẓreg talqayt",
"collections.hints.accounts_counter": "{count} / {max} n yimiḍanen",
"collections.manage_accounts": "Sefrek imiḍanen",
"collections.name_length_hint": "talast n 40 n yisekkilen",
"collections.new_collection": "Talkensit tamaynut",
"collections.no_collections_yet": "Ur ɛad llant tilkensa.",
"collections.remove_account": "Kkes amiḍan-a",
"collections.report_collection": "Cetki ɣef telkensit-a",
"collections.revoke_collection_inclusion": "Kkes-iyi seg telkensit-a",
"collections.search_accounts_label": "Nadi ɣef imiḍanen ara ternuḍ…",
"collections.view_collection": "Wali talkensit",
"collections.view_other_collections_by_user": "Wali tilkensa nniḍen sɣur useqdac-a",
"collections.visibility_public": "Azayaz",
@ -346,8 +343,6 @@
"confirmations.discard_draft.post.cancel": "Tuɣalin ar urewway",
"confirmations.discard_draft.post.title": "Deggeṛ arewway n yizen?",
"confirmations.discard_edit_media.confirm": "Sefsex",
"confirmations.follow_to_collection.confirm": "Ḍfeṛ-it sakin rnu-t ɣer telkensit",
"confirmations.follow_to_collection.title": "Ad tefreḍ amiḍan-a?",
"confirmations.follow_to_list.confirm": "Ḍfeṛ-it sakin rnu-t ɣer tebdart",
"confirmations.follow_to_list.title": "Ḍfer aseqdac?",
"confirmations.hide_featured_tab.confirm": "Ffer accer",

View File

@ -358,7 +358,6 @@
"collection.share_template_other": "緊看覓chit ê時行ê收藏:{link}",
"collection.share_template_own": "緊看覓我ê收藏:{link}",
"collections.account_count": "{count, plural, other {# ê口座}}",
"collections.accounts.empty_description": "加lí跟tuè ê口座上tsē {count} ê",
"collections.accounts.empty_title": "收藏內底無半項",
"collections.by_account": "tuì {account_handle}",
"collections.collection_description": "說明",
@ -369,7 +368,6 @@
"collections.confirm_account_removal": "Lí確定beh對收藏suá掉tsit ê口座?",
"collections.content_warning": "內容警告",
"collections.continue": "繼續",
"collections.create.accounts_subtitle": "Kan-ta通加lí所綴而且選擇加入探索ê。",
"collections.create.accounts_title": "Lí想beh佇收藏內底予siáng標做特色ê",
"collections.create.basic_details_title": "基本ê資料",
"collections.create.steps": "第 {step} 步,總共 {total} 步",
@ -387,7 +385,6 @@
"collections.detail.you_are_in_this_collection": "Lí已經hőng加kàu tsit ê收藏",
"collections.edit_details": "編輯詳細",
"collections.error_loading_collections": "佇載入lí ê收藏ê時陣出tshê。",
"collections.hints.accounts_counter": "{count} / {max} ê口座",
"collections.last_updated_at": "上尾更新tī{date}",
"collections.manage_accounts": "管理口座",
"collections.mark_as_sensitive": "標做敏感ê",
@ -395,13 +392,10 @@
"collections.name_length_hint": "限制 40 字",
"collections.new_collection": "新ê收藏",
"collections.no_collections_yet": "Iáu無收藏。",
"collections.old_last_post_note": "頂改佇超過一禮拜進前PO文",
"collections.remove_account": "Suá掉tsit ê口座",
"collections.report_collection": "檢舉tsit ê收藏",
"collections.revoke_collection_inclusion": "Kā我對收藏內底suá掉",
"collections.revoke_inclusion.confirmation": "Lí已經對「{collection}」hőng suá掉",
"collections.revoke_inclusion.error": "出tshê ah請小等leh koh試。",
"collections.search_accounts_label": "Tshuē口座來加添……",
"collections.search_accounts_max_reached": "Lí已經加kàu口座數ê盡磅ah。",
"collections.sensitive": "敏感ê",
"collections.topic_hint": "加 hashtag幫tsān別lâng了解tsit ê收藏ê主題。",
@ -497,9 +491,6 @@
"confirmations.discard_draft.post.title": "Kám beh棄sak lí PO文ê草稿",
"confirmations.discard_edit_media.confirm": "棄sak",
"confirmations.discard_edit_media.message": "Lí佇媒體敘述á是先看māi ê所在有iáu buē儲存ê改變kám beh kā in棄sak",
"confirmations.follow_to_collection.confirm": "跟tuè加入kàu收藏",
"confirmations.follow_to_collection.message": "Beh kā {name} 加添kàu收藏lí tio̍h先跟tuè伊。",
"confirmations.follow_to_collection.title": "敢beh跟tuè口座",
"confirmations.follow_to_list.confirm": "跟tuè加入kàu列單",
"confirmations.follow_to_list.message": "Beh kā {name} 加添kàu列單lí tio̍h先跟tuè伊。",
"confirmations.follow_to_list.title": "Kám beh跟tuè tsit ê用者?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Bekijk deze coole verzameling: {link}",
"collection.share_template_own": "Bekijk mijn nieuwe verzameling: {link}",
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
"collections.accounts.empty_description": "Tot {count} accounts die je volgt toevoegen",
"collections.accounts.empty_description": "Tot {count} accounts toevoegen",
"collections.accounts.empty_editor_title": "Er is nog nog niemand in deze verzameling",
"collections.accounts.empty_title": "Deze verzameling is leeg",
"collections.block_collection_owner": "Account blokkeren",
"collections.by_account": "door {account_handle}",
"collections.collection_description": "Omschrijving",
"collections.collection_language": "Taal",
@ -370,7 +372,8 @@
"collections.confirm_account_removal": "Weet je zeker dat je dit account uit deze verzameling wilt verwijderen?",
"collections.content_warning": "Inhoudswaarschuwing",
"collections.continue": "Doorgaan",
"collections.create.accounts_subtitle": "Alleen accounts die je volgt en ontdekt willen worden, kunnen worden toegevoegd.",
"collections.copy_link": "Link kopiëren",
"collections.copy_link_confirmation": "Verzamelinglink naar het klembord gekopieerd",
"collections.create.accounts_title": "Wie wil je aan deze verzameling toevoegen?",
"collections.create.basic_details_title": "Basisgegevens",
"collections.create.steps": "Stap {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Er is een fout opgetreden bij het laden van je verzamelingen.",
"collections.hidden_accounts_description": "Je hebt {count, plural, one {deze gebruiker} other {deze gebruikers}} geblokkeerd",
"collections.hidden_accounts_link": "{count, plural, one {# verborgen account} other {# verborgen accounts}}",
"collections.hints.accounts_counter": "{count} / {max} accounts",
"collections.hints.accounts_counter": "{count}/{max} accounts",
"collections.last_updated_at": "Laatst bijgewerkt: {date}",
"collections.manage_accounts": "Accounts beheren",
"collections.mark_as_sensitive": "Als gevoelig markeren",
@ -398,23 +401,25 @@
"collections.name_length_hint": "Limiet van 40 tekens",
"collections.new_collection": "Nieuwe verzameling",
"collections.no_collections_yet": "Nog geen verzamelingen.",
"collections.old_last_post_note": "Meer dan een week geleden voor het laatst een bericht geplaatst",
"collections.remove_account": "Dit account verwijderen",
"collections.remove_account": "Verwijderen",
"collections.report_collection": "Deze verzameling rapporteren",
"collections.revoke_collection_inclusion": "Mezelf uit deze verzameling verwijderen",
"collections.revoke_inclusion.confirmation": "Je bent uit \"{collection}\" verwijderd",
"collections.revoke_inclusion.error": "Er is een fout opgetreden. Probeer het later opnieuw.",
"collections.search_accounts_label": "Naar accounts zoeken om toe te voegen",
"collections.search_accounts_label": "Naar een account zoeken om toe te voegen",
"collections.search_accounts_max_reached": "Je hebt het maximum aantal accounts toegevoegd",
"collections.sensitive": "Gevoelig",
"collections.share_short": "Delen",
"collections.topic_hint": "Voeg een hashtag toe die anderen helpt het hoofdonderwerp van deze verzameling te begrijpen.",
"collections.topic_special_chars_hint": "Speciale tekens worden bij het opslaan verwijderd",
"collections.unlisted_collections_description": "Deze verschijnen niet voor anderen op je profiel. Iedereen met de link kan ze ontdekken.",
"collections.unlisted_collections_with_count": "Onzichtbare verzamelingen ({count})",
"collections.view_collection": "Verzameling bekijken",
"collections.view_other_collections_by_user": "Bekijk andere verzamelingen van deze gebruiker",
"collections.visibility_public": "Openbaar",
"collections.visibility_public_hint": "Te zien onder zoekresultaten en in andere gebieden waar aanbevelingen verschijnen.",
"collections.visibility_title": "Zichtbaarheid",
"collections.visibility_unlisted": "Niet zichtbaar",
"collections.visibility_unlisted": "Onzichtbaar",
"collections.visibility_unlisted_hint": "Zichtbaar voor iedereen met een link. Verborgen onder zoekresultaten en aanbevelingen.",
"column.about": "Over",
"column.blocks": "Geblokkeerde gebruikers",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Conceptbericht verwijderen?",
"confirmations.discard_edit_media.confirm": "Verwijderen",
"confirmations.discard_edit_media.message": "Je hebt niet-opgeslagen wijzigingen in de mediabeschrijving of voorvertonning, wil je deze toch verwijderen?",
"confirmations.follow_to_collection.confirm": "Volgen en aan verzameling toevoegen",
"confirmations.follow_to_collection.message": "Je moet {name} volgen om dit account aan een verzameling toe te kunnen voegen.",
"confirmations.follow_to_collection.title": "Account volgen?",
"confirmations.follow_to_list.confirm": "Volgen en toevoegen aan de lijst",
"confirmations.follow_to_list.message": "Je moet {name} volgen om dit account aan een lijst toe te kunnen voegen.",
"confirmations.follow_to_list.title": "Gebruiker volgen?",

View File

@ -356,7 +356,6 @@
"collection.share_template_other": "Sjekk denne samlinga: {link}",
"collection.share_template_own": "Sjekk den nye samlinga mi: {link}",
"collections.account_count": "{count, plural, one {# konto} other {# kontoar}}",
"collections.accounts.empty_description": "Legg til opp til {count} kontoar du fylgjer",
"collections.accounts.empty_title": "Denne samlinga er tom",
"collections.by_account": "av {account_handle}",
"collections.collection_description": "Skildring",
@ -367,7 +366,6 @@
"collections.confirm_account_removal": "Er du sikker på at du vil fjerna denne brukarkontoen frå samlinga?",
"collections.content_warning": "Innhaldsåtvaring",
"collections.continue": "Hald fram",
"collections.create.accounts_subtitle": "Du kan berre leggja til kontoar du fylgjer og som har sagt ja til å bli oppdaga.",
"collections.create.accounts_title": "Kven vil du leggja vekt på denne samlinga?",
"collections.create.basic_details_title": "Grunnleggjande opplysingar",
"collections.create.steps": "Steg {step}/{total}",
@ -385,7 +383,6 @@
"collections.detail.you_are_in_this_collection": "Du er framheva i denne samlinga",
"collections.edit_details": "Rediger detaljar",
"collections.error_loading_collections": "Noko gjekk gale då me prøvde å henta samlingane dine.",
"collections.hints.accounts_counter": "{count} av {max} kontoar",
"collections.last_updated_at": "Sist oppdatert: {date}",
"collections.manage_accounts": "Handter kontoar",
"collections.mark_as_sensitive": "Merk som ømtolig",
@ -393,13 +390,10 @@
"collections.name_length_hint": "Maks 40 teikn",
"collections.new_collection": "Ny samling",
"collections.no_collections_yet": "Du har ingen samlingar enno.",
"collections.old_last_post_note": "Sist lagt ut for over ei veke sidan",
"collections.remove_account": "Fjern denne kontoen",
"collections.report_collection": "Rapporter denne samlinga",
"collections.revoke_collection_inclusion": "Fjern meg frå denne samlinga",
"collections.revoke_inclusion.confirmation": "Du er fjerna frå «{collection}»",
"collections.revoke_inclusion.error": "Noko gjekk gale, prøv att seinare.",
"collections.search_accounts_label": "Søk etter kontoar å leggja til…",
"collections.search_accounts_max_reached": "Du har nådd grensa for kor mange kontoar du kan leggja til",
"collections.sensitive": "Ømtolig",
"collections.topic_hint": "Legg til ein emneknagg som hjelper andre å forstå hovudemnet for denne samlinga.",
@ -493,9 +487,6 @@
"confirmations.discard_draft.post.title": "Forkast kladden?",
"confirmations.discard_edit_media.confirm": "Forkast",
"confirmations.discard_edit_media.message": "Du har ulagra endringar i mediaskildringa eller førehandsvisinga. Vil du forkasta dei likevel?",
"confirmations.follow_to_collection.confirm": "Fylg og legg til samlinga",
"confirmations.follow_to_collection.message": "Du må fylgja {name} for å leggja dei til ei samling.",
"confirmations.follow_to_collection.title": "Fylg kontoen?",
"confirmations.follow_to_list.confirm": "Fylg og legg til lista",
"confirmations.follow_to_list.message": "Du må fylgja {name} for å leggja dei til ei liste.",
"confirmations.follow_to_list.title": "Vil du fylgja brukaren?",

View File

@ -278,7 +278,6 @@
"confirmations.delete_list.title": "Slett liste?",
"confirmations.discard_edit_media.confirm": "Forkast",
"confirmations.discard_edit_media.message": "Du har ulagrede endringer i mediebeskrivelsen eller i forhåndsvisning, forkast dem likevel?",
"confirmations.follow_to_collection.title": "Følg konto?",
"confirmations.follow_to_list.confirm": "Følg og legg til i liste",
"confirmations.follow_to_list.message": "Du må følge {name} for å kunne legge vedkommende til i en liste.",
"confirmations.follow_to_list.title": "Følg bruker?",

View File

@ -359,7 +359,6 @@
"collection.share_template_other": "Confira esta coleção incrível: {link}",
"collection.share_template_own": "Confira minha nova coleção: {link}",
"collections.account_count": "{count, plural, one {# conta} other {# conta}}",
"collections.accounts.empty_description": "Adicionar até {count} contas que você segue",
"collections.accounts.empty_title": "Esta coleção está vazia",
"collections.by_account": "por {account_handle}",
"collections.collection_description": "Descrição",
@ -370,7 +369,6 @@
"collections.confirm_account_removal": "Tem certeza que deseja remover esta conta da sua coleção?",
"collections.content_warning": "Aviso de conteúdo",
"collections.continue": "Continuar",
"collections.create.accounts_subtitle": "Somente é possível adicionar contas que você segue e que optaram por serem descobertas.",
"collections.create.accounts_title": "Quen você vai apresentar nesta coleção?",
"collections.create.basic_details_title": "Detalhes básicos",
"collections.create.steps": "Passo {step}/{total}",
@ -390,7 +388,6 @@
"collections.error_loading_collections": "Houve um erro ao tentar carregar suas coleções.",
"collections.hidden_accounts_description": "Você bloqueou ou silencilou {count, plural, one{este usuário} other {estes usuários}}",
"collections.hidden_accounts_link": "{count, plural, one {# conta oculta} other {# contas ocultas}}",
"collections.hints.accounts_counter": "{count} / {max} contas",
"collections.last_updated_at": "Atualizado pela última vez em {date}",
"collections.manage_accounts": "Gerenciar contas",
"collections.mark_as_sensitive": "Marcar como sensível",
@ -398,13 +395,10 @@
"collections.name_length_hint": "limite de 40 caracteres",
"collections.new_collection": "Nova coleção",
"collections.no_collections_yet": "Ainda não há coleções.",
"collections.old_last_post_note": "Publicado pela última vez semana passada",
"collections.remove_account": "Remover esta conta",
"collections.report_collection": "Denunciar esta coleção",
"collections.revoke_collection_inclusion": "Remover-me desta coleção",
"collections.revoke_inclusion.confirmation": "Você foi removido de \"{collection}\"",
"collections.revoke_inclusion.error": "Houve um erro, por favor tente novamente mais tarde.",
"collections.search_accounts_label": "Buscar contas para adicionar…",
"collections.search_accounts_max_reached": "Você acrescentou o numero máximo de contas",
"collections.sensitive": "Sensível",
"collections.topic_hint": "Adicione uma hashtag que ajude os outros a entender o tópico principal desta coleção.",
@ -500,9 +494,6 @@
"confirmations.discard_draft.post.title": "Eliminar seu rascunho de publicação?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Há mudanças não salvas na descrição ou pré-visualização da mídia. Descartar assim mesmo?",
"confirmations.follow_to_collection.confirm": "Seguir e adicionar a coleção",
"confirmations.follow_to_collection.message": "Você deve seguir {name} para adicionar a uma coleção.",
"confirmations.follow_to_collection.title": "Seguir conta?",
"confirmations.follow_to_list.confirm": "Seguir e adicionar à lista",
"confirmations.follow_to_list.message": "Você precisa seguir {name} para adicioná-lo à lista.",
"confirmations.follow_to_list.title": "Seguir usuário?",

View File

@ -358,8 +358,8 @@
"collection.share_template_other": "Veja esta coleção interessante: {link}",
"collection.share_template_own": "Veja a minha nova coleção: {link}",
"collections.account_count": "{count, plural, one {# conta} other {# contas}}",
"collections.accounts.empty_description": "Adicione até {count} contas que segue",
"collections.accounts.empty_title": "Esta coleção está vazia",
"collections.block_collection_owner": "Bloquear conta",
"collections.by_account": "por {account_handle}",
"collections.collection_description": "Descrição",
"collections.collection_language": "Idioma",
@ -369,7 +369,8 @@
"collections.confirm_account_removal": "Tem a certeza que quer remover esta conta desta coleção?",
"collections.content_warning": "Aviso de conteúdo",
"collections.continue": "Continuar",
"collections.create.accounts_subtitle": "Apenas as contas que segue e que optaram por ser descobertas podem ser adicionadas.",
"collections.copy_link": "Copiar hiperligação",
"collections.copy_link_confirmation": "A hiperligação da coleção foi copiada para a área de transferência",
"collections.create.accounts_title": "Quem vai destacar nesta coleção?",
"collections.create.basic_details_title": "Informações básicas",
"collections.create.steps": "Passo {step}/{total}",
@ -387,7 +388,6 @@
"collections.detail.you_are_in_this_collection": "Está em destaque nesta coleção",
"collections.edit_details": "Editar detalhes",
"collections.error_loading_collections": "Ocorreu um erro ao tentar carregar as suas coleções.",
"collections.hints.accounts_counter": "{count} / {max} contas",
"collections.last_updated_at": "Última atualização: {date}",
"collections.manage_accounts": "Gerir contas",
"collections.mark_as_sensitive": "Marcar como sensível",
@ -395,15 +395,13 @@
"collections.name_length_hint": "Limite de 40 carateres",
"collections.new_collection": "Nova coleção",
"collections.no_collections_yet": "Ainda não existem coleções.",
"collections.old_last_post_note": "Última publicação há mais de uma semana",
"collections.remove_account": "Remover esta conta",
"collections.report_collection": "Denunciar esta coleção",
"collections.revoke_collection_inclusion": "Remover-me desta coleção",
"collections.revoke_inclusion.confirmation": "Foi removido da coleção \"{collection}\"",
"collections.revoke_inclusion.error": "Ocorreu um erro, por favor tente novamente mais tarde.",
"collections.search_accounts_label": "Procurar contas para adicionar…",
"collections.search_accounts_max_reached": "Já adicionou o máximo de contas",
"collections.sensitive": "Sensível",
"collections.share_short": "Partilhar",
"collections.topic_hint": "Adicione uma etiqueta para ajudar outros a entender o tópico principal desta coleção.",
"collections.topic_special_chars_hint": "Os carateres especiais serão removidos ao guardar",
"collections.view_collection": "Ver coleções",
@ -497,9 +495,6 @@
"confirmations.discard_draft.post.title": "Descartar o rascunho da publicação?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tem alterações não guardadas na descrição ou na pré-visualização do ficheiro multimédia. Deseja ignorá-las na mesma?",
"confirmations.follow_to_collection.confirm": "Seguir e adicionar à coleção",
"confirmations.follow_to_collection.message": "Precisa de seguir {name} para o/a adicionar a uma coleção.",
"confirmations.follow_to_collection.title": "Seguir conta?",
"confirmations.follow_to_list.confirm": "Seguir e adicionar à lista",
"confirmations.follow_to_list.message": "Tens de seguir {name} para o adicionares a uma lista.",
"confirmations.follow_to_list.title": "Seguir utilizador?",

View File

@ -330,7 +330,6 @@
"collection.share_template_other": "Зацените эту замечательную подборку: {link}",
"collection.share_template_own": "Зацените мою новую подборку: {link}",
"collections.account_count": "{count, plural, one {# пользователь} few {# пользователя} other {# пользователей}}",
"collections.accounts.empty_description": "Вы можете добавить максимум {count} пользователей, на которых вы подписаны",
"collections.accounts.empty_title": "В этой подборке пока никого нет",
"collections.by_account": "составил(а) {account_handle}",
"collections.collection_description": "Описание",
@ -341,7 +340,6 @@
"collections.confirm_account_removal": "Вы уверены, что хотите исключить этого пользователя из подборки?",
"collections.content_warning": "Предупреждение о содержании",
"collections.continue": "Продолжить",
"collections.create.accounts_subtitle": "Вы можете добавить пользователя в подборку, только если вы подписаны на него, а его профиль доступен для поиска.",
"collections.create.accounts_title": "Кого вы выберете для этой подборки?",
"collections.create.basic_details_title": "Основная информация",
"collections.create.steps": "Шаг {step}/{total}",
@ -356,7 +354,6 @@
"collections.manage_accounts": "Управление аккаунтами",
"collections.name_length_hint": "Не более 40 символов",
"collections.new_collection": "Новая подборка",
"collections.remove_account": "Исключить этого пользователя",
"collections.report_collection": "Пожаловаться на подборку",
"collections.revoke_collection_inclusion": "Исключить себя из этой подборки",
"collections.revoke_inclusion.confirmation": "Вы были исключены из подборки «{collection}»",
@ -450,9 +447,6 @@
"confirmations.discard_draft.post.title": "Стереть несохранённый черновик поста?",
"confirmations.discard_edit_media.confirm": "Сбросить",
"confirmations.discard_edit_media.message": "У вас есть несохранённые изменения, касающиеся описания медиа или области предпросмотра. Сбросить их?",
"confirmations.follow_to_collection.confirm": "Подписаться и добавить",
"confirmations.follow_to_collection.message": "Чтобы добавить пользователя {name} в подборку, вы должны быть на него подписаны.",
"confirmations.follow_to_collection.title": "Подписаться на пользователя?",
"confirmations.follow_to_list.confirm": "Подписаться и добавить",
"confirmations.follow_to_list.message": "Чтобы добавить пользователя {name} в список, вы должны быть на него подписаны.",
"confirmations.follow_to_list.title": "Подписаться на пользователя?",

View File

@ -354,8 +354,10 @@
"collection.share_template_other": "Shihni këtë koleksion të hijshëm: {link}",
"collection.share_template_own": "Shihni koleksionin tim të ri: {link}",
"collections.account_count": "{count, plural, one {# llogari} other {# llogari}}",
"collections.accounts.empty_description": "Shtoni deri në {count} llogari që ndiqni",
"collections.accounts.empty_description": "Shtoni deri në {count} llogari",
"collections.accounts.empty_editor_title": "Në këtë koleksion ska ende njeri",
"collections.accounts.empty_title": "Ky koleksion është i zbrazët",
"collections.block_collection_owner": "Bllokoje llogarinë",
"collections.by_account": "nga {account_handle}",
"collections.collection_description": "Përshkrim",
"collections.collection_language": "Gjuhë",
@ -365,7 +367,8 @@
"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.",
"collections.copy_link": "Kopjoji lidhjen",
"collections.copy_link_confirmation": "U kopjua në të papastër lidhja e koleksionit",
"collections.create.accounts_title": "Kë do të shfaqni në këtë koleksion?",
"collections.create.basic_details_title": "Hollësi bazë",
"collections.create.steps": "Hapi {step}/{total}",
@ -385,7 +388,7 @@
"collections.error_loading_collections": "Pati një gabim teksa provohej të ngarkoheshin koleksionet tuaj.",
"collections.hidden_accounts_description": "Bllokuat, ose heshtuat {count, plural, one {këtë përdorues} other {këta përdorues}}",
"collections.hidden_accounts_link": "{count, plural, one {# llogari e fshehur} other {# llogari të fshehura}}",
"collections.hints.accounts_counter": "{count} / {max} llogari",
"collections.hints.accounts_counter": "{count}/{max} llogari",
"collections.last_updated_at": "Përditësuar së fundi më: {date}",
"collections.manage_accounts": "Administroni llogari",
"collections.mark_as_sensitive": "Vëri shenjë si rezervat",
@ -393,17 +396,19 @@
"collections.name_length_hint": "Kufi 40 shenja",
"collections.new_collection": "Koleksion i ri",
"collections.no_collections_yet": "Ende pa koleksione.",
"collections.old_last_post_note": "Të postuarat e fundit gjatë një jave më parë",
"collections.remove_account": "Hiqe këtë llogari",
"collections.remove_account": "Hiqe",
"collections.report_collection": "Raportojeni këtë koleksion",
"collections.revoke_collection_inclusion": "Hiqmëni nga ky koleksion",
"collections.revoke_inclusion.confirmation": "U hoqët nga “{collection}”",
"collections.revoke_inclusion.error": "Pati një gabim, ju lutemi, riprovoni më vonë.",
"collections.search_accounts_label": "Kërkoni për llogari për shtim…",
"collections.search_accounts_label": "Kërkoni për një llogari për tu shtuar",
"collections.search_accounts_max_reached": "Keni shtuar numrin maksimum të llogarive",
"collections.sensitive": "Rezervat",
"collections.share_short": "Ndajeni me të tjerë",
"collections.topic_hint": "Shtoni një hashtag që ndihmon të tjerët të kuptojnë temën kryesore të këtij koleksion.",
"collections.topic_special_chars_hint": "Shenjat e posaçme do të hiqen, kur ruhet",
"collections.unlisted_collections_description": "Këto nuk shfaqen në profilin tuaj për të tjerët. Cilido me lidhjen mund ti zbulojë.",
"collections.unlisted_collections_with_count": "Koleksione të pashfaqur ({count})",
"collections.view_collection": "Shiheni koleksionin",
"collections.view_other_collections_by_user": "Shihni koleksione të tjera nga ky përdorues",
"collections.visibility_public": "Publik",
@ -495,9 +500,6 @@
"confirmations.discard_draft.post.title": "Të hidhet tej skica e postimit tuaj?",
"confirmations.discard_edit_media.confirm": "Hidhe tej",
"confirmations.discard_edit_media.message": "Keni ndryshime të paruajtura te përshkrimi ose paraparja e medias, të hidhen tej, sido qoftë?",
"confirmations.follow_to_collection.confirm": "Ndiqe dhe shtoje në koleksion",
"confirmations.follow_to_collection.message": "Që ta shtoni te një koleksion, duhet të jeni duke e ndjekur {name}.",
"confirmations.follow_to_collection.title": "Të ndiqet llogaria?",
"confirmations.follow_to_list.confirm": "Ndiqe dhe shtoje te listë",
"confirmations.follow_to_list.message": "Lypset të jeni duke e ndjekur {name}, që të shtohte te një listë.",
"confirmations.follow_to_list.title": "Të ndiqet përdoruesi?",

View File

@ -357,8 +357,8 @@
"collection.share_template_other": "Kolla in denna coola samling: {link}",
"collection.share_template_own": "Kolla in min nya samling: {link}",
"collections.account_count": "{count, plural, one {# konto} other {# konton}}",
"collections.accounts.empty_description": "Lägg till upp till {count} konton som du följer",
"collections.accounts.empty_title": "Denna samling är tom",
"collections.block_collection_owner": "Blockera konto",
"collections.by_account": "av {account_handle}",
"collections.collection_description": "Beskrivning",
"collections.collection_language": "Språk",
@ -368,7 +368,7 @@
"collections.confirm_account_removal": "Är du säker på att du vill ta bort detta konto från denna samling?",
"collections.content_warning": "Innehållsvarning",
"collections.continue": "Fortsätt",
"collections.create.accounts_subtitle": "Endast konton som du följer som har valt att upptäcka kan läggas till.",
"collections.copy_link": "Kopiera länk",
"collections.create.accounts_title": "Vem kommer du att visa i den här samlingen?",
"collections.create.basic_details_title": "Grundläggande detaljer",
"collections.create.steps": "Steg {step}/{total}",
@ -386,7 +386,6 @@
"collections.detail.you_are_in_this_collection": "Du är med i denna samling",
"collections.edit_details": "Redigera detaljer",
"collections.error_loading_collections": "Det uppstod ett fel när dina samlingar skulle laddas.",
"collections.hints.accounts_counter": "{count} / {max} konton",
"collections.last_updated_at": "Senast uppdaterad: {date}",
"collections.manage_accounts": "Hantera konton",
"collections.mark_as_sensitive": "Markera som känsligt innehåll",
@ -394,15 +393,13 @@
"collections.name_length_hint": "Begränsat till 40 tecken",
"collections.new_collection": "Ny samling",
"collections.no_collections_yet": "Inga samlingar än.",
"collections.old_last_post_note": "Skapade senast ett inlägg för över en vecka sedan",
"collections.remove_account": "Ta bort detta konto",
"collections.report_collection": "Rapportera denna samling",
"collections.revoke_collection_inclusion": "Ta bort mig själv från denna samling",
"collections.revoke_inclusion.confirmation": "Du har tagits bort från \"{collection}\"",
"collections.revoke_inclusion.error": "Ett fel uppstod, försök igen senare.",
"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.sensitive": "Känsligt",
"collections.share_short": "Dela",
"collections.topic_hint": "Lägg till en hashtagg som hjälper andra att förstå huvudämnet i denna samling.",
"collections.topic_special_chars_hint": "Specialtecken kommer att tas bort när du sparar",
"collections.view_collection": "Visa samling",
@ -494,9 +491,6 @@
"confirmations.discard_draft.post.title": "Vill du förkasta utkastet?",
"confirmations.discard_edit_media.confirm": "Kasta",
"confirmations.discard_edit_media.message": "Du har osparade ändringar till mediabeskrivningen eller förhandsgranskningen, kasta bort dem ändå?",
"confirmations.follow_to_collection.confirm": "Följ och lägg till i samlingen",
"confirmations.follow_to_collection.message": "Du måste följa {name} för att lägga till dem i en samling.",
"confirmations.follow_to_collection.title": "Följ kontot?",
"confirmations.follow_to_list.confirm": "Följ och lägg till i listan",
"confirmations.follow_to_list.message": "Du måste följa {name} för att lägga till dem i en lista.",
"confirmations.follow_to_list.title": "Följ användare?",

View File

@ -152,7 +152,6 @@
"closed_registrations_modal.find_another_server": "ค้นหาเซิร์ฟเวอร์อื่น",
"closed_registrations_modal.preamble": "Mastodon เป็นแบบกระจายศูนย์ ดังนั้นไม่ว่าคุณจะสร้างบัญชีของคุณที่ใด คุณจะสามารถติดตามและโต้ตอบกับใครก็ตามในเซิร์ฟเวอร์นี้ คุณยังสามารถโฮสต์บัญชีด้วยตนเองได้อีกด้วย!",
"closed_registrations_modal.title": "การลงทะเบียนใน Mastodon",
"collections.accounts.empty_description": "สามารถเพิ่มได้สูงสุด {count} บัญชีที่คุณติดตาม",
"column.about": "เกี่ยวกับ",
"column.blocks": "ผู้ใช้ที่ปิดกั้นอยู่",
"column.bookmarks": "ที่คั่นหน้า",
@ -221,9 +220,6 @@
"confirmations.discard_draft.post.title": "ละทิ้งโพสต์แบบร่างของคุณ?",
"confirmations.discard_edit_media.confirm": "ละทิ้ง",
"confirmations.discard_edit_media.message": "คุณมีการเปลี่ยนแปลงคำอธิบายหรือตัวอย่างสื่อที่ยังไม่ได้บันทึก ละทิ้งการเปลี่ยนแปลงเหล่านั้นต่อไป?",
"confirmations.follow_to_collection.confirm": "ติดตามและเพิ่มไปยังคอลเล็กชัน",
"confirmations.follow_to_collection.message": "คุณต้องติดตาม {name} ถึงจะสามารถเพิ่มพวกเขาลงคอลเล็กชันได้",
"confirmations.follow_to_collection.title": "ติดตามบัญชีนี้ไหม?",
"confirmations.follow_to_list.confirm": "ติดตามและเพิ่มไปยังรายการ",
"confirmations.follow_to_list.message": "คุณต้องติดตาม {name} ถึงจะสามารถเพิ่มพวกเขาลงลิสต์ได้",
"confirmations.follow_to_list.title": "ติดตามผู้ใช้?",

View File

@ -359,8 +359,8 @@
"collection.share_template_other": "Bu harika koleksiyona göz atın: {link}",
"collection.share_template_own": "Yeni koleksiyonuma göz atın: {link}",
"collections.account_count": "{count, plural, one {# hesap} other {# hesap}}",
"collections.accounts.empty_description": "Takip ettiğiniz hesapların sayısını {count} kadar artırın",
"collections.accounts.empty_title": "Bu koleksiyon boş",
"collections.block_collection_owner": "Hesabı engelle",
"collections.by_account": "yazan {account_handle}",
"collections.collection_description": "Açıklama",
"collections.collection_language": "Dil",
@ -370,7 +370,8 @@
"collections.confirm_account_removal": "Bu hesabı bu koleksiyondan çıkarmak istediğinizden emin misiniz?",
"collections.content_warning": "İçerik uyarısı",
"collections.continue": "Devam et",
"collections.create.accounts_subtitle": "Yalnızca keşif seçeneğini etkinleştirmiş takip ettiğiniz hesaplar eklenebilir.",
"collections.copy_link": "Bağlantıyı kopyala",
"collections.copy_link_confirmation": "Koleksiyon bağlantısı panoya kopyalandı",
"collections.create.accounts_title": "Bu koleksiyonda kimleri öne çıkaracaksınız?",
"collections.create.basic_details_title": "Temel bilgiler",
"collections.create.steps": "Adım {step}/{total}",
@ -390,7 +391,6 @@
"collections.error_loading_collections": "Koleksiyonlarınızı yüklemeye çalışırken bir hata oluştu.",
"collections.hidden_accounts_description": "Bu {count, plural, one {kullanıcıyı} other {kullanıcıları}} engellediniz veya sessize aldınız",
"collections.hidden_accounts_link": "{count, plural, one {# gizli hesap} other {# gizli hesap}}",
"collections.hints.accounts_counter": "{count} / {max} hesap",
"collections.last_updated_at": "Son güncelleme: {date}",
"collections.manage_accounts": "Hesapları yönet",
"collections.mark_as_sensitive": "Hassas olarak işaretle",
@ -398,17 +398,17 @@
"collections.name_length_hint": "40 karakterle sınırlı",
"collections.new_collection": "Yeni koleksiyon",
"collections.no_collections_yet": "Henüz hiçbir koleksiyon yok.",
"collections.old_last_post_note": "Son gönderi bir haftadan önce",
"collections.remove_account": "Bu hesabı çıkar",
"collections.report_collection": "Bu koleksiyonu bildir",
"collections.revoke_collection_inclusion": "Beni bu koleksiyondan çıkar",
"collections.revoke_inclusion.confirmation": "\"{collection}\" koleksiyonundan çıkarıldınız",
"collections.revoke_inclusion.error": "Bir hata oluştu, lütfen daha sonra tekrar deneyin.",
"collections.search_accounts_label": "Eklemek için hesap arayın…",
"collections.search_accounts_max_reached": "Maksimum hesabı eklediniz",
"collections.sensitive": "Hassas",
"collections.share_short": "Paylaş",
"collections.topic_hint": "Bu koleksiyonun ana konusunu başkalarının anlamasına yardımcı olacak bir etiket ekleyin.",
"collections.topic_special_chars_hint": "Kaydederken özel karakterler silinecektir",
"collections.unlisted_collections_description": "Bunlar profilinizde başkalarına görünmez. Bağlantıya sahip olan herkes bunları görebilir.",
"collections.unlisted_collections_with_count": "Listelenmeyen koleksiyonlar ({count})",
"collections.view_collection": "Koleksiyonu görüntüle",
"collections.view_other_collections_by_user": "Bu kullanıcının diğer koleksiyonlarını görüntüle",
"collections.visibility_public": "Herkese açık",
@ -500,9 +500,6 @@
"confirmations.discard_draft.post.title": "Taslak gönderiniz silinsin mi?",
"confirmations.discard_edit_media.confirm": "Vazgeç",
"confirmations.discard_edit_media.message": "Medya açıklaması veya ön izlemede kaydedilmemiş değişiklikleriniz var, yine de vazgeçmek istiyor musunuz?",
"confirmations.follow_to_collection.confirm": "Takip et ve koleksiyona ekle",
"confirmations.follow_to_collection.message": "Bir koleksiyona eklemek için {name} kişisini takip etmeniz gerekiyor.",
"confirmations.follow_to_collection.title": "Hesabı takip et?",
"confirmations.follow_to_list.confirm": "Takip et ve yapılacaklar listesine ekle",
"confirmations.follow_to_list.message": "Bir listeye eklemek için {name} kişisini takip etmeniz gerekiyor.",
"confirmations.follow_to_list.title": "Kullanıcıyı takip et?",

View File

@ -191,7 +191,6 @@
"closed_registrations_modal.title": "Реєстрація на Mastodon",
"collections.detail.accounts_heading": "Облікові записи",
"collections.edit_details": "Редагувати подробиці",
"collections.remove_account": "Вилучити обліковий запис",
"column.about": "Про застосунок",
"column.blocks": "Заблоковані користувачі",
"column.bookmarks": "Закладки",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "Xem ngay gói khởi đầu tuyệt vời này: {link}",
"collection.share_template_own": "Xem ngay gói khởi đầu mới của tôi: {link}",
"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_description": "Thêm tối đa {count} tài khoản",
"collections.accounts.empty_editor_title": "Chưa có ai trong gói khởi đầu này",
"collections.accounts.empty_title": "Gói khởi đầu này trống",
"collections.block_collection_owner": "Chặn tài khoản",
"collections.by_account": "bởi {account_handle}",
"collections.collection_description": "Mô tả",
"collections.collection_language": "Ngôn ngữ",
@ -370,7 +372,8 @@
"collections.confirm_account_removal": "Bạn có chắc muốn gỡ tài khoản này khỏi gói khởi đầu?",
"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 khám phá mới có thể được thêm vào.",
"collections.copy_link": "Sao chép liên kết",
"collections.copy_link_confirmation": "Sao chép liên kết tài khoản vào clipboard",
"collections.create.accounts_title": "Bạn sẽ chọn ai để giới thiệu trong gói khởi đầu này?",
"collections.create.basic_details_title": "Thông tin cơ bản",
"collections.create.steps": "Bước {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "Đã xảy ra lỗi khi tải những gói khởi đầu của bạn.",
"collections.hidden_accounts_description": "Bạn vừa chặn hoặc phớt lờ {count, plural, other {tài khoản này}}",
"collections.hidden_accounts_link": "{count, plural, other {# tài khoản đã ẩn}}",
"collections.hints.accounts_counter": "{count} / {max} tài khoản",
"collections.hints.accounts_counter": "{count}/{max} tài khoản",
"collections.last_updated_at": "Lần cuối cập nhật: {date}",
"collections.manage_accounts": "Quản lý tài khoản",
"collections.mark_as_sensitive": "Đánh dấu nhạy cảm",
@ -398,17 +401,19 @@
"collections.name_length_hint": "Giới hạn 40 ký tự",
"collections.new_collection": "Gói khởi đầu mới",
"collections.no_collections_yet": "Chưa có gói khởi đầu.",
"collections.old_last_post_note": "Đăng lần cuối hơn một tuần trước",
"collections.remove_account": "Gỡ tài khoản này",
"collections.remove_account": "Gỡ",
"collections.report_collection": "Báo cáo gói khởi đầu này",
"collections.revoke_collection_inclusion": "Xóa tôi khỏi gói khởi đầu này",
"collections.revoke_inclusion.confirmation": "Bạn đã được gỡ khỏi \"{collection}\"",
"collections.revoke_inclusion.error": "Đã có lỗi, xin vui lòng thử lại.",
"collections.search_accounts_label": "Tìm tài khoản để thêm",
"collections.search_accounts_label": "Tìm tài khoản để thêm",
"collections.search_accounts_max_reached": "Bạn đã đạt đến số lượng tài khoản tối đa",
"collections.sensitive": "Nhạy cảm",
"collections.share_short": "Chia sẻ",
"collections.topic_hint": "Thêm hashtag giúp người khác hiểu chủ đề chính của gói khởi đầu này.",
"collections.topic_special_chars_hint": "Những ký tự đặc biệt sẽ bị xóa khi lưu",
"collections.unlisted_collections_description": "Những thông tin này sẽ không hiển thị trên hồ sơ của bạn. Bất cứ ai có liên kết đều có thể tìm thấy chúng.",
"collections.unlisted_collections_with_count": "Gói khởi đầu hạn chế ({count})",
"collections.view_collection": "Xem gói khởi đầu",
"collections.view_other_collections_by_user": "Xem những gói khởi đầu khác từ tài khoản này",
"collections.visibility_public": "Công khai",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "Bỏ tút đang soạn?",
"confirmations.discard_edit_media.confirm": "Bỏ qua",
"confirmations.discard_edit_media.message": "Bạn chưa lưu thay đổi của phần mô tả hoặc bản xem trước của media, vẫn bỏ qua?",
"confirmations.follow_to_collection.confirm": "Theo dõi & thêm vào gói khởi đầu",
"confirmations.follow_to_collection.message": "Bạn cần theo dõi {name} trước khi thêm họ vào gói khởi đầu.",
"confirmations.follow_to_collection.title": "Theo dõi tài khoản?",
"confirmations.follow_to_list.confirm": "Theo dõi & thêm vào danh sách",
"confirmations.follow_to_list.message": "Bạn cần theo dõi {name} trước khi thêm họ vào danh sách.",
"confirmations.follow_to_list.title": "Theo dõi tài khoản?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "发现了个收藏列表,来看看:{link}",
"collection.share_template_own": "我的新收藏列表,来看看:{link}",
"collections.account_count": "{count, plural, other {# 个账号}}",
"collections.accounts.empty_description": "添加你关注的账号,最多 {count} 个",
"collections.accounts.empty_description": "添加最多 {count} 个账号",
"collections.accounts.empty_editor_title": "此收藏列表中暂无用户",
"collections.accounts.empty_title": "收藏列表为空",
"collections.block_collection_owner": "屏蔽账户",
"collections.by_account": "由 {account_handle}",
"collections.collection_description": "说明",
"collections.collection_language": "语言",
@ -370,7 +372,8 @@
"collections.confirm_account_removal": "你确定要将从收藏列表中移除此账号吗?",
"collections.content_warning": "内容警告",
"collections.continue": "继续",
"collections.create.accounts_subtitle": "只有你关注的且已经主动加入发现功能的账号可以添加。",
"collections.copy_link": "复制链接",
"collections.copy_link_confirmation": "已复制收藏列表链接到剪贴板",
"collections.create.accounts_title": "你想在收藏列表中添加哪些人?",
"collections.create.basic_details_title": "基本信息",
"collections.create.steps": "第 {step} 步(共 {total} 步)",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "加载你的收藏列表时发生错误。",
"collections.hidden_accounts_description": "你已屏蔽或隐藏{count, plural, other {部分用户}}",
"collections.hidden_accounts_link": "{count, plural, other {# 个隐藏账号}}",
"collections.hints.accounts_counter": "{count} / {max} 个账号",
"collections.hints.accounts_counter": "{count}/{max} 个账号",
"collections.last_updated_at": "最后更新:{date}",
"collections.manage_accounts": "管理账户",
"collections.mark_as_sensitive": "标记为敏感内容",
@ -398,17 +401,19 @@
"collections.name_length_hint": "40 字限制",
"collections.new_collection": "新建收藏列表",
"collections.no_collections_yet": "尚无收藏列表。",
"collections.old_last_post_note": "上次发言于一周多以前",
"collections.remove_account": "移除此账号",
"collections.remove_account": "移除",
"collections.report_collection": "举报此收藏列表",
"collections.revoke_collection_inclusion": "将自己从此收藏列表中移除",
"collections.revoke_inclusion.confirmation": "你已被从“{collection}”中移除",
"collections.revoke_inclusion.error": "出现错误,请稍后重试。",
"collections.search_accounts_label": "搜索要添加的账号",
"collections.search_accounts_label": "搜索要添加的账号",
"collections.search_accounts_max_reached": "你添加的账号数量已达上限",
"collections.sensitive": "敏感内容",
"collections.share_short": "分享",
"collections.topic_hint": "添加话题标签,帮助他人了解此收藏列表的主题。",
"collections.topic_special_chars_hint": "保存时将自动移除特殊字符",
"collections.unlisted_collections_description": "这些收藏列表在他人查看时不会出现在你的个人资料上。任何知晓链接的人可以发现这些列表。",
"collections.unlisted_collections_with_count": "静默公开的收藏列表({count}",
"collections.view_collection": "查看收藏列表",
"collections.view_other_collections_by_user": "查看此用户的其他收藏列表",
"collections.visibility_public": "公开",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "丢弃你的嘟文草稿?",
"confirmations.discard_edit_media.confirm": "丢弃",
"confirmations.discard_edit_media.message": "你还有未保存的媒体描述或预览修改,仍要丢弃吗?",
"confirmations.follow_to_collection.confirm": "关注并添加到收藏列表",
"confirmations.follow_to_collection.message": "你需要先关注 {name},才能将其添加到收藏列表。",
"confirmations.follow_to_collection.title": "要关注账号吗?",
"confirmations.follow_to_list.confirm": "关注并添加到列表",
"confirmations.follow_to_list.message": "你需要先关注 {name},才能将其添加到列表。",
"confirmations.follow_to_list.title": "确定要关注此用户?",

View File

@ -359,8 +359,10 @@
"collection.share_template_other": "來看看這個酷酷的收藏名單:{link}",
"collection.share_template_own": "來看看我的新收藏名單:{link}",
"collections.account_count": "{count, plural, other {# 個帳號}}",
"collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號",
"collections.accounts.empty_description": "加入最多 {count} 個帳號",
"collections.accounts.empty_editor_title": "此收藏名單尚未有任何人",
"collections.accounts.empty_title": "此收藏名單是空的",
"collections.block_collection_owner": "封鎖帳號",
"collections.by_account": "來自 {account_handle}",
"collections.collection_description": "說明",
"collections.collection_language": "語言",
@ -370,7 +372,8 @@
"collections.confirm_account_removal": "您是否確定要自此收藏名單中移除此帳號?",
"collections.content_warning": "內容警告",
"collections.continue": "繼續",
"collections.create.accounts_subtitle": "僅能加入您跟隨並選擇加入探索功能之帳號。",
"collections.copy_link": "複製連結",
"collections.copy_link_confirmation": "複製收藏名單連結至剪貼簿",
"collections.create.accounts_title": "您想於此收藏名單推薦誰?",
"collections.create.basic_details_title": "基本資料",
"collections.create.steps": "步驟 {step}/{total}",
@ -390,7 +393,7 @@
"collections.error_loading_collections": "讀取您的收藏名單時發生錯誤。",
"collections.hidden_accounts_description": "您已封鎖或靜音{count, plural, other {這些使用者}}",
"collections.hidden_accounts_link": "{count, plural, other {# 個隱藏帳號}}",
"collections.hints.accounts_counter": "{count} / {max} 個帳號",
"collections.hints.accounts_counter": "{count}/{max} 個帳號",
"collections.last_updated_at": "最後更新:{date}",
"collections.manage_accounts": "管理帳號",
"collections.mark_as_sensitive": "標記為敏感內容",
@ -398,17 +401,19 @@
"collections.name_length_hint": "40 字限制",
"collections.new_collection": "新增收藏名單",
"collections.no_collections_yet": "您沒有任何收藏名單。",
"collections.old_last_post_note": "上次發表嘟文已超過一週",
"collections.remove_account": "移除此帳號",
"collections.remove_account": "移除",
"collections.report_collection": "檢舉此收藏名單",
"collections.revoke_collection_inclusion": "將我自此收藏名單中移除",
"collections.revoke_inclusion.confirmation": "您已自「{collection}」中被移除",
"collections.revoke_inclusion.error": "發生錯誤,請稍候重試。",
"collections.search_accounts_label": "搜尋帳號以加入...",
"collections.search_accounts_label": "搜尋帳號以加入",
"collections.search_accounts_max_reached": "您新增之帳號數已達上限",
"collections.sensitive": "敏感內容",
"collections.share_short": "分享",
"collections.topic_hint": "新增主題標籤以協助其他人瞭解此收藏名單之主題。",
"collections.topic_special_chars_hint": "特殊字元將於儲存時被移除",
"collections.unlisted_collections_description": "這些將不會於您的個人檔案對他人顯示。但任何擁有連結之人皆能發現其內容。",
"collections.unlisted_collections_with_count": "未公開收藏名單 ({count})",
"collections.view_collection": "檢視收藏名單",
"collections.view_other_collections_by_user": "檢視此使用者之其他收藏名單",
"collections.visibility_public": "公開",
@ -500,9 +505,6 @@
"confirmations.discard_draft.post.title": "是否捨棄您的嘟文草稿?",
"confirmations.discard_edit_media.confirm": "捨棄",
"confirmations.discard_edit_media.message": "您於媒體描述或預覽區塊有未儲存的變更。是否要捨棄這些變更?",
"confirmations.follow_to_collection.confirm": "跟隨並加入至收藏名單",
"confirmations.follow_to_collection.message": "您必須先跟隨 {name} 以將其加入至收藏名單。",
"confirmations.follow_to_collection.title": "是否跟隨此帳號?",
"confirmations.follow_to_list.confirm": "跟隨並加入至列表",
"confirmations.follow_to_list.message": "您必須先跟隨 {name} 以將其加入至列表。",
"confirmations.follow_to_list.title": "是否跟隨該使用者?",

View File

@ -31,6 +31,8 @@ class ActivityPub::TagManager
short_account_status_url(target.account, target)
when :flag
target.uri
when :featured_collection
account_collection_url(target.account, target)
end
end

Some files were not shown because too many files have changed in this diff Show More