Merge commit 'a706fce6781104b3aa1315be14f1fb8ccd4fcb1b' into glitch-soc/merge-upstream
This commit is contained in:
@@ -26,7 +26,6 @@
|
||||
z-index: 9999;
|
||||
box-sizing: border-box;
|
||||
max-height: min(320px, 50dvh);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-primary);
|
||||
@@ -38,22 +37,18 @@
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
.groupLabel {
|
||||
padding-block: 12px 4px;
|
||||
padding-inline: 12px;
|
||||
.groupTitle {
|
||||
padding: 8px 16px 4px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
|
||||
ul:first-child > & {
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-inline: 4px;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
@@ -62,6 +57,14 @@
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&[data-highlighted='true'] {
|
||||
background: var(--color-bg-overlay-highlight);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { ComboboxField } from './combobox_field';
|
||||
import { ComboboxField, ComboboxMenuItem } from './combobox_field';
|
||||
|
||||
interface Fruit {
|
||||
id: string;
|
||||
@@ -60,7 +60,7 @@ const ComboboxDemo: React.FC<{ withGroups?: boolean }> = ({ withGroups }) => {
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback(
|
||||
(fruit: Fruit) => <span>{fruit.name}</span>,
|
||||
(fruit: Fruit) => <ComboboxMenuItem>{fruit.name}</ComboboxMenuItem>,
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -93,6 +93,7 @@ const ComboboxDemo: React.FC<{ withGroups?: boolean }> = ({ withGroups }) => {
|
||||
const meta = {
|
||||
title: 'Components/Form Fields/ComboboxField',
|
||||
component: ComboboxField,
|
||||
subcomponents: { ComboboxMenuItem },
|
||||
render: () => <ComboboxDemo />,
|
||||
} satisfies Meta<typeof ComboboxField>;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
createContext,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useContext,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -109,6 +111,58 @@ interface ComboboxProps<
|
||||
interface Props<Item extends ComboboxItem, GroupKey extends string>
|
||||
extends ComboboxProps<Item, GroupKey>, CommonFieldWrapperProps {}
|
||||
|
||||
interface ComboboxItemPropsContext {
|
||||
role: 'option';
|
||||
'data-highlighted': boolean;
|
||||
'aria-selected': boolean;
|
||||
'aria-disabled': boolean;
|
||||
'data-item-id': string;
|
||||
onMouseEnter: React.MouseEventHandler<HTMLLIElement>;
|
||||
onClick: React.MouseEventHandler<HTMLLIElement>;
|
||||
}
|
||||
|
||||
const ComboboxItemPropsContext = createContext<ComboboxItemPropsContext | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function useComboboxItemProps() {
|
||||
const context = useContext(ComboboxItemPropsContext);
|
||||
|
||||
if (context === null) {
|
||||
throw new Error(
|
||||
'useComboboxItemProps must be used within a Combobox component',
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export const ComboboxMenuItem: React.FC<{
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ className, children }) => {
|
||||
const props = useComboboxItemProps();
|
||||
return (
|
||||
<li className={classNames(className, classes.menuItem)} {...props}>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComboboxMenuGroupTitle: React.FC<
|
||||
React.ComponentPropsWithoutRef<'li'>
|
||||
> = ({ className, children, ...otherProps }) => {
|
||||
return (
|
||||
<li
|
||||
{...otherProps}
|
||||
role='presentation'
|
||||
className={classNames(className, classes.groupTitle)}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* The combobox field allows users to select one or more items
|
||||
* by searching or filtering a large or dynamic list of options.
|
||||
@@ -371,7 +425,7 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
||||
const renderItems = (items: Item[]) =>
|
||||
items.map((item) => {
|
||||
const id = getItemId(item);
|
||||
const isDisabled = getIsItemDisabled?.(item);
|
||||
const isDisabled = getIsItemDisabled?.(item) ?? false;
|
||||
const isHighlighted = id === highlightedItemId;
|
||||
// If `getIsItemSelected` is defined, we assume 'multi-select'
|
||||
// behaviour and don't set `aria-selected` based on highlight,
|
||||
@@ -380,23 +434,23 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
||||
? getIsItemSelected(item)
|
||||
: isHighlighted;
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<li
|
||||
<ComboboxItemPropsContext.Provider
|
||||
key={id}
|
||||
role='option'
|
||||
className={classes.menuItem}
|
||||
data-highlighted={isHighlighted}
|
||||
aria-selected={isSelected}
|
||||
aria-disabled={isDisabled}
|
||||
data-item-id={id}
|
||||
onMouseEnter={handleItemMouseEnter}
|
||||
onClick={handleSelectItem}
|
||||
value={{
|
||||
role: 'option',
|
||||
'data-highlighted': isHighlighted,
|
||||
'aria-selected': isSelected,
|
||||
'aria-disabled': isDisabled,
|
||||
'data-item-id': id,
|
||||
onMouseEnter: handleItemMouseEnter,
|
||||
onClick: handleSelectItem,
|
||||
}}
|
||||
>
|
||||
{renderItem(item, {
|
||||
isSelected,
|
||||
isDisabled: isDisabled ?? false,
|
||||
isDisabled,
|
||||
})}
|
||||
</li>
|
||||
</ComboboxItemPropsContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -498,16 +552,12 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
||||
role='group'
|
||||
aria-labelledby={hasTitle ? groupTitleId : undefined}
|
||||
>
|
||||
{hasTitle && (
|
||||
<li
|
||||
role='presentation'
|
||||
className={classes.groupLabel}
|
||||
>
|
||||
{customGroupTitle ?? (
|
||||
<span id={groupTitleId}>{groupKey}</span>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
{hasTitle &&
|
||||
(customGroupTitle ?? (
|
||||
<ComboboxMenuGroupTitle id={groupTitleId}>
|
||||
{groupKey}
|
||||
</ComboboxMenuGroupTitle>
|
||||
))}
|
||||
{renderItems(groupItems)}
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useId, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Combobox } from '@/mastodon/components/form_fields';
|
||||
import { ComboboxMenuItem } from '@/mastodon/components/form_fields/combobox_field';
|
||||
import { useSearchTags } from '@/mastodon/hooks/useSearchTags';
|
||||
import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags';
|
||||
import { addFeaturedTags } from '@/mastodon/reducers/slices/profile_edit';
|
||||
@@ -77,4 +78,6 @@ export const AccountEditTagSearch: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderItem = (item: TagSearchResult) => item.label ?? `#${item.name}`;
|
||||
const renderItem = (item: TagSearchResult) => (
|
||||
<ComboboxMenuItem>{item.label ?? `#${item.name}`}</ComboboxMenuItem>
|
||||
);
|
||||
|
||||
@@ -65,16 +65,23 @@ export const ImageUploadModal: FC<
|
||||
|
||||
const handleFile = useCallback((file: File) => {
|
||||
try {
|
||||
parseImageFile(file, (result, isAnimated) => {
|
||||
if (isAnimated) {
|
||||
// If the image is animated, skip cropping and go straight to alt text.
|
||||
setImageBlob(file);
|
||||
setStep('alt');
|
||||
} else {
|
||||
setImageSrc(result);
|
||||
setStep('crop');
|
||||
// If the image is animated, skip cropping and go straight to alt text.
|
||||
if (file.type === 'image/gif') {
|
||||
setImageBlob(file);
|
||||
setStep('alt');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const dataUri = reader.result;
|
||||
if (typeof dataUri !== 'string') {
|
||||
throw new Error('Expected a string');
|
||||
}
|
||||
});
|
||||
setImageSrc(dataUri);
|
||||
setStep('crop');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (error) {
|
||||
console.warn('Error with image parsing:', error);
|
||||
setStep('select');
|
||||
@@ -408,54 +415,6 @@ const StepAlt: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses an image file and determines if it's an animated GIF and returns a data URI for cropping.
|
||||
* Based on https://gist.github.com/zakirt/faa4a58cec5a7505b10e3686a226f285.
|
||||
*/
|
||||
function parseImageFile(
|
||||
file: File,
|
||||
cb: (buffer: string, isAnimated: boolean) => void,
|
||||
): void {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const buffer = reader.result;
|
||||
if (!(buffer instanceof ArrayBuffer)) {
|
||||
throw new Error('Expected an ArrayBuffer');
|
||||
}
|
||||
|
||||
// Convert the ArrayBuffer to a base64 data URI.
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const base64 = btoa(String.fromCharCode(...bytes));
|
||||
const dataUri = `data:${file.type};base64,${base64}`;
|
||||
|
||||
// If the file type is not a GIF, then it's not animated as we don't support animated WebP or PNG.
|
||||
if (file.type !== 'image/gif') {
|
||||
cb(dataUri, false);
|
||||
}
|
||||
|
||||
const view = new DataView(buffer, 10); // Start from the last 4 bytes of the Logical Screen Descriptor.
|
||||
let offset = 3;
|
||||
|
||||
// Check the first bit for the global color table flag.
|
||||
const globalColorTable = view.getInt8(0);
|
||||
if (globalColorTable & 0x08) {
|
||||
// Grab last three bits to calculate the global color table size, and skip it.
|
||||
offset += 3 * Math.pow(2, (globalColorTable & 0x7) + 1);
|
||||
}
|
||||
|
||||
// Check Graphics Control Extension and Graphics Control Label to access animated data.
|
||||
let delayTime = 0;
|
||||
if (view.getUint8(offset) & 0x21 && view.getUint8(offset + 1) & 0xf9) {
|
||||
// Skip to the delay time, which is stored in the next two bytes.
|
||||
delayTime = view.getUint16(offset + 4);
|
||||
}
|
||||
|
||||
// If there is a delay time, the GIF is animated.
|
||||
cb(dataUri, delayTime > 0);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
async function calculateCroppedImage(
|
||||
imageSrc: string,
|
||||
crop: Area,
|
||||
|
||||
@@ -4,6 +4,9 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { useComboboxItemProps } from '@/mastodon/components/form_fields/combobox_field';
|
||||
import type { ApiMutedAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { AccountListItem } from 'mastodon/components/account_list_item';
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
import type { Relationship } from 'mastodon/models/relationship';
|
||||
import {
|
||||
addCollectionItem,
|
||||
getCollectionItemIds,
|
||||
@@ -64,18 +68,18 @@ const AddedAccountItem: React.FC<{
|
||||
const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => {
|
||||
const account = useAccount(id);
|
||||
const handle = useAccountHandle(account, domain);
|
||||
const comboboxItemProps = useComboboxItemProps();
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<ListItemWrapper
|
||||
className={classes.suggestion}
|
||||
icon={<Avatar account={account} size={40} />}
|
||||
>
|
||||
<ListItemContent subtitle={handle}>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
<li {...comboboxItemProps} className={classes.suggestion}>
|
||||
<ListItemWrapper icon={<Avatar account={account} size={40} />}>
|
||||
<ListItemContent subtitle={handle}>
|
||||
<DisplayName account={account} variant='simple' />
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -85,19 +89,37 @@ const renderAccountItem = (account: ApiMutedAccountJSON) => (
|
||||
|
||||
type GroupKey = 'available' | 'mustFollow' | 'disabled';
|
||||
|
||||
function groupSuggestions(accounts: ApiMutedAccountJSON[]) {
|
||||
const { available, disabled } = Object.groupBy(accounts, (account) => {
|
||||
if (getIsItemDisabled(account)) {
|
||||
const canAccountBeAdded = (account: ApiMutedAccountJSON) =>
|
||||
['automatic', 'manual'].includes(account.feature_approval.current_user);
|
||||
|
||||
function groupSuggestions(
|
||||
accounts: ApiMutedAccountJSON[],
|
||||
relationships: ImmutableMap<string, Relationship>,
|
||||
) {
|
||||
const { available, mustFollow, disabled } = Object.groupBy(
|
||||
accounts,
|
||||
(account): GroupKey => {
|
||||
if (canAccountBeAdded(account)) {
|
||||
return 'available';
|
||||
}
|
||||
|
||||
const canAccountBeAddedByFollowers =
|
||||
account.feature_approval.automatic.includes('followers') ||
|
||||
account.feature_approval.manual.includes('followers');
|
||||
|
||||
if (
|
||||
canAccountBeAddedByFollowers &&
|
||||
!relationships.get(account.id)?.following
|
||||
) {
|
||||
return 'mustFollow';
|
||||
}
|
||||
|
||||
return 'disabled';
|
||||
}
|
||||
// if (account.locked && !relationship?.following) {
|
||||
// return 'mustFollow';
|
||||
// }
|
||||
return 'available';
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Returning a new object ensures a fixed property order
|
||||
return { available, disabled };
|
||||
return { available, mustFollow, disabled };
|
||||
}
|
||||
|
||||
const renderGroupTitle = (groupKey: GroupKey, titleId: string) => {
|
||||
@@ -137,19 +159,19 @@ const renderGroupTitle = (groupKey: GroupKey, titleId: string) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItemWrapper className={classes.suggestionGroup}>
|
||||
<ListItemContent id={titleId} subtitle={description}>
|
||||
{title}
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
<li role='presentation'>
|
||||
<ListItemWrapper className={classes.suggestionGroup}>
|
||||
<ListItemContent id={titleId} subtitle={description}>
|
||||
{title}
|
||||
</ListItemContent>
|
||||
</ListItemWrapper>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
!canAccountBeAdded(account);
|
||||
|
||||
export const CollectionAccounts: React.FC<{
|
||||
collection?: ApiCollectionJSON | null;
|
||||
@@ -189,6 +211,10 @@ export const CollectionAccounts: React.FC<{
|
||||
filterResults: (account) => !accountIds.includes(account.id),
|
||||
});
|
||||
|
||||
const relationships = useAppSelector((state) => state.relationships);
|
||||
|
||||
const groupedItems = groupSuggestions(suggestedAccounts, relationships);
|
||||
|
||||
const handleSearchValueChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.target.value);
|
||||
@@ -330,7 +356,7 @@ export const CollectionAccounts: React.FC<{
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
disabled={hasMaxAccounts}
|
||||
isLoading={isLoadingSuggestions}
|
||||
items={groupSuggestions(suggestedAccounts)}
|
||||
items={groupedItems}
|
||||
getItemId={getItemId}
|
||||
getIsItemDisabled={getIsItemDisabled}
|
||||
renderItem={renderAccountItem}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import { ComboboxMenuItem } from '@/mastodon/components/form_fields/combobox_field';
|
||||
import { languages } from '@/mastodon/initial_state';
|
||||
import {
|
||||
hasSpecialCharacters,
|
||||
@@ -373,7 +374,9 @@ const TopicField: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderTagItem = (item: TagSearchResult) => item.label ?? `#${item.name}`;
|
||||
const renderTagItem = (item: TagSearchResult) => (
|
||||
<ComboboxMenuItem>{item.label ?? `#${item.name}`}</ComboboxMenuItem>
|
||||
);
|
||||
|
||||
const LanguageField: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -60,17 +60,20 @@
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
|
||||
[aria-disabled='true'] & {
|
||||
&[data-highlighted='true'] {
|
||||
background: var(--color-bg-overlay-highlight);
|
||||
}
|
||||
|
||||
&[aria-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestionGroup {
|
||||
padding: 4px 0;
|
||||
|
||||
// Undo default group styles:
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -411,7 +411,9 @@
|
||||
"collections.sensitive": "Inhaltswarnung",
|
||||
"collections.share_short": "Teilen",
|
||||
"collections.suggestions.can_not_add": "Kann nicht hinzugefügt werden",
|
||||
"collections.suggestions.can_not_add_desc": "Diese Konten möchten möglicherweise nicht entdeckt werden oder deren Server unterstützt noch keine Sammlungen.",
|
||||
"collections.suggestions.must_follow": "Zuerst Profil folgen",
|
||||
"collections.suggestions.must_follow_desc": "Diese Konten überprüfen ihre Follower-Anfragen manuell. Follower können sie zu Sammlungen hinzufügen.",
|
||||
"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.",
|
||||
|
||||
@@ -666,7 +666,7 @@
|
||||
"featured_carousel.header": "{count, plural, one {Καρφιτσωμένη Ανάρτηση} other {Καρφιτσωμένες Αναρτήσεις}}",
|
||||
"featured_carousel.slide": "Ανάρτηση {current, number} από {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Τον τελευταίο καιρό έχετε δημοσιεύσει για {items}. Προσθέστε αυτά ως αναδεδειγμένες ετικέτες;",
|
||||
"featured_tags.suggestions": "Τον τελευταίο καιρό έχεις δημοσιεύσει για {items}. Να προστεθούν αυτά ως αναδεδειγμένες ετικέτες;",
|
||||
"featured_tags.suggestions.add": "Προσθήκη",
|
||||
"featured_tags.suggestions.added": "Διαχειριστείτε τις αναδεδειγμένες ετικέτες σας οποιαδήποτε στιγμή κάτω από το <link>Επεξεργασία προφίλ > Αναδεδειγμένες ετικέτες</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Όχι, ευχαριστώ",
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"account.name.copy": "Copy handle",
|
||||
"account.name.help.domain": "{domain} is the server that hosts the user’s profile and posts.",
|
||||
"account.name.help.domain_self": "{domain} is your server that hosts your profile and posts.",
|
||||
"account.name.help.footer": "Just like you can send emails to people using different email providers, you can interact with people on other Mastodon servers, and with anyone on other Mastodon-compatible social apps.",
|
||||
"account.name.help.header": "A handle is like an email address",
|
||||
"account.name.help.username": "{username} is this account’s username on their server. Someone on another server might have the same username.",
|
||||
"account.name.help.username_self": "{username} is your username on this server. Someone on another server might have the same username.",
|
||||
@@ -350,6 +351,7 @@
|
||||
"closed_registrations_modal.find_another_server": "Find another server",
|
||||
"closed_registrations_modal.preamble": "Mastodon is decentralised, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
|
||||
"closed_registrations_modal.title": "Signing up on Mastodon",
|
||||
"collection.share_modal.share_link_label": "Share link",
|
||||
"collection.share_modal.share_via_post": "Post on Mastodon",
|
||||
"collection.share_modal.share_via_system": "Share to…",
|
||||
"collection.share_modal.title": "Share collection",
|
||||
@@ -357,7 +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",
|
||||
"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": "Delete",
|
||||
"collections.collection_description": "Description",
|
||||
"collections.collection_language": "Language",
|
||||
@@ -367,6 +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.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}",
|
||||
@@ -384,6 +391,9 @@
|
||||
"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.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.last_updated_at": "Last updated: {date}",
|
||||
"collections.manage_accounts": "Manage accounts",
|
||||
"collections.mark_as_sensitive": "Mark as sensitive",
|
||||
@@ -391,14 +401,23 @@
|
||||
"collections.name_length_hint": "40 characters limit",
|
||||
"collections.new_collection": "New collection",
|
||||
"collections.no_collections_yet": "No collections yet.",
|
||||
"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 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.visibility_public": "Public",
|
||||
"collections.visibility_public_hint": "Discoverable in search results and other areas where recommendations appear.",
|
||||
@@ -891,6 +910,7 @@
|
||||
"navigation_panel.expand_followed_tags": "Expand followed hashtags menu",
|
||||
"navigation_panel.expand_lists": "Expand list menu",
|
||||
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
|
||||
"notification.added_to_collection": "{name} added you to a collection",
|
||||
"notification.admin.report": "{name} reported {target}",
|
||||
"notification.admin.report_account": "{name} reported {count, plural, one {one post} other {# posts}} from {target} for {category}",
|
||||
"notification.admin.report_account_other": "{name} reported {count, plural, one {one post} other {# posts}} from {target}",
|
||||
@@ -900,6 +920,7 @@
|
||||
"notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up",
|
||||
"notification.annual_report.message": "Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!",
|
||||
"notification.annual_report.view": "View #Wrapstodon",
|
||||
"notification.collection_update": "{name} edited a collection you’re in",
|
||||
"notification.favourite": "{name} favourited your post",
|
||||
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favourited your post",
|
||||
"notification.favourite_pm": "{name} favourited your private mention",
|
||||
|
||||
@@ -141,8 +141,8 @@
|
||||
"account.unmute_notifications_short": "Lõpeta teavituste summutamine",
|
||||
"account.unmute_short": "Lõpeta summutamine",
|
||||
"account_edit.advanced_settings.bot_hint": "Teavita teisi, et sel kontol tehakse peamiselt automatiseeritud toiminguid ja seda ei pruugita jälgida",
|
||||
"account_edit.advanced_settings.bot_label": "Automatiseeritud konto",
|
||||
"account_edit.advanced_settings.title": "Täpsemad seaded",
|
||||
"account_edit.advanced_settings.bot_label": "Automatiseeritud kasutajakonto",
|
||||
"account_edit.advanced_settings.title": "Täpsemad seadistused",
|
||||
"account_edit.bio.add_label": "Lisa elulugu",
|
||||
"account_edit.bio.edit_label": "Muuda elulugu",
|
||||
"account_edit.bio.placeholder": "Lisa lühike tutvustus, et aidata teistel sinust pilti luua.",
|
||||
@@ -158,7 +158,7 @@
|
||||
"account_edit.custom_fields.tip_content": "Saad Mastodoni konto usaldusväärsust hõlpsasti suurendada, kinnitades lingid lehtedele, mis sulle kuuluvad.",
|
||||
"account_edit.custom_fields.tip_title": "Soovitus: Kinnitatud linkide lisamine",
|
||||
"account_edit.custom_fields.title": "Kohandatud väljad",
|
||||
"account_edit.custom_fields.verified_hint": "Kuidas lisada kinnitatud link?",
|
||||
"account_edit.custom_fields.verified_hint": "Kuidas lisada kinnitatud linki?",
|
||||
"account_edit.display_name.add_label": "Lisa kuvatav nimi",
|
||||
"account_edit.display_name.edit_label": "Muuda kuvatavat nime",
|
||||
"account_edit.display_name.placeholder": "Kuvatav nimi määrab, kuidas sinu nimi on näha sinu profiilis ja ajajoontel.",
|
||||
@@ -170,7 +170,7 @@
|
||||
"account_edit.field_actions.edit": "Muuda välja",
|
||||
"account_edit.field_delete_modal.confirm": "Kas oled kindel, et soovid selle kohandatud välja kustutada? Seda toimingut ei saa tagasi võtta.",
|
||||
"account_edit.field_delete_modal.delete_button": "Kustuta",
|
||||
"account_edit.field_delete_modal.title": "Kustutada kohandatud väli?",
|
||||
"account_edit.field_delete_modal.title": "Kas kustutame kohandatud välja?",
|
||||
"account_edit.field_edit_modal.add_title": "Lisa kohandatud väli",
|
||||
"account_edit.field_edit_modal.discard_confirm": "Loobu",
|
||||
"account_edit.field_edit_modal.discard_message": "Sul on salvestamata muudatusi. Kas soovid tõesti neist loobuda?",
|
||||
@@ -211,14 +211,14 @@
|
||||
"account_edit.profile_tab.button_label": "Kohanda",
|
||||
"account_edit.profile_tab.hint.description": "Need seaded määravad, mida kasutajad näevad {server} ametlikes rakendustes, kuid ei pruugi kehtida teiste serverite kasutajate ja kolmandate osapoolte rakenduste puhul.",
|
||||
"account_edit.profile_tab.hint.title": "Näitamine siiski erinev",
|
||||
"account_edit.profile_tab.show_featured.description": "\"Esile tõstetud\" on valikuline taab, kus saad näidata teisi kontosid.",
|
||||
"account_edit.profile_tab.show_featured.title": "Näita esiletõstetute taabi",
|
||||
"account_edit.profile_tab.show_media.description": "\"Meedia\" on valikuline taab, mis näitab sinu piltide või videotega postitusi.",
|
||||
"account_edit.profile_tab.show_media.title": "Näita meedia taabi",
|
||||
"account_edit.profile_tab.show_media_replies.description": "Sisse lülitatuna näitab meedia taab mõlemat, nii sinu postitusi kui su vastuseid teiste postitustele.",
|
||||
"account_edit.profile_tab.show_media_replies.title": "Kaasa meedia taabile ka vastused",
|
||||
"account_edit.profile_tab.show_featured.description": "„Esiletõstetud“ on valikuline vahekaart, kus saad näidata teisi kontosid.",
|
||||
"account_edit.profile_tab.show_featured.title": "Näita vahekaarti „Esiletõstetud“",
|
||||
"account_edit.profile_tab.show_media.description": "„Meedium“ on valikuline vahekaart, mis näitab sinu piltide või videotega postitusi.",
|
||||
"account_edit.profile_tab.show_media.title": "Näita multimeedia vahekaarti",
|
||||
"account_edit.profile_tab.show_media_replies.description": "Sisselülitatuna näitab mediumi vahekaart, nii sinu postitusi kui su vastuseid teiste kasutajate postitustele.",
|
||||
"account_edit.profile_tab.show_media_replies.title": "Kaasa vahekaardile „Meedium“ ka vastused",
|
||||
"account_edit.profile_tab.subtitle": "Kohanda oma profiili vahekaarte ja nende sisu.",
|
||||
"account_edit.profile_tab.title": "Profiili taabide seaded",
|
||||
"account_edit.profile_tab.title": "Profiili vahekaartide seadistused",
|
||||
"account_edit.save": "Salvesta",
|
||||
"account_edit.upload_modal.back": "Tagasi",
|
||||
"account_edit.upload_modal.done": "Valmis",
|
||||
@@ -511,10 +511,10 @@
|
||||
"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?",
|
||||
"confirmations.hide_featured_tab.confirm": "Peida taab",
|
||||
"confirmations.hide_featured_tab.confirm": "Peida vahekaart",
|
||||
"confirmations.hide_featured_tab.intro": "Saad seda igal ajal muuta menüüs <i>Profiili muutmine > Profiili vahekaardi seaded</i>.",
|
||||
"confirmations.hide_featured_tab.message": "See peidab vahekaardi serveri {serverName} kasutajatel ja teistel serveritel, kus kasutatakse Mastodoni uusimat versiooni. Muudel serveritel võib kuvamine erineda.",
|
||||
"confirmations.hide_featured_tab.title": "Peita taab Esiletõstetud?",
|
||||
"confirmations.hide_featured_tab.title": "Kas peidame vahekaardi „Esiletõstetud“?",
|
||||
"confirmations.logout.confirm": "Välju",
|
||||
"confirmations.logout.message": "Kas oled kindel, et soovid välja logida?",
|
||||
"confirmations.logout.title": "Logida välja?",
|
||||
@@ -621,7 +621,7 @@
|
||||
"emoji_button.travel": "Reisimine & kohad",
|
||||
"empty_column.account_featured.other": "{acct} pole veel midagi esile tõstnud.",
|
||||
"empty_column.account_featured_self.no_collections_button": "Loo kogumik",
|
||||
"empty_column.account_featured_self.no_collections_hide_tab": "Selle asemel peida see taab",
|
||||
"empty_column.account_featured_self.no_collections_hide_tab": "Selle asemel peida see vahekaart",
|
||||
"empty_column.account_featured_self.pre_collections": "Jälgi kogumikke",
|
||||
"empty_column.account_featured_self.pre_collections_desc": "Kogumikud (lisanduvad Mastodon V4.6) võimaldavad sul luua enda koostatud kontode nimekirju, mida teistele soovitada.",
|
||||
"empty_column.account_featured_self.showcase_accounts": "Näita oma lemmikkontosid",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"account.media": "Multivide",
|
||||
"account.mention": "Pieminēt @{name}",
|
||||
"account.menu.block": "Bloķēt kontu",
|
||||
"account.menu.copy": "Kopēt saiti",
|
||||
"account.moved_to": "{name} norādīja, ka viņu jaunais konts tagad ir:",
|
||||
"account.mute": "Apklusināt @{name}",
|
||||
"account.mute_notifications_short": "Apklusināt paziņojumus",
|
||||
@@ -71,8 +72,10 @@
|
||||
"account.muted": "Apklusināts",
|
||||
"account.muting": "Apklusināts",
|
||||
"account.mutual": "Jūs sekojat viens otram",
|
||||
"account.name_info": "Ko tas nozīmē?",
|
||||
"account.no_bio": "Apraksts nav sniegts.",
|
||||
"account.node_modal.save": "Saglabāt",
|
||||
"account.note.edit_button": "Labot",
|
||||
"account.open_original_page": "Atvērt pirmavota lapu",
|
||||
"account.posts": "Ieraksti",
|
||||
"account.remove_from_followers": "Dzēst sekotāju {name}",
|
||||
@@ -91,6 +94,7 @@
|
||||
"account.unmute": "Noņemt apklusinājumu @{name}",
|
||||
"account.unmute_notifications_short": "Atcelet paziņojumu apklusināšanu",
|
||||
"account.unmute_short": "Noņemt apklusinājumu",
|
||||
"account_edit.bio.edit_label": "Labot biogrāfiju",
|
||||
"account_edit.bio.title": "Biogrāfija",
|
||||
"account_edit.bio_modal.add_title": "Pievienot biogrāfiju",
|
||||
"account_edit.bio_modal.edit_title": "Labot biogrāfiju",
|
||||
@@ -98,8 +102,23 @@
|
||||
"account_edit.column_title": "Labot profilu",
|
||||
"account_edit.custom_fields.add_label": "Pievienot lauku",
|
||||
"account_edit.custom_fields.edit_label": "Labot lauku",
|
||||
"account_edit.field_actions.delete": "Dzēst lauku",
|
||||
"account_edit.field_actions.edit": "Labot lauku",
|
||||
"account_edit.field_delete_modal.delete_button": "Dzēst",
|
||||
"account_edit.field_edit_modal.discard_confirm": "Atmest",
|
||||
"account_edit.image_delete_modal.delete_button": "Dzēst",
|
||||
"account_edit.image_delete_modal.title": "Dzēst attēlu?",
|
||||
"account_edit.image_edit.add_button": "Pievienot attēlu",
|
||||
"account_edit.image_edit.remove_button": "Noņemt attēlu",
|
||||
"account_edit.save": "Saglabāt",
|
||||
"account_edit.upload_modal.back": "Atpakaļ",
|
||||
"account_edit.upload_modal.done": "Gatavs",
|
||||
"account_edit.upload_modal.step_upload.button": "Pārlūkot failus",
|
||||
"account_edit.upload_modal.title_add.avatar": "Pievienot profila fotoattēlu",
|
||||
"account_edit.upload_modal.title_add.header": "Pievienot titullapas fotoattēlu",
|
||||
"account_edit.upload_modal.title_replace.avatar": "Nomainīt profila fotoattēlu",
|
||||
"account_edit.upload_modal.title_replace.header": "Nomainīt titullapas fotoattēlu",
|
||||
"account_edit_tags.suggestions": "Ieteikumi:",
|
||||
"account_note.placeholder": "Noklikšķini, lai pievienotu piezīmi",
|
||||
"admin.dashboard.daily_retention": "Lietotāju saglabāšanas rādītājs dienā pēc reģistrēšanās",
|
||||
"admin.dashboard.monthly_retention": "Lietotāju saglabāšanas rādītājs mēnesī pēc reģistrēšanās",
|
||||
@@ -123,6 +142,11 @@
|
||||
"alt_text_modal.describe_for_people_with_visual_impairments": "Aprakstīt šo cilvēkiem ar redzes traucējumiem…",
|
||||
"alt_text_modal.done": "Gatavs",
|
||||
"announcement.announcement": "Paziņojums",
|
||||
"annual_report.shared_page.donate": "Ziedot",
|
||||
"annual_report.summary.archetype.title_public": "Lietotāja \"{name}\" arhetips",
|
||||
"annual_report.summary.archetype.title_self": "Tavs arhetips",
|
||||
"annual_report.summary.close": "Aizvērt",
|
||||
"annual_report.summary.copy_link": "Kopēt saiti",
|
||||
"annual_report.summary.most_used_app.most_used_app": "visizmantotākā lietotne",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "visizmantotākais tēmturis",
|
||||
"annual_report.summary.new_posts.new_posts": "jauni ieraksti",
|
||||
@@ -158,6 +182,19 @@
|
||||
"closed_registrations_modal.find_another_server": "Atrast citu serveri",
|
||||
"closed_registrations_modal.preamble": "Mastodon ir decentralizēts, tāpēc neatkarīgi no tā, kur Tu izveido savu kontu, varēsi sekot un mijiedarboties ar ikvienu šajā serverī. Tu pat vari to pašizvietot!",
|
||||
"closed_registrations_modal.title": "Reģistrēšanās Mastodon",
|
||||
"collections.accounts.empty_title": "Šī kolekcija ir tukša",
|
||||
"collections.block_collection_owner": "Bloķēt kontu",
|
||||
"collections.collection_description": "Apraksts",
|
||||
"collections.collection_language": "Valoda",
|
||||
"collections.collection_language_none": "Nav",
|
||||
"collections.collection_name": "Nosaukums",
|
||||
"collections.collection_topic": "Tēma",
|
||||
"collections.continue": "Turpināt",
|
||||
"collections.copy_link": "Kopēt saiti",
|
||||
"collections.manage_accounts": "Pārvaldīt kontus",
|
||||
"collections.remove_account": "Noņemt",
|
||||
"collections.suggestions.can_not_add": "Nevar pievienot",
|
||||
"collections.visibility_title": "Redzamība",
|
||||
"column.about": "Par",
|
||||
"column.blocks": "Liegtie lietotāji",
|
||||
"column.bookmarks": "Grāmatzīmes",
|
||||
@@ -185,6 +222,8 @@
|
||||
"column_header.show_settings": "Rādīt iestatījumus",
|
||||
"column_header.unpin": "Atspraust",
|
||||
"column_search.cancel": "Atcelt",
|
||||
"combobox.loading": "Ielādē",
|
||||
"combobox.open_results": "Atvērt rezultātus",
|
||||
"community.column_settings.local_only": "Tikai vietējie",
|
||||
"community.column_settings.media_only": "Tikai multivide",
|
||||
"community.column_settings.remote_only": "Tikai attālinātie",
|
||||
@@ -218,6 +257,7 @@
|
||||
"confirmations.delete.confirm": "Dzēst",
|
||||
"confirmations.delete.message": "Vai tiešām izdzēst šo ierakstu?",
|
||||
"confirmations.delete.title": "Izdzēst ierakstu?",
|
||||
"confirmations.delete_collection.confirm": "Dzēst",
|
||||
"confirmations.delete_list.confirm": "Dzēst",
|
||||
"confirmations.delete_list.message": "Vai tiešām neatgriezeniski izdzēst šo sarakstu?",
|
||||
"confirmations.delete_list.title": "Izdzēst sarakstu?",
|
||||
|
||||
Reference in New Issue
Block a user