diff --git a/.nvmrc b/.nvmrc index fd655f8a35..a2e33f6e2c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.14 +24.15 diff --git a/Gemfile.lock b/Gemfile.lock index 25c9d571f5..e2b64a9f06 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb index 6ec53224d3..565409612c 100644 --- a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb +++ b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb @@ -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! diff --git a/app/helpers/invites_helper.rb b/app/helpers/invites_helper.rb deleted file mode 100644 index c189061db0..0000000000 --- a/app/helpers/invites_helper.rb +++ /dev/null @@ -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 diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 011ef9d6c8..4f2d68f3f9 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -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? diff --git a/app/javascript/flavours/glitch/components/empty_state/empty_state.module.scss b/app/javascript/flavours/glitch/components/empty_state/empty_state.module.scss index e9602f2e41..b58c565ac5 100644 --- a/app/javascript/flavours/glitch/components/empty_state/empty_state.module.scss +++ b/app/javascript/flavours/glitch/components/empty_state/empty_state.module.scss @@ -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; +} diff --git a/app/javascript/flavours/glitch/components/empty_state/empty_state.stories.tsx b/app/javascript/flavours/glitch/components/empty_state/empty_state.stories.tsx index 8515a6ea1a..83fce03468 100644 --- a/app/javascript/flavours/glitch/components/empty_state/empty_state.stories.tsx +++ b/app/javascript/flavours/glitch/components/empty_state/empty_state.stories.tsx @@ -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: , }, }; + +export const WithoutImage: Story = { + args: { + ...Default.args, + image: null, + }, +}; + +export const WithoutMessage: Story = { + args: { + ...Default.args, + message: undefined, + }, +}; diff --git a/app/javascript/flavours/glitch/components/empty_state/index.tsx b/app/javascript/flavours/glitch/components/empty_state/index.tsx index 59210bbc87..0ef0d67995 100644 --- a/app/javascript/flavours/glitch/components/empty_state/index.tsx +++ b/app/javascript/flavours/glitch/components/empty_state/index.tsx @@ -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: , +}; + /** * 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 = ( ), message, children, }) => { + const imageToRender = typeof image === 'string' ? images[image] : image; + return (
- {(title || message || image) && ( + {(title || message || imageToRender) && (
- {image} + {imageToRender} {!!title &&

{title}

} {!!message &&

{message}

}
diff --git a/app/javascript/flavours/glitch/components/form_fields/combobox.module.scss b/app/javascript/flavours/glitch/components/form_fields/combobox.module.scss index 732893200c..ef230a77b8 100644 --- a/app/javascript/flavours/glitch/components/form_fields/combobox.module.scss +++ b/app/javascript/flavours/glitch/components/form_fields/combobox.module.scss @@ -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'] { diff --git a/app/javascript/flavours/glitch/components/form_fields/combobox_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/combobox_field.stories.tsx index 2c4b82fdcd..32fc736423 100644 --- a/app/javascript/flavours/glitch/components/form_fields/combobox_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/combobox_field.stories.tsx @@ -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 ( ; -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: () => , + args: dummyArgs, }; diff --git a/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx b/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx index bc7bf44f69..8ad8ea7e96 100644 --- a/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx @@ -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 extends Omit< - TextInputProps, - 'icon' -> { +interface ComboboxProps< + Item extends ComboboxItem, + GroupKey extends string, +> extends Omit { /** * The value of the combobox's text input */ @@ -46,34 +53,45 @@ interface ComboboxProps 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>; /** * 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 extends Omit< suppressMenu?: boolean; } -interface Props - extends ComboboxProps, CommonFieldWrapperProps {} +interface Props + extends ComboboxProps, CommonFieldWrapperProps {} /** * The combobox field allows users to select one or more items @@ -100,8 +118,11 @@ interface Props * [research & implementations](https://sarahmhigley.com/writing/select-your-poison/). */ -export const ComboboxFieldWithRef = ( - { id, label, hint, status, required, ...otherProps }: Props, +export const ComboboxFieldWithRef = < + Item extends ComboboxItem, + GroupKey extends string, +>( + { id, label, hint, status, required, ...otherProps }: Props, ref: React.ForwardedRef, ) => ( ( // 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 { - ( - props: Props & { ref?: React.ForwardedRef }, + ( + props: Props & { + ref?: React.ForwardedRef; + }, ): ReturnType; displayName: string; }; ComboboxField.displayName = 'ComboboxField'; -const ComboboxWithRef = ( +const ComboboxWithRef = ( { value, isLoading = false, @@ -135,6 +158,7 @@ const ComboboxWithRef = ( getIsItemDisabled, getIsItemSelected, disabled, + renderGroupTitle, renderItem, onSelectItem, onChange, @@ -144,7 +168,7 @@ const ComboboxWithRef = ( icon = SearchIcon, className, ...otherProps - }: ComboboxProps, + }: ComboboxProps, ref: React.ForwardedRef, ) => { const intl = useIntl(); @@ -157,15 +181,28 @@ const ComboboxWithRef = ( ); 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 = ( }, []); 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 = ( 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 = ( } inputRef.current?.focus(); }, - [closeMenu, closeOnSelect, getIsItemDisabled, items, onSelectItem], + [closeMenu, closeOnSelect, getIsItemDisabled, flatItems, onSelectItem], ); const handleSelectItem = useCallback( @@ -246,38 +283,38 @@ const ComboboxWithRef = ( 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 = ( ], ); + 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 +
  • + {renderItem(item, { + isSelected, + isDisabled: isDisabled ?? false, + })} +
  • + ); + }); + const mergeRefs = useCallback( (element: HTMLInputElement | null) => { inputRef.current = element; @@ -406,42 +475,50 @@ const ComboboxWithRef = ( > {({ props, placement }) => (
    - {showStatusMessageInMenu ? ( - {statusMessage} - ) : ( -
      - {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 -
    • - {renderItem(item, { - isSelected, - isDisabled: isDisabled ?? false, - })} -
    • - ); - })} -
    - )} + + {hasGroups ? ( +
    + {(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 ( +
      + {hasTitle && ( +
    • + {customGroupTitle ?? ( + {groupKey} + )} +
    • + )} + {renderItems(groupItems)} +
    + ); + })} +
    + ) : ( +
      + {renderItems(items)} +
    + )} +
    )} @@ -452,14 +529,28 @@ const ComboboxWithRef = ( // 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 { - ( - props: ComboboxProps & { ref?: React.ForwardedRef }, + ( + props: ComboboxProps & { + ref?: React.ForwardedRef; + }, ): ReturnType; displayName: string; }; Combobox.displayName = 'Combobox'; +const StatusMessageWrapper: React.FC<{ + showStatus: boolean; + status: string; + children: React.ReactNode; +}> = ({ showStatus, status, children }) => { + if (showStatus) { + return {status}; + } + + return children; +}; + function useGetA11yStatusMessage({ itemCount, value, diff --git a/app/javascript/flavours/glitch/components/list_item/index.tsx b/app/javascript/flavours/glitch/components/list_item/index.tsx index 66758bd4de..263e206fb9 100644 --- a/app/javascript/flavours/glitch/components/list_item/index.tsx +++ b/app/javascript/flavours/glitch/components/list_item/index.tsx @@ -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 = ({ icon, @@ -33,10 +34,31 @@ export const ListItemWrapper: React.FC = ({ ); }; -interface LinkProps extends React.ComponentPropsWithoutRef { +interface WithSubtitle { subtitle?: React.ReactNode; } +interface ContentProps + extends React.ComponentPropsWithoutRef<'h3'>, WithSubtitle {} + +export const ListItemContent: React.FC = ({ + subtitle, + children, + ...otherProps +}) => { + return ( + <> +

    + {children} +

    + {subtitle &&
    {subtitle}
    } + + ); +}; + +interface LinkProps + extends React.ComponentPropsWithoutRef, WithSubtitle {} + export const ListItemLink: React.FC = ({ subtitle, children, @@ -44,20 +66,16 @@ export const ListItemLink: React.FC = ({ ...otherProps }) => { return ( - <> -

    - - {children} - -

    - {subtitle &&
    {subtitle}
    } - + + + {children} + + ); }; -interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { - subtitle?: React.ReactNode; -} +interface ButtonProps + extends React.ComponentPropsWithoutRef<'button'>, WithSubtitle {} export const ListItemButton: React.FC = ({ subtitle, @@ -66,17 +84,14 @@ export const ListItemButton: React.FC = ({ ...otherProps }) => { return ( - <> -

    - -

    - {subtitle &&
    {subtitle}
    } - + + + ); }; diff --git a/app/javascript/flavours/glitch/components/list_item/list_item.stories.tsx b/app/javascript/flavours/glitch/components/list_item/list_item.stories.tsx index d46102c1c3..dd066683c9 100644 --- a/app/javascript/flavours/glitch/components/list_item/list_item.stories.tsx +++ b/app/javascript/flavours/glitch/components/list_item/list_item.stories.tsx @@ -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; export default meta; type Story = StoryObj; +export const NonInteractive: Story = { + render: () => ( + }> + View more + + ), +}; + export const WithButton: Story = { render: () => ( + {Component => ( + {Component => ( + {Component => ( 1) { media.push( - + {Component => ( + {Component => ( + {Component => ( = ({ const hasCollections = areCollectionsEnabled(); - const image = ; - if (me === accountId) { if (hasCollections) { // Return only here to insert the "Create a collection" button as the action for the empty state. return ( = ({ } } - return ; + return ; }; diff --git a/app/javascript/flavours/glitch/features/collections/components/collection_menu.tsx b/app/javascript/flavours/glitch/features/collections/components/collection_menu.tsx index 874caeff42..b4ab4770af 100644 --- a/app/javascript/flavours/glitch/features/collections/components/collection_menu.tsx +++ b/app/javascript/flavours/glitch/features/collections/components/collection_menu.tsx @@ -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 ( diff --git a/app/javascript/flavours/glitch/features/collections/detail/index.tsx b/app/javascript/flavours/glitch/features/collections/detail/index.tsx index 1e6ec7bb4f..4c592949ed 100644 --- a/app/javascript/flavours/glitch/features/collections/detail/index.tsx +++ b/app/javascript/flavours/glitch/features/collections/detail/index.tsx @@ -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 (
    @@ -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} /> 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)) && ( - - } - icon={} - className={classes.accountBadge} + const renderButton = useCallback( + () => ( + + ), + [handleRemoveAccount], ); - return ( - - - - ); + return ; }; -interface SuggestionItem { - id: string; - isSelected: boolean; -} - -const SuggestedAccountItem: React.FC = ({ id, isSelected }) => { +const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => { const account = useAccount(id); + const handle = useAccountHandle(account, domain); if (!account) return null; return ( - <> - - - {isSelected && ( - - )} - + } + > + + + + ); }; -const renderAccountItem = (item: SuggestionItem) => ( - +const renderAccountItem = (account: ApiMutedAccountJSON) => ( + ); -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 = ( + + ); + description = ( + + ); + } else { + title = ( + + ); + description = ( + + ); + } + + return ( + + + {title} + + + ); +}; + +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) => { 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 (
    - {!id && ( - - } - description={ - - } - /> - )} - - - {hasMaxAccounts && ( - - )} - - - - } - message={ - - } - /> - } - > - {accountIds.map((accountId, index) => ( -
    - -
    - ))} -
    -
    -
    - {!isEditMode && ( -
    -
    - - {(text) =>
    {text}
    } -
    - -
    + } + /> + )} + +
    + +
    + {hasAccounts && ( + + + + )} + + + + } + message={ + + } + /> + } + > + {accountIds.map((accountId, index) => ( +
    + +
    + ))} +
    +
    +
    + + {!isEditMode && hasAccounts && ( +
    +
    )} diff --git a/app/javascript/flavours/glitch/features/collections/editor/details.tsx b/app/javascript/flavours/glitch/features/collections/editor/details.tsx index 35fac2e09a..5518570fe9 100644 --- a/app/javascript/flavours/glitch/features/collections/editor/details.tsx +++ b/app/javascript/flavours/glitch/features/collections/editor/details.tsx @@ -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 = () => {
    {!id && ( - {
    -
    - -
    +
    ); diff --git a/app/javascript/flavours/glitch/features/collections/editor/index.tsx b/app/javascript/flavours/glitch/features/collections/editor/index.tsx index 948b82b74f..596988de73 100644 --- a/app/javascript/flavours/glitch/features/collections/editor/index.tsx +++ b/app/javascript/flavours/glitch/features/collections/editor/index.tsx @@ -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, diff --git a/app/javascript/flavours/glitch/features/collections/editor/styles.module.scss b/app/javascript/flavours/glitch/features/collections/editor/styles.module.scss index 1991aa4211..1befc06cc1 100644 --- a/app/javascript/flavours/glitch/features/collections/editor/styles.module.scss +++ b/app/javascript/flavours/glitch/features/collections/editor/styles.module.scss @@ -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; +} diff --git a/app/javascript/flavours/glitch/features/collections/editor/wizard_step_header.tsx b/app/javascript/flavours/glitch/features/collections/editor/wizard_step_header.tsx deleted file mode 100644 index 3bc9a4d7ef..0000000000 --- a/app/javascript/flavours/glitch/features/collections/editor/wizard_step_header.tsx +++ /dev/null @@ -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 ( -
    - - {(content) =>

    {content}

    } -
    -

    {title}

    - {!!description &&

    {description}

    } -
    - ); -}; diff --git a/app/javascript/flavours/glitch/features/collections/editor/wizard_step_title.tsx b/app/javascript/flavours/glitch/features/collections/editor/wizard_step_title.tsx new file mode 100644 index 0000000000..4d328521e7 --- /dev/null +++ b/app/javascript/flavours/glitch/features/collections/editor/wizard_step_title.tsx @@ -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 ( +
    +

    + +

    +

    {title}

    +
    + ); +}; diff --git a/app/javascript/flavours/glitch/features/lists/members.tsx b/app/javascript/flavours/glitch/features/lists/members.tsx index 1a18e8e499..9df96cf7c2 100644 --- a/app/javascript/flavours/glitch/features/lists/members.tsx +++ b/app/javascript/flavours/glitch/features/lists/members.tsx @@ -164,7 +164,7 @@ const ListMembers: React.FC<{ const [mode, setMode] = useState('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; } diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_collection.module.scss b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_collection.module.scss new file mode 100644 index 0000000000..93627f5c95 --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_collection.module.scss @@ -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; + } +} diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_collection.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_collection.tsx index e9427dccc6..a82ab85fc6 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_collection.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_collection.tsx @@ -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 (
    + ), }} /> @@ -54,7 +64,12 @@ export const NotificationCollection: React.FC<{ defaultMessage='{name} edited a collection you’re in' values={{ name: ( - + ), }} /> @@ -63,6 +78,26 @@ export const NotificationCollection: React.FC<{
    + +
    + + + +
    ); diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/follow_to_collection.tsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/follow_to_collection.tsx deleted file mode 100644 index ade85c174b..0000000000 --- a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/follow_to_collection.tsx +++ /dev/null @@ -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 ( - @{account?.acct} }} - /> - } - confirm={intl.formatMessage(messages.confirm)} - onConfirm={onConfirm} - onClose={onClose} - /> - ); -}; diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts index 4011b4db06..c27597fb52 100644 --- a/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts +++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modals/index.ts @@ -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'; diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx index ce2fd0ed8e..77c559ba0f 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx @@ -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 }), diff --git a/app/javascript/flavours/glitch/hooks/useSearchAccounts.ts b/app/javascript/flavours/glitch/hooks/useSearchAccounts.ts index 576d124e24..f9af2a1eb6 100644 --- a/app/javascript/flavours/glitch/hooks/useSearchAccounts.ts +++ b/app/javascript/flavours/glitch/hooks/useSearchAccounts.ts @@ -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([]); + const [accounts, setAccounts] = useState([]); 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', }; diff --git a/app/javascript/mastodon/components/empty_state/empty_state.module.scss b/app/javascript/mastodon/components/empty_state/empty_state.module.scss index e9602f2e41..b58c565ac5 100644 --- a/app/javascript/mastodon/components/empty_state/empty_state.module.scss +++ b/app/javascript/mastodon/components/empty_state/empty_state.module.scss @@ -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; +} diff --git a/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx b/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx index 8515a6ea1a..83fce03468 100644 --- a/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx +++ b/app/javascript/mastodon/components/empty_state/empty_state.stories.tsx @@ -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: , }, }; + +export const WithoutImage: Story = { + args: { + ...Default.args, + image: null, + }, +}; + +export const WithoutMessage: Story = { + args: { + ...Default.args, + message: undefined, + }, +}; diff --git a/app/javascript/mastodon/components/empty_state/index.tsx b/app/javascript/mastodon/components/empty_state/index.tsx index 59210bbc87..0ef0d67995 100644 --- a/app/javascript/mastodon/components/empty_state/index.tsx +++ b/app/javascript/mastodon/components/empty_state/index.tsx @@ -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: , +}; + /** * 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 = ( ), message, children, }) => { + const imageToRender = typeof image === 'string' ? images[image] : image; + return (
    - {(title || message || image) && ( + {(title || message || imageToRender) && (
    - {image} + {imageToRender} {!!title &&

    {title}

    } {!!message &&

    {message}

    }
    diff --git a/app/javascript/mastodon/components/form_fields/combobox.module.scss b/app/javascript/mastodon/components/form_fields/combobox.module.scss index 732893200c..ef230a77b8 100644 --- a/app/javascript/mastodon/components/form_fields/combobox.module.scss +++ b/app/javascript/mastodon/components/form_fields/combobox.module.scss @@ -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'] { diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx index 2c4b82fdcd..32fc736423 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx @@ -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 ( ; -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: () => , + args: dummyArgs, }; diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx index 057258847e..64cbb3ce8a 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx @@ -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 extends Omit< - TextInputProps, - 'icon' -> { +interface ComboboxProps< + Item extends ComboboxItem, + GroupKey extends string, +> extends Omit { /** * The value of the combobox's text input */ @@ -46,34 +53,45 @@ interface ComboboxProps 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>; /** * 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 extends Omit< suppressMenu?: boolean; } -interface Props - extends ComboboxProps, CommonFieldWrapperProps {} +interface Props + extends ComboboxProps, CommonFieldWrapperProps {} /** * The combobox field allows users to select one or more items @@ -100,8 +118,11 @@ interface Props * [research & implementations](https://sarahmhigley.com/writing/select-your-poison/). */ -export const ComboboxFieldWithRef = ( - { id, label, hint, status, required, ...otherProps }: Props, +export const ComboboxFieldWithRef = < + Item extends ComboboxItem, + GroupKey extends string, +>( + { id, label, hint, status, required, ...otherProps }: Props, ref: React.ForwardedRef, ) => ( ( // 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 { - ( - props: Props & { ref?: React.ForwardedRef }, + ( + props: Props & { + ref?: React.ForwardedRef; + }, ): ReturnType; displayName: string; }; ComboboxField.displayName = 'ComboboxField'; -const ComboboxWithRef = ( +const ComboboxWithRef = ( { value, isLoading = false, @@ -135,6 +158,7 @@ const ComboboxWithRef = ( getIsItemDisabled, getIsItemSelected, disabled, + renderGroupTitle, renderItem, onSelectItem, onChange, @@ -144,7 +168,7 @@ const ComboboxWithRef = ( icon = SearchIcon, className, ...otherProps - }: ComboboxProps, + }: ComboboxProps, ref: React.ForwardedRef, ) => { const intl = useIntl(); @@ -157,15 +181,28 @@ const ComboboxWithRef = ( ); 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 = ( }, []); 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 = ( 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 = ( } inputRef.current?.focus(); }, - [closeMenu, closeOnSelect, getIsItemDisabled, items, onSelectItem], + [closeMenu, closeOnSelect, getIsItemDisabled, flatItems, onSelectItem], ); const handleSelectItem = useCallback( @@ -246,38 +283,38 @@ const ComboboxWithRef = ( 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 = ( ], ); + 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 +
  • + {renderItem(item, { + isSelected, + isDisabled: isDisabled ?? false, + })} +
  • + ); + }); + const mergeRefs = useCallback( (element: HTMLInputElement | null) => { inputRef.current = element; @@ -406,42 +475,50 @@ const ComboboxWithRef = ( > {({ props, placement }) => (
    - {showStatusMessageInMenu ? ( - {statusMessage} - ) : ( -
      - {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 -
    • - {renderItem(item, { - isSelected, - isDisabled: isDisabled ?? false, - })} -
    • - ); - })} -
    - )} + + {hasGroups ? ( +
    + {(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 ( +
      + {hasTitle && ( +
    • + {customGroupTitle ?? ( + {groupKey} + )} +
    • + )} + {renderItems(groupItems)} +
    + ); + })} +
    + ) : ( +
      + {renderItems(items)} +
    + )} +
    )} @@ -452,14 +529,28 @@ const ComboboxWithRef = ( // 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 { - ( - props: ComboboxProps & { ref?: React.ForwardedRef }, + ( + props: ComboboxProps & { + ref?: React.ForwardedRef; + }, ): ReturnType; displayName: string; }; Combobox.displayName = 'Combobox'; +const StatusMessageWrapper: React.FC<{ + showStatus: boolean; + status: string; + children: React.ReactNode; +}> = ({ showStatus, status, children }) => { + if (showStatus) { + return {status}; + } + + return children; +}; + function useGetA11yStatusMessage({ itemCount, value, diff --git a/app/javascript/mastodon/components/list_item/index.tsx b/app/javascript/mastodon/components/list_item/index.tsx index 66758bd4de..263e206fb9 100644 --- a/app/javascript/mastodon/components/list_item/index.tsx +++ b/app/javascript/mastodon/components/list_item/index.tsx @@ -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 = ({ icon, @@ -33,10 +34,31 @@ export const ListItemWrapper: React.FC = ({ ); }; -interface LinkProps extends React.ComponentPropsWithoutRef { +interface WithSubtitle { subtitle?: React.ReactNode; } +interface ContentProps + extends React.ComponentPropsWithoutRef<'h3'>, WithSubtitle {} + +export const ListItemContent: React.FC = ({ + subtitle, + children, + ...otherProps +}) => { + return ( + <> +

    + {children} +

    + {subtitle &&
    {subtitle}
    } + + ); +}; + +interface LinkProps + extends React.ComponentPropsWithoutRef, WithSubtitle {} + export const ListItemLink: React.FC = ({ subtitle, children, @@ -44,20 +66,16 @@ export const ListItemLink: React.FC = ({ ...otherProps }) => { return ( - <> -

    - - {children} - -

    - {subtitle &&
    {subtitle}
    } - + + + {children} + + ); }; -interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { - subtitle?: React.ReactNode; -} +interface ButtonProps + extends React.ComponentPropsWithoutRef<'button'>, WithSubtitle {} export const ListItemButton: React.FC = ({ subtitle, @@ -66,17 +84,14 @@ export const ListItemButton: React.FC = ({ ...otherProps }) => { return ( - <> -

    - -

    - {subtitle &&
    {subtitle}
    } - + + + ); }; diff --git a/app/javascript/mastodon/components/list_item/list_item.stories.tsx b/app/javascript/mastodon/components/list_item/list_item.stories.tsx index d46102c1c3..dd066683c9 100644 --- a/app/javascript/mastodon/components/list_item/list_item.stories.tsx +++ b/app/javascript/mastodon/components/list_item/list_item.stories.tsx @@ -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; export default meta; type Story = StoryObj; +export const NonInteractive: Story = { + render: () => ( + }> + View more + + ), +}; + export const WithButton: Story = { render: () => ( + {Component => ( + {Component => ( + {Component => ( 1) { media = ( - + {Component => ( + {Component => ( + {Component => ( = ({ const hasCollections = areCollectionsEnabled(); - const image = ; - if (me === accountId) { if (hasCollections) { // Return only here to insert the "Create a collection" button as the action for the empty state. return ( = ({ } } - return ; + return ; }; diff --git a/app/javascript/mastodon/features/collections/components/collection_menu.tsx b/app/javascript/mastodon/features/collections/components/collection_menu.tsx index 90c6315cbb..661e879df5 100644 --- a/app/javascript/mastodon/features/collections/components/collection_menu.tsx +++ b/app/javascript/mastodon/features/collections/components/collection_menu.tsx @@ -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 ( diff --git a/app/javascript/mastodon/features/collections/detail/index.tsx b/app/javascript/mastodon/features/collections/detail/index.tsx index 404cd8a074..d3fbd4c2cb 100644 --- a/app/javascript/mastodon/features/collections/detail/index.tsx +++ b/app/javascript/mastodon/features/collections/detail/index.tsx @@ -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 (
    @@ -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} /> 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)) && ( - - } - icon={} - className={classes.accountBadge} + const renderButton = useCallback( + () => ( + + ), + [handleRemoveAccount], ); - return ( - - - - ); + return ; }; -interface SuggestionItem { - id: string; - isSelected: boolean; -} - -const SuggestedAccountItem: React.FC = ({ id, isSelected }) => { +const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => { const account = useAccount(id); + const handle = useAccountHandle(account, domain); if (!account) return null; return ( - <> - - - {isSelected && ( - - )} - + } + > + + + + ); }; -const renderAccountItem = (item: SuggestionItem) => ( - +const renderAccountItem = (account: ApiMutedAccountJSON) => ( + ); -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 = ( + + ); + description = ( + + ); + } else { + title = ( + + ); + description = ( + + ); + } + + return ( + + + {title} + + + ); +}; + +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) => { 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 (
    - {!id && ( - - } - description={ - - } - /> - )} - - - {hasMaxAccounts && ( - - )} - - - - } - message={ - - } - /> - } - > - {accountIds.map((accountId, index) => ( -
    - -
    - ))} -
    -
    -
    - {!isEditMode && ( -
    -
    - - {(text) =>
    {text}
    } -
    - -
    + } + /> + )} + +
    + +
    + {hasAccounts && ( + + + + )} + + + + } + message={ + + } + /> + } + > + {accountIds.map((accountId, index) => ( +
    + +
    + ))} +
    +
    +
    + + {!isEditMode && hasAccounts && ( +
    +
    )} diff --git a/app/javascript/mastodon/features/collections/editor/details.tsx b/app/javascript/mastodon/features/collections/editor/details.tsx index 04a6ad1428..ae944ba41f 100644 --- a/app/javascript/mastodon/features/collections/editor/details.tsx +++ b/app/javascript/mastodon/features/collections/editor/details.tsx @@ -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 = () => {
    {!id && ( - {
    -
    - -
    +
    ); diff --git a/app/javascript/mastodon/features/collections/editor/index.tsx b/app/javascript/mastodon/features/collections/editor/index.tsx index ff1549b942..63a9e4171e 100644 --- a/app/javascript/mastodon/features/collections/editor/index.tsx +++ b/app/javascript/mastodon/features/collections/editor/index.tsx @@ -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, diff --git a/app/javascript/mastodon/features/collections/editor/styles.module.scss b/app/javascript/mastodon/features/collections/editor/styles.module.scss index 1991aa4211..1befc06cc1 100644 --- a/app/javascript/mastodon/features/collections/editor/styles.module.scss +++ b/app/javascript/mastodon/features/collections/editor/styles.module.scss @@ -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; +} diff --git a/app/javascript/mastodon/features/collections/editor/wizard_step_header.tsx b/app/javascript/mastodon/features/collections/editor/wizard_step_header.tsx deleted file mode 100644 index 3bc9a4d7ef..0000000000 --- a/app/javascript/mastodon/features/collections/editor/wizard_step_header.tsx +++ /dev/null @@ -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 ( -
    - - {(content) =>

    {content}

    } -
    -

    {title}

    - {!!description &&

    {description}

    } -
    - ); -}; diff --git a/app/javascript/mastodon/features/collections/editor/wizard_step_title.tsx b/app/javascript/mastodon/features/collections/editor/wizard_step_title.tsx new file mode 100644 index 0000000000..4d328521e7 --- /dev/null +++ b/app/javascript/mastodon/features/collections/editor/wizard_step_title.tsx @@ -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 ( +
    +

    + +

    +

    {title}

    +
    + ); +}; diff --git a/app/javascript/mastodon/features/lists/members.tsx b/app/javascript/mastodon/features/lists/members.tsx index dd4cf3171d..c4bd99d9b4 100644 --- a/app/javascript/mastodon/features/lists/members.tsx +++ b/app/javascript/mastodon/features/lists/members.tsx @@ -164,7 +164,7 @@ const ListMembers: React.FC<{ const [mode, setMode] = useState('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; } diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_collection.module.scss b/app/javascript/mastodon/features/notifications_v2/components/notification_collection.module.scss new file mode 100644 index 0000000000..93627f5c95 --- /dev/null +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_collection.module.scss @@ -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; + } +} diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_collection.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_collection.tsx index dd8cb0376e..6b70bf68cb 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_collection.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_collection.tsx @@ -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 (
    + ), }} /> @@ -54,7 +64,12 @@ export const NotificationCollection: React.FC<{ defaultMessage='{name} edited a collection you’re in' values={{ name: ( - + ), }} /> @@ -63,6 +78,26 @@ export const NotificationCollection: React.FC<{
    + +
    + + + +
    ); diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/follow_to_collection.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/follow_to_collection.tsx deleted file mode 100644 index c4700a1938..0000000000 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/follow_to_collection.tsx +++ /dev/null @@ -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 ( - @{account?.acct} }} - /> - } - confirm={intl.formatMessage(messages.confirm)} - onConfirm={onConfirm} - onClose={onClose} - /> - ); -}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts index 4011b4db06..c27597fb52 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts @@ -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'; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index f32234d467..ee59e3f8cf 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -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 }), diff --git a/app/javascript/mastodon/hooks/useSearchAccounts.ts b/app/javascript/mastodon/hooks/useSearchAccounts.ts index 1fc71f54ef..c19f08e734 100644 --- a/app/javascript/mastodon/hooks/useSearchAccounts.ts +++ b/app/javascript/mastodon/hooks/useSearchAccounts.ts @@ -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([]); + const [accounts, setAccounts] = useState([]); 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', }; diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 00a81da5b0..2756471b30 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -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": "Падпісацца на карыстальніка?", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 1f0dddbf5a..f1251fae17 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -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.", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 7b1fca1ca5..18d3043a8a 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -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?", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 9dbc85b070..e8f708ac11 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -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?", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 1de89f237a..ad7d273914 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -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?", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 254f2b69dc..fd5a84cdfd 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -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": "Ακολούθηση χρήστη;", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index d048b6254d..74f9bdd7cc 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -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?", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 8b05678fc7..601293fb22 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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": "You’ve 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": "Can’t be added", + "collections.suggestions.can_not_add_desc": "These accounts may have opted out of discovery, or they might be on a server that doesn’t 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 don’t 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?", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index c74d77eb91..a238950969 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -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?", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 8003419ad1..7941096cbc 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -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?", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index f8cb187f14..2e41b99125 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -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?", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 6f060f28df..1f73933dea 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -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": "Kiirklahv {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", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 4cfcccccc2..980fcc83d2 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -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ää?", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index f32a934a18..542e68dee2 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -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?", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index e474a9597d..d06b2fc1ed 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -106,6 +106,7 @@ "account.name.copy": "Copier l’identifiant", "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 ?", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 7066d261a9..e6e6c8145e 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -106,6 +106,7 @@ "account.name.copy": "Copier l’identifiant", "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 ?", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 80e713bbfe..3a7382f704 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -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í. D’fhé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?", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index ffc0fc7d3f..a7a789898b 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -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 dh’fheuch 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?", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 80550059b7..a8adb8953f 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -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", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index d41324b495..2319a6381e 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -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": "לעקוב אחר המשתמש.ת?", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 6b9926de7a..f6d6281cbf 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -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?", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index f3007fc25f..5287814001 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -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?", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 5801715ac3..4bfcee28ca 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -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?", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index ac63875904..0d6c7fbf7c 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -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": "検索結果やその他おすすめに表示されて、見つけてもらうことができます。", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 74e957bf2a..1bd1bc8c88 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -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", diff --git a/app/javascript/mastodon/locales/nan-TW.json b/app/javascript/mastodon/locales/nan-TW.json index 1406d80f90..454e004432 100644 --- a/app/javascript/mastodon/locales/nan-TW.json +++ b/app/javascript/mastodon/locales/nan-TW.json @@ -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 ê用者?", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 67256e2149..9f1acc5684 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -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?", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index 3f0833efcb..f6b0cd9255 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -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?", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 981f50e79f..37128f1d8c 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -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?", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 1ca1ab1d26..9162bcc4a5 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -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?", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index bdb382860c..8013055645 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -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?", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index e9b7a0888d..c78429d240 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -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": "Подписаться на пользователя?", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 33dfb83f46..849884b8a1 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -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 s’ka 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 t’u 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 t’i 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?", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 52a383a3a7..472b49d24e 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -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?", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index e2450305c0..0555f4eca2 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -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": "ติดตามผู้ใช้?", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index bba78d5d2b..0d1ece0c91 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -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?", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index d01c04ec70..f643718e5e 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -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": "Закладки", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 38636f80da..2e5d481938 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -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?", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 58a15ef752..944c311350 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -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": "确定要关注此用户?", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 128129af53..839c32b684 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -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": "是否跟隨該使用者?", diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 9036811217..37c45d68f1 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -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 diff --git a/app/lib/request.rb b/app/lib/request.rb index 66d7ece70f..376a37638c 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -292,11 +292,9 @@ class Request begin addresses = [IPAddr.new(host)] rescue IPAddr::InvalidAddressError - Resolv::DNS.open do |dns| - dns.timeouts = 5 - addresses = dns.getaddresses(host) - addresses = addresses.grep(Resolv::IPv6).take(2) + addresses.grep_v(Resolv::IPv6).take(2) - end + resolvers = [Resolv::Hosts.new, Resolv::DNS.new.tap { |dns| dns.timeouts = 5 }] + addresses = Resolv.new(resolvers).getaddresses(host) + addresses = addresses.grep(Resolv::IPv6::Regex).take(2) + addresses.grep_v(Resolv::IPv6::Regex).take(2) end socks = [] diff --git a/app/models/account.rb b/app/models/account.rb index 14ed6c1ac8..da3217b00f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -469,7 +469,7 @@ class Account < ApplicationRecord end def featureable_by?(other_account) - return discoverable? if local? + return discoverable? && (!locked? || followed_by?(other_account)) if local? feature_policy_for_account(other_account).in?(%i(automatic manual)) end diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb index c2c6355a18..ffa74cd21f 100644 --- a/app/models/account_migration.rb +++ b/app/models/account_migration.rb @@ -60,6 +60,10 @@ class AccountMigration < ApplicationRecord created_at + COOLDOWN_PERIOD end + def remaining_cooldown_days + ((cooldown_at - Time.current) / 1.day).ceil + end + private def set_target_account diff --git a/app/models/block.rb b/app/models/block.rb index 662cc1ac20..a5e8ad7ffa 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -25,7 +25,7 @@ class Block < ApplicationRecord false # Force uri_for to use uri attribute end - before_validation :set_uri, only: :create + before_validation :set_uri, on: :create after_commit :invalidate_blocking_cache after_commit :invalidate_follow_recommendations_cache diff --git a/app/models/collection.rb b/app/models/collection.rb index c8d4fb99bc..8d372eb8f4 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -15,6 +15,7 @@ # original_number_of_items :integer # sensitive :boolean not null # uri :string +# url :string # created_at :datetime not null # updated_at :datetime not null # account_id :bigint(8) not null @@ -64,6 +65,7 @@ class Collection < ApplicationRecord def items_for(account = nil) result = collection_items.with_accounts + result = account == self.account ? result.pending_or_accepted : result.accepted result = result.not_blocked_by(account) unless account.nil? result end diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb index c2ba2e9b68..cc77583674 100644 --- a/app/models/collection_item.rb +++ b/app/models/collection_item.rb @@ -36,13 +36,14 @@ class CollectionItem < ApplicationRecord validates :uri, presence: true, if: :remote_item_with_remote_account? before_validation :set_position, on: :create - before_validation :set_activity_uri, only: :create, if: :local_item_with_remote_account? + before_validation :set_activity_uri, on: :create, if: :local_item_with_remote_account? scope :ordered, -> { order(position: :asc) } scope :with_accounts, -> { includes(account: [:account_stat, :user]) } scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) } scope :local, -> { joins(:collection).merge(Collection.local) } scope :accepted_partial, ->(account) { joins(:account).merge(Account.local).accepted.where(uri: nil, account_id: account.id) } + scope :pending_or_accepted, -> { where(state: [:pending, :accepted]) } def revoke! update!(state: :revoked) diff --git a/app/models/concerns/account/attribution_domains.rb b/app/models/concerns/account/attribution_domains.rb index 22bf27cc5d..ba5c092e6c 100644 --- a/app/models/concerns/account/attribution_domains.rb +++ b/app/models/concerns/account/attribution_domains.rb @@ -12,8 +12,7 @@ module Account::AttributionDomains end def can_be_attributed_from?(domain) - segments = domain.split('.') - variants = segments.map.with_index { |_, i| segments[i..].join('.') }.to_set + variants = self.class.domain_variants(domain).to_set self[:attribution_domains].to_set.intersect?(variants) end end diff --git a/app/models/concerns/account/interaction_policy_concern.rb b/app/models/concerns/account/interaction_policy_concern.rb index 8fe9eda1ba..3966053e05 100644 --- a/app/models/concerns/account/interaction_policy_concern.rb +++ b/app/models/concerns/account/interaction_policy_concern.rb @@ -64,6 +64,6 @@ module Account::InteractionPolicyConcern def local_feature_policy(kind) return [] if kind == :manual || !discoverable? - [:public] + [locked? ? :followers : :public] end end diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb index 6571a40c54..2746280f0c 100644 --- a/app/models/concerns/domain_normalizable.rb +++ b/app/models/concerns/domain_normalizable.rb @@ -17,6 +17,11 @@ module DomainNormalizable SQL ) end + + def domain_variants(domain) + segments = domain.to_s.split('.') + Array.new(segments.size) { |i| segments[i..].join('.') } + end end private diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 74a494517a..72d2d78e41 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -69,9 +69,7 @@ class DomainBlock < ApplicationRecord return if domain.blank? uri = Addressable::URI.new.tap { |u| u.host = domain.strip.delete('/') } - segments = uri.normalized_host.split('.') - variants = segments.map.with_index { |_, i| segments[i..].join('.') } - + variants = domain_variants(uri.normalized_host) where(domain: variants).by_domain_length.first rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError nil diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index c4ab8b9ed2..c1b961517f 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -67,9 +67,7 @@ class EmailDomainBlock < ApplicationRecord @uris.flat_map do |uri| next if uri.nil? - segments = uri.normalized_host.split('.') - - segments.map.with_index { |_, i| segments[i..].join('.') } + self.class.module_parent.domain_variants(uri.normalized_host) end.uniq end diff --git a/app/models/follow.rb b/app/models/follow.rb index c8930c2f86..e8da0077c9 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -42,7 +42,7 @@ class Follow < ApplicationRecord destroy! end - before_validation :set_uri, only: :create + before_validation :set_uri, on: :create after_create :increment_cache_counters after_destroy :remove_endorsements after_destroy :decrement_cache_counters diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 906de95db2..5d8b5f9c08 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -51,7 +51,7 @@ class FollowRequest < ApplicationRecord false # Force uri_for to use uri attribute end - before_validation :set_uri, only: :create + before_validation :set_uri, on: :create after_commit :invalidate_follow_recommendations_cache private diff --git a/app/models/invite.rb b/app/models/invite.rb index ca692d937e..8f3463a322 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -21,7 +21,9 @@ class Invite < ApplicationRecord COMMENT_SIZE_LIMIT = 420 ELIGIBLE_CODE_CHARACTERS = [*('a'..'z'), *('A'..'Z'), *('0'..'9')].freeze + EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze HOMOGLYPHS = %w(0 1 I l O).freeze + MAX_USES_COUNTS = [1, 5, 10, 25, 50, 100].freeze VALID_CODE_CHARACTERS = (ELIGIBLE_CODE_CHARACTERS - HOMOGLYPHS).freeze belongs_to :user, inverse_of: :invites diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb index 5bbfc1fd24..6d766c8d0a 100644 --- a/app/models/ip_block.rb +++ b/app/models/ip_block.rb @@ -20,6 +20,8 @@ class IpBlock < ApplicationRecord include InetContainer include Paginable + EXPIRATION_DURATIONS = [1.day, 2.weeks, 1.month, 6.months, 1.year, 3.years].freeze + enum :severity, { sign_up_requires_approval: 5000, sign_up_block: 5500, diff --git a/app/models/preview_card_provider.rb b/app/models/preview_card_provider.rb index 9d77d45e22..5c052de1e6 100644 --- a/app/models/preview_card_provider.rb +++ b/app/models/preview_card_provider.rb @@ -36,7 +36,6 @@ class PreviewCardProvider < ApplicationRecord scope :not_trendable, -> { where(trendable: false) } def self.matching_domain(domain) - segments = domain.split('.') - where(domain: segments.map.with_index { |_, i| segments[i..].join('.') }).by_domain_length.first + where(domain: domain_variants(domain)).by_domain_length.first end end diff --git a/app/models/quote.rb b/app/models/quote.rb index 44c3599a7b..c49a66c278 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -35,7 +35,7 @@ class Quote < ApplicationRecord belongs_to :quoted_account, class_name: 'Account', optional: true before_validation :set_accounts - before_validation :set_activity_uri, only: :create, if: -> { account.local? && quoted_account&.remote? } + before_validation :set_activity_uri, on: :create, if: -> { account.local? && quoted_account&.remote? } validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? } validates :approval_uri, absence: true, if: -> { quoted_account&.local? } validate :validate_visibility diff --git a/app/models/report.rb b/app/models/report.rb index 282a1f7570..117005c862 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -68,7 +68,7 @@ class Report < ApplicationRecord violation: 2_000, } - before_validation :set_uri, only: :create + before_validation :set_uri, on: :create after_create_commit :trigger_create_webhooks after_update_commit :trigger_update_webhooks diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 8d1a3c8b46..2f32f8d5dd 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -40,7 +40,7 @@ class UserSettings setting :missing_alt_text_modal, default: true setting :reduce_motion, default: false setting :expand_content_warnings, default: false - setting :display_media, default: 'default', in: %w(default show_all hide_all) + setting :display_media, default: 'default', in: %w(hide_all default show_all) setting :auto_play, default: false setting :emoji_style, default: 'auto', in: %w(auto native twemoji) setting :color_scheme, default: 'auto', in: %w(auto light dark) diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index a8c639fe34..8212b607f6 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -176,7 +176,15 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end def interaction_policy - uri = object.discoverable? ? ActivityPub::TagManager::COLLECTIONS[:public] : ActivityPub::TagManager.instance.uri_for(object) + uri = begin + if !object.discoverable? + ActivityPub::TagManager.instance.uri_for(object) + elsif object.locked? + ActivityPub::TagManager.instance.followers_uri_for(object) + else + ActivityPub::TagManager::COLLECTIONS[:public] + end + end { canFeature: { diff --git a/app/serializers/activitypub/featured_collection_serializer.rb b/app/serializers/activitypub/featured_collection_serializer.rb index 00fdb368e6..b83e0bf9ea 100644 --- a/app/serializers/activitypub/featured_collection_serializer.rb +++ b/app/serializers/activitypub/featured_collection_serializer.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer - attributes :id, :type, :total_items, :name, :attributed_to, + attributes :id, :type, :total_items, :name, :attributed_to, :url, :sensitive, :discoverable, :published, :updated attribute :summary, unless: :language_present? attribute :summary_map, if: :language_present? - has_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer + has_one :topic, serializer: ActivityPub::NoteSerializer::TagSerializer has_many :collection_items, key: :ordered_items, serializer: ActivityPub::FeaturedItemSerializer @@ -31,6 +31,10 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer ActivityPub::TagManager.instance.uri_for(object.account) end + def url + ActivityPub::TagManager.instance.url_for(object) + end + def total_items object.accepted_collection_items.size end @@ -50,4 +54,8 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer def collection_items object.accepted_collection_items end + + def topic + object.tag + end end diff --git a/app/serializers/activitypub/featured_item_serializer.rb b/app/serializers/activitypub/featured_item_serializer.rb index 56c0b4390f..529bfd797f 100644 --- a/app/serializers/activitypub/featured_item_serializer.rb +++ b/app/serializers/activitypub/featured_item_serializer.rb @@ -4,7 +4,7 @@ class ActivityPub::FeaturedItemSerializer < ActivityPub::Serializer include RoutingHelper attributes :id, :type, :featured_object, :featured_object_type, - :feature_authorization + :feature_authorization, :published def id ActivityPub::TagManager.instance.uri_for(object) @@ -29,4 +29,8 @@ class ActivityPub::FeaturedItemSerializer < ActivityPub::Serializer object.approval_uri end end + + def published + object.created_at.iso8601 + end end diff --git a/app/serializers/rest/collection_item_serializer.rb b/app/serializers/rest/collection_item_serializer.rb index 8628960403..4d75910a7a 100644 --- a/app/serializers/rest/collection_item_serializer.rb +++ b/app/serializers/rest/collection_item_serializer.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true class REST::CollectionItemSerializer < ActiveModel::Serializer - delegate :accepted?, to: :object + attributes :id, :state, :created_at - attributes :id, :state - - attribute :account_id, if: :accepted? + attribute :account_id, if: :accepted_or_pending? def id object.id.to_s @@ -14,4 +12,8 @@ class REST::CollectionItemSerializer < ActiveModel::Serializer def account_id object.account_id.to_s end + + def accepted_or_pending? + object.pending? || object.accepted? + end end diff --git a/app/serializers/rest/collection_serializer.rb b/app/serializers/rest/collection_serializer.rb index 8354699710..c1c2c70d32 100644 --- a/app/serializers/rest/collection_serializer.rb +++ b/app/serializers/rest/collection_serializer.rb @@ -2,7 +2,7 @@ class REST::CollectionSerializer < ActiveModel::Serializer attributes :id, :uri, :name, :description, :language, :account_id, - :local, :sensitive, :discoverable, :item_count, + :local, :sensitive, :discoverable, :url, :item_count, :created_at, :updated_at belongs_to :tag, serializer: REST::ShallowTagSerializer @@ -17,6 +17,10 @@ class REST::CollectionSerializer < ActiveModel::Serializer ActivityPub::TagManager.instance.uri_for(object) end + def url + ActivityPub::TagManager.instance.url_for(object) + end + def description return object.description if object.local? return if object.description_html.nil? @@ -25,7 +29,11 @@ class REST::CollectionSerializer < ActiveModel::Serializer end def items - object.items_for(current_user&.account) + @items ||= object.items_for(current_user&.account) + end + + def item_count + items.size end def account_id diff --git a/app/services/activitypub/process_featured_collection_service.rb b/app/services/activitypub/process_featured_collection_service.rb index f7ae08dd26..18e0da32d7 100644 --- a/app/services/activitypub/process_featured_collection_service.rb +++ b/app/services/activitypub/process_featured_collection_service.rb @@ -46,6 +46,13 @@ class ActivityPub::ProcessFeaturedCollectionService @json['summaryMap']&.keys&.first end + def url + url = url_to_href(@json['url'], 'text/html') + return @json['id'] if url.blank? || unsupported_uri_scheme?(url) + + url + end + def collection_attributes { local: false, @@ -56,6 +63,7 @@ class ActivityPub::ProcessFeaturedCollectionService discoverable: @json['discoverable'], original_number_of_items: @json['totalItems'] || 0, tag_name: @json.dig('topic', 'name'), + url:, } end diff --git a/app/services/activitypub/process_featured_item_service.rb b/app/services/activitypub/process_featured_item_service.rb index 718a7d1f23..a4323b41c6 100644 --- a/app/services/activitypub/process_featured_item_service.rb +++ b/app/services/activitypub/process_featured_item_service.rb @@ -43,7 +43,9 @@ class ActivityPub::ProcessFeaturedItemService end def new_item - @collection.collection_items.new + @collection.collection_items.new( + created_at: @item_json['published'] + ) end def verify_authorization! diff --git a/app/views/admin/ip_blocks/new.html.haml b/app/views/admin/ip_blocks/new.html.haml index acf632e476..3ef5351d3e 100644 --- a/app/views/admin/ip_blocks/new.html.haml +++ b/app/views/admin/ip_blocks/new.html.haml @@ -12,7 +12,7 @@ .fields-group = f.input :expires_in, - collection: [1.day, 2.weeks, 1.month, 6.months, 1.year, 3.years].map(&:to_i), + collection: IpBlock::EXPIRATION_DURATIONS.map(&:to_i), label_method: ->(i) { I18n.t("admin.ip_blocks.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt'), wrapper: :with_block_label diff --git a/app/views/invites/_form.html.haml b/app/views/invites/_form.html.haml index dbbb785e83..514b9cf016 100644 --- a/app/views/invites/_form.html.haml +++ b/app/views/invites/_form.html.haml @@ -2,12 +2,21 @@ .fields-row .fields-row__column.fields-row__column-6.fields-group - = form.input :max_uses, wrapper: :with_label, collection: invites_max_uses_options, label_method: ->(num) { I18n.t('invites.max_uses', count: num) }, prompt: I18n.t('invites.max_uses_prompt') + = form.input :max_uses, + collection: Invite::MAX_USES_COUNTS, + label_method: ->(count) { I18n.t('invites.max_uses', count:) }, + prompt: I18n.t('invites.max_uses_prompt'), + wrapper: :with_label .fields-row__column.fields-row__column-6.fields-group - = form.input :expires_in, wrapper: :with_label, collection: invites_expires_options.map(&:to_i), label_method: ->(i) { I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt') + = form.input :expires_in, + collection: Invite::EXPIRATION_DURATIONS.map(&:to_i), + label_method: ->(duration) { I18n.t("invites.expires_in.#{duration}") }, + prompt: I18n.t('invites.expires_in_prompt'), + wrapper: :with_label .fields-group - = form.input :autofollow, wrapper: :with_label + = form.input :autofollow, + wrapper: :with_label .actions = form.button :button, t('invites.generate'), type: :submit diff --git a/app/views/settings/migrations/show.html.haml b/app/views/settings/migrations/show.html.haml index 296075a28e..8c3bae704f 100644 --- a/app/views/settings/migrations/show.html.haml +++ b/app/views/settings/migrations/show.html.haml @@ -24,7 +24,7 @@ = simple_form_for @migration, url: settings_migration_path do |f| - if on_cooldown? %p.hint - %span.warning-hint= t('migrations.on_cooldown', count: ((@cooldown.cooldown_at - Time.now.utc) / 1.day.seconds).ceil) + %span.warning-hint= t('migrations.on_cooldown', count: @cooldown.remaining_cooldown_days) - else %p.hint= t('migrations.warning.before') diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index f46f6fb37a..6439fd666c 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -26,7 +26,7 @@ .input.horizontal-options = ff.input :'web.color_scheme', as: :radio_buttons, - collection: %w(auto light dark), + collection: user_settings_collection('web.color_scheme'), include_blank: false, label: I18n.t('simple_form.labels.defaults.setting_color_scheme'), label_method: ->(contrast) { I18n.t("color_scheme.#{contrast}", default: contrast) }, @@ -35,7 +35,7 @@ .input.horizontal-options = ff.input :'web.contrast', as: :radio_buttons, - collection: %w(auto high), + collection: user_settings_collection('web.contrast'), include_blank: false, label: I18n.t('simple_form.labels.defaults.setting_contrast'), label_method: ->(contrast) { I18n.t("contrast.#{contrast}", default: contrast) }, @@ -45,7 +45,7 @@ .fields-group = f.simple_fields_for :settings, current_user.settings do |ff| = ff.input :'web.emoji_style', - collection: %w(auto twemoji native), + collection: user_settings_collection('web.emoji_style'), include_blank: false, hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'), label: I18n.t('simple_form.labels.defaults.setting_emoji_style'), @@ -102,7 +102,7 @@ = ff.input :'web.display_media', as: :radio_buttons, collection_wrapper_tag: 'ul', - collection: %w(default show_all hide_all), + collection: user_settings_collection('web.display_media'), hint: false, item_wrapper_tag: 'li', label_method: ->(item) { t("simple_form.hints.defaults.setting_display_media_#{item}") }, diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index f0c366b6e2..d8d32fc04d 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -37,7 +37,7 @@ - if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops) .fields-group = ff.input :'notification_emails.software_updates', - collection: %w(none critical patch all), + collection: user_settings_collection('notification_emails.software_updates'), hint: false, include_blank: false, label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") }, diff --git a/app/views/settings/preferences/posting_defaults/show.html.haml b/app/views/settings/preferences/posting_defaults/show.html.haml index a3bef1ff9b..4ac182a87e 100644 --- a/app/views/settings/preferences/posting_defaults/show.html.haml +++ b/app/views/settings/preferences/posting_defaults/show.html.haml @@ -26,7 +26,7 @@ .fields-group = ff.input :default_quote_policy, - collection: %w(public followers nobody), + collection: user_settings_collection('default_quote_policy'), include_blank: false, label_method: ->(policy) { I18n.t("statuses.quote_policies.#{policy}") }, label: I18n.t('simple_form.labels.defaults.setting_default_quote_policy'), diff --git a/config/locales/de.yml b/config/locales/de.yml index adf0596f46..353f262fbd 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -2226,7 +2226,7 @@ de: extra_instructions_html: Hinweis: Der Link auf deiner Website kann unsichtbar sein. Der wichtige Teil ist rel="me". Du kannst auch den Tag link im head (statt a im body) verwenden, jedoch muss die Internetseite ohne JavaScript abrufbar sein. here_is_how: So funktioniert’s hint_html: "Alle können ihre Identität auf Mastodon verifizieren. Basierend auf offenen Standards – jetzt und für immer kostenlos. Alles, was du brauchst, ist eine eigene Website. Wenn du von deinem Profil auf diese Website verlinkst, überprüfen wir, ob die Website zu deinem Profil zurückverlinkt, und zeigen einen visuellen Hinweis an." - instructions_html: Kopiere den unten stehenden Code und füge ihn in den HTML-Code deiner Website ein. Trage anschließend die Adresse deiner Website in ein Zusatzfeld auf deinem Profil ein und speichere die Änderungen. Die Zusatzfelder können „Profil bearbeiten“ verwaltet werden. + instructions_html: Kopiere den unten stehenden Code und füge ihn in den HTML-Code deiner Website ein. Trage anschließend die Adresse deiner Website in ein Zusatzfeld auf deinem Profil ein und speichere die Änderungen. Die Zusatzfelder können unter „Profil bearbeiten“ verwaltet werden. verification: Verifizierung verified_links: Deine verifizierten Links website_verification: Verifizierung einer Website diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 7e24896d8f..ce50173b5c 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -562,7 +562,7 @@ gl: confirm_purge: Tes a certeza de querer eliminar permanentemente os datos deste dominio? content_policies: comment: Nota interna - description_html: Podes definir políticas acerca do contido que serán aplicadas a tódalas contas deste dominio e tódolos seus subdominios. + description_html: Podes definir políticas sobre o contido que serán aplicadas a todas as contas de este dominio e todos os seus subdominios. limited_federation_mode_description_html: Podes escoller se permites ou non a federación con este dominio. policies: reject_media: Rexeitar multimedia diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 7102746076..72f6548532 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -61,9 +61,9 @@ en: setting_default_quote_policy_private: Followers-only posts authored on Mastodon can't be quoted by others. setting_default_quote_policy_unlisted: When people quote you, their post will also be hidden from trending timelines. setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click - setting_display_media_default: Hide media marked as sensitive - setting_display_media_hide_all: Always hide media - setting_display_media_show_all: Always show media + setting_display_media_default: Warn before showing media marked as sensitive + setting_display_media_hide_all: Warn before showing all media + setting_display_media_show_all: Show all media without warning, including media marked as sensitive setting_emoji_style: How to display emojis. "Auto" will try using native emoji, but falls back to Twemoji for legacy browsers. setting_quick_boosting_html: When enabled, clicking on the %{boost_icon} Boost icon will immediately boost instead of opening the boost/quote dropdown menu. Relocates the quoting action to the %{options_icon} (Options) menu. setting_system_scrollbars_ui: Applies only to desktop browsers based on Safari and Chrome diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index 18f6dac9bc..e625ee5395 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -233,7 +233,7 @@ gl: locale: Idioma da interface max_uses: Número máximo de usos new_password: Novo contrasinal - note: Acerca de ti + note: Sobre ti otp_attempt: Código do Segundo Factor password: Contrasinal phrase: Palabra chave ou frase diff --git a/db/migrate/20260415133505_add_url_to_collections.rb b/db/migrate/20260415133505_add_url_to_collections.rb new file mode 100644 index 0000000000..b40a792fc4 --- /dev/null +++ b/db/migrate/20260415133505_add_url_to_collections.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddURLToCollections < ActiveRecord::Migration[8.1] + def change + add_column :collections, :url, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index e33414fddd..cf13d14374 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_04_10_083500) do +ActiveRecord::Schema[8.1].define(version: 2026_04_15_133505) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -400,6 +400,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_04_10_083500) do t.bigint "tag_id" t.datetime "updated_at", null: false t.string "uri" + t.string "url" t.index ["account_id"], name: "index_collections_on_account_id" t.index ["tag_id"], name: "index_collections_on_tag_id" t.index ["uri"], name: "index_collections_on_uri", unique: true, where: "(uri IS NOT NULL)" diff --git a/spec/helpers/settings_helper_spec.rb b/spec/helpers/settings_helper_spec.rb index ecff2edbfa..63773634e9 100644 --- a/spec/helpers/settings_helper_spec.rb +++ b/spec/helpers/settings_helper_spec.rb @@ -3,6 +3,22 @@ require 'rails_helper' RSpec.describe SettingsHelper do + describe '#user_settings_collection' do + subject { helper.user_settings_collection(value) } + + context 'with valid value' do + let(:value) { 'web.contrast' } + + it { is_expected.to eq(%w(auto high)) } + end + + context 'with invalid value' do + let(:value) { 'web.nothing_at_this_key_at_all_fake_fake_fake' } + + it { is_expected.to be_empty } + end + end + describe 'session_device_icon' do context 'with a mobile device' do let(:session) { SessionActivation.new(user_agent: 'Mozilla/5.0 (iPhone)') } diff --git a/spec/lib/fasp/request_spec.rb b/spec/lib/fasp/request_spec.rb index 6456057b5e..cdc20270cd 100644 --- a/spec/lib/fasp/request_spec.rb +++ b/spec/lib/fasp/request_spec.rb @@ -86,14 +86,16 @@ RSpec.describe Fasp::Request do WebMock.enable! end + let(:resolv_service) { instance_double(Resolv) } + + before do + allow(Resolv).to receive(:new).and_return(resolv_service) + allow(resolv_service).to receive(:getaddresses).with('reqprov.example.com').and_return(%w(0.0.0.0 2001:db8::face)) + end + it 'raises Mastodon::ValidationError' do - resolver = instance_double(Resolv::DNS) - - allow(resolver).to receive(:getaddresses).with('reqprov.example.com').and_return(%w(0.0.0.0 2001:db8::face)) - allow(resolver).to receive(:timeouts=).and_return(nil) - allow(Resolv::DNS).to receive(:open).and_yield(resolver) - - expect { subject.send(method, '/test_path') }.to raise_error(Mastodon::ValidationError) + expect { subject.send(method, '/test_path') } + .to raise_error(Mastodon::ValidationError) end end end diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb index da61823455..228c8bbb67 100644 --- a/spec/lib/request_spec.rb +++ b/spec/lib/request_spec.rb @@ -66,15 +66,20 @@ RSpec.describe Request do expect(a_request(:get, 'http://example.com')).to have_been_made.once end - it 'executes a HTTP request when the first address is private' do - resolver = instance_double(Resolv::DNS) + context 'when first address is private' do + let(:resolv_service) { instance_double(Resolv) } - allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844)) - allow(resolver).to receive(:timeouts=).and_return(nil) - allow(Resolv::DNS).to receive(:open).and_yield(resolver) + before do + allow(Resolv).to receive(:new).and_return(resolv_service) + allow(resolv_service).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:4860:4860::8844)) + end - expect { |block| subject.perform(&block) }.to yield_control - expect(a_request(:get, 'http://example.com')).to have_been_made.once + it 'executes a HTTP request' do + expect { |block| subject.perform(&block) } + .to yield_control + expect(a_request(:get, 'http://example.com')) + .to have_been_made.once + end end it 'makes a request with expected headers, yields, and closes the underlying connection' do @@ -123,14 +128,16 @@ RSpec.describe Request do WebMock.enable! end + let(:resolv_service) { instance_double(Resolv) } + + before do + allow(Resolv).to receive(:new).with([be_a(Resolv::Hosts), be_a(Resolv::DNS)]).and_return(resolv_service) + allow(resolv_service).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face)) + end + it 'raises Mastodon::ValidationError' do - resolver = instance_double(Resolv::DNS) - - allow(resolver).to receive(:getaddresses).with('example.com').and_return(%w(0.0.0.0 2001:db8::face)) - allow(resolver).to receive(:timeouts=).and_return(nil) - allow(Resolv::DNS).to receive(:open).and_yield(resolver) - - expect { subject.perform }.to raise_error Mastodon::ValidationError + expect { subject.perform } + .to raise_error Mastodon::ValidationError end end diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index 1bb238f7ef..55d299799c 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -51,4 +51,24 @@ RSpec.describe AccountMigration do it { is_expected.to_not allow_value(target_acct).for(:acct) } end end + + describe '#remaining_cooldown_days' do + subject { account_migration.remaining_cooldown_days } + + before { stub_const('AccountMigration::COOLDOWN_PERIOD', 30.days) } + + let(:account_migration) { Fabricate :account_migration, created_at: } + + context 'with a record still in cooldown' do + let(:created_at) { 15.days.ago } + + it { is_expected.to eq(15) } + end + + context 'with a record out of cooldown' do + let(:created_at) { 150.days.ago } + + it { is_expected.to be_negative } + end + end end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index a128a9b2f8..0d76b26887 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -772,19 +772,42 @@ RSpec.describe Account do end describe '#featureable_by?' do - subject { Fabricate.build(:account, domain: (local ? nil : 'example.com'), discoverable:, feature_approval_policy:) } + subject { Fabricate.build(:account, domain: (local ? nil : 'example.com'), discoverable:, locked:, feature_approval_policy:) } + let(:locked) { false } let(:local_account) { Fabricate(:account) } context 'when account is local' do let(:local) { true } - let(:feature_approval_policy) { nil } + let(:feature_approval_policy) { 0 } context 'when account is discoverable' do let(:discoverable) { true } - it 'returns `true`' do - expect(subject.featureable_by?(local_account)).to be true + context 'when the account is not locked' do + let(:locked) { false } + + it 'returns `true`' do + expect(subject.featureable_by?(local_account)).to be true + end + end + + context 'when the account is locked' do + let(:locked) { true } + + it 'returns `false`' do + expect(subject.featureable_by?(local_account)).to be false + end + + context 'when the other account is a follower' do + before do + local_account.follow!(subject) + end + + it 'returns `true`' do + expect(subject.featureable_by?(local_account)).to be true + end + end end end diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb index 62d7e40e28..e5dfc909b7 100644 --- a/spec/models/block_spec.rb +++ b/spec/models/block_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Block do end context 'when URI is blank' do - subject { Fabricate.build :follow, uri: nil } + subject { Fabricate.build :block, uri: nil } it 'populates the value' do expect { subject.save } diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index 64e93b6797..619359a19d 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -64,11 +64,18 @@ RSpec.describe Collection do describe '#item_for' do subject { Fabricate(:collection) } - let!(:items) { Fabricate.times(2, :collection_item, collection: subject) } + let!(:accepted_items) { Fabricate.times(2, :collection_item, collection: subject, state: :accepted) } + let!(:pending_item) { Fabricate(:collection_item, collection: subject, state: :pending) } + + before do + %i(rejected revoked).each do |state| + Fabricate(:collection_item, collection: subject, state:) + end + end context 'when given no account' do - it 'returns all items' do - expect(subject.items_for).to match_array(items) + it 'returns all accepted items' do + expect(subject.items_for).to match_array(accepted_items) end end @@ -76,11 +83,19 @@ RSpec.describe Collection do let(:account) { Fabricate(:account) } before do - account.block!(items.first.account) + account.block!(accepted_items.first.account) end it 'does not return items blocked by this account' do - expect(subject.items_for(account)).to contain_exactly(items.last) + expect(subject.items_for(account)).to contain_exactly(accepted_items.last) + end + end + + context 'when given the owner of the collection' do + let(:account) { subject.account } + + it 'returns accepted and pending items' do + expect(subject.items_for(account)).to match_array(accepted_items + [pending_item]) end end end diff --git a/spec/serializers/activitypub/actor_serializer_spec.rb b/spec/serializers/activitypub/actor_serializer_spec.rb index 734da6673c..73702c979c 100644 --- a/spec/serializers/activitypub/actor_serializer_spec.rb +++ b/spec/serializers/activitypub/actor_serializer_spec.rb @@ -56,6 +56,18 @@ RSpec.describe ActivityPub::ActorSerializer do }, }) end + + context 'when actor is locked' do + let(:record) { Fabricate(:account, locked: true) } + + it 'includes an automatic policy allowing followers' do + expect(subject).to include('interactionPolicy' => { + 'canFeature' => { + 'automaticApproval' => [ActivityPub::TagManager.instance.followers_uri_for(record)], + }, + }) + end + end end context 'when actor is not discoverable' do diff --git a/spec/serializers/activitypub/featured_collection_serializer_spec.rb b/spec/serializers/activitypub/featured_collection_serializer_spec.rb index 78b9daf613..ad62e7f8ad 100644 --- a/spec/serializers/activitypub/featured_collection_serializer_spec.rb +++ b/spec/serializers/activitypub/featured_collection_serializer_spec.rb @@ -25,6 +25,7 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do 'attributedTo' => ActivityPub::TagManager.instance.uri_for(collection.account), 'sensitive' => false, 'discoverable' => false, + 'url' => account_collection_url(collection.account, collection), 'topic' => { 'href' => match(%r{/tags/people$}), 'type' => 'Hashtag', @@ -38,6 +39,7 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.first.account), 'featuredObjectType' => 'Person', 'featureAuthorization' => ap_account_feature_authorization_url(collection_items.first.account_id, collection_items.first), + 'published' => match_api_datetime_format, }, { 'id' => ActivityPub::TagManager.instance.uri_for(collection_items.last), @@ -45,6 +47,7 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.last.account), 'featuredObjectType' => 'Person', 'featureAuthorization' => ap_account_feature_authorization_url(collection_items.last.account_id, collection_items.last), + 'published' => match_api_datetime_format, }, ], 'published' => match_api_datetime_format, diff --git a/spec/serializers/activitypub/featured_item_serializer_spec.rb b/spec/serializers/activitypub/featured_item_serializer_spec.rb index 7aca086192..18afdd10fe 100644 --- a/spec/serializers/activitypub/featured_item_serializer_spec.rb +++ b/spec/serializers/activitypub/featured_item_serializer_spec.rb @@ -7,7 +7,7 @@ RSpec.describe ActivityPub::FeaturedItemSerializer do subject { serialized_record_json(collection_item, described_class, adapter: ActivityPub::Adapter) } - let(:collection_item) { Fabricate(:collection_item) } + let(:collection_item) { Fabricate(:collection_item, created_at: Time.utc(2026, 4, 16, 1)) } context 'when a local account is featured' do it 'serializes to the expected structure' do @@ -17,6 +17,7 @@ RSpec.describe ActivityPub::FeaturedItemSerializer do 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_item.account), 'featuredObjectType' => 'Person', 'featureAuthorization' => ap_account_feature_authorization_url(collection_item.account_id, collection_item), + 'published' => '2026-04-16T01:00:00Z', }) end end diff --git a/spec/serializers/rest/account_serializer_spec.rb b/spec/serializers/rest/account_serializer_spec.rb index 998de6b0fb..0e3ce1caca 100644 --- a/spec/serializers/rest/account_serializer_spec.rb +++ b/spec/serializers/rest/account_serializer_spec.rb @@ -93,6 +93,18 @@ RSpec.describe REST::AccountSerializer do 'current_user' => 'automatic', }) end + + context 'when account is locked' do + let(:account) { Fabricate(:account, locked: true) } + + it 'includes a policy that allows featuring for followers' do + expect(subject['feature_approval']).to include({ + 'automatic' => ['followers'], + 'manual' => [], + 'current_user' => 'automatic', + }) + end + end end context 'when account is not discoverable' do diff --git a/spec/serializers/rest/collection_item_serializer_spec.rb b/spec/serializers/rest/collection_item_serializer_spec.rb index 348107fb91..b57daf1157 100644 --- a/spec/serializers/rest/collection_item_serializer_spec.rb +++ b/spec/serializers/rest/collection_item_serializer_spec.rb @@ -11,22 +11,33 @@ RSpec.describe REST::CollectionItemSerializer do state:) end - context 'when state is `accepted`' do - let(:state) { :accepted } - + shared_examples 'full result' do it 'includes the relevant attributes including the account' do expect(subject) .to include( 'id' => '2342', 'account_id' => collection_item.account_id.to_s, - 'state' => 'accepted' + 'state' => state.to_s, + 'created_at' => match_api_datetime_format ) end end - %i(pending rejected revoked).each do |unaccepted_state| - context "when state is `#{unaccepted_state}`" do - let(:state) { unaccepted_state } + context 'when state is `accepted`' do + let(:state) { :accepted } + + it_behaves_like 'full result' + end + + context 'when state is `pending`' do + let(:state) { :pending } + + it_behaves_like 'full result' + end + + %i(rejected revoked).each do |rejected_state| + context "when state is `#{rejected_state}`" do + let(:state) { rejected_state } it 'does not include an account' do expect(subject.keys).to_not include('account_id') diff --git a/spec/serializers/rest/collection_serializer_spec.rb b/spec/serializers/rest/collection_serializer_spec.rb index ded12f229e..8055c2afbe 100644 --- a/spec/serializers/rest/collection_serializer_spec.rb +++ b/spec/serializers/rest/collection_serializer_spec.rb @@ -37,6 +37,7 @@ RSpec.describe REST::CollectionSerializer do 'local' => true, 'sensitive' => true, 'discoverable' => false, + 'url' => ActivityPub::TagManager.instance.url_for(collection), 'tag' => a_hash_including('name' => 'discovery'), 'created_at' => match_api_datetime_format, 'updated_at' => match_api_datetime_format, @@ -46,7 +47,11 @@ RSpec.describe REST::CollectionSerializer do end context 'when the collection is remote' do - let(:collection) { Fabricate(:remote_collection, description_html: '

    remote

    ') } + let(:collection) { Fabricate(:remote_collection, description_html: '

    remote

    ', url: 'https://example.com/c/1') } + + it 'includes the uri' do + expect(subject).to include('url' => 'https://example.com/c/1') + end it 'includes the html description' do expect(subject) @@ -62,4 +67,30 @@ RSpec.describe REST::CollectionSerializer do end end end + + context 'when the collection has items in different states' do + before do + %i(accepted pending rejected revoked).each do |state| + Fabricate(:collection_item, collection:, state:) + end + end + + context 'when `current_user` is the owner of the collection' do + let(:current_user) { collection.account.user } + + it 'includes accepted and pending items' do + expect(subject['item_count']).to eq 2 + expect(subject['items'].size).to eq 2 + expect(subject['items'].pluck('state')).to contain_exactly('accepted', 'pending') + end + end + + context 'when `current_user` is not the owner of the collection' do + it 'only includes the accepted item' do + expect(subject['item_count']).to eq 1 + expect(subject['items'].size).to eq 1 + expect(subject['items'].first['state']).to eq 'accepted' + end + end + end end diff --git a/spec/services/activitypub/process_featured_collection_service_spec.rb b/spec/services/activitypub/process_featured_collection_service_spec.rb index 343d3ac3b2..6a74eaa398 100644 --- a/spec/services/activitypub/process_featured_collection_service_spec.rb +++ b/spec/services/activitypub/process_featured_collection_service_spec.rb @@ -16,6 +16,7 @@ RSpec.describe ActivityPub::ProcessFeaturedCollectionService do 'name' => 'Good people from other servers', 'sensitive' => false, 'discoverable' => true, + 'url' => 'https://example.com/c/1', 'topic' => { 'type' => 'Hashtag', 'name' => '#people', @@ -50,6 +51,7 @@ RSpec.describe ActivityPub::ProcessFeaturedCollectionService do expect(new_collection.description_html).to eq '

    A list of remote actors you should follow.

    ' expect(new_collection.sensitive).to be false expect(new_collection.discoverable).to be true + expect(new_collection.url).to eq 'https://example.com/c/1' expect(new_collection.tag.formatted_name).to eq '#people' expect(ActivityPub::ProcessFeaturedItemWorker).to have_enqueued_sidekiq_job.with(new_collection.id, 'https://example.com/featured_items/1', 1, nil) @@ -75,6 +77,18 @@ RSpec.describe ActivityPub::ProcessFeaturedCollectionService do end end + context 'when the json does not include a `url`' do + let(:featured_collection_json) do + base_json.except('url') + end + + it 'uses the `id` instead' do + subject.call(account, featured_collection_json) + + expect(Collection.last.url).to eq 'https://example.com/featured_collections/1' + end + end + context 'when the collection already exists' do let(:collection) { Fabricate(:remote_collection, account:, uri: base_json['id'], name: 'placeholder') } diff --git a/spec/services/activitypub/process_featured_item_service_spec.rb b/spec/services/activitypub/process_featured_item_service_spec.rb index bfcea2a5da..14147b3784 100644 --- a/spec/services/activitypub/process_featured_item_service_spec.rb +++ b/spec/services/activitypub/process_featured_item_service_spec.rb @@ -19,6 +19,7 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do 'featuredObject' => featured_object_uri, 'featuredObjectType' => 'Person', 'featureAuthorization' => feature_authorization_uri, + 'published' => '2026-04-16T01:00:00Z', } end let(:stubbed_service) do @@ -56,6 +57,7 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do new_item = collection.collection_items.last expect(new_item.object_uri).to eq 'https://example.com/actor/1' expect(new_item.approval_uri).to be_nil + expect(new_item.created_at).to eq Time.utc(2026, 4, 16, 1) expect(new_item.position).to eq 3 end end diff --git a/yarn.lock b/yarn.lock index 4cd6de5f0b..5d3fb10db9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1241,26 +1241,26 @@ __metadata: languageName: node linkType: hard -"@csstools/css-calc@npm:^3.1.1": - version: 3.1.1 - resolution: "@csstools/css-calc@npm:3.1.1" +"@csstools/css-calc@npm:^3.1.1, @csstools/css-calc@npm:^3.2.0": + version: 3.2.0 + resolution: "@csstools/css-calc@npm:3.2.0" peerDependencies: "@csstools/css-parser-algorithms": ^4.0.0 "@csstools/css-tokenizer": ^4.0.0 - checksum: 10c0/6efcc016d988edf66e54c7bad03e352d61752cbd1b56c7557fd013868aab23505052ded8f912cd4034e216943ea1e04c957d81012489e3eddc14a57b386510ef + checksum: 10c0/a4dffeef3cb8ec9e8c1e44fa68c7634033050be52ea0b56ba6ac3815b635b587c6f3a8f8cd7b8f53881c2dd0ab9ec0af77227c532ed81b8e24a05aa997d22337 languageName: node linkType: hard -"@csstools/css-color-parser@npm:^4.0.2": - version: 4.0.2 - resolution: "@csstools/css-color-parser@npm:4.0.2" +"@csstools/css-color-parser@npm:^4.0.2, @csstools/css-color-parser@npm:^4.1.0": + version: 4.1.0 + resolution: "@csstools/css-color-parser@npm:4.1.0" dependencies: "@csstools/color-helpers": "npm:^6.0.2" - "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-calc": "npm:^3.2.0" peerDependencies: "@csstools/css-parser-algorithms": ^4.0.0 "@csstools/css-tokenizer": ^4.0.0 - checksum: 10c0/487cf507ef4630f74bd67d84298294ed269900b206ade015a968d20047e07ff46f235b72e26fe0c6b949a03f8f9f00a22c363da49c1b06ca60b32d0188e546be + checksum: 10c0/b5b8a697b4c1b22dd535b4d93b2ffce338d38e587ac1ab20b781c08328bfa99e5f763a99d990983560df543862fa9bd578ee966c18f9d3381c8e33c641d32a0e languageName: node linkType: hard @@ -1302,18 +1302,18 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-alpha-function@npm:^2.0.3": - version: 2.0.3 - resolution: "@csstools/postcss-alpha-function@npm:2.0.3" +"@csstools/postcss-alpha-function@npm:^2.0.4": + version: 2.0.4 + resolution: "@csstools/postcss-alpha-function@npm:2.0.4" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/79d2b5c19b59cdf64377cae88c83e39411b82d4b87bd55a6f39bf85e0c2e4904ae73d329c2db713274dafd044dbd1cfcbcf5018536ccb16a3024555c3f9d0927 + checksum: 10c0/143e13dd1be8701736f5e4236634f3c1e64b03fa9ee56ba0195203fafad4fa52fa32801d1beb201ca6eb49265f9b1a4a54ca0ade62ac1745ab1827d7496cf267 languageName: node linkType: hard @@ -1329,63 +1329,63 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-color-function-display-p3-linear@npm:^2.0.2": - version: 2.0.2 - resolution: "@csstools/postcss-color-function-display-p3-linear@npm:2.0.2" +"@csstools/postcss-color-function-display-p3-linear@npm:^2.0.3": + version: 2.0.3 + resolution: "@csstools/postcss-color-function-display-p3-linear@npm:2.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/e20a66b4c186e66b47fa6b01d01b0d6ea061b520a06720a759d12c72c41a6286d97fdb4f5d23b28ffeff1b8d30ecc963e1b7266dae86a4bc63e14a3743a77bc4 + checksum: 10c0/263e7ce1a6ef9c4d9a070ac11168d94567121cebffdc001f2a7c97d5f171aaf69930076114c0132e8a26164572f4a9d863098f4a206cdb038b34bc7aa6e73b4b languageName: node linkType: hard -"@csstools/postcss-color-function@npm:^5.0.2": - version: 5.0.2 - resolution: "@csstools/postcss-color-function@npm:5.0.2" +"@csstools/postcss-color-function@npm:^5.0.3": + version: 5.0.3 + resolution: "@csstools/postcss-color-function@npm:5.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/ca255931d37d07f02e4401eef30f70bd3aa9c56ca6cac9d9beeca8e2f07c9909f31f5431347fc408522ebff892a51312afee0fcb9bdf6bc0e680e1585e0f9247 + checksum: 10c0/a43178f6dcffbbe4acdbcdf881a463ba6891eb6eff5ecfe46f14712fac2dddaa1dbb34dd0f067fdb09ee1f9902aef171bc75acc8ad35483a871700a9c2cc19b1 languageName: node linkType: hard -"@csstools/postcss-color-mix-function@npm:^4.0.2": - version: 4.0.2 - resolution: "@csstools/postcss-color-mix-function@npm:4.0.2" +"@csstools/postcss-color-mix-function@npm:^4.0.3": + version: 4.0.3 + resolution: "@csstools/postcss-color-mix-function@npm:4.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/c3411f91d1d1923fc1cd27f74dad4371466941236b30a25d97f2d50e255fd5fb37c3ec1f785958e13e4b5a851c098913da507e9f0ea2e176997daa16b6679c8c + checksum: 10c0/450f9f4eaf3beda9bf54b3245defd7dd4d10d29b9e61f2a238770f9d06f975a4164d8ac7554442b919cdffab7521d7d167f6691c152c53a1dea162dadac2f939 languageName: node linkType: hard -"@csstools/postcss-color-mix-variadic-function-arguments@npm:^2.0.2": - version: 2.0.2 - resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:2.0.2" +"@csstools/postcss-color-mix-variadic-function-arguments@npm:^2.0.3": + version: 2.0.3 + resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:2.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/3b658ca2e2c7f7224c01e4661178a0c2e69101cd815f86c65742d98cd5575b073232a8b5f7600113d929b4d0f9f5ada4364f58a74e26d26f73378a7e1fa8c7cb + checksum: 10c0/e154e98f529a2fd19fae26baedb55957041ec46aca2a7c40b240b592b0950ba8febba0970cfa2d22333fe6eaf0bf4e0a5e7e453c24475cd2a413365663a35195 languageName: node linkType: hard @@ -1403,31 +1403,31 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-contrast-color-function@npm:^3.0.2": - version: 3.0.2 - resolution: "@csstools/postcss-contrast-color-function@npm:3.0.2" +"@csstools/postcss-contrast-color-function@npm:^3.0.3": + version: 3.0.3 + resolution: "@csstools/postcss-contrast-color-function@npm:3.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/f8865c5a387a8c54c91a9dc25c2ba64311eabed2cea992843979ca672d3244089a1adf1d1ba1533c0d99f0e143d90ad5af2b30e841609bd3ceccb7f9920edfaf + checksum: 10c0/b3c218e627e757724eb208501be195d65c08d68749e54af8f8a7f0e2590aabb76ea5c8500b68d00af9b4e661a4506a6919fddb9897cbf6e2e29ac2f3e084d3dd languageName: node linkType: hard -"@csstools/postcss-exponential-functions@npm:^3.0.1": - version: 3.0.1 - resolution: "@csstools/postcss-exponential-functions@npm:3.0.1" +"@csstools/postcss-exponential-functions@npm:^3.0.2": + version: 3.0.2 + resolution: "@csstools/postcss-exponential-functions@npm:3.0.2" dependencies: - "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-calc": "npm:^3.2.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/3cffb90647640bae836e91b7909b4c92fdb71fd4ca5498c49868e70a924a17307206623f5f66129ece2feec0b3abb54d3259653364d1073935af4b0c8afe40a5 + checksum: 10c0/70be8c3cd42b72d3955ab122cf89a70a61674341947452f33dfdc3b2371f969bbd366e5824b4ad62ec4227067b830346783b5deb9170cc8c23ccca3c2fb04cc5 languageName: node linkType: hard @@ -1454,46 +1454,46 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-gamut-mapping@npm:^3.0.2": - version: 3.0.2 - resolution: "@csstools/postcss-gamut-mapping@npm:3.0.2" +"@csstools/postcss-gamut-mapping@npm:^3.0.3": + version: 3.0.3 + resolution: "@csstools/postcss-gamut-mapping@npm:3.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/af4eea62e651be5d1470c425feea29a832f092008d4d3a88a0ee2b3b703c2cd1707b36456611806bfacdbd05754e007f0e0d9f604b3e0aadf09a94e808735439 + checksum: 10c0/161a8485b91fd1d9228978619e2037936353ac60cf7e7a829c300efc308598b376d734e50582a6c44de51c124486685e217ab549c8a97ee1a7427ba15debb242 languageName: node linkType: hard -"@csstools/postcss-gradients-interpolation-method@npm:^6.0.2": - version: 6.0.2 - resolution: "@csstools/postcss-gradients-interpolation-method@npm:6.0.2" +"@csstools/postcss-gradients-interpolation-method@npm:^6.0.3": + version: 6.0.3 + resolution: "@csstools/postcss-gradients-interpolation-method@npm:6.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/85f472ed7609dd608a3bce267b9bae9f06a6241e52df0901f1a51cb5432cb642fadaa24812abbe12b14fd9c49a4333ea191f8d11815c7f7e9c904b127ed9f6e0 + checksum: 10c0/8bf699dc7e5c071512ebba84b1b9f0367bf43d96b55a19d34032865883f7f33adb9cf21c824915dd451b7eebb24102e61cf5e38343301290594d7dae3e1ac610 languageName: node linkType: hard -"@csstools/postcss-hwb-function@npm:^5.0.2": - version: 5.0.2 - resolution: "@csstools/postcss-hwb-function@npm:5.0.2" +"@csstools/postcss-hwb-function@npm:^5.0.3": + version: 5.0.3 + resolution: "@csstools/postcss-hwb-function@npm:5.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/376c3e0a22856afb3304cc4a24e27d502e580637d9495426195f269d9d8348e42754f0d8a2aaac6d8fb5a64e82954f90e3ee358ca7af520ca295edd9fc7ddd20 + checksum: 10c0/16ba746bcd992bba0c34c22f84ed64a67ff03274266832bf32aa0a553fe96c32a803ae7484c248652db0ddab9f3ad942dbe684d33bbb247551b2be9cfc918959 languageName: node linkType: hard @@ -1595,17 +1595,17 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-media-minmax@npm:^3.0.1": - version: 3.0.1 - resolution: "@csstools/postcss-media-minmax@npm:3.0.1" +"@csstools/postcss-media-minmax@npm:^3.0.2": + version: 3.0.2 + resolution: "@csstools/postcss-media-minmax@npm:3.0.2" dependencies: - "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-calc": "npm:^3.2.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/media-query-list-parser": "npm:^5.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/d5675f4b3b07c41763d28f0ab7f89548c4c5e1633fa09c94a8289ea099bcc4a9c123bed7f2c12af3c2adb32f6afb2b3b107eeb071f9eff4da2676b778ca602af + checksum: 10c0/aefdab19cd1c839bbb455536b80b308f6e16e3302deac944ed826f3b6333c78ba7b5f2df39259f0cefa19c01c6569cdbbd9f52914de0e781a2f039031fdf931d languageName: node linkType: hard @@ -1657,18 +1657,18 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-oklab-function@npm:^5.0.2": - version: 5.0.2 - resolution: "@csstools/postcss-oklab-function@npm:5.0.2" +"@csstools/postcss-oklab-function@npm:^5.0.3": + version: 5.0.3 + resolution: "@csstools/postcss-oklab-function@npm:5.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/675c43880ebc92ecdbf14e9b614f8c7ca3ecbb4c58d68b615ac1056eeafc017eaeff360c65b271836268e7ab26983a0c6a3bbdaa974986409a2516288a0efe40 + checksum: 10c0/5995f9589295e9e60ab0c39f200b5dc3254cf33d8e264f21fcd804387eabc64c989e8d97a950c99c2b4c9ecb6957a93a0a50e18fa643726b0e364c41f63c9c84 languageName: node linkType: hard @@ -1704,31 +1704,31 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-random-function@npm:^3.0.1": - version: 3.0.1 - resolution: "@csstools/postcss-random-function@npm:3.0.1" +"@csstools/postcss-random-function@npm:^3.0.2": + version: 3.0.2 + resolution: "@csstools/postcss-random-function@npm:3.0.2" dependencies: - "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-calc": "npm:^3.2.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/de495b71abec80bbe80d16dbd7c74edf2f1e4dcb8ef6061ee027713416a69a8840accb2db8a52f93750f2d2e1bc27b9548fd11ba349f1ed25f995fb96201418d + checksum: 10c0/724ae46ee669c8e17315c3354ebf0e3ed7cc3cfb1633017685cfbbeb8870fe671574dee03d4470f50f68089d141f24dc507c586cac16ab2aca0a7db7b241943a languageName: node linkType: hard -"@csstools/postcss-relative-color-syntax@npm:^4.0.2": - version: 4.0.2 - resolution: "@csstools/postcss-relative-color-syntax@npm:4.0.2" +"@csstools/postcss-relative-color-syntax@npm:^4.0.3": + version: 4.0.3 + resolution: "@csstools/postcss-relative-color-syntax@npm:4.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/d4ad2d3a5b30a52749db95e1b987d0d3fa31a4f589620201514725707978de870a74702d7010eaef662c9a13e675c5877eb77a84bd47f3e1d853d10e93de5a06 + checksum: 10c0/e4f99ec2a54438f399ae4011d856395806dba614b893cdc86476cc35ecab6c322bd8b5f28dc689452681bc7fe7f86100aa36427a6e8a90f84ef3d777eb856fa4 languageName: node linkType: hard @@ -1743,29 +1743,29 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-sign-functions@npm:^2.0.1": - version: 2.0.1 - resolution: "@csstools/postcss-sign-functions@npm:2.0.1" +"@csstools/postcss-sign-functions@npm:^2.0.2": + version: 2.0.2 + resolution: "@csstools/postcss-sign-functions@npm:2.0.2" dependencies: - "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-calc": "npm:^3.2.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/c521746dc96b755a42d05defb21b8f3835eca46881c38f1919375711950b6138991a3b39ecb0198224804626cb55e69bca9906d9b0806b451e2e7fb452896e37 + checksum: 10c0/dfb4714fe49793b8e5466ae1498109e35c843eda513ed20827fd32469f6cafd633f4d2d616c9f7e9df9512a3cda8daef9c56e3ce4bc1436dddb7892a46a8cf39 languageName: node linkType: hard -"@csstools/postcss-stepped-value-functions@npm:^5.0.1": - version: 5.0.1 - resolution: "@csstools/postcss-stepped-value-functions@npm:5.0.1" +"@csstools/postcss-stepped-value-functions@npm:^5.0.2": + version: 5.0.2 + resolution: "@csstools/postcss-stepped-value-functions@npm:5.0.2" dependencies: - "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-calc": "npm:^3.2.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/f5f90977919bec2aabdfddfebe4cf9e4278cb5c53b2e2d16b23162011f4c112048dc365614b25c429e0fa793c95e1cd77e2697a255d5c03d73e25a614f27deab + checksum: 10c0/c8e06be85cbfee602cfb429695484ca9420a0f6cb1e9d5627cc35d3a03b09a8e059aa9b20f7bbd91b356dfd79c53c06043a962a611c5b36253826904fbc17f67 languageName: node linkType: hard @@ -1804,16 +1804,16 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-trigonometric-functions@npm:^5.0.1": - version: 5.0.1 - resolution: "@csstools/postcss-trigonometric-functions@npm:5.0.1" +"@csstools/postcss-trigonometric-functions@npm:^5.0.2": + version: 5.0.2 + resolution: "@csstools/postcss-trigonometric-functions@npm:5.0.2" dependencies: - "@csstools/css-calc": "npm:^3.1.1" + "@csstools/css-calc": "npm:^3.2.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/3a941fab1bfdeb5ce1ca388824a9606e3b973078b3a153aaf6bad5edf6401fd421ff7fd7cc8acc41b4e05cd67d2f02cfac3b2015dd3e9f5fc0135b15ef3d3d2c + checksum: 10c0/95a25be56a6e1bd390a025b5c72fb6bf7db329711717a663454d8c13e70971d92126c33c683047f283cd44dd99ac4101e37f9f56a44afbee8161cf95aeaec482 languageName: node linkType: hard @@ -3826,10 +3826,10 @@ __metadata: linkType: hard "@rolldown/plugin-babel@npm:^0.2.2": - version: 0.2.2 - resolution: "@rolldown/plugin-babel@npm:0.2.2" + version: 0.2.3 + resolution: "@rolldown/plugin-babel@npm:0.2.3" dependencies: - picomatch: "npm:^4.0.3" + picomatch: "npm:^4.0.4" peerDependencies: "@babel/core": ^7.29.0 || ^8.0.0-rc.1 "@babel/plugin-transform-runtime": ^7.29.0 || ^8.0.0-rc.1 @@ -3843,7 +3843,7 @@ __metadata: optional: true vite: optional: true - checksum: 10c0/d00d6afd831c1efa5eac8bbe9eb4c78abfe731c744ffac99d2246e78a94ac8546ee26d0a304541143427e59a846c96d3d5728da540293d02c37bcbe3972428b3 + checksum: 10c0/84afe9854137b576f963f4fd11080dc0b8afe6243d565513d33ddfed9ea9149652110def881a3911b5e889f5fa505df036c6839f4d0fd01d4222297d7f2baa21 languageName: node linkType: hard @@ -6897,9 +6897,9 @@ __metadata: linkType: hard "dotenv@npm:^17.0.0": - version: 17.4.1 - resolution: "dotenv@npm:17.4.1" - checksum: 10c0/eb6ae592ee1e8f6b7f62e0f0c2827c4f27bbe9e00abe9d7910e06456e63dccd42f011de94da9450a63c07ccc52c318f527c1b56ad8e2c81a1a16314b53d4fd99 + version: 17.4.2 + resolution: "dotenv@npm:17.4.2" + checksum: 10c0/164f8e77a646c8446867d5b588d26ea6005c8ea7c5eb41cf926f6113d23f2191355f6e0cfd95ea9bab98394a5b0a3f1e51a8399711b666fe55cc7b0bd745f942 languageName: node linkType: hard @@ -11261,18 +11261,18 @@ __metadata: languageName: node linkType: hard -"postcss-color-functional-notation@npm:^8.0.2": - version: 8.0.2 - resolution: "postcss-color-functional-notation@npm:8.0.2" +"postcss-color-functional-notation@npm:^8.0.3": + version: 8.0.3 + resolution: "postcss-color-functional-notation@npm:8.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/62322c62db70098112f00d4827465d07407e3394471b197f938a7c47d087025021281d36a882a9631b4840bef5c3413f61dca0afcf59c5e4a0f51c53b5edc3c8 + checksum: 10c0/e7e40e4173734b19fe714ccbc907460743a46a93a59c0fb045496a070f98b5e309f2b5a5e773510af445d4ecbe8dda25dff2228106948764423e9de609d03414 languageName: node linkType: hard @@ -11419,18 +11419,18 @@ __metadata: languageName: node linkType: hard -"postcss-lab-function@npm:^8.0.2": - version: 8.0.2 - resolution: "postcss-lab-function@npm:8.0.2" +"postcss-lab-function@npm:^8.0.3": + version: 8.0.3 + resolution: "postcss-lab-function@npm:8.0.3" dependencies: - "@csstools/css-color-parser": "npm:^4.0.2" + "@csstools/css-color-parser": "npm:^4.1.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" "@csstools/css-tokenizer": "npm:^4.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/eb1410dd93492e83a9a2af91e616b75f1346a4427e130b60d3b7497d28d3e90819235eff5d8d703d4a01864f9d75b679430b59226fc65307cc14d7f7ab359501 + checksum: 10c0/f2d1246d3f92333e0ad5cbae9060146080ea65b48b9f57779bf5ed8cca73f6bc1ff35217a10688cdfcd3427c37e14b1453485b6e535de55fe5c8945dbbb2201c languageName: node linkType: hard @@ -11557,23 +11557,23 @@ __metadata: linkType: hard "postcss-preset-env@npm:^11.0.0": - version: 11.2.0 - resolution: "postcss-preset-env@npm:11.2.0" + version: 11.2.1 + resolution: "postcss-preset-env@npm:11.2.1" dependencies: - "@csstools/postcss-alpha-function": "npm:^2.0.3" + "@csstools/postcss-alpha-function": "npm:^2.0.4" "@csstools/postcss-cascade-layers": "npm:^6.0.0" - "@csstools/postcss-color-function": "npm:^5.0.2" - "@csstools/postcss-color-function-display-p3-linear": "npm:^2.0.2" - "@csstools/postcss-color-mix-function": "npm:^4.0.2" - "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^2.0.2" + "@csstools/postcss-color-function": "npm:^5.0.3" + "@csstools/postcss-color-function-display-p3-linear": "npm:^2.0.3" + "@csstools/postcss-color-mix-function": "npm:^4.0.3" + "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^2.0.3" "@csstools/postcss-content-alt-text": "npm:^3.0.0" - "@csstools/postcss-contrast-color-function": "npm:^3.0.2" - "@csstools/postcss-exponential-functions": "npm:^3.0.1" + "@csstools/postcss-contrast-color-function": "npm:^3.0.3" + "@csstools/postcss-exponential-functions": "npm:^3.0.2" "@csstools/postcss-font-format-keywords": "npm:^5.0.0" "@csstools/postcss-font-width-property": "npm:^1.0.0" - "@csstools/postcss-gamut-mapping": "npm:^3.0.2" - "@csstools/postcss-gradients-interpolation-method": "npm:^6.0.2" - "@csstools/postcss-hwb-function": "npm:^5.0.2" + "@csstools/postcss-gamut-mapping": "npm:^3.0.3" + "@csstools/postcss-gradients-interpolation-method": "npm:^6.0.3" + "@csstools/postcss-hwb-function": "npm:^5.0.3" "@csstools/postcss-ic-unit": "npm:^5.0.0" "@csstools/postcss-initial": "npm:^3.0.0" "@csstools/postcss-is-pseudo-class": "npm:^6.0.0" @@ -11583,24 +11583,24 @@ __metadata: "@csstools/postcss-logical-overscroll-behavior": "npm:^3.0.0" "@csstools/postcss-logical-resize": "npm:^4.0.0" "@csstools/postcss-logical-viewport-units": "npm:^4.0.0" - "@csstools/postcss-media-minmax": "npm:^3.0.1" + "@csstools/postcss-media-minmax": "npm:^3.0.2" "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^4.0.0" "@csstools/postcss-mixins": "npm:^1.0.0" "@csstools/postcss-nested-calc": "npm:^5.0.0" "@csstools/postcss-normalize-display-values": "npm:^5.0.1" - "@csstools/postcss-oklab-function": "npm:^5.0.2" + "@csstools/postcss-oklab-function": "npm:^5.0.3" "@csstools/postcss-position-area-property": "npm:^2.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" "@csstools/postcss-property-rule-prelude-list": "npm:^2.0.0" - "@csstools/postcss-random-function": "npm:^3.0.1" - "@csstools/postcss-relative-color-syntax": "npm:^4.0.2" + "@csstools/postcss-random-function": "npm:^3.0.2" + "@csstools/postcss-relative-color-syntax": "npm:^4.0.3" "@csstools/postcss-scope-pseudo-class": "npm:^5.0.0" - "@csstools/postcss-sign-functions": "npm:^2.0.1" - "@csstools/postcss-stepped-value-functions": "npm:^5.0.1" + "@csstools/postcss-sign-functions": "npm:^2.0.2" + "@csstools/postcss-stepped-value-functions": "npm:^5.0.2" "@csstools/postcss-syntax-descriptor-syntax-production": "npm:^2.0.0" "@csstools/postcss-system-ui-font-family": "npm:^2.0.0" "@csstools/postcss-text-decoration-shorthand": "npm:^5.0.3" - "@csstools/postcss-trigonometric-functions": "npm:^5.0.1" + "@csstools/postcss-trigonometric-functions": "npm:^5.0.2" "@csstools/postcss-unset-value": "npm:^5.0.0" autoprefixer: "npm:^10.4.24" browserslist: "npm:^4.28.1" @@ -11610,7 +11610,7 @@ __metadata: cssdb: "npm:^8.8.0" postcss-attribute-case-insensitive: "npm:^8.0.0" postcss-clamp: "npm:^4.1.0" - postcss-color-functional-notation: "npm:^8.0.2" + postcss-color-functional-notation: "npm:^8.0.3" postcss-color-hex-alpha: "npm:^11.0.0" postcss-color-rebeccapurple: "npm:^11.0.0" postcss-custom-media: "npm:^12.0.1" @@ -11623,7 +11623,7 @@ __metadata: postcss-font-variant: "npm:^5.0.0" postcss-gap-properties: "npm:^7.0.0" postcss-image-set-function: "npm:^8.0.0" - postcss-lab-function: "npm:^8.0.2" + postcss-lab-function: "npm:^8.0.3" postcss-logical: "npm:^9.0.0" postcss-nesting: "npm:^14.0.0" postcss-opacity-percentage: "npm:^3.0.0" @@ -11635,7 +11635,7 @@ __metadata: postcss-selector-not: "npm:^9.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/df4254d95efcb06825b393bb7b65ea908614e3fa8c21accfa232984123cd33ffdebadbea542f925f48a7e501a4051ed97624b7012e5b31a479055eb71af96208 + checksum: 10c0/fe1a295feac4cab319a3ed6ddb375e1ee5543683d8121eb20959d325f6b1fdd40d4e38c1719e3c76f56579c48a2f45910b4a20b4519c09d5690e46a5b95c6392 languageName: node linkType: hard