Merge pull request #3272 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to 1c3e7545cb6137025a6efd208d195352c54ffda8
This commit is contained in:
Claire 2025-11-11 12:07:29 +01:00 committed by GitHub
commit 2759bafe09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
147 changed files with 1154 additions and 970 deletions

View File

@ -47,7 +47,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
rootClose
onHide={handleClose}
show={open}
target={anchorRef.current}
target={anchorRef}
placement='top-end'
flip
offset={offset}

View File

@ -76,6 +76,11 @@ export const Carousel = <
// Handle slide change
const [slideIndex, setSlideIndex] = useState(0);
const wrapperRef = useRef<HTMLDivElement>(null);
// Handle slide heights
const [currentSlideHeight, setCurrentSlideHeight] = useState(
() => wrapperRef.current?.scrollHeight ?? 0,
);
const previousSlideHeight = usePrevious(currentSlideHeight);
const handleSlideChange = useCallback(
(direction: number) => {
setSlideIndex((prev) => {
@ -101,16 +106,11 @@ export const Carousel = <
[items.length, onChangeSlide],
);
// Handle slide heights
const [currentSlideHeight, setCurrentSlideHeight] = useState(
wrapperRef.current?.scrollHeight ?? 0,
);
const previousSlideHeight = usePrevious(currentSlideHeight);
const observerRef = useRef<ResizeObserver>(
new ResizeObserver(() => {
handleSlideChange(0);
}),
);
const observerRef = useRef<ResizeObserver | null>(null);
observerRef.current ??= new ResizeObserver(() => {
handleSlideChange(0);
});
const wrapperStyles = useSpring({
x: `-${slideIndex * 100}%`,
height: currentSlideHeight,
@ -200,7 +200,7 @@ export const Carousel = <
};
type CarouselSlideWrapperProps<SlideProps extends CarouselSlideProps> = {
observer: ResizeObserver;
observer: ResizeObserver | null;
className: string;
active: boolean;
item: SlideProps;
@ -217,7 +217,7 @@ const CarouselSlideWrapper = <SlideProps extends CarouselSlideProps>({
}: CarouselSlideWrapperProps<SlideProps>) => {
const handleRef = useCallback(
(instance: HTMLDivElement | null) => {
if (instance) {
if (observer && instance) {
observer.observe(instance);
}
},

View File

@ -1,4 +1,4 @@
import { useCallback, useState, useEffect, useRef } from 'react';
import { useCallback, useState, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
@ -12,11 +12,15 @@ export const ColumnSearchHeader: React.FC<{
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState('');
useEffect(() => {
// Reset the component when it turns from active to inactive.
// [More on this pattern](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
const [previousActive, setPreviousActive] = useState(active);
if (active !== previousActive) {
setPreviousActive(active);
if (!active) {
setValue('');
}
}, [active]);
}
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -109,7 +109,7 @@ export const Dropdown: FC<
placement='bottom-start'
onHide={handleClose}
flip
target={buttonRef.current}
target={buttonRef}
popperConfig={{
strategy: 'fixed',
modifiers: [matchWidth],

View File

@ -42,16 +42,10 @@ import { IconButton } from './icon_button';
let id = 0;
export interface RenderItemFnHandlers {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
}
export type RenderItemFn<Item = MenuItem> = (
item: Item,
index: number,
handlers: RenderItemFnHandlers,
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void,
onClick: React.MouseEventHandler,
) => React.ReactNode;
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
@ -101,7 +95,6 @@ export const DropdownMenu = <Item = MenuItem,>({
onItemClick,
}: DropdownMenuProps<Item>) => {
const nodeRef = useRef<HTMLDivElement>(null);
const focusedItemRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const handleDocumentClick = (e: MouseEvent) => {
@ -163,8 +156,11 @@ export const DropdownMenu = <Item = MenuItem,>({
document.addEventListener('click', handleDocumentClick, { capture: true });
document.addEventListener('keydown', handleKeyDown, { capture: true });
if (focusedItemRef.current && openedViaKeyboard) {
focusedItemRef.current.focus({ preventScroll: true });
if (openedViaKeyboard) {
const firstMenuItem = nodeRef.current?.querySelector<
HTMLAnchorElement | HTMLButtonElement
>('li:first-child > :is(a, button)');
firstMenuItem?.focus({ preventScroll: true });
}
return () => {
@ -175,13 +171,6 @@ export const DropdownMenu = <Item = MenuItem,>({
};
}, [onClose, openedViaKeyboard]);
const handleFocusedItemRef = useCallback(
(c: HTMLAnchorElement | HTMLButtonElement | null) => {
focusedItemRef.current = c as HTMLElement;
},
[],
);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
@ -207,15 +196,6 @@ export const DropdownMenu = <Item = MenuItem,>({
[onClose, onItemClick, items],
);
const handleItemKeyUp = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
handleItemClick(e);
}
},
[handleItemClick],
);
const nativeRenderItem = (option: Item, i: number) => {
if (!isMenuItem(option)) {
return null;
@ -232,9 +212,7 @@ export const DropdownMenu = <Item = MenuItem,>({
if (isActionItem(option)) {
element = (
<button
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
aria-disabled={disabled}
type='button'
@ -249,9 +227,7 @@ export const DropdownMenu = <Item = MenuItem,>({
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<DropdownMenuItemContent item={option} />
@ -259,13 +235,7 @@ export const DropdownMenu = <Item = MenuItem,>({
);
} else {
element = (
<Link
to={option.to}
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<Link to={option.to} onClick={handleItemClick} data-index={i}>
<DropdownMenuItemContent item={option} />
</Link>
);
@ -308,15 +278,7 @@ export const DropdownMenu = <Item = MenuItem,>({
})}
>
{items.map((option, i) =>
renderItemMethod(
option,
i,
{
onClick: handleItemClick,
onKeyUp: handleItemKeyUp,
},
i === 0 ? handleFocusedItemRef : undefined,
),
renderItemMethod(option, i, handleItemClick),
)}
</ul>
)}
@ -400,7 +362,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
}, [dispatch, currentId]);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
(e: React.MouseEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = items?.[i];
@ -421,10 +383,20 @@ export const Dropdown = <Item extends object | null = MenuItem>({
[handleClose, onItemClick, items],
);
const toggleDropdown = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const { type } = e;
const isKeypressRef = useRef(false);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
isKeypressRef.current = true;
}
}, []);
const unsetIsKeypress = useCallback(() => {
isKeypressRef.current = false;
}, []);
const toggleDropdown = useCallback(
(e: React.MouseEvent) => {
if (open) {
handleClose();
} else {
@ -451,10 +423,11 @@ export const Dropdown = <Item extends object | null = MenuItem>({
dispatch(
openDropdownMenu({
id: currentId,
keyboard: type !== 'click',
keyboard: isKeypressRef.current,
scrollKey,
}),
);
isKeypressRef.current = false;
}
}
},
@ -485,6 +458,9 @@ export const Dropdown = <Item extends object | null = MenuItem>({
const buttonProps = {
disabled,
onClick: toggleDropdown,
onKeyDown: handleKeyDown,
onKeyUp: unsetIsKeypress,
onBlur: unsetIsKeypress,
'aria-expanded': open,
'aria-controls': menuId,
ref: buttonRef,

View File

@ -58,17 +58,7 @@ export const EditedTimestamp: React.FC<{
}, []);
const renderItem = useCallback(
(
item: HistoryItem,
index: number,
{
onClick,
onKeyUp,
}: {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
},
) => {
(item: HistoryItem, index: number, onClick: React.MouseEventHandler) => {
const formattedDate = (
<RelativeTimestamp
timestamp={item.get('created_at') as string}
@ -98,12 +88,7 @@ export const EditedTimestamp: React.FC<{
className='dropdown-menu__item edited-timestamp__history__item'
key={item.get('created_at') as string}
>
<button
data-index={index}
onClick={onClick}
onKeyUp={onKeyUp}
type='button'
>
<button data-index={index} onClick={onClick} type='button'>
{label}
</button>
</li>

View File

@ -27,22 +27,23 @@ export const ExitAnimationWrapper: React.FC<{
*/
children: (delayedIsActive: boolean) => React.ReactNode;
}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => {
const [delayedIsActive, setDelayedIsActive] = useState(false);
const [delayedIsActive, setDelayedIsActive] = useState(
isActive && !withEntryDelay,
);
useEffect(() => {
if (isActive && !withEntryDelay) {
setDelayedIsActive(true);
const withDelay = !isActive || withEntryDelay;
return () => '';
} else {
const timeout = setTimeout(() => {
const timeout = setTimeout(
() => {
setDelayedIsActive(isActive);
}, delayMs);
},
withDelay ? delayMs : 0,
);
return () => {
clearTimeout(timeout);
};
}
return () => {
clearTimeout(timeout);
};
}, [isActive, delayMs, withEntryDelay]);
if (!isActive && !delayedIsActive) {

View File

@ -27,7 +27,6 @@ export const HoverCardController: React.FC = () => {
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
const [setScrollTimeout] = useTimeout();
const location = useLocation();
const handleClose = useCallback(() => {
cancelEnterTimeout();
@ -36,9 +35,12 @@ export const HoverCardController: React.FC = () => {
setAnchor(null);
}, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]);
useEffect(() => {
const location = useLocation();
const [previousLocation, setPreviousLocation] = useState(location);
if (location !== previousLocation) {
setPreviousLocation(location);
handleClose();
}, [handleClose, location]);
}
useEffect(() => {
let isScrolling = false;

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, forwardRef } from 'react';
import { useCallback, forwardRef } from 'react';
import classNames from 'classnames';
@ -59,23 +59,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
},
buttonRef,
) => {
const [activate, setActivate] = useState(false);
const [deactivate, setDeactivate] = useState(false);
useEffect(() => {
if (!animate) {
return;
}
if (activate && !active) {
setActivate(false);
setDeactivate(true);
} else if (!activate && active) {
setActivate(true);
setDeactivate(false);
}
}, [setActivate, setDeactivate, animate, active, activate]);
const handleClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(
(e) => {
e.preventDefault();
@ -116,8 +99,8 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
active,
disabled,
inverted,
activate,
deactivate,
activate: animate && active,
deactivate: animate && !active,
overlayed: overlay,
'icon-button--with-counter': typeof counter !== 'undefined',
});

View File

@ -35,6 +35,9 @@ const messages = defineMessages({
},
});
const isPollExpired = (expiresAt: Model.Poll['expires_at']) =>
new Date(expiresAt).getTime() < Date.now();
interface PollProps {
pollId: string;
status: Status;
@ -58,8 +61,7 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
if (!poll) {
return false;
}
const expiresAt = poll.expires_at;
return poll.expired || new Date(expiresAt).getTime() < Date.now();
return poll.expired || isPollExpired(poll.expires_at);
}, [poll]);
const timeRemaining = useMemo(() => {
if (!poll) {

View File

@ -14,7 +14,7 @@ import type { Status } from '@/flavours/glitch/models/status';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import type { SomeRequired } from '@/flavours/glitch/utils/types';
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
import type { RenderItemFn } from '../dropdown_menu';
import { Dropdown, DropdownMenuItemContent } from '../dropdown_menu';
import { IconButton } from '../icon_button';
@ -74,18 +74,12 @@ const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
);
};
const renderMenuItem: RenderItemFn<ActionMenuItem> = (
item,
index,
handlers,
focusRefCallback,
) => (
const renderMenuItem: RenderItemFn<ActionMenuItem> = (item, index, onClick) => (
<ReblogMenuItem
index={index}
item={item}
handlers={handlers}
onClick={onClick}
key={`${item.text}-${index}`}
focusRefCallback={focusRefCallback}
/>
);
@ -118,6 +112,18 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
const statusId = status.get('id') as string;
const wasBoosted = !!status.get('reblogged');
let count: number | undefined;
if (counters) {
count = 0;
// Ensure count is a valid integer.
if (Number.isInteger(status.get('reblogs_count'))) {
count += status.get('reblogs_count') as number;
}
if (Number.isInteger(status.get('quotes_count'))) {
count += status.get('quotes_count') as number;
}
}
const showLoginPrompt = useCallback(() => {
dispatch(
openModal({
@ -193,12 +199,7 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
)}
icon='retweet'
iconComponent={boostIcon}
counter={
counters
? (status.get('reblogs_count') as number) +
(status.get('quotes_count') as number)
: undefined
}
counter={count}
active={isReblogged}
/>
</Dropdown>
@ -208,16 +209,10 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
interface ReblogMenuItemProps {
item: ActionMenuItem;
index: number;
handlers: RenderItemFnHandlers;
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void;
onClick: React.MouseEventHandler;
}
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
index,
item,
handlers,
focusRefCallback,
}) => {
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({ index, item, onClick }) => {
const { text, highlighted, disabled } = item;
return (
@ -228,8 +223,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
key={`${text}-${index}`}
>
<button
{...handlers}
ref={focusRefCallback}
onClick={onClick}
aria-disabled={disabled}
data-index={index}
type='button'

View File

@ -44,6 +44,7 @@ export const RemoveQuoteHint: React.FC<{
if (!firstHintId) {
firstHintId = uniqueId;
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsOnlyHint(true);
}
@ -64,8 +65,8 @@ export const RemoveQuoteHint: React.FC<{
flip
offset={[12, 10]}
placement='bottom-end'
target={anchorRef.current}
container={anchorRef.current}
target={anchorRef}
container={anchorRef}
>
{({ props, placement }) => (
<div

View File

@ -86,6 +86,7 @@ export const ScrollContext: React.FC<ScrollContextProps> = ({
) =>
// Hack to allow accessing scrollBehavior._stateStorage
shouldUpdateScroll.call(
// eslint-disable-next-line react-hooks/immutability
scrollBehavior,
prevLocationContext,
locationContext,

View File

@ -101,16 +101,17 @@ const Preview: React.FC<{
position: FocalPoint;
onPositionChange: (arg0: FocalPoint) => void;
}> = ({ mediaId, position, onPositionChange }) => {
const draggingRef = useRef<boolean>(false);
const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
const [dragging, setDragging] = useState<'started' | 'moving' | null>(null);
const [x, y] = position;
const style = useSpring({
to: {
left: `${x * 100}%`,
top: `${y * 100}%`,
},
immediate: draggingRef.current,
immediate: dragging === 'moving',
});
const media = useAppSelector((state) =>
(
@ -123,8 +124,6 @@ const Preview: React.FC<{
me ? state.accounts.get(me) : undefined,
);
const [dragging, setDragging] = useState(false);
const setRef = useCallback(
(e: HTMLImageElement | HTMLVideoElement | null) => {
nodeRef.current = e;
@ -140,20 +139,20 @@ const Preview: React.FC<{
const handleMouseMove = (e: MouseEvent) => {
const { x, y } = getPointerPosition(nodeRef.current, e);
draggingRef.current = true; // This will disable the animation for quicker feedback, only do this if the mouse actually moves
setDragging('moving'); // This will disable the animation for quicker feedback, only do this if the mouse actually moves
onPositionChange([x, y]);
};
const handleMouseUp = () => {
setDragging(false);
draggingRef.current = false;
setDragging(null);
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
const { x, y } = getPointerPosition(nodeRef.current, e.nativeEvent);
setDragging(true);
setDragging('started');
onPositionChange([x, y]);
document.addEventListener('mouseup', handleMouseUp);

View File

@ -31,15 +31,13 @@ export const AnnualReport: React.FC<{
year: string;
}> = ({ year }) => {
const [response, setResponse] = useState<AnnualReportResponse | null>(null);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const currentAccount = useAppSelector((state) =>
me ? state.accounts.get(me) : undefined,
);
const dispatch = useAppDispatch();
useEffect(() => {
setLoading(true);
apiRequestGet<AnnualReportResponse>(`v1/annual_reports/${year}`)
.then((data) => {
dispatch(importFetchedStatuses(data.statuses));

View File

@ -55,6 +55,8 @@ const getFrequentlyUsedLanguages = createSelector(
.toArray(),
);
const isTextLongEnoughForGuess = (text: string) => text.length > 20;
const LanguageDropdownMenu: React.FC<{
value: string;
guess?: string;
@ -375,14 +377,27 @@ export const LanguageDropdown: React.FC = () => {
);
useEffect(() => {
if (text.length > 20) {
if (isTextLongEnoughForGuess(text)) {
debouncedGuess(text, setGuess);
} else {
debouncedGuess.cancel();
setGuess('');
}
}, [text, setGuess]);
// Keeping track of the previous render's text length here
// to be able to reset the guess when the text length drops
// below the threshold needed to make a guess
const [wasLongText, setWasLongText] = useState(() =>
isTextLongEnoughForGuess(text),
);
if (wasLongText !== isTextLongEnoughForGuess(text)) {
setWasLongText(isTextLongEnoughForGuess(text));
if (wasLongText) {
setGuess('');
}
}
return (
<div ref={targetRef}>
<button

View File

@ -1,4 +1,4 @@
import { useCallback, useState, useRef, useEffect } from 'react';
import { useCallback, useState, useRef, useEffect, useMemo } from 'react';
import {
defineMessages,
@ -97,173 +97,13 @@ export const Search: React.FC<{
const [expanded, setExpanded] = useState(false);
const [selectedOption, setSelectedOption] = useState(-1);
const [quickActions, setQuickActions] = useState<SearchOption[]>([]);
useEffect(() => {
setValue(initialValue ?? '');
setQuickActions([]);
}, [initialValue]);
const searchOptions: SearchOption[] = [];
const unfocus = useCallback(() => {
document.querySelector('.ui')?.parentElement?.focus();
setExpanded(false);
}, []);
if (searchEnabled) {
searchOptions.push(
{
key: 'prompt-has',
label: (
<>
<mark>has:</mark>{' '}
<FormattedList
type='disjunction'
value={['media', 'poll', 'embed']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('has:');
},
},
{
key: 'prompt-is',
label: (
<>
<mark>is:</mark>{' '}
<FormattedList type='disjunction' value={['reply', 'sensitive']} />
</>
),
action: (e) => {
e.preventDefault();
insertText('is:');
},
},
{
key: 'prompt-language',
label: (
<>
<mark>language:</mark>{' '}
<FormattedMessage
id='search_popout.language_code'
defaultMessage='ISO language code'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('language:');
},
},
{
key: 'prompt-from',
label: (
<>
<mark>from:</mark>{' '}
<FormattedMessage id='search_popout.user' defaultMessage='user' />
</>
),
action: (e) => {
e.preventDefault();
insertText('from:');
},
},
{
key: 'prompt-before',
label: (
<>
<mark>before:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('before:');
},
},
{
key: 'prompt-during',
label: (
<>
<mark>during:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('during:');
},
},
{
key: 'prompt-after',
label: (
<>
<mark>after:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('after:');
},
},
{
key: 'prompt-in',
label: (
<>
<mark>in:</mark>{' '}
<FormattedList
type='disjunction'
value={['all', 'library', 'public']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('in:');
},
},
);
}
const recentOptions: SearchOption[] = recent.map((search) => ({
key: `${search.type}/${search.q}`,
label: labelForRecentSearch(search),
action: () => {
setValue(search.q);
if (search.type === 'account') {
history.push(`/@${search.q}`);
} else if (search.type === 'hashtag') {
history.push(`/tags/${search.q}`);
} else {
const queryParams = new URLSearchParams({ q: search.q });
if (search.type) queryParams.set('type', search.type);
history.push({ pathname: '/search', search: queryParams.toString() });
}
unfocus();
},
forget: (e) => {
e.stopPropagation();
void dispatch(forgetSearchResult(search));
},
}));
const navigableOptions = hasValue
? quickActions.concat(searchOptions)
: recentOptions.concat(quickActions, searchOptions);
const insertText = (text: string) => {
const insertText = useCallback((text: string) => {
setValue((currentValue) => {
if (currentValue === '') {
return text;
@ -273,7 +113,181 @@ export const Search: React.FC<{
return `${currentValue} ${text}`;
}
});
};
}, []);
const searchOptions = useMemo(() => {
if (!searchEnabled) {
return [];
} else {
const options: SearchOption[] = [
{
key: 'prompt-has',
label: (
<>
<mark>has:</mark>{' '}
<FormattedList
type='disjunction'
value={['media', 'poll', 'embed']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('has:');
},
},
{
key: 'prompt-is',
label: (
<>
<mark>is:</mark>{' '}
<FormattedList
type='disjunction'
value={['reply', 'sensitive']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('is:');
},
},
{
key: 'prompt-language',
label: (
<>
<mark>language:</mark>{' '}
<FormattedMessage
id='search_popout.language_code'
defaultMessage='ISO language code'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('language:');
},
},
{
key: 'prompt-from',
label: (
<>
<mark>from:</mark>{' '}
<FormattedMessage id='search_popout.user' defaultMessage='user' />
</>
),
action: (e) => {
e.preventDefault();
insertText('from:');
},
},
{
key: 'prompt-before',
label: (
<>
<mark>before:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('before:');
},
},
{
key: 'prompt-during',
label: (
<>
<mark>during:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('during:');
},
},
{
key: 'prompt-after',
label: (
<>
<mark>after:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('after:');
},
},
{
key: 'prompt-in',
label: (
<>
<mark>in:</mark>{' '}
<FormattedList
type='disjunction'
value={['all', 'library', 'public']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('in:');
},
},
];
return options;
}
}, [insertText]);
const recentOptions: SearchOption[] = useMemo(
() =>
recent.map((search) => ({
key: `${search.type}/${search.q}`,
label: labelForRecentSearch(search),
action: () => {
setValue(search.q);
if (search.type === 'account') {
history.push(`/@${search.q}`);
} else if (search.type === 'hashtag') {
history.push(`/tags/${search.q}`);
} else {
const queryParams = new URLSearchParams({ q: search.q });
if (search.type) queryParams.set('type', search.type);
history.push({
pathname: '/search',
search: queryParams.toString(),
});
}
unfocus();
},
forget: (e) => {
e.stopPropagation();
void dispatch(forgetSearchResult(search));
},
})),
[dispatch, history, recent, unfocus],
);
const navigableOptions: SearchOption[] = useMemo(
() =>
hasValue
? quickActions.concat(searchOptions)
: recentOptions.concat(quickActions, searchOptions),
[hasValue, quickActions, recentOptions, searchOptions],
);
const submit = useCallback(
(q: string, type?: SearchType) => {

View File

@ -55,6 +55,11 @@ type ColumnMap = ImmutableMap<'id' | 'uuid' | 'params', string>;
const glitchProbability = 1 - 0.0420215528;
const totalElefriends = 3;
const pickRandomFriend = () =>
Math.random() < glitchProbability
? Math.floor(Math.random() * totalElefriends)
: totalElefriends;
const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
@ -75,11 +80,7 @@ const Compose: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
false,
) as boolean,
);
const [elefriend, setElefriend] = useState(
Math.random() < glitchProbability
? Math.floor(Math.random() * totalElefriends)
: totalElefriends,
);
const [elefriend, setElefriend] = useState(pickRandomFriend());
useEffect(() => {
dispatch(mountCompose());

View File

@ -19,14 +19,12 @@ const messages = defineMessages({
const Blocks: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const intl = useIntl();
const [domains, setDomains] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [next, setNext] = useState<string | undefined>();
const hasMore = !!next;
const columnRef = useRef<ColumnRef>(null);
useEffect(() => {
setLoading(true);
void apiGetDomainBlocks()
.then(({ domains, links }) => {
const next = links.refs.find((link) => link.rel === 'next');
@ -40,7 +38,7 @@ const Blocks: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
.catch(() => {
setLoading(false);
});
}, [setLoading, setDomains, setNext]);
}, []);
const handleLoadMore = useCallback(() => {
setLoading(true);

View File

@ -35,12 +35,17 @@ export const Announcement: FC<AnnouncementProps> = ({
}, [active, id, dispatch, read]);
// But visually show the announcement as read only when it goes out of view.
const [unread, setUnread] = useState(!read);
useEffect(() => {
if (!active && unread !== !read) {
setUnread(!read);
const [isVisuallyRead, setIsVisuallyRead] = useState(read);
const [previousActive, setPreviousActive] = useState(active);
if (active !== previousActive) {
setPreviousActive(active);
// This marks the announcement as read in the UI only after it
// went from active to inactive.
if (!active && isVisuallyRead !== read) {
setIsVisuallyRead(read);
}
}, [active, unread, read]);
}
return (
<AnimateEmojiProvider>
@ -63,7 +68,7 @@ export const Announcement: FC<AnnouncementProps> = ({
<ReactionsBar reactions={announcement.reactions} id={announcement.id} />
{unread && <span className='announcements__unread' />}
{!isVisuallyRead && <span className='announcements__unread' />}
</AnimateEmojiProvider>
);
};

View File

@ -164,12 +164,11 @@ const ListMembers: React.FC<{
const [searching, setSearching] = useState(false);
const [accountIds, setAccountIds] = useState<string[]>([]);
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(!!id);
const [mode, setMode] = useState<Mode>('remove');
useEffect(() => {
if (id) {
setLoading(true);
dispatch(fetchList(id));
void apiGetAccounts(id)

View File

@ -27,17 +27,15 @@ export const ListPanel: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const lists = useAppSelector((state) => getOrderedLists(state));
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
void dispatch(fetchLists()).then(() => {
setLoading(false);
return '';
});
}, [dispatch, setLoading]);
}, [dispatch]);
return (
<CollapsiblePanel

View File

@ -225,7 +225,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
/>
<div className='explore__search-header'>
<Search singleColumn initialValue={trimmedValue} />
<Search singleColumn initialValue={trimmedValue} key={trimmedValue} />
</div>
<div className='account__section-headline'>

View File

@ -295,7 +295,7 @@ export const RefreshController: React.FC<{
if (loadingState === 'loading') {
return (
<div
className='load-more load-gap'
className='load-more load-more--large'
aria-busy
aria-live='polite'
aria-label={intl.formatMessage(messages.loadingInitial)}

View File

@ -53,8 +53,6 @@ export const DomainBlockModal: React.FC<{
}, [dispatch]);
useEffect(() => {
setLoading(true);
apiRequest<DomainBlockPreviewResponse>('GET', 'v1/domain_blocks/preview', {
params: { domain },
timeout: 5000,
@ -68,7 +66,7 @@ export const DomainBlockModal: React.FC<{
setPreview('error');
setLoading(false);
});
}, [setPreview, setLoading, domain]);
}, [domain]);
return (
<div className='modal-root__modal safety-action-modal' aria-live='polite'>

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useIntl, defineMessages } from 'react-intl';
@ -41,40 +41,44 @@ const isHashtagLink = (
};
interface TargetParams {
hashtag?: string;
accountId?: string;
element: HTMLAnchorElement | null;
hashtag: string;
accountId: string;
}
export const HashtagMenuController: React.FC = () => {
const intl = useIntl();
const { signedIn } = useIdentity();
const [open, setOpen] = useState(false);
const [{ accountId, hashtag }, setTargetParams] = useState<TargetParams>({});
const targetRef = useRef<HTMLAnchorElement | null>(null);
const location = useLocation();
const [target, setTarget] = useState<TargetParams | null>(null);
const { element = null, accountId, hashtag } = target ?? {};
const open = !!element;
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
useEffect(() => {
setOpen(false);
targetRef.current = null;
}, [setOpen, location]);
const location = useLocation();
const [previousLocation, setPreviousLocation] = useState(location);
if (location !== previousLocation) {
setPreviousLocation(location);
setTarget(null);
}
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = (e.target as HTMLElement).closest('a');
const targetElement = (e.target as HTMLElement).closest('a');
if (e.button !== 0 || e.ctrlKey || e.metaKey) {
return;
}
if (!isHashtagLink(target)) {
if (!isHashtagLink(targetElement)) {
return;
}
const hashtag = target.text.replace(/^#/, '');
const accountId = target.getAttribute('data-menu-hashtag');
const hashtag = targetElement.text.replace(/^#/, '');
const accountId = targetElement.getAttribute('data-menu-hashtag');
if (!hashtag || !accountId) {
return;
@ -82,9 +86,7 @@ export const HashtagMenuController: React.FC = () => {
e.preventDefault();
e.stopPropagation();
targetRef.current = target;
setOpen(true);
setTargetParams({ hashtag, accountId });
setTarget({ element: targetElement, hashtag, accountId });
};
document.addEventListener('click', handleClick, { capture: true });
@ -92,12 +94,11 @@ export const HashtagMenuController: React.FC = () => {
return () => {
document.removeEventListener('click', handleClick);
};
}, [setTargetParams, setOpen]);
}, []);
const handleClose = useCallback(() => {
setOpen(false);
targetRef.current = null;
}, [setOpen]);
setTarget(null);
}, []);
const menu = useMemo(() => {
const arr: MenuItem[] = [
@ -139,7 +140,7 @@ export const HashtagMenuController: React.FC = () => {
offset={offset}
placement='bottom'
flip
target={targetRef}
target={element}
popperConfig={popperConfig}
>
{({ props, arrowProps, placement }) => (

View File

@ -66,6 +66,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
_ref,
) => {
const [index, setIndex] = useState(startIndex);
const [zoomedIn, setZoomedIn] = useState(false);
const currentMedia = media.get(index);
const handleChangeIndex = useCallback(
@ -134,7 +135,6 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
}
}, []);
const [zoomedIn, setZoomedIn] = useState(false);
const zoomable =
currentMedia?.get('type') === 'image' &&
((currentMedia.getIn(['meta', 'original', 'width']) as number) >

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useSyncExternalStore } from 'react';
const breakpoints = {
narrow: 479, // Device width under which horizontal space is constrained
@ -9,25 +9,20 @@ const breakpoints = {
type Breakpoint = keyof typeof breakpoints;
export const useBreakpoint = (breakpoint: Breakpoint) => {
const [isMatching, setIsMatching] = useState(false);
const query = `(max-width: ${breakpoints[breakpoint]}px)`;
useEffect(() => {
const mediaWatcher = window.matchMedia(
`(max-width: ${breakpoints[breakpoint]}px)`,
);
const isMatching = useSyncExternalStore(
(callback) => {
const mediaWatcher = window.matchMedia(query);
setIsMatching(mediaWatcher.matches);
mediaWatcher.addEventListener('change', callback);
const handleChange = (e: MediaQueryListEvent) => {
setIsMatching(e.matches);
};
mediaWatcher.addEventListener('change', handleChange);
return () => {
mediaWatcher.removeEventListener('change', handleChange);
};
}, [breakpoint, setIsMatching]);
return () => {
mediaWatcher.removeEventListener('change', callback);
};
},
() => window.matchMedia(query).matches,
);
return isMatching;
};

View File

@ -1,4 +1,4 @@
import { useRef, useEffect } from 'react';
import { useState } from 'react';
/**
* Returns the previous state of the passed in value.
@ -6,11 +6,21 @@ import { useRef, useEffect } from 'react';
*/
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
const [{ previous, current }, setMemory] = useState<{
previous: T | undefined;
current: T;
}>(() => ({ previous: undefined, current: value }));
useEffect(() => {
ref.current = value;
}, [value]);
let result = previous;
return ref.current;
if (value !== current) {
setMemory({
previous: current,
current: value,
});
// Ensure that the returned result updates synchronously
result = current;
}
return result;
}

View File

@ -3246,20 +3246,21 @@ a.account__display-name {
}
.column__alert {
--alert-height: 54px;
position: sticky;
bottom: 0;
z-index: 10;
box-sizing: border-box;
display: grid;
grid-template-rows: minmax(var(--alert-height), max-content);
align-items: end;
width: 100%;
max-width: 360px;
padding: 1rem;
margin: auto auto 0;
overflow: clip;
&:empty {
padding: 0;
}
pointer-events: none;
@media (max-width: #{$mobile-menu-breakpoint - 1}) {
// Compensate for mobile menubar
@ -3270,6 +3271,7 @@ a.account__display-name {
// Make all nested alerts occupy the same space
// rather than stack
grid-area: 1 / 1;
pointer-events: initial;
}
}
@ -4572,13 +4574,19 @@ a.status-card {
box-sizing: border-box;
text-decoration: none;
&:hover {
background: var(--on-surface-color);
&--large {
padding-block: 32px;
}
&:focus-visible {
outline: 2px solid $ui-button-focus-outline-color;
outline-offset: -2px;
&:is(button) {
&:hover {
background: var(--on-surface-color);
}
&:focus-visible {
outline: 2px solid $ui-button-focus-outline-color;
outline-offset: -2px;
}
}
.icon {
@ -6433,6 +6441,8 @@ a.status-card {
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
align-items: center;
justify-content: space-around; // If set to center, the fullscreen image overlay is misaligned.
> div {
flex-shrink: 0;

View File

@ -47,7 +47,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
rootClose
onHide={handleClose}
show={open}
target={anchorRef.current}
target={anchorRef}
placement='top-end'
flip
offset={offset}

View File

@ -76,6 +76,11 @@ export const Carousel = <
// Handle slide change
const [slideIndex, setSlideIndex] = useState(0);
const wrapperRef = useRef<HTMLDivElement>(null);
// Handle slide heights
const [currentSlideHeight, setCurrentSlideHeight] = useState(
() => wrapperRef.current?.scrollHeight ?? 0,
);
const previousSlideHeight = usePrevious(currentSlideHeight);
const handleSlideChange = useCallback(
(direction: number) => {
setSlideIndex((prev) => {
@ -101,16 +106,11 @@ export const Carousel = <
[items.length, onChangeSlide],
);
// Handle slide heights
const [currentSlideHeight, setCurrentSlideHeight] = useState(
wrapperRef.current?.scrollHeight ?? 0,
);
const previousSlideHeight = usePrevious(currentSlideHeight);
const observerRef = useRef<ResizeObserver>(
new ResizeObserver(() => {
handleSlideChange(0);
}),
);
const observerRef = useRef<ResizeObserver | null>(null);
observerRef.current ??= new ResizeObserver(() => {
handleSlideChange(0);
});
const wrapperStyles = useSpring({
x: `-${slideIndex * 100}%`,
height: currentSlideHeight,
@ -200,7 +200,7 @@ export const Carousel = <
};
type CarouselSlideWrapperProps<SlideProps extends CarouselSlideProps> = {
observer: ResizeObserver;
observer: ResizeObserver | null;
className: string;
active: boolean;
item: SlideProps;
@ -217,7 +217,7 @@ const CarouselSlideWrapper = <SlideProps extends CarouselSlideProps>({
}: CarouselSlideWrapperProps<SlideProps>) => {
const handleRef = useCallback(
(instance: HTMLDivElement | null) => {
if (instance) {
if (observer && instance) {
observer.observe(instance);
}
},

View File

@ -1,4 +1,4 @@
import { useCallback, useState, useEffect, useRef } from 'react';
import { useCallback, useState, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
@ -12,11 +12,15 @@ export const ColumnSearchHeader: React.FC<{
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState('');
useEffect(() => {
// Reset the component when it turns from active to inactive.
// [More on this pattern](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
const [previousActive, setPreviousActive] = useState(active);
if (active !== previousActive) {
setPreviousActive(active);
if (!active) {
setValue('');
}
}, [active]);
}
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -109,7 +109,7 @@ export const Dropdown: FC<
placement='bottom-start'
onHide={handleClose}
flip
target={buttonRef.current}
target={buttonRef}
popperConfig={{
strategy: 'fixed',
modifiers: [matchWidth],

View File

@ -42,16 +42,10 @@ import { IconButton } from './icon_button';
let id = 0;
export interface RenderItemFnHandlers {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
}
export type RenderItemFn<Item = MenuItem> = (
item: Item,
index: number,
handlers: RenderItemFnHandlers,
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void,
onClick: React.MouseEventHandler,
) => React.ReactNode;
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
@ -101,7 +95,6 @@ export const DropdownMenu = <Item = MenuItem,>({
onItemClick,
}: DropdownMenuProps<Item>) => {
const nodeRef = useRef<HTMLDivElement>(null);
const focusedItemRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const handleDocumentClick = (e: MouseEvent) => {
@ -163,8 +156,11 @@ export const DropdownMenu = <Item = MenuItem,>({
document.addEventListener('click', handleDocumentClick, { capture: true });
document.addEventListener('keydown', handleKeyDown, { capture: true });
if (focusedItemRef.current && openedViaKeyboard) {
focusedItemRef.current.focus({ preventScroll: true });
if (openedViaKeyboard) {
const firstMenuItem = nodeRef.current?.querySelector<
HTMLAnchorElement | HTMLButtonElement
>('li:first-child > :is(a, button)');
firstMenuItem?.focus({ preventScroll: true });
}
return () => {
@ -175,13 +171,6 @@ export const DropdownMenu = <Item = MenuItem,>({
};
}, [onClose, openedViaKeyboard]);
const handleFocusedItemRef = useCallback(
(c: HTMLAnchorElement | HTMLButtonElement | null) => {
focusedItemRef.current = c as HTMLElement;
},
[],
);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
@ -207,15 +196,6 @@ export const DropdownMenu = <Item = MenuItem,>({
[onClose, onItemClick, items],
);
const handleItemKeyUp = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
handleItemClick(e);
}
},
[handleItemClick],
);
const nativeRenderItem = (option: Item, i: number) => {
if (!isMenuItem(option)) {
return null;
@ -232,9 +212,7 @@ export const DropdownMenu = <Item = MenuItem,>({
if (isActionItem(option)) {
element = (
<button
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
aria-disabled={disabled}
type='button'
@ -249,9 +227,7 @@ export const DropdownMenu = <Item = MenuItem,>({
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<DropdownMenuItemContent item={option} />
@ -259,13 +235,7 @@ export const DropdownMenu = <Item = MenuItem,>({
);
} else {
element = (
<Link
to={option.to}
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<Link to={option.to} onClick={handleItemClick} data-index={i}>
<DropdownMenuItemContent item={option} />
</Link>
);
@ -308,15 +278,7 @@ export const DropdownMenu = <Item = MenuItem,>({
})}
>
{items.map((option, i) =>
renderItemMethod(
option,
i,
{
onClick: handleItemClick,
onKeyUp: handleItemKeyUp,
},
i === 0 ? handleFocusedItemRef : undefined,
),
renderItemMethod(option, i, handleItemClick),
)}
</ul>
)}
@ -400,7 +362,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
}, [dispatch, currentId]);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
(e: React.MouseEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = items?.[i];
@ -421,10 +383,20 @@ export const Dropdown = <Item extends object | null = MenuItem>({
[handleClose, onItemClick, items],
);
const toggleDropdown = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const { type } = e;
const isKeypressRef = useRef(false);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
isKeypressRef.current = true;
}
}, []);
const unsetIsKeypress = useCallback(() => {
isKeypressRef.current = false;
}, []);
const toggleDropdown = useCallback(
(e: React.MouseEvent) => {
if (open) {
handleClose();
} else {
@ -451,10 +423,11 @@ export const Dropdown = <Item extends object | null = MenuItem>({
dispatch(
openDropdownMenu({
id: currentId,
keyboard: type !== 'click',
keyboard: isKeypressRef.current,
scrollKey,
}),
);
isKeypressRef.current = false;
}
}
},
@ -485,6 +458,9 @@ export const Dropdown = <Item extends object | null = MenuItem>({
const buttonProps = {
disabled,
onClick: toggleDropdown,
onKeyDown: handleKeyDown,
onKeyUp: unsetIsKeypress,
onBlur: unsetIsKeypress,
'aria-expanded': open,
'aria-controls': menuId,
ref: buttonRef,

View File

@ -58,17 +58,7 @@ export const EditedTimestamp: React.FC<{
}, []);
const renderItem = useCallback(
(
item: HistoryItem,
index: number,
{
onClick,
onKeyUp,
}: {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
},
) => {
(item: HistoryItem, index: number, onClick: React.MouseEventHandler) => {
const formattedDate = (
<RelativeTimestamp
timestamp={item.get('created_at') as string}
@ -98,12 +88,7 @@ export const EditedTimestamp: React.FC<{
className='dropdown-menu__item edited-timestamp__history__item'
key={item.get('created_at') as string}
>
<button
data-index={index}
onClick={onClick}
onKeyUp={onKeyUp}
type='button'
>
<button data-index={index} onClick={onClick} type='button'>
{label}
</button>
</li>

View File

@ -27,22 +27,23 @@ export const ExitAnimationWrapper: React.FC<{
*/
children: (delayedIsActive: boolean) => React.ReactNode;
}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => {
const [delayedIsActive, setDelayedIsActive] = useState(false);
const [delayedIsActive, setDelayedIsActive] = useState(
isActive && !withEntryDelay,
);
useEffect(() => {
if (isActive && !withEntryDelay) {
setDelayedIsActive(true);
const withDelay = !isActive || withEntryDelay;
return () => '';
} else {
const timeout = setTimeout(() => {
const timeout = setTimeout(
() => {
setDelayedIsActive(isActive);
}, delayMs);
},
withDelay ? delayMs : 0,
);
return () => {
clearTimeout(timeout);
};
}
return () => {
clearTimeout(timeout);
};
}, [isActive, delayMs, withEntryDelay]);
if (!isActive && !delayedIsActive) {

View File

@ -27,7 +27,6 @@ export const HoverCardController: React.FC = () => {
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
const [setScrollTimeout] = useTimeout();
const location = useLocation();
const handleClose = useCallback(() => {
cancelEnterTimeout();
@ -36,9 +35,12 @@ export const HoverCardController: React.FC = () => {
setAnchor(null);
}, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]);
useEffect(() => {
const location = useLocation();
const [previousLocation, setPreviousLocation] = useState(location);
if (location !== previousLocation) {
setPreviousLocation(location);
handleClose();
}, [handleClose, location]);
}
useEffect(() => {
let isScrolling = false;

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, forwardRef } from 'react';
import { useCallback, forwardRef } from 'react';
import classNames from 'classnames';
@ -55,23 +55,6 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
},
buttonRef,
) => {
const [activate, setActivate] = useState(false);
const [deactivate, setDeactivate] = useState(false);
useEffect(() => {
if (!animate) {
return;
}
if (activate && !active) {
setActivate(false);
setDeactivate(true);
} else if (!activate && active) {
setActivate(true);
setDeactivate(false);
}
}, [setActivate, setDeactivate, animate, active, activate]);
const handleClick: React.MouseEventHandler<HTMLButtonElement> = useCallback(
(e) => {
e.preventDefault();
@ -112,8 +95,8 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(
active,
disabled,
inverted,
activate,
deactivate,
activate: animate && active,
deactivate: animate && !active,
overlayed: overlay,
'icon-button--with-counter': typeof counter !== 'undefined',
});

View File

@ -35,6 +35,9 @@ const messages = defineMessages({
},
});
const isPollExpired = (expiresAt: Model.Poll['expires_at']) =>
new Date(expiresAt).getTime() < Date.now();
interface PollProps {
pollId: string;
status: Status;
@ -58,8 +61,7 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
if (!poll) {
return false;
}
const expiresAt = poll.expires_at;
return poll.expired || new Date(expiresAt).getTime() < Date.now();
return poll.expired || isPollExpired(poll.expires_at);
}, [poll]);
const timeRemaining = useMemo(() => {
if (!poll) {

View File

@ -14,7 +14,7 @@ import type { Status } from '@/mastodon/models/status';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import type { SomeRequired } from '@/mastodon/utils/types';
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
import type { RenderItemFn } from '../dropdown_menu';
import { Dropdown, DropdownMenuItemContent } from '../dropdown_menu';
import { IconButton } from '../icon_button';
@ -74,18 +74,12 @@ const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
);
};
const renderMenuItem: RenderItemFn<ActionMenuItem> = (
item,
index,
handlers,
focusRefCallback,
) => (
const renderMenuItem: RenderItemFn<ActionMenuItem> = (item, index, onClick) => (
<ReblogMenuItem
index={index}
item={item}
handlers={handlers}
onClick={onClick}
key={`${item.text}-${index}`}
focusRefCallback={focusRefCallback}
/>
);
@ -118,6 +112,18 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
const statusId = status.get('id') as string;
const wasBoosted = !!status.get('reblogged');
let count: number | undefined;
if (counters) {
count = 0;
// Ensure count is a valid integer.
if (Number.isInteger(status.get('reblogs_count'))) {
count += status.get('reblogs_count') as number;
}
if (Number.isInteger(status.get('quotes_count'))) {
count += status.get('quotes_count') as number;
}
}
const showLoginPrompt = useCallback(() => {
dispatch(
openModal({
@ -193,12 +199,7 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
)}
icon='retweet'
iconComponent={boostIcon}
counter={
counters
? (status.get('reblogs_count') as number) +
(status.get('quotes_count') as number)
: undefined
}
counter={count}
active={isReblogged}
/>
</Dropdown>
@ -208,16 +209,10 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
interface ReblogMenuItemProps {
item: ActionMenuItem;
index: number;
handlers: RenderItemFnHandlers;
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void;
onClick: React.MouseEventHandler;
}
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
index,
item,
handlers,
focusRefCallback,
}) => {
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({ index, item, onClick }) => {
const { text, highlighted, disabled } = item;
return (
@ -228,8 +223,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
key={`${text}-${index}`}
>
<button
{...handlers}
ref={focusRefCallback}
onClick={onClick}
aria-disabled={disabled}
data-index={index}
type='button'

View File

@ -44,6 +44,7 @@ export const RemoveQuoteHint: React.FC<{
if (!firstHintId) {
firstHintId = uniqueId;
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsOnlyHint(true);
}
@ -64,8 +65,8 @@ export const RemoveQuoteHint: React.FC<{
flip
offset={[12, 10]}
placement='bottom-end'
target={anchorRef.current}
container={anchorRef.current}
target={anchorRef}
container={anchorRef}
>
{({ props, placement }) => (
<div

View File

@ -86,6 +86,7 @@ export const ScrollContext: React.FC<ScrollContextProps> = ({
) =>
// Hack to allow accessing scrollBehavior._stateStorage
shouldUpdateScroll.call(
// eslint-disable-next-line react-hooks/immutability
scrollBehavior,
prevLocationContext,
locationContext,

View File

@ -101,16 +101,17 @@ const Preview: React.FC<{
position: FocalPoint;
onPositionChange: (arg0: FocalPoint) => void;
}> = ({ mediaId, position, onPositionChange }) => {
const draggingRef = useRef<boolean>(false);
const nodeRef = useRef<HTMLImageElement | HTMLVideoElement | null>(null);
const [dragging, setDragging] = useState<'started' | 'moving' | null>(null);
const [x, y] = position;
const style = useSpring({
to: {
left: `${x * 100}%`,
top: `${y * 100}%`,
},
immediate: draggingRef.current,
immediate: dragging === 'moving',
});
const media = useAppSelector((state) =>
(
@ -123,8 +124,6 @@ const Preview: React.FC<{
me ? state.accounts.get(me) : undefined,
);
const [dragging, setDragging] = useState(false);
const setRef = useCallback(
(e: HTMLImageElement | HTMLVideoElement | null) => {
nodeRef.current = e;
@ -140,20 +139,20 @@ const Preview: React.FC<{
const handleMouseMove = (e: MouseEvent) => {
const { x, y } = getPointerPosition(nodeRef.current, e);
draggingRef.current = true; // This will disable the animation for quicker feedback, only do this if the mouse actually moves
setDragging('moving'); // This will disable the animation for quicker feedback, only do this if the mouse actually moves
onPositionChange([x, y]);
};
const handleMouseUp = () => {
setDragging(false);
draggingRef.current = false;
setDragging(null);
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
const { x, y } = getPointerPosition(nodeRef.current, e.nativeEvent);
setDragging(true);
setDragging('started');
onPositionChange([x, y]);
document.addEventListener('mouseup', handleMouseUp);

View File

@ -31,15 +31,13 @@ export const AnnualReport: React.FC<{
year: string;
}> = ({ year }) => {
const [response, setResponse] = useState<AnnualReportResponse | null>(null);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const currentAccount = useAppSelector((state) =>
me ? state.accounts.get(me) : undefined,
);
const dispatch = useAppDispatch();
useEffect(() => {
setLoading(true);
apiRequestGet<AnnualReportResponse>(`v1/annual_reports/${year}`)
.then((data) => {
dispatch(importFetchedStatuses(data.statuses));

View File

@ -55,6 +55,8 @@ const getFrequentlyUsedLanguages = createSelector(
.toArray(),
);
const isTextLongEnoughForGuess = (text: string) => text.length > 20;
const LanguageDropdownMenu: React.FC<{
value: string;
guess?: string;
@ -375,14 +377,27 @@ export const LanguageDropdown: React.FC = () => {
);
useEffect(() => {
if (text.length > 20) {
if (isTextLongEnoughForGuess(text)) {
debouncedGuess(text, setGuess);
} else {
debouncedGuess.cancel();
setGuess('');
}
}, [text, setGuess]);
// Keeping track of the previous render's text length here
// to be able to reset the guess when the text length drops
// below the threshold needed to make a guess
const [wasLongText, setWasLongText] = useState(() =>
isTextLongEnoughForGuess(text),
);
if (wasLongText !== isTextLongEnoughForGuess(text)) {
setWasLongText(isTextLongEnoughForGuess(text));
if (wasLongText) {
setGuess('');
}
}
return (
<div ref={targetRef}>
<button

View File

@ -1,4 +1,4 @@
import { useCallback, useState, useRef, useEffect } from 'react';
import { useCallback, useState, useRef, useEffect, useMemo } from 'react';
import {
defineMessages,
@ -97,173 +97,13 @@ export const Search: React.FC<{
const [expanded, setExpanded] = useState(false);
const [selectedOption, setSelectedOption] = useState(-1);
const [quickActions, setQuickActions] = useState<SearchOption[]>([]);
useEffect(() => {
setValue(initialValue ?? '');
setQuickActions([]);
}, [initialValue]);
const searchOptions: SearchOption[] = [];
const unfocus = useCallback(() => {
document.querySelector('.ui')?.parentElement?.focus();
setExpanded(false);
}, []);
if (searchEnabled) {
searchOptions.push(
{
key: 'prompt-has',
label: (
<>
<mark>has:</mark>{' '}
<FormattedList
type='disjunction'
value={['media', 'poll', 'embed']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('has:');
},
},
{
key: 'prompt-is',
label: (
<>
<mark>is:</mark>{' '}
<FormattedList type='disjunction' value={['reply', 'sensitive']} />
</>
),
action: (e) => {
e.preventDefault();
insertText('is:');
},
},
{
key: 'prompt-language',
label: (
<>
<mark>language:</mark>{' '}
<FormattedMessage
id='search_popout.language_code'
defaultMessage='ISO language code'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('language:');
},
},
{
key: 'prompt-from',
label: (
<>
<mark>from:</mark>{' '}
<FormattedMessage id='search_popout.user' defaultMessage='user' />
</>
),
action: (e) => {
e.preventDefault();
insertText('from:');
},
},
{
key: 'prompt-before',
label: (
<>
<mark>before:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('before:');
},
},
{
key: 'prompt-during',
label: (
<>
<mark>during:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('during:');
},
},
{
key: 'prompt-after',
label: (
<>
<mark>after:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('after:');
},
},
{
key: 'prompt-in',
label: (
<>
<mark>in:</mark>{' '}
<FormattedList
type='disjunction'
value={['all', 'library', 'public']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('in:');
},
},
);
}
const recentOptions: SearchOption[] = recent.map((search) => ({
key: `${search.type}/${search.q}`,
label: labelForRecentSearch(search),
action: () => {
setValue(search.q);
if (search.type === 'account') {
history.push(`/@${search.q}`);
} else if (search.type === 'hashtag') {
history.push(`/tags/${search.q}`);
} else {
const queryParams = new URLSearchParams({ q: search.q });
if (search.type) queryParams.set('type', search.type);
history.push({ pathname: '/search', search: queryParams.toString() });
}
unfocus();
},
forget: (e) => {
e.stopPropagation();
void dispatch(forgetSearchResult(search));
},
}));
const navigableOptions = hasValue
? quickActions.concat(searchOptions)
: recentOptions.concat(quickActions, searchOptions);
const insertText = (text: string) => {
const insertText = useCallback((text: string) => {
setValue((currentValue) => {
if (currentValue === '') {
return text;
@ -273,7 +113,181 @@ export const Search: React.FC<{
return `${currentValue} ${text}`;
}
});
};
}, []);
const searchOptions = useMemo(() => {
if (!searchEnabled) {
return [];
} else {
const options: SearchOption[] = [
{
key: 'prompt-has',
label: (
<>
<mark>has:</mark>{' '}
<FormattedList
type='disjunction'
value={['media', 'poll', 'embed']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('has:');
},
},
{
key: 'prompt-is',
label: (
<>
<mark>is:</mark>{' '}
<FormattedList
type='disjunction'
value={['reply', 'sensitive']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('is:');
},
},
{
key: 'prompt-language',
label: (
<>
<mark>language:</mark>{' '}
<FormattedMessage
id='search_popout.language_code'
defaultMessage='ISO language code'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('language:');
},
},
{
key: 'prompt-from',
label: (
<>
<mark>from:</mark>{' '}
<FormattedMessage id='search_popout.user' defaultMessage='user' />
</>
),
action: (e) => {
e.preventDefault();
insertText('from:');
},
},
{
key: 'prompt-before',
label: (
<>
<mark>before:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('before:');
},
},
{
key: 'prompt-during',
label: (
<>
<mark>during:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('during:');
},
},
{
key: 'prompt-after',
label: (
<>
<mark>after:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('after:');
},
},
{
key: 'prompt-in',
label: (
<>
<mark>in:</mark>{' '}
<FormattedList
type='disjunction'
value={['all', 'library', 'public']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('in:');
},
},
];
return options;
}
}, [insertText]);
const recentOptions: SearchOption[] = useMemo(
() =>
recent.map((search) => ({
key: `${search.type}/${search.q}`,
label: labelForRecentSearch(search),
action: () => {
setValue(search.q);
if (search.type === 'account') {
history.push(`/@${search.q}`);
} else if (search.type === 'hashtag') {
history.push(`/tags/${search.q}`);
} else {
const queryParams = new URLSearchParams({ q: search.q });
if (search.type) queryParams.set('type', search.type);
history.push({
pathname: '/search',
search: queryParams.toString(),
});
}
unfocus();
},
forget: (e) => {
e.stopPropagation();
void dispatch(forgetSearchResult(search));
},
})),
[dispatch, history, recent, unfocus],
);
const navigableOptions: SearchOption[] = useMemo(
() =>
hasValue
? quickActions.concat(searchOptions)
: recentOptions.concat(quickActions, searchOptions),
[hasValue, quickActions, recentOptions, searchOptions],
);
const submit = useCallback(
(q: string, type?: SearchType) => {

View File

@ -19,14 +19,12 @@ const messages = defineMessages({
const Blocks: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const intl = useIntl();
const [domains, setDomains] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [next, setNext] = useState<string | undefined>();
const hasMore = !!next;
const columnRef = useRef<ColumnRef>(null);
useEffect(() => {
setLoading(true);
void apiGetDomainBlocks()
.then(({ domains, links }) => {
const next = links.refs.find((link) => link.rel === 'next');
@ -40,7 +38,7 @@ const Blocks: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
.catch(() => {
setLoading(false);
});
}, [setLoading, setDomains, setNext]);
}, []);
const handleLoadMore = useCallback(() => {
setLoading(true);

View File

@ -35,12 +35,17 @@ export const Announcement: FC<AnnouncementProps> = ({
}, [active, id, dispatch, read]);
// But visually show the announcement as read only when it goes out of view.
const [unread, setUnread] = useState(!read);
useEffect(() => {
if (!active && unread !== !read) {
setUnread(!read);
const [isVisuallyRead, setIsVisuallyRead] = useState(read);
const [previousActive, setPreviousActive] = useState(active);
if (active !== previousActive) {
setPreviousActive(active);
// This marks the announcement as read in the UI only after it
// went from active to inactive.
if (!active && isVisuallyRead !== read) {
setIsVisuallyRead(read);
}
}, [active, unread, read]);
}
return (
<AnimateEmojiProvider>
@ -63,7 +68,7 @@ export const Announcement: FC<AnnouncementProps> = ({
<ReactionsBar reactions={announcement.reactions} id={announcement.id} />
{unread && <span className='announcements__unread' />}
{!isVisuallyRead && <span className='announcements__unread' />}
</AnimateEmojiProvider>
);
};

View File

@ -164,12 +164,11 @@ const ListMembers: React.FC<{
const [searching, setSearching] = useState(false);
const [accountIds, setAccountIds] = useState<string[]>([]);
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(!!id);
const [mode, setMode] = useState<Mode>('remove');
useEffect(() => {
if (id) {
setLoading(true);
dispatch(fetchList(id));
void apiGetAccounts(id)

View File

@ -27,17 +27,15 @@ export const ListPanel: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const lists = useAppSelector((state) => getOrderedLists(state));
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
void dispatch(fetchLists()).then(() => {
setLoading(false);
return '';
});
}, [dispatch, setLoading]);
}, [dispatch]);
return (
<CollapsiblePanel

View File

@ -225,7 +225,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
/>
<div className='explore__search-header'>
<Search singleColumn initialValue={trimmedValue} />
<Search singleColumn initialValue={trimmedValue} key={trimmedValue} />
</div>
<div className='account__section-headline'>

View File

@ -295,7 +295,7 @@ export const RefreshController: React.FC<{
if (loadingState === 'loading') {
return (
<div
className='load-more load-gap'
className='load-more load-more--large'
aria-busy
aria-live='polite'
aria-label={intl.formatMessage(messages.loadingInitial)}

View File

@ -53,8 +53,6 @@ export const DomainBlockModal: React.FC<{
}, [dispatch]);
useEffect(() => {
setLoading(true);
apiRequest<DomainBlockPreviewResponse>('GET', 'v1/domain_blocks/preview', {
params: { domain },
timeout: 5000,
@ -68,7 +66,7 @@ export const DomainBlockModal: React.FC<{
setPreview('error');
setLoading(false);
});
}, [setPreview, setLoading, domain]);
}, [domain]);
return (
<div className='modal-root__modal safety-action-modal' aria-live='polite'>

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useIntl, defineMessages } from 'react-intl';
@ -41,40 +41,44 @@ const isHashtagLink = (
};
interface TargetParams {
hashtag?: string;
accountId?: string;
element: HTMLAnchorElement | null;
hashtag: string;
accountId: string;
}
export const HashtagMenuController: React.FC = () => {
const intl = useIntl();
const { signedIn } = useIdentity();
const [open, setOpen] = useState(false);
const [{ accountId, hashtag }, setTargetParams] = useState<TargetParams>({});
const targetRef = useRef<HTMLAnchorElement | null>(null);
const location = useLocation();
const [target, setTarget] = useState<TargetParams | null>(null);
const { element = null, accountId, hashtag } = target ?? {};
const open = !!element;
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
useEffect(() => {
setOpen(false);
targetRef.current = null;
}, [setOpen, location]);
const location = useLocation();
const [previousLocation, setPreviousLocation] = useState(location);
if (location !== previousLocation) {
setPreviousLocation(location);
setTarget(null);
}
useEffect(() => {
const handleClick = (e: MouseEvent) => {
const target = (e.target as HTMLElement).closest('a');
const targetElement = (e.target as HTMLElement).closest('a');
if (e.button !== 0 || e.ctrlKey || e.metaKey) {
return;
}
if (!isHashtagLink(target)) {
if (!isHashtagLink(targetElement)) {
return;
}
const hashtag = target.text.replace(/^#/, '');
const accountId = target.getAttribute('data-menu-hashtag');
const hashtag = targetElement.text.replace(/^#/, '');
const accountId = targetElement.getAttribute('data-menu-hashtag');
if (!hashtag || !accountId) {
return;
@ -82,9 +86,7 @@ export const HashtagMenuController: React.FC = () => {
e.preventDefault();
e.stopPropagation();
targetRef.current = target;
setOpen(true);
setTargetParams({ hashtag, accountId });
setTarget({ element: targetElement, hashtag, accountId });
};
document.addEventListener('click', handleClick, { capture: true });
@ -92,12 +94,11 @@ export const HashtagMenuController: React.FC = () => {
return () => {
document.removeEventListener('click', handleClick);
};
}, [setTargetParams, setOpen]);
}, []);
const handleClose = useCallback(() => {
setOpen(false);
targetRef.current = null;
}, [setOpen]);
setTarget(null);
}, []);
const menu = useMemo(() => {
const arr: MenuItem[] = [
@ -139,7 +140,7 @@ export const HashtagMenuController: React.FC = () => {
offset={offset}
placement='bottom'
flip
target={targetRef}
target={element}
popperConfig={popperConfig}
>
{({ props, arrowProps, placement }) => (

View File

@ -66,6 +66,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
_ref,
) => {
const [index, setIndex] = useState(startIndex);
const [zoomedIn, setZoomedIn] = useState(false);
const currentMedia = media.get(index);
const handleChangeIndex = useCallback(
@ -134,7 +135,6 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
}
}, []);
const [zoomedIn, setZoomedIn] = useState(false);
const zoomable =
currentMedia?.get('type') === 'image' &&
((currentMedia.getIn(['meta', 'original', 'width']) as number) >

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useSyncExternalStore } from 'react';
const breakpoints = {
narrow: 479, // Device width under which horizontal space is constrained
@ -9,25 +9,20 @@ const breakpoints = {
type Breakpoint = keyof typeof breakpoints;
export const useBreakpoint = (breakpoint: Breakpoint) => {
const [isMatching, setIsMatching] = useState(false);
const query = `(max-width: ${breakpoints[breakpoint]}px)`;
useEffect(() => {
const mediaWatcher = window.matchMedia(
`(max-width: ${breakpoints[breakpoint]}px)`,
);
const isMatching = useSyncExternalStore(
(callback) => {
const mediaWatcher = window.matchMedia(query);
setIsMatching(mediaWatcher.matches);
mediaWatcher.addEventListener('change', callback);
const handleChange = (e: MediaQueryListEvent) => {
setIsMatching(e.matches);
};
mediaWatcher.addEventListener('change', handleChange);
return () => {
mediaWatcher.removeEventListener('change', handleChange);
};
}, [breakpoint, setIsMatching]);
return () => {
mediaWatcher.removeEventListener('change', callback);
};
},
() => window.matchMedia(query).matches,
);
return isMatching;
};

View File

@ -1,4 +1,4 @@
import { useRef, useEffect } from 'react';
import { useState } from 'react';
/**
* Returns the previous state of the passed in value.
@ -6,11 +6,21 @@ import { useRef, useEffect } from 'react';
*/
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
const [{ previous, current }, setMemory] = useState<{
previous: T | undefined;
current: T;
}>(() => ({ previous: undefined, current: value }));
useEffect(() => {
ref.current = value;
}, [value]);
let result = previous;
return ref.current;
if (value !== current) {
setMemory({
previous: current,
current: value,
});
// Ensure that the returned result updates synchronously
result = current;
}
return result;
}

View File

@ -24,7 +24,7 @@
"account.blocking": "Блакіраванне",
"account.cancel_follow_request": "Скасаваць запыт на падпіску",
"account.copy": "Скапіраваць спасылку на профіль",
"account.direct": "Згадаць асабіста @{name}",
"account.direct": "Згадаць прыватна @{name}",
"account.disable_notifications": "Не паведамляць мне пра публікацыі @{name}",
"account.domain_blocking": "Блакіраванне дамена",
"account.edit_profile": "Рэдагаваць профіль",
@ -250,6 +250,10 @@
"confirmations.missing_alt_text.title": "Дадаць альтэрнатыўны тэкст?",
"confirmations.mute.confirm": "Ігнараваць",
"confirmations.private_quote_notify.cancel": "Звяртацца да рэдагавання",
"confirmations.private_quote_notify.confirm": "Апублікаваць допіс",
"confirmations.private_quote_notify.do_not_show_again": "Больш не паказваць мне гэтае паведамленне",
"confirmations.private_quote_notify.message": "Асоба, якую Вы цытуеце, і іншыя, хто быў узгаданы, атрымаюць апавяшчэнні і змогуць пабачыць Ваш допіс, нават калі яны не падпісаныя на Вас.",
"confirmations.private_quote_notify.title": "Падзяліцца з падпісчыкамі і ўзгаданымі карыстальнікамі?",
"confirmations.quiet_post_quote_info.dismiss": "Не нагадваць зноў",
"confirmations.quiet_post_quote_info.got_it": "Зразумела",
"confirmations.quiet_post_quote_info.message": "Калі будзеце цытаваць ціхі публічны допіс, Ваш допіс будзе схаваны ад трэндавых стужак.",
@ -761,6 +765,7 @@
"privacy_policy.title": "Палітыка канфідэнцыйнасці",
"quote_error.edit": "Нельга дадаваць цытаты пры рэдагаванні допісаў.",
"quote_error.poll": "Нельга цытаваць з апытаннямі.",
"quote_error.private_mentions": "Цытаванне не дазваляецца ў прамых узгадваннях.",
"quote_error.quote": "За раз дазволена рабіць толькі адну цытату.",
"quote_error.unauthorized": "Вы не ўвайшлі, каб цытаваць гэты допіс.",
"quote_error.upload": "Нельга цытаваць з медыя далучэннямі.",
@ -1014,6 +1019,8 @@
"video.volume_down": "Паменшыць гучнасць",
"video.volume_up": "Павялічыць гучнасць",
"visibility_modal.button_title": "Вызначыць бачнасць",
"visibility_modal.direct_quote_warning.text": "Калі Вы захавайце бягучыя налады, прымацаваная цытата будзе пераробленая ў спасылку.",
"visibility_modal.direct_quote_warning.title": "Цытаты нельга далучаць да прыватных узгадванняў",
"visibility_modal.header": "Бачнасць і ўзаемадзеянне",
"visibility_modal.helper.direct_quoting": "Прыватныя згадванні, створаныя на Mastodon, нельга цытаваць іншым людзям.",
"visibility_modal.helper.privacy_editing": "Бачнасць нельга змяніць у апублікаваным допісе.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Postio beth bynnag",
"confirmations.missing_alt_text.title": "Ychwanegu testun amgen?",
"confirmations.mute.confirm": "Tewi",
"confirmations.private_quote_notify.cancel": "Nôl i olygu",
"confirmations.private_quote_notify.confirm": "Cyhoeddi postiad",
"confirmations.private_quote_notify.do_not_show_again": "Peidio dangos y neges hon i mi eto",
"confirmations.private_quote_notify.message": "Bydd y person rydych chi'n ei ddyfynnu a chrybwylliadau eraill yn cael gwybod a bydd yn gallu gweld eich postiad, hyd yn oed os nad ydyn nhw'n eich dilyn chi.",
"confirmations.private_quote_notify.title": "Rhannu gyda dilynwyr a defnyddwyr sy'n cael eu crybwyll?",
"confirmations.quiet_post_quote_info.dismiss": "Peidio fy atgoff eto",
"confirmations.quiet_post_quote_info.got_it": "Iawn",
"confirmations.quiet_post_quote_info.message": "Wrth ddyfynnu postiad cyhoeddus tawel, bydd eich postiad yn cael ei guddio rhag llinellau amser sy'n trendio.",
@ -760,6 +765,7 @@
"privacy_policy.title": "Polisi Preifatrwydd",
"quote_error.edit": "Does dim modd ychwanegu dyfyniadau wrth olygu postiad.",
"quote_error.poll": "Dyw dyfynnu ddim yn cael ei ganiatáu gyda pholau.",
"quote_error.private_mentions": "Does dim caniatâd i ddyfynnu gyda chrybwylliadau uniongyrchol.",
"quote_error.quote": "Dim ond un dyfyniad ar y tro sy'n cael ei ganiatáu.",
"quote_error.unauthorized": "Does gennych chi ddim awdurdod i ddyfynnu'r postiad hwn.",
"quote_error.upload": "Dyw dyfynnu ddim yn cael ei ganiatáu gydag atodiadau cyfryngau.",
@ -1013,6 +1019,8 @@
"video.volume_down": "Lefel sain i lawr",
"video.volume_up": "Lefel sain i fyny",
"visibility_modal.button_title": "Gosod gwelededd",
"visibility_modal.direct_quote_warning.text": "Os byddwch chi'n cadw'r gosodiadau cyfredol, bydd y dyfyniad sydd wedi'i fewnosod yn cael ei drawsnewid yn ddolen.",
"visibility_modal.direct_quote_warning.title": "Does dim modd mewnblannu dyfyniadau mewn crybwylliadau preifat",
"visibility_modal.header": "Gwelededd a rhyngweithio",
"visibility_modal.helper.direct_quoting": "Does dim modd dyfynnu crybwylliadau preifat ysgrifennwyd ar Mastodon.",
"visibility_modal.helper.privacy_editing": "Does dim modd newid gwelededd ar ôl i bostiad gael ei gyhoeddi.",

View File

@ -43,7 +43,7 @@
"account.follow_back": "Ebenfalls folgen",
"account.follow_back_short": "Ebenfalls folgen",
"account.follow_request": "Anfrage zum Folgen",
"account.follow_request_cancel": "Anfrage zurückziehen",
"account.follow_request_cancel": "Anfrage abbrechen",
"account.follow_request_cancel_short": "Abbrechen",
"account.follow_request_short": "Anfragen",
"account.followers": "Follower",
@ -272,7 +272,7 @@
"confirmations.unfollow.confirm": "Entfolgen",
"confirmations.unfollow.title": "{name} entfolgen?",
"confirmations.withdraw_request.confirm": "Anfrage zurückziehen",
"confirmations.withdraw_request.title": "Anfrage zum Folgen von {name} zurückziehen?",
"confirmations.withdraw_request.title": "Anfrage zum Folgen von {name} widerrufen?",
"content_warning.hide": "Beitrag ausblenden",
"content_warning.show": "Trotzdem anzeigen",
"content_warning.show_more": "Beitrag anzeigen",
@ -882,7 +882,7 @@
"status.block": "@{name} blockieren",
"status.bookmark": "Lesezeichen setzen",
"status.cancel_reblog_private": "Beitrag nicht mehr teilen",
"status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren",
"status.cannot_quote": "Beitrag kann nicht zitiert werden",
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
"status.contains_quote": "Enthält Zitat",
"status.context.loading": "Weitere Antworten laden",
@ -933,7 +933,7 @@
"status.quote_manual_review": "Zitierte*r überprüft manuell",
"status.quote_noun": "Zitat",
"status.quote_policy_change": "Ändern, wer zitieren darf",
"status.quote_post_author": "Zitierte einen Beitrag von @{name}",
"status.quote_post_author": "Zitierter Beitrag von @{name}",
"status.quote_private": "Private Beiträge können nicht zitiert werden",
"status.quotes": "{count, plural, one {Mal zitiert} other {Mal zitiert}}",
"status.quotes.empty": "Diesen Beitrag hat bisher noch niemand zitiert. Sobald es jemand tut, wird das Profil hier erscheinen.",
@ -1021,7 +1021,7 @@
"visibility_modal.button_title": "Sichtbarkeit festlegen",
"visibility_modal.direct_quote_warning.text": "Wenn diese Einstellungen gespeichert werden, wird das eingebettete Zitat in einen Link umgewandelt.",
"visibility_modal.direct_quote_warning.title": "Zitate können in privaten Erwähnungen nicht eingebettet werden",
"visibility_modal.header": "Sichtbarkeit und Interaktion",
"visibility_modal.header": "Sichtbarkeit und Zitate",
"visibility_modal.helper.direct_quoting": "Private Erwähnungen, die auf Mastodon verfasst wurden, können nicht von anderen zitiert werden.",
"visibility_modal.helper.privacy_editing": "Die Sichtbarkeit eines bereits veröffentlichten Beitrags kann nachträglich nicht mehr geändert werden.",
"visibility_modal.helper.privacy_private_self_quote": "Beiträge mit privaten Erwähnungen können öffentlich nicht zitiert werden.",

View File

@ -28,6 +28,7 @@
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocking": "Blocking domain",
"account.edit_profile": "Edit profile",
"account.edit_profile_short": "Edit",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile",
"account.familiar_followers_many": "Followed by {name1}, {name2}, and {othersCount, plural, one {one other you know} other {# others you know}}",
@ -40,6 +41,11 @@
"account.featured_tags.last_status_never": "No posts",
"account.follow": "Follow",
"account.follow_back": "Follow back",
"account.follow_back_short": "Follow back",
"account.follow_request": "Request to follow",
"account.follow_request_cancel": "Cancel request",
"account.follow_request_cancel_short": "Cancel",
"account.follow_request_short": "Request",
"account.followers": "Followers",
"account.followers.empty": "No one follows this user yet.",
"account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}",
@ -151,6 +157,8 @@
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this screen.",
"bundle_modal_error.retry": "Try again",
"carousel.current": "<sr>Slide</sr> {current, number} / {max, number}",
"carousel.slide": "Slide {current, number} of {max, number}",
"closed_registrations.other_server_instructions": "Since Mastodon is decentralised, you can create an account on another server and still interact with this one.",
"closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
"closed_registrations_modal.find_another_server": "Find another server",
@ -167,6 +175,8 @@
"column.edit_list": "Edit list",
"column.favourites": "Favourites",
"column.firehose": "Live feeds",
"column.firehose_local": "Live feed for this server",
"column.firehose_singular": "Live feed",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.list_members": "Manage list members",
@ -186,6 +196,7 @@
"community.column_settings.local_only": "Local only",
"community.column_settings.media_only": "Media Only",
"community.column_settings.remote_only": "Remote only",
"compose.error.blank_post": "Post can't be blank.",
"compose.language.change": "Change language",
"compose.language.search": "Search languages...",
"compose.published.body": "Post published.",
@ -238,13 +249,30 @@
"confirmations.missing_alt_text.secondary": "Post anyway",
"confirmations.missing_alt_text.title": "Add alt text?",
"confirmations.mute.confirm": "Mute",
"confirmations.private_quote_notify.cancel": "Back to editing",
"confirmations.private_quote_notify.confirm": "Publish post",
"confirmations.private_quote_notify.do_not_show_again": "Don't show me this message again",
"confirmations.private_quote_notify.message": "The person you are quoting and other mentions will be notified and will be able to view your post, even if they're not following you.",
"confirmations.private_quote_notify.title": "Share with followers and mentioned users?",
"confirmations.quiet_post_quote_info.dismiss": "Don't remind me again",
"confirmations.quiet_post_quote_info.got_it": "Got it",
"confirmations.quiet_post_quote_info.message": "When quoting a quiet public post, your post will be hidden from trending timelines.",
"confirmations.quiet_post_quote_info.title": "Quoting quiet public posts",
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
"confirmations.redraft.title": "Delete & redraft post?",
"confirmations.remove_from_followers.confirm": "Remove follower",
"confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?",
"confirmations.remove_from_followers.title": "Remove follower?",
"confirmations.revoke_quote.confirm": "Remove post",
"confirmations.revoke_quote.message": "This action cannot be undone.",
"confirmations.revoke_quote.title": "Remove post?",
"confirmations.unblock.confirm": "Unblock",
"confirmations.unblock.title": "Unblock {name}?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.title": "Unfollow {name}?",
"confirmations.withdraw_request.confirm": "Withdraw request",
"confirmations.withdraw_request.title": "Withdraw request to follow {name}?",
"content_warning.hide": "Hide post",
"content_warning.show": "Show anyway",
"content_warning.show_more": "Show more",
@ -286,6 +314,7 @@
"domain_pill.your_handle": "Your handle:",
"domain_pill.your_server": "Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.",
"domain_pill.your_username": "Your unique identifier on this server. Its possible to find users with the same username on different servers.",
"dropdown.empty": "Select an option",
"embed.instructions": "Embed this post on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
@ -314,6 +343,7 @@
"empty_column.bookmarked_statuses": "You don't have any bookmarked posts yet. When you bookmark one, it will show up here.",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "You don't have any private mentions yet. When you send or receive one, it will show up here.",
"empty_column.disabled_feed": "This feed has been disabled by your server administrators.",
"empty_column.domain_blocks": "There are no blocked domains yet.",
"empty_column.explore_statuses": "Nothing is trending right now. Check back later!",
"empty_column.favourited_statuses": "You don't have any favourite posts yet. When you favourite one, it will show up here.",
@ -338,7 +368,9 @@
"explore.trending_links": "News",
"explore.trending_statuses": "Posts",
"explore.trending_tags": "Hashtags",
"featured_carousel.current": "<sr>Post</sr> {current, number} / {max, number}",
"featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}",
"featured_carousel.slide": "Post {current, number} of {max, number}",
"filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.",
"filter_modal.added.context_mismatch_title": "Context mismatch!",
"filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.",
@ -381,6 +413,7 @@
"follow_suggestions.who_to_follow": "Who to follow",
"followed_tags": "Followed hashtags",
"footer.about": "About",
"footer.about_this_server": "About",
"footer.directory": "Profiles directory",
"footer.get_app": "Get the app",
"footer.keyboard_shortcuts": "Keyboard shortcuts",
@ -438,10 +471,12 @@
"ignore_notifications_modal.private_mentions_title": "Ignore notifications from unsolicited Private Mentions?",
"info_button.label": "Help",
"info_button.what_is_alt_text": "<h1>What is alt text?</h1> <p>Alt text provides image descriptions for people with vision impairments, low-bandwidth connections, or those seeking extra context.</p> <p>You can improve accessibility and understanding for everyone by writing clear, concise, and objective alt text.</p> <ul> <li>Capture important elements</li> <li>Summarise text in images</li> <li>Use regular sentence structure</li> <li>Avoid redundant information</li> <li>Focus on trends and key findings in complex visuals (like diagrams or maps)</li> </ul>",
"interaction_modal.action": "To interact with {name}'s post, you need to sign into your account on whatever Mastodon server you use.",
"interaction_modal.go": "Go",
"interaction_modal.no_account_yet": "Don't have an account yet?",
"interaction_modal.on_another_server": "On a different server",
"interaction_modal.on_this_server": "On this server",
"interaction_modal.title": "Sign in to continue",
"interaction_modal.username_prompt": "E.g. {example}",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
@ -462,6 +497,7 @@
"keyboard_shortcuts.home": "Open home timeline",
"keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.load_more": "Focus \"Load more\" button",
"keyboard_shortcuts.local": "to open local timeline",
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.muted": "to open muted users list",
@ -470,6 +506,7 @@
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned posts list",
"keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.quote": "Quote post",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search",
@ -481,6 +518,8 @@
"keyboard_shortcuts.translate": "to translate a post",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "Move up in the list",
"learn_more_link.got_it": "Got it",
"learn_more_link.learn_more": "Learn more",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
@ -581,6 +620,7 @@
"notification.label.mention": "Mention",
"notification.label.private_mention": "Private mention",
"notification.label.private_reply": "Private reply",
"notification.label.quote": "{name} quoted your post",
"notification.label.reply": "Reply",
"notification.mention": "Mention",
"notification.mentioned_you": "{name} mentioned you",
@ -595,6 +635,7 @@
"notification.moderation_warning.action_suspend": "Your account has been suspended.",
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you voted in has ended",
"notification.quoted_update": "{name} edited a post you have quoted",
"notification.reblog": "{name} boosted your post",
"notification.reblog.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post",
"notification.relationships_severance_event": "Lost connections with {name}",
@ -638,6 +679,7 @@
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.poll": "Poll results:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.quote": "Quotes:",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
@ -713,10 +755,18 @@
"privacy.private.short": "Followers",
"privacy.public.long": "Anyone on and off Mastodon",
"privacy.public.short": "Public",
"privacy.quote.anyone": "{visibility}, anyone can quote",
"privacy.quote.disabled": "{visibility}, quotes disabled",
"privacy.quote.limited": "{visibility}, quotes limited",
"privacy.unlisted.additional": "This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.",
"privacy.unlisted.long": "Hidden from Mastodon search results, trending, and public timelines",
"privacy.unlisted.short": "Quiet public",
"privacy_policy.last_updated": "Last updated {date}",
"privacy_policy.title": "Privacy Policy",
"quote_error.edit": "Quotes cannot be added when editing a post.",
"quote_error.poll": "Quoting is not allowed with polls.",
"quote_error.private_mentions": "Quoting is not allowed with direct mentions.",
"quote_error.quote": "Only one quote at a time is allowed.",
"recommended": "Recommended",
"refresh": "Refresh",
"regeneration_indicator.please_stand_by": "Please stand by.",
@ -925,5 +975,13 @@
"video.skip_forward": "Skip forward",
"video.unmute": "Unmute",
"video.volume_down": "Volume down",
"video.volume_up": "Volume up"
"video.volume_up": "Volume up",
"visibility_modal.helper.unlisted_quoting": "When people quote you, their post will also be hidden from trending timelines.",
"visibility_modal.instructions": "Control who can interact with this post. You can also apply settings to all future posts by navigating to <link>Preferences > Posting defaults</link>.",
"visibility_modal.privacy_label": "Visibility",
"visibility_modal.quote_followers": "Followers only",
"visibility_modal.quote_label": "Who can quote",
"visibility_modal.quote_nobody": "Just me",
"visibility_modal.quote_public": "Anyone",
"visibility_modal.save": "Save"
}

View File

@ -742,10 +742,10 @@
"poll.refresh": "Refrescar",
"poll.reveal": "Ver resultados",
"poll.total_people": "{count, plural, one {# persona} other {# personas}}",
"poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
"poll.total_votes": "{count, plural, one {voto} other {votos}}",
"poll.vote": "Votar",
"poll.voted": "Votaste esta opción",
"poll.votes": "{votes, plural, one {# voto} other {# votos}}",
"poll.votes": "{votes, plural, one {voto} other {votos}}",
"poll_button.add_poll": "Agregar encuesta",
"poll_button.remove_poll": "Quitar encuesta",
"privacy.change": "Configurar privacidad del mensaje",
@ -760,7 +760,7 @@
"privacy.quote.limited": "{visibility}, citas limitadas",
"privacy.unlisted.additional": "Esto se comporta exactamente igual que con la configuración de privacidad de mensaje «Público», excepto que el mensaje no aparecerá en las líneas temporales en vivo, ni en las etiquetas, ni en la línea temporal «Explorá», ni en la búsqueda de Mastodon; incluso si optaste por hacer tu cuenta visible.",
"privacy.unlisted.long": "Oculto de los resultados de búsqueda, tendencias y líneas temporales públicas de Mastodon",
"privacy.unlisted.short": "Público silencioso",
"privacy.unlisted.short": "Público pero silencioso",
"privacy_policy.last_updated": "Última actualización: {date}",
"privacy_policy.title": "Política de privacidad",
"quote_error.edit": "Las citas no se pueden agregar al editar un mensaje.",
@ -935,7 +935,7 @@
"status.quote_policy_change": "Cambiá quién puede citar",
"status.quote_post_author": "Se citó un mensaje de @{name}",
"status.quote_private": "No se pueden citar los mensajes privados",
"status.quotes": "{count, plural, one {# voto} other {# votos}}",
"status.quotes": "{count, plural, one {voto} other {votos}}",
"status.quotes.empty": "Todavía nadie citó este mensaje. Cuando alguien lo haga, se mostrará acá.",
"status.quotes.local_other_disclaimer": "Las citas rechazadas por el autor no serán mostradas.",
"status.quotes.remote_other_disclaimer": "Solo las citas de {domain} están garantizadas de ser mostradas acá. Las citas rechazadas por el autor no serán mostradas.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Publier quand-même",
"confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
"confirmations.mute.confirm": "Masquer",
"confirmations.private_quote_notify.cancel": "Retour à l'édition",
"confirmations.private_quote_notify.confirm": "Publier",
"confirmations.private_quote_notify.do_not_show_again": "Ne plus afficher ce message",
"confirmations.private_quote_notify.message": "La personne citée et celles mentionnées seront notifiées et pourront voir le message, même si elles ne vous suivent pas.",
"confirmations.private_quote_notify.title": "Partager avec les personnes abonnées et mentionnées?",
"confirmations.quiet_post_quote_info.dismiss": "Ne plus me rappeler",
"confirmations.quiet_post_quote_info.got_it": "Compris",
"confirmations.quiet_post_quote_info.message": "Lorsque vous citez un message public silencieux, votre message sera caché des fils tendances.",
@ -760,6 +765,7 @@
"privacy_policy.title": "Politique de confidentialité",
"quote_error.edit": "Les citations ne peuvent pas être ajoutés lors de l'édition d'un message.",
"quote_error.poll": "Les citations ne sont pas autorisées avec les sondages.",
"quote_error.private_mentions": "La citation n'est pas autorisée avec les mentions privées.",
"quote_error.quote": "Une seule citation à la fois est autorisée.",
"quote_error.unauthorized": "Vous n'êtes pas autorisé⋅e à citer cette publication.",
"quote_error.upload": "La citation n'est pas autorisée avec un média joint.",
@ -1013,6 +1019,8 @@
"video.volume_down": "Baisser le volume",
"video.volume_up": "Augmenter le volume",
"visibility_modal.button_title": "Définir la visibilité",
"visibility_modal.direct_quote_warning.text": "Si vous enregistrez les paramètres actuels, la citation sera convertie en lien.",
"visibility_modal.direct_quote_warning.title": "Les citations ne peuvent pas être intégrées dans les mentions privées",
"visibility_modal.header": "Visibilité et interactions",
"visibility_modal.helper.direct_quoting": "Les mentions privées rédigées sur Mastodon ne peuvent pas être citées par d'autres personnes.",
"visibility_modal.helper.privacy_editing": "La visibilité ne peut pas être modifiée après la publication d'un message.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Publier quand-même",
"confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
"confirmations.mute.confirm": "Masquer",
"confirmations.private_quote_notify.cancel": "Retour à l'édition",
"confirmations.private_quote_notify.confirm": "Publier",
"confirmations.private_quote_notify.do_not_show_again": "Ne plus afficher ce message",
"confirmations.private_quote_notify.message": "La personne citée et celles mentionnées seront notifiées et pourront voir le message, même si elles ne vous suivent pas.",
"confirmations.private_quote_notify.title": "Partager avec les personnes abonnées et mentionnées?",
"confirmations.quiet_post_quote_info.dismiss": "Ne plus me rappeler",
"confirmations.quiet_post_quote_info.got_it": "Compris",
"confirmations.quiet_post_quote_info.message": "Lorsque vous citez un message public silencieux, votre message sera caché des fils tendances.",
@ -760,6 +765,7 @@
"privacy_policy.title": "Politique de confidentialité",
"quote_error.edit": "Les citations ne peuvent pas être ajoutés lors de l'édition d'un message.",
"quote_error.poll": "Les citations ne sont pas autorisées avec les sondages.",
"quote_error.private_mentions": "La citation n'est pas autorisée avec les mentions privées.",
"quote_error.quote": "Une seule citation à la fois est autorisée.",
"quote_error.unauthorized": "Vous n'êtes pas autorisé⋅e à citer cette publication.",
"quote_error.upload": "La citation n'est pas autorisée avec un média joint.",
@ -1013,6 +1019,8 @@
"video.volume_down": "Baisser le volume",
"video.volume_up": "Augmenter le volume",
"visibility_modal.button_title": "Définir la visibilité",
"visibility_modal.direct_quote_warning.text": "Si vous enregistrez les paramètres actuels, la citation sera convertie en lien.",
"visibility_modal.direct_quote_warning.title": "Les citations ne peuvent pas être intégrées dans les mentions privées",
"visibility_modal.header": "Visibilité et interactions",
"visibility_modal.helper.direct_quoting": "Les mentions privées rédigées sur Mastodon ne peuvent pas être citées par d'autres personnes.",
"visibility_modal.helper.privacy_editing": "La visibilité ne peut pas être modifiée après la publication d'un message.",

View File

@ -1,6 +1,7 @@
{
"about.blocks": "मॉडरेट सर्वर",
"about.contact": "कांटेक्ट:",
"about.default_locale": "Default",
"about.disclaimer": "मास्टोडन एक ओपन सोर्स सॉफ्टवेयर है, और मास्टोडन gGmbH का ट्रेडमार्क है।",
"about.domain_blocks.no_reason_available": "कारण उपलब्ध नहीं है!",
"about.domain_blocks.preamble": "मास्टोडन आम तौर पर आपको कंटेंट को देखने और फेडिवेर्से में किसी अन्य सर्वर से उपयोगकर्ताओं के साथ बातचीत करने की अनुमति देता है। ये अपवाद हैं जो इस विशेष सर्वर पर बनाए गए हैं।",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "就按呢PO出去",
"confirmations.missing_alt_text.title": "Kám beh加添說明文字",
"confirmations.mute.confirm": "消音",
"confirmations.private_quote_notify.cancel": "轉去編輯",
"confirmations.private_quote_notify.confirm": "發布PO文",
"confirmations.private_quote_notify.do_not_show_again": "Mài koh展示tsit ê訊息",
"confirmations.private_quote_notify.message": "Lí所引用kap提起ê ē受通知而且ē當看lí êPO文就算in無跟tuè汝。",
"confirmations.private_quote_notify.title": "Kám beh kap跟tuè lí ê hām所提起ê用者分享",
"confirmations.quiet_post_quote_info.dismiss": "請mài koh提醒我",
"confirmations.quiet_post_quote_info.got_it": "知ah",
"confirmations.quiet_post_quote_info.message": "Nā是引用無tī公共時間線內底êPO文lí êPO文bē當tī趨勢ê時間線顯示。",
@ -760,6 +765,7 @@
"privacy_policy.title": "隱私權政策",
"quote_error.edit": "佇編輯PO文ê時陣bē當加引文。",
"quote_error.poll": "有投票ê PO文bē當引用。",
"quote_error.private_mentions": "私訊bē當允准引用。",
"quote_error.quote": "Tsi̍t改kan-ta ē當引用tsi̍t篇PO文。",
"quote_error.unauthorized": "Lí bô權利引用tsit篇PO文。",
"quote_error.upload": "有媒體附件ê PO文無允准引用。",

View File

@ -765,6 +765,7 @@
"privacy_policy.title": "Privacybeleid",
"quote_error.edit": "Citaten kunnen niet tijdens het bewerken van een bericht worden toegevoegd.",
"quote_error.poll": "Het is niet mogelijk om polls te citeren.",
"quote_error.private_mentions": "Citaten zijn niet toegestaan met directe vermeldingen.",
"quote_error.quote": "Je kunt maar één bericht per keer citeren.",
"quote_error.unauthorized": "Je bent niet gemachtigd om dit bericht te citeren.",
"quote_error.upload": "Je kunt geen mediabijlage aan een bericht met een citaat toevoegen.",

View File

@ -28,6 +28,7 @@
"account.disable_notifications": "Не уведомлять о постах пользователя @{name}",
"account.domain_blocking": "Домен заблокирован",
"account.edit_profile": "Редактировать",
"account.edit_profile_short": "Редактировать",
"account.enable_notifications": "Уведомлять о постах пользователя @{name}",
"account.endorse": "Рекомендовать в профиле",
"account.familiar_followers_many": "В подписках у {name1}, {name2}, и ещё {othersCount, plural, one {# человека, которого вы знаете} other {# человек, которых вы знаете}}",
@ -40,6 +41,11 @@
"account.featured_tags.last_status_never": "Нет постов",
"account.follow": "Подписаться",
"account.follow_back": "Подписаться в ответ",
"account.follow_back_short": "Подписаться",
"account.follow_request": "Отправить запрос на подписку",
"account.follow_request_cancel": "Отозвать запрос",
"account.follow_request_cancel_short": "Отозвать",
"account.follow_request_short": "Отправить запрос",
"account.followers": "Подписчики",
"account.followers.empty": "На этого пользователя пока никто не подписан.",
"account.followers_counter": "{count, plural, one {{counter} подписчик} few {{counter} подписчика} other {{counter} подписчиков}}",
@ -167,6 +173,8 @@
"column.edit_list": "Редактировать список",
"column.favourites": "Избранное",
"column.firehose": "Живая лента",
"column.firehose_local": "Живая лента этого сервера",
"column.firehose_singular": "Живая лента",
"column.follow_requests": "Запросы на подписку",
"column.home": "Главная",
"column.list_members": "Пользователи в списке",
@ -186,6 +194,7 @@
"community.column_settings.local_only": "Только локальные",
"community.column_settings.media_only": "Только с медиафайлами",
"community.column_settings.remote_only": "Только с других серверов",
"compose.error.blank_post": "Нельзя опубликовать пустой пост.",
"compose.language.change": "Изменить язык",
"compose.language.search": "Найти язык...",
"compose.published.body": "Пост опубликован.",
@ -238,20 +247,30 @@
"confirmations.missing_alt_text.secondary": "Опубликовать",
"confirmations.missing_alt_text.title": "Добавить альтернативный текст?",
"confirmations.mute.confirm": "Игнорировать",
"confirmations.quiet_post_quote_info.dismiss": "Больше не показывать",
"confirmations.private_quote_notify.cancel": "Вернуться к редактированию",
"confirmations.private_quote_notify.confirm": "Опубликовать",
"confirmations.private_quote_notify.do_not_show_again": "Не показывать это сообщение снова",
"confirmations.private_quote_notify.message": "Пользователь, которого вы процитировали, а также другие упомянутые пользователи будут уведомлены и смогут просмотреть ваш пост, даже если они не подписаны на вас.",
"confirmations.private_quote_notify.title": "Поделиться с подписчиками и упомянутыми пользователями?",
"confirmations.quiet_post_quote_info.dismiss": "Больше не напоминать",
"confirmations.quiet_post_quote_info.got_it": "Понятно",
"confirmations.quiet_post_quote_info.message": "Если вы цитируете публикацию, опубликованную в открытом доступе, ваша публикация будет скрыта от новостных лент.",
"confirmations.quiet_post_quote_info.title": "Цитирование тихих публичных постов",
"confirmations.quiet_post_quote_info.message": "Если ваш пост содержит цитирование «тихого публичного» поста, он будет скрыт из алгоритмических лент.",
"confirmations.quiet_post_quote_info.title": "Цитирование «тихих публичных» постов",
"confirmations.redraft.confirm": "Удалить и исправить",
"confirmations.redraft.message": "Вы уверены, что хотите удалить этот пост и создать его заново? Взаимодействия, такие как добавление в избранное и продвижение, будут потеряны, а ответы к оригинальному посту перестанут на него ссылаться.",
"confirmations.redraft.title": "Удалить и создать пост заново?",
"confirmations.remove_from_followers.confirm": "Убрать подписчика",
"confirmations.remove_from_followers.message": "Пользователь {name} перестанет быть подписан на вас. Продолжить?",
"confirmations.remove_from_followers.title": "Убрать подписчика?",
"confirmations.revoke_quote.confirm": "Удалить публикацию",
"confirmations.revoke_quote.message": "Это действие нельзя будет отменить.",
"confirmations.revoke_quote.title": "Удалить публикацию?",
"confirmations.revoke_quote.confirm": "Убрать пост",
"confirmations.revoke_quote.message": "Это действие невозможно отменить.",
"confirmations.revoke_quote.title": "Убрать пост?",
"confirmations.unblock.confirm": "Разблокировать",
"confirmations.unblock.title": "Разблокировать {name}?",
"confirmations.unfollow.confirm": "Отписаться",
"confirmations.unfollow.title": "Отписаться от {name}?",
"confirmations.withdraw_request.confirm": "Отозвать запрос",
"confirmations.withdraw_request.title": "Отозвать запрос на подписку на {name}?",
"content_warning.hide": "Скрыть пост",
"content_warning.show": "Всё равно показать",
"content_warning.show_more": "Развернуть",
@ -322,6 +341,7 @@
"empty_column.bookmarked_statuses": "У вас пока нет закладок. Когда вы добавляете пост в закладки, он появляется здесь.",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
"empty_column.direct": "Вы ещё не упоминали кого-либо и сами не были ни разу упомянуты лично. Все личные упоминания будут показаны здесь.",
"empty_column.disabled_feed": "Эта лента была отключена администраторами вашего сервера.",
"empty_column.domain_blocks": "Заблокированных доменов пока нет.",
"empty_column.explore_statuses": "Сейчас нет популярных постов. Проверьте позже!",
"empty_column.favourited_statuses": "Вы ещё не добавили ни одного поста в избранное. Все добавленные вами в избранное посты будут показаны здесь.",
@ -346,7 +366,9 @@
"explore.trending_links": "Новости",
"explore.trending_statuses": "Посты",
"explore.trending_tags": "Хештеги",
"featured_carousel.current": "<sr>Пост</sr> {current, number} / {max, number}",
"featured_carousel.header": "{count, plural, other {Закреплённые посты}}",
"featured_carousel.slide": "Пост {current, number} из {max, number}",
"filter_modal.added.context_mismatch_explanation": "Этот фильтр не применяется в том контексте, в котором вы видели этот пост. Если вы хотите, чтобы пост был отфильтрован в текущем контексте, необходимо редактировать фильтр.",
"filter_modal.added.context_mismatch_title": "Несоответствие контекста",
"filter_modal.added.expired_explanation": "Этот фильтр истёк. Чтобы он был применён, вам нужно изменить срок действия фильтра.",
@ -389,6 +411,7 @@
"follow_suggestions.who_to_follow": "На кого подписаться",
"followed_tags": "Подписки на хештеги",
"footer.about": "О проекте",
"footer.about_this_server": "Об этом сервере",
"footer.directory": "Каталог профилей",
"footer.get_app": "Скачать приложение",
"footer.keyboard_shortcuts": "Сочетания клавиш",
@ -472,7 +495,7 @@
"keyboard_shortcuts.home": "перейти к домашней ленте",
"keyboard_shortcuts.hotkey": "Горячая клавиша",
"keyboard_shortcuts.legend": "показать эту справку",
"keyboard_shortcuts.load_more": "Акцент на кнопке «Загрузить ещё»",
"keyboard_shortcuts.load_more": "фокус на кнопке «Загрузить ещё»",
"keyboard_shortcuts.local": "перейти к локальной ленте",
"keyboard_shortcuts.mention": "упомянуть автора поста",
"keyboard_shortcuts.muted": "открыть список игнорируемых пользователей",
@ -481,7 +504,7 @@
"keyboard_shortcuts.open_media": "открыть медиа",
"keyboard_shortcuts.pinned": "перейти к закреплённым постам",
"keyboard_shortcuts.profile": "перейти к профилю автора",
"keyboard_shortcuts.quote": "Цитировать пост",
"keyboard_shortcuts.quote": "цитировать пост",
"keyboard_shortcuts.reply": "ответить",
"keyboard_shortcuts.requests": "перейти к запросам на подписку",
"keyboard_shortcuts.search": "перейти к поиску",
@ -730,14 +753,15 @@
"privacy.private.short": "Для подписчиков",
"privacy.public.long": "Для кого угодно в интернете",
"privacy.public.short": "Публичный",
"privacy.quote.anyone": "{visibility}, каждый может процитировать",
"privacy.quote.disabled": "{visibility}, цитаты отключены",
"privacy.quote.limited": "{visibility}, цитаты ограничены",
"privacy.quote.anyone": "{visibility}, цитировать разрешено всем",
"privacy.quote.disabled": "{visibility}, без возможности цитирования",
"privacy.quote.limited": "{visibility}, с ограничениями цитирования",
"privacy.unlisted.additional": "Похоже на «Публичный» за исключением того, что пост не появится ни в живых лентах, ни в лентах хештегов, ни в разделе «Актуальное», ни в поиске Mastodon, даже если вы разрешили поиск по своим постам в настройках профиля.",
"privacy.unlisted.long": "Скрыто от результатов поиска Mastodon, трендов и публичных графиков",
"privacy.unlisted.long": "Не показывать в результатах поиска Mastodon, трендах и публичных лентах",
"privacy.unlisted.short": "Тихий публичный",
"privacy_policy.last_updated": "Последнее обновление: {date}",
"privacy_policy.title": "Политика конфиденциальности",
"quote_error.edit": "Нельзя добавить цитирование к уже опубликованному посту.",
"quote_error.poll": "Цитирование не допускается при голосовании.",
"quote_error.quote": "Одновременно допускается только одна цитата.",
"quote_error.unauthorized": "Вы не имеете права цитировать этот пост.",
@ -759,7 +783,6 @@
"relative_time.today": "сегодня",
"remove_quote_hint.button_label": "Понятно",
"remove_quote_hint.message": "Вы можете сделать это из меню настроек {icon}.",
"remove_quote_hint.title": "Хотите удалить цитируемый вами пост?",
"reply_indicator.attachments": "{count, plural, one {# вложение} few {# вложения} other {# вложений}}",
"reply_indicator.cancel": "Отмена",
"reply_indicator.poll": "Опрос",
@ -851,15 +874,22 @@
"status.admin_account": "Открыть интерфейс модератора для @{name}",
"status.admin_domain": "Открыть интерфейс модератора для {domain}",
"status.admin_status": "Открыть этот пост в интерфейсе модератора",
"status.all_disabled": "Бусты и цитаты отключены",
"status.all_disabled": "Нельзя продвинуть или процитировать",
"status.block": "Заблокировать @{name}",
"status.bookmark": "Добавить в закладки",
"status.cancel_reblog_private": "Отменить продвижение",
"status.cannot_quote": "Вы не можете процитировать этот пост",
"status.cannot_reblog": "Этот пост не может быть продвинут",
"status.context.loading": "Загрузка ответов",
"status.context.loading_error": "Не удалось загрузить новые ответы",
"status.context.loading_success": "Загружены новые ответы",
"status.context.more_replies_found": "Найдены другие ответы",
"status.context.retry": "Повторить",
"status.context.show": "Показать",
"status.continued_thread": "Продолжение предыдущего поста",
"status.copy": "Скопировать ссылку на пост",
"status.delete": "Удалить",
"status.delete.success": "Пост удален",
"status.delete.success": "Пост удалён",
"status.detailed_status": "Подробный просмотр обсуждения",
"status.direct": "Упомянуть @{name} лично",
"status.direct_indicator": "Личное упоминание",
@ -884,22 +914,28 @@
"status.pin": "Закрепить в профиле",
"status.quote": "Цитировать",
"status.quote.cancel": "Отменить цитирование",
"status.quote_error.blocked_account_hint.title": "Этот пост был скрыт, поскольку вы заблокировали @{name}.",
"status.quote_error.blocked_domain_hint.title": "Этот пост был скрыт, поскольку вы заблокировали сервер {domain}.",
"status.quote_error.filtered": "Скрыто одним из ваших фильтров",
"status.quote_error.limited_account_hint.action": "Всё равно показать",
"status.quote_error.limited_account_hint.title": "Этот профиль был скрыт модераторами сервера {domain}.",
"status.quote_error.muted_account_hint.title": "Этот пост был скрыт, поскольку вы игнорируете @{name}.",
"status.quote_error.not_available": "Пост недоступен",
"status.quote_error.pending_approval": "Пост ожидает подтверждения",
"status.quote_error.pending_approval_popout.body": "На Mastodon вы можете контролировать, будет ли кто-то цитировать вас. Этот пост находится на рассмотрении, пока мы не получим одобрение автора.",
"status.quote_error.revoked": "Сообщение удалено автором",
"status.quote_error.revoked": "Пост удалён автором",
"status.quote_followers_only": "Только подписчики могут цитировать этот пост",
"status.quote_manual_review": "Автор будет проверять вручную",
"status.quote_policy_change": "Измените, кто может цитировать",
"status.quote_post_author": "Пост цитировал @{name}",
"status.quote_noun": "Цитата",
"status.quote_policy_change": "Изменить настройки цитирования",
"status.quote_post_author": "Процитировал(а) пост @{name}",
"status.quote_private": "Приватные записи не могут быть цитированы",
"status.quotes": "{count, plural, one {# голос} few {# голоса} many {# голосов} other {# голосов}}",
"status.quotes.empty": "Никто еще не цитировал этот пост. Когда кто-нибудь это сделает, он появится здесь.",
"status.quotes": "{count, plural, one {цитирование} few {цитирования} other {цитирований}}",
"status.quotes.empty": "Никто еще не процитировал этот пост. Все цитирования этого поста будут показаны здесь.",
"status.read_more": "Читать далее",
"status.reblog": "Продвинуть",
"status.reblog_or_quote": "Буст или цитата",
"status.reblog_private": "Поделиться снова со своими подписчиками",
"status.reblog_or_quote": "Продвинуть или процитировать",
"status.reblog_private": "Поделиться со своими подписчиками ещё раз",
"status.reblogged_by": "{name} продвинул(а)",
"status.reblogs": "{count, plural, one {продвижение} few {продвижения} other {продвижений}}",
"status.reblogs.empty": "Никто ещё не продвинул этот пост. Все пользователи, которые продвинут этот пост, будут показаны здесь.",
@ -913,7 +949,7 @@
"status.replyAll": "Ответить в обсуждении",
"status.report": "Пожаловаться на @{name}",
"status.request_quote": "Запрос на цитирование",
"status.revoke_quote": "Удалить мой пост из поста @{name}",
"status.revoke_quote": "Убрать мой пост из поста @{name}",
"status.sensitive_warning": "Медиа деликатного характера",
"status.share": "Поделиться",
"status.show_less_all": "Свернуть все предупреждения о содержании в ветке",
@ -951,7 +987,7 @@
"upload_button.label": "Прикрепить фото, видео или аудио",
"upload_error.limit": "Превышено максимальное количество вложений.",
"upload_error.poll": "К опросам нельзя прикреплять файлы.",
"upload_error.quote": "К опросам нельзя прикреплять файлы.",
"upload_error.quote": "К цитированиям нельзя прикреплять файлы.",
"upload_form.drag_and_drop.instructions": "Чтобы выбрать вложение, нажмите \"Пробел\" (Space) или \"Ввод\" (Enter). Используйте клавиши со стрелками, чтобы передвинуть вложение в любом направлении. Нажмите \"Пробел\" (Space) или \"Ввод\" (Enter) ещё раз, чтобы переместить вложение на новое место, либо нажмите кнопку \"Выйти\" (Escape) для отмены перемещения.",
"upload_form.drag_and_drop.on_drag_cancel": "Перемещение отменено. Вложение {item} было оставлено на прежнем месте.",
"upload_form.drag_and_drop.on_drag_end": "Вложение {item} было перемещено.",
@ -976,12 +1012,19 @@
"video.volume_down": "Громкость уменьшена",
"video.volume_up": "Громкость увеличена",
"visibility_modal.button_title": "Настроить видимость",
"visibility_modal.direct_quote_warning.text": "Если вы сохраните эти настройки, вложенное цитирование будет преобразовано в ссылку.",
"visibility_modal.direct_quote_warning.title": "Нельзя добавить цитирование к личным упоминаниям",
"visibility_modal.header": "Видимость и взаимодействие",
"visibility_modal.helper.direct_quoting": "Частные упоминания, созданные на Mastodon не могут быть цитированы другими.",
"visibility_modal.helper.privacy_editing": "После публикации поста его видимость нельзя изменить.",
"visibility_modal.helper.privacy_editing": "Видимость поста невозможно изменить после публикации.",
"visibility_modal.helper.privacy_private_self_quote": "Цитаты из личных сообщений не могут быть опубликованы публично.",
"visibility_modal.helper.private_quoting": "Публикации на Mastodon, доступные исключительно подписчикам, не подлежат цитированию со стороны других пользователей.",
"visibility_modal.helper.unlisted_quoting": "Когда люди цитируют вас, их посты также скрываются из ленты трендов.",
"visibility_modal.instructions": "Регулируйте, кто сможет взаимодействовать с этим сообщением. Также можно установить параметры для всех будущих публикаций, перейдя в <link>Настройки > Стандартные параметры публикации</link>.",
"visibility_modal.privacy_label": "Видимость"
"visibility_modal.privacy_label": "Видимость",
"visibility_modal.quote_followers": "Только подписчики",
"visibility_modal.quote_label": "Кто может цитировать вас",
"visibility_modal.quote_nobody": "Только я",
"visibility_modal.quote_public": "Кто угодно",
"visibility_modal.save": "Сохранить"
}

View File

@ -192,6 +192,7 @@
"community.column_settings.local_only": "Iba miestne",
"community.column_settings.media_only": "Iba médiá",
"community.column_settings.remote_only": "Iba vzdialené",
"compose.error.blank_post": "Príspevok nemôže byť prázdny.",
"compose.language.change": "Zmeniť jazyk",
"compose.language.search": "Vyhľadávať jazyky…",
"compose.published.body": "Príspevok zverejnený.",
@ -226,6 +227,7 @@
"confirmations.delete_list.title": "Vymazať zoznam?",
"confirmations.discard_draft.confirm": "Zahodiť a pokračovať",
"confirmations.discard_draft.edit.cancel": "Pokračovať v úpravách",
"confirmations.discard_draft.post.cancel": "Pokračuj v rozpísanom",
"confirmations.discard_edit_media.confirm": "Zahodiť",
"confirmations.discard_edit_media.message": "Máte neuložené zmeny v popise alebo náhľade média, zahodiť ich aj tak?",
"confirmations.follow_to_list.confirm": "Nasleduj a pridaj do zoznamu",
@ -239,14 +241,21 @@
"confirmations.missing_alt_text.secondary": "Aj tak uverejniť",
"confirmations.missing_alt_text.title": "Pridať opis?",
"confirmations.mute.confirm": "Stíšiť",
"confirmations.private_quote_notify.cancel": "Späť na úpravu",
"confirmations.private_quote_notify.confirm": "Zverejni príspevok",
"confirmations.private_quote_notify.do_not_show_again": "Neukazuj mi túto spravu znova",
"confirmations.quiet_post_quote_info.dismiss": "Nepripomínaj mi znova",
"confirmations.redraft.confirm": "Vymazať a prepísať",
"confirmations.redraft.message": "Určite chcete tento príspevok vymazať a prepísať? Prídete o jeho zdieľania a ohviezdičkovania a odpovede na pôvodný príspevok budú odlúčené.",
"confirmations.redraft.title": "Vymazať a prepísať príspevok?",
"confirmations.remove_from_followers.confirm": "Odstrániť nasledovateľa",
"confirmations.revoke_quote.confirm": "Vymazať príspevok",
"confirmations.revoke_quote.message": "Tento úkon sa nedá navrátiť.",
"confirmations.revoke_quote.title": "Vymazať príspevok?",
"confirmations.unblock.confirm": "Odblokovať",
"confirmations.unblock.title": "Odblokovať {name}?",
"confirmations.unfollow.confirm": "Zrušiť sledovanie",
"confirmations.unfollow.title": "Prestať sledovať {name}?",
"content_warning.hide": "Skryť príspevok",
"content_warning.show": "Aj tak zobraziť",
"content_warning.show_more": "Ukázať viac",
@ -277,6 +286,7 @@
"domain_pill.server": "Server",
"domain_pill.their_server": "Ich digitálny domov, kde žijú všetky ich príspevky.",
"domain_pill.username": "Používateľské meno",
"dropdown.empty": "Vyber možnosť",
"embed.instructions": "Tento príspevok môžete pridať na svoju webovú stránku použitím tohto kódu.",
"embed.preview": "Takto bude vyzerať:",
"emoji_button.activity": "Aktivita",
@ -377,6 +387,7 @@
"getting_started.heading": "Začíname",
"hashtag.admin_moderation": "Otvor moderovacie rozhranie pre #{name}",
"hashtag.browse": "Prehľadávať príspevky pod #{hashtag}",
"hashtag.browse_from_account": "Prehľadávať príspevky od @{name} pre #{hashtag}",
"hashtag.column_header.tag_mode.all": "a {additional}",
"hashtag.column_header.tag_mode.any": "alebo {additional}",
"hashtag.column_header.tag_mode.none": "bez {additional}",
@ -460,6 +471,7 @@
"keyboard_shortcuts.translate": "Preložiť príspevok",
"keyboard_shortcuts.unfocus": "Odísť z textového poľa",
"keyboard_shortcuts.up": "Posunúť sa vyššie v zozname",
"learn_more_link.learn_more": "Zistiť viac",
"lightbox.close": "Zatvoriť",
"lightbox.next": "Ďalej",
"lightbox.previous": "Späť",
@ -542,6 +554,7 @@
"notification.label.mention": "Označenie",
"notification.label.private_mention": "Súkromné označenie",
"notification.label.private_reply": "Súkromná odpoveď",
"notification.label.quote": "{name} citoval/a tvoj príspevok",
"notification.label.reply": "Odpoveď",
"notification.mention": "Označenie",
"notification.mentioned_you": "{name} ťa spomenul/a",
@ -556,6 +569,7 @@
"notification.moderation_warning.action_suspend": "Tvoj účet bol pozastavený.",
"notification.own_poll": "Vaša anketa sa skončila",
"notification.poll": "Anketa, v ktorej si hlasoval/a, skončila",
"notification.quoted_update": "{name} upravil/a príspevok, ktorý si citoval/a",
"notification.reblog": "{name} zdieľa váš príspevok",
"notification.reblog.name_and_others_with_link": "{name} a <a>{count, plural, one {# ďalší človek} few {# ďalší ľudia} many {#} other {# ďalších ľudí}}</a> zdieľa váš príspevok",
"notification.relationships_severance_event": "Stratené prepojenia s {name}",
@ -585,6 +599,7 @@
"notifications.column_settings.mention": "Označenia:",
"notifications.column_settings.poll": "Výsledky ankety:",
"notifications.column_settings.push": "Upozornenia push",
"notifications.column_settings.quote": "Citácie:",
"notifications.column_settings.reblog": "Zdieľania:",
"notifications.column_settings.show": "Zobraziť v stĺpci",
"notifications.column_settings.sound": "Prehrať zvuk",
@ -655,10 +670,16 @@
"privacy.private.short": "Sledovatelia",
"privacy.public.long": "Ktokoľvek na Mastodone aj mimo neho",
"privacy.public.short": "Verejné",
"privacy.quote.anyone": "{visibility}, hocikto môže citovať",
"privacy.quote.disabled": "{visibility}, citovanie nepovolené",
"privacy.quote.limited": "{visibility}, citovanie obmedzené",
"privacy.unlisted.additional": "Presne ako verejné, s tým rozdielom, že sa príspevok nezobrazí v živých kanáloch, hashtagoch, objavovaní či vo vyhľadávaní na Mastodone, aj keď máte pre účet objaviteľnosť zapnutú.",
"privacy.unlisted.short": "Tiché verejné",
"privacy_policy.last_updated": "Posledná úprava {date}",
"privacy_policy.title": "Pravidlá ochrany súkromia",
"quote_error.poll": "Citovanie ankiet nieje povolené.",
"quote_error.quote": "Je povolená iba jedna citácia súčasne.",
"quote_error.unauthorized": "Nemáš oprávnenie citovať tento príspevok.",
"recommended": "Odporúčané",
"refresh": "Obnoviť",
"regeneration_indicator.please_stand_by": "Prosím, čakajte.",
@ -674,6 +695,7 @@
"relative_time.minutes": "{number} min",
"relative_time.seconds": "{number} sek",
"relative_time.today": "Dnes",
"remove_quote_hint.title": "Chceš vymazať svoju citáciu príspevku?",
"reply_indicator.attachments": "{count, plural, one {# príloha} few {# prílohy} other {# príloh}}",
"reply_indicator.cancel": "Zrušiť",
"reply_indicator.poll": "Anketa",

View File

@ -31,7 +31,7 @@
"account.edit_profile_short": "編輯",
"account.enable_notifications": "當 @{name} 嘟文時通知我",
"account.endorse": "於個人檔案推薦對方",
"account.familiar_followers_many": "被 {name1}、{name2}、及 {othersCount, plural, other {其他您認識的 # 人}} 跟隨",
"account.familiar_followers_many": "被 {name1}、{name2}、及{othersCount, plural, other {其他您認識的 # 人}} 跟隨",
"account.familiar_followers_one": "被 {name1} 跟隨",
"account.familiar_followers_two": "被 {name1} 與 {name2} 跟隨",
"account.featured": "精選內容",
@ -48,10 +48,10 @@
"account.follow_request_short": "跟隨請求",
"account.followers": "跟隨者",
"account.followers.empty": "尚未有人跟隨這位使用者。",
"account.followers_counter": "被 {count, plural, other {{count} 人}}跟隨",
"account.followers_counter": "被 {count, plural, other {{counter} 人}}跟隨",
"account.followers_you_know_counter": "{counter} 位您知道的跟隨者",
"account.following": "跟隨中",
"account.following_counter": "正在跟隨 {count,plural,other {{count} 人}}",
"account.following_counter": "正在跟隨 {count,plural,other {{counter} 人}}",
"account.follows.empty": "這位使用者尚未跟隨任何人。",
"account.follows_you": "已跟隨您",
"account.go_to_profile": "前往個人檔案",
@ -80,7 +80,7 @@
"account.requests_to_follow_you": "要求跟隨您",
"account.share": "分享 @{name} 的個人檔案",
"account.show_reblogs": "顯示來自 @{name} 的轉嘟",
"account.statuses_counter": "{count, plural, other {{count} 則嘟文}}",
"account.statuses_counter": "{count, plural, other {{counter} 則嘟文}}",
"account.unblock": "解除封鎖 @{name}",
"account.unblock_domain": "解除封鎖網域 {domain}",
"account.unblock_domain_short": "解除封鎖",
@ -104,9 +104,9 @@
"alert.rate_limited.title": "已限速",
"alert.unexpected.message": "發生非預期的錯誤。",
"alert.unexpected.title": "哎呀!",
"alt_text_badge.title": "ALT 說明文字",
"alt_text_modal.add_alt_text": "新增 ALT 說明文字",
"alt_text_modal.add_text_from_image": "自圖片新增 ALT 說明文字",
"alt_text_badge.title": "替代文字",
"alt_text_modal.add_alt_text": "新增替代文字",
"alt_text_modal.add_text_from_image": "自圖片新增替代文字",
"alt_text_modal.cancel": "取消",
"alt_text_modal.change_thumbnail": "變更預覽圖",
"alt_text_modal.describe_for_people_with_hearing_impairments": "替聽覺障礙人士描述...",
@ -244,10 +244,10 @@
"confirmations.logout.confirm": "登出",
"confirmations.logout.message": "您確定要登出嗎?",
"confirmations.logout.title": "您確定要登出嗎?",
"confirmations.missing_alt_text.confirm": "新增 ALT 說明文字",
"confirmations.missing_alt_text.message": "您的嘟文中的多媒體內容未附上 ALT 說明文字。添加說明文字描述能幫助更多人存取您的內容。",
"confirmations.missing_alt_text.confirm": "新增替代文字",
"confirmations.missing_alt_text.message": "您的嘟文中的多媒體內容未附上替代文字。添加描述能幫助更多人存取您的內容。",
"confirmations.missing_alt_text.secondary": "仍要發嘟",
"confirmations.missing_alt_text.title": "是否新增 ALT 說明文字?",
"confirmations.missing_alt_text.title": "是否新增替代文字?",
"confirmations.mute.confirm": "靜音",
"confirmations.private_quote_notify.cancel": "返回至編輯",
"confirmations.private_quote_notify.confirm": "發表嘟文",
@ -435,9 +435,9 @@
"hashtag.column_settings.tag_mode.any": "任一",
"hashtag.column_settings.tag_mode.none": "全不",
"hashtag.column_settings.tag_toggle": "將額外標籤加入到這個欄位",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} 名} other {{counter} 名}}參與者",
"hashtag.counter_by_uses": "{count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
"hashtag.counter_by_uses_today": "本日有 {count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
"hashtag.counter_by_accounts": "{count, plural, other {{counter} 名參與者}}",
"hashtag.counter_by_uses": "{count, plural, other {{counter} 則嘟文}}",
"hashtag.counter_by_uses_today": "本日有 {count, plural, other {{counter} 則嘟文}}",
"hashtag.feature": "於個人檔案推薦",
"hashtag.follow": "跟隨主題標籤",
"hashtag.mute": "靜音 #{hashtag}",
@ -470,7 +470,7 @@
"ignore_notifications_modal.not_following_title": "忽略來自您未跟隨帳號之推播通知?",
"ignore_notifications_modal.private_mentions_title": "忽略來自不請自來私訊之推播通知?",
"info_button.label": "幫助",
"info_button.what_is_alt_text": "<h1>何謂 ALT 說明文字?</h1> <p>ALT 說明文字為視覺障礙者、低網路頻寬或尋求額外上下文語境的人們提供圖片描述。</p> <p>您可以透過撰寫清晰、簡潔及客觀的說明文字以替所有人改善無障礙特性與協助理解。</p> <ul> <li>掌握幾個重要元素</li> <li>替圖片提供文字摘要</li> <li>使用常規行文結構</li> <li>避免冗贅資訊</li> <li>聚焦於趨勢與複雜視覺中之關鍵(如圖表或地圖)</li> </ul>",
"info_button.what_is_alt_text": "<h1>何謂替代文字?</h1> <p>替代文字為視覺障礙者、低網路頻寬或尋求額外上下文語境的人們提供圖片描述。</p> <p>您可以透過撰寫清晰、簡潔及客觀的替代文字以替所有人改善無障礙特性與協助理解。</p> <ul> <li>掌握幾個重要元素</li> <li>替圖片提供文字摘要</li> <li>使用常規行文結構</li> <li>避免冗贅資訊</li> <li>聚焦於趨勢與複雜視覺中之關鍵(如圖表或地圖)</li> </ul>",
"interaction_modal.action": "若欲與 {name} 之嘟文互動,您必須登入您帳號所註冊之 Mastodon 伺服器。",
"interaction_modal.go": "Go!",
"interaction_modal.no_account_yet": "仍尚未有帳號嗎?",
@ -529,7 +529,7 @@
"limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。",
"link_preview.author": "來自 {name}",
"link_preview.more_from_author": "來自 {name} 之更多內容",
"link_preview.shares": "{count, plural, other {{count} 則嘟文}}",
"link_preview.shares": "{count, plural, other {{counter} 則嘟文}}",
"lists.add_member": "新增",
"lists.add_to_list": "新增至列表",
"lists.add_to_lists": "新增 {name} 至列表",
@ -561,7 +561,7 @@
"moved_to_account_banner.text": "您的帳號 {disabledAccount} 目前已停用,因為您已搬家至 {movedToAccount}。",
"mute_modal.hide_from_notifications": "於推播通知中隱藏",
"mute_modal.hide_options": "隱藏選項",
"mute_modal.indefinite": "直到我解除靜音他們",
"mute_modal.indefinite": "直到我解除靜音",
"mute_modal.show_options": "顯示選項",
"mute_modal.they_can_mention_and_follow": "他們仍可提及或跟隨您,但您不會見到他們。",
"mute_modal.they_wont_know": "他們不會知道他們已被靜音。",
@ -649,10 +649,10 @@
"notification_requests.accept_multiple": "{count, plural, other {接受 # 則請求...}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, other {接受請求}}",
"notification_requests.confirm_accept_multiple.message": "您將接受 {count, plural, other {# 則推播通知請求}}。您確定要繼續?",
"notification_requests.confirm_accept_multiple.title": "接受推播通知請求?",
"notification_requests.confirm_accept_multiple.title": "是否接受推播通知請求?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, other {忽略請求}}",
"notification_requests.confirm_dismiss_multiple.message": "您將忽略 {count, plural, other {# 則推播通知請求}}。您將不再能輕易存取{count, plural, other {這些}}推播通知。您確定要繼續?",
"notification_requests.confirm_dismiss_multiple.title": "忽略推播通知請求?",
"notification_requests.confirm_dismiss_multiple.title": "是否忽略推播通知請求?",
"notification_requests.dismiss": "關閉",
"notification_requests.dismiss_multiple": "{count, plural, other {忽略 # 則請求...}}",
"notification_requests.edit_selection": "編輯",
@ -710,7 +710,7 @@
"notifications.policy.filter_limited_accounts_title": "受管制帳號",
"notifications.policy.filter_new_accounts.hint": "新增於過去 {days, plural, other {# 日}}",
"notifications.policy.filter_new_accounts_title": "新帳號",
"notifications.policy.filter_not_followers_hint": "包含最近 {days, plural, other {# 日}} 內跟隨您之使用者",
"notifications.policy.filter_not_followers_hint": "包含最近 {days, plural, other {# 日}}內跟隨您之使用者",
"notifications.policy.filter_not_followers_title": "未跟隨您之使用者",
"notifications.policy.filter_not_following_hint": "直至您手動核准他們",
"notifications.policy.filter_not_following_title": "您未跟隨之使用者",
@ -830,7 +830,7 @@
"report.thanks.title_actionable": "感謝您的檢舉,我們將會著手處理。",
"report.unfollow": "取消跟隨 @{name}",
"report.unfollow_explanation": "您正在跟隨此帳號。如不欲於首頁時間軸再見到他們的嘟文,請取消跟隨。",
"report_notification.attached_statuses": "{count, plural, one {{count} 則} other {{count} 則}} 嘟文",
"report_notification.attached_statuses": "已附加 {count, plural, other {{count} 則嘟文}}",
"report_notification.categories.legal": "合法性",
"report_notification.categories.legal_sentence": "違法內容",
"report_notification.categories.other": "其他",
@ -900,7 +900,7 @@
"status.direct_indicator": "私訊",
"status.edit": "編輯",
"status.edited": "上次編輯於 {date}",
"status.edited_x_times": "已編輯 {count, plural, one {{count} 次} other {{count} 次}}",
"status.edited_x_times": "已編輯 {count, plural, other {{count} 次}}",
"status.embed": "取得嵌入程式碼",
"status.favourite": "最愛",
"status.favourites": "{count, plural, other {則最愛}}",
@ -980,11 +980,11 @@
"terms_of_service.title": "服務條款",
"terms_of_service.upcoming_changes_on": "{date} 起即將發生之異動",
"time_remaining.days": "剩餘 {number, plural, other {# 天}}",
"time_remaining.hours": "剩餘{number, plural, other {# 小時}}",
"time_remaining.minutes": "剩餘{number, plural, other {# 分鐘}}",
"time_remaining.hours": "剩餘 {number, plural, other {# 小時}}",
"time_remaining.minutes": "剩餘 {number, plural, other {# 分鐘}}",
"time_remaining.moments": "剩餘時間",
"time_remaining.seconds": "剩餘{number, plural, other {# 秒}}",
"trends.counter_by_accounts": "{count, plural, one {{counter} 人} other {{counter} 人}}於過去 {days, plural, one {日} other {{days} 日}} 之間",
"time_remaining.seconds": "剩餘 {number, plural, other {# 秒}}",
"trends.counter_by_accounts": "{count, plural, other {{counter} 人}}於過去 {days, plural, other {{days} 日}}之間",
"trends.trending_now": "現正熱門趨勢",
"ui.beforeunload": "如果離開 Mastodon您的草稿將會不見。",
"units.short.billion": "{count}B",
@ -1015,7 +1015,7 @@
"video.play": "播放",
"video.skip_backward": "上一部",
"video.skip_forward": "下一部",
"video.unmute": "取消靜音",
"video.unmute": "解除靜音",
"video.volume_down": "降低音量",
"video.volume_up": "提高音量",
"visibility_modal.button_title": "設定可見性",

View File

@ -3181,20 +3181,21 @@ a.account__display-name {
}
.column__alert {
--alert-height: 54px;
position: sticky;
bottom: 0;
z-index: 10;
box-sizing: border-box;
display: grid;
grid-template-rows: minmax(var(--alert-height), max-content);
align-items: end;
width: 100%;
max-width: 360px;
padding: 1rem;
margin: auto auto 0;
overflow: clip;
&:empty {
padding: 0;
}
pointer-events: none;
@media (max-width: #{$mobile-menu-breakpoint - 1}) {
// Compensate for mobile menubar
@ -3205,6 +3206,7 @@ a.account__display-name {
// Make all nested alerts occupy the same space
// rather than stack
grid-area: 1 / 1;
pointer-events: initial;
}
}
@ -4484,13 +4486,19 @@ a.status-card {
box-sizing: border-box;
text-decoration: none;
&:hover {
background: var(--on-surface-color);
&--large {
padding-block: 32px;
}
&:focus-visible {
outline: 2px solid $ui-button-focus-outline-color;
outline-offset: -2px;
&:is(button) {
&:hover {
background: var(--on-surface-color);
}
&:focus-visible {
outline: 2px solid $ui-button-focus-outline-color;
outline-offset: -2px;
}
}
.icon {
@ -6196,6 +6204,8 @@ a.status-card {
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
align-items: center;
justify-content: space-around; // If set to center, the fullscreen image overlay is misaligned.
> div {
flex-shrink: 0;

View File

@ -1750,6 +1750,7 @@ be:
disabled_account: Пасля гэтага ваш бягучы ўліковы запіс не будзе цалкам даступны. Аднак у вас будзе доступ да экспарту даных, а таксама да паўторнай актывацыі.
followers: Гэтае дзеянне будзе «пераносіць» усіх падпісчыкаў з бягучага ўліковага запісу на новы
only_redirect_html: Альбо вы можаце <a href="%{path}"> толькі наладзіць перанакіраванне на ваш профіль</a>.
other_data: Сістэма не будзе аўтаматычна перамяшчаць астатнія даныя (у тым ліку Вашы допісы і спіс уліковых запісаў, на якія Вы падпісаліся)
redirect: Профіль вашага бягучага ўліковага запісу будзе абноўлены з паведамленнем аб перанакіраванні і выключаны з пошуку
moderation:
title: Мадэрацыя

View File

@ -1750,6 +1750,7 @@ cs:
disabled_account: Váš aktuální účet nebude poté zcela použitelný. Budete však mít přístup k datovým exportům a budete ho moci znovu aktivovat.
followers: Touto akcí přesunete všechny sledující z aktuálního účtu na nový
only_redirect_html: Alternativně můžete <a href="%{path}">nastavit pouze přesměrování na váš profil</a>.
other_data: Žádná další data nebudou automaticky přesunuta (včetně vašich příspěvků a seznamu účtů, které sledujete)
redirect: Profil vašeho aktuálního účtu bude aktualizován s oznámením o přesměrování a bude vyloučen z výsledků hledání
moderation:
title: Moderování

View File

@ -1828,6 +1828,7 @@ cy:
disabled_account: Ni fydd modd defnyddio'ch cyfrif cyfredol yn llawn wedyn. Fodd bynnag, bydd gennych fynediad i allforio data yn ogystal ag ail agor.
followers: Bydd y weithred hon yn symud yr holl ddilynwyr o'r cyfrif cyfredol i'r cyfrif newydd
only_redirect_html: Fel arall, <a href="%{path}">dim ond ailgyfeiriad y gallwch chi ei osod ar eich proffil</a>.
other_data: Bydd dim ddata arall yn cael ei symud yn awtomatig (gan gynnwys eich postiadau a'r rhestr o gyfrifon rydych chi'n eu dilyn)
redirect: Bydd proffil eich cyfrif presennol yn cael ei diweddaru gyda hysbysiad ailgyfeirio ac yn cael ei eithrio o chwiliadau
moderation:
title: Cymedroil
@ -2100,6 +2101,7 @@ cy:
errors:
in_reply_not_found: Nid yw'n ymddangos bod y postiad rydych chi'n ceisio ei ateb yn bodoli.
quoted_status_not_found: Nid yw'n ymddangos bod y postiad rydych chi'n ceisio'i ddyfynnu yn bodoli.
quoted_user_not_mentioned: Does dim modd dyfynnu defnyddiwr heb ei grybwyll mewn postiad Crybwyll Preifat.
over_character_limit: wedi mynd y tu hwnt i'r terfyn nodau o %{max}
pin_errors:
direct: Nid oes modd pinio postiadau sy'n weladwy i ddefnyddwyr a grybwyllwyd yn unig

View File

@ -1672,6 +1672,7 @@ de:
disabled_account: Dein altes Konto ist nur noch eingeschränkt verwendbar. Du kannst jedoch deine Daten exportieren und das Konto wieder reaktivieren.
followers: Alle Follower werden vom alten zum neuen Konto übertragen
only_redirect_html: Alternativ kannst du auch <a href="%{path}">nur eine Weiterleitung zu deinem neuen Konto</a> einrichten, ohne die Follower zu übertragen.
other_data: Es werden keine weiteren Daten automatisch übertragen (einschließlich deiner Beiträge und der Liste der Konten, denen du folgst)
redirect: Dein altes Konto wird einen Hinweis erhalten, dass du umgezogen bist. Außerdem wird das Profil von Suchanfragen ausgeschlossen
moderation:
title: Moderation

View File

@ -517,13 +517,13 @@ es:
title: Fediverse Auxiliary Service Providers (Proveedores de Servicio Auxiliares del Fediverso)
title: FASP
follow_recommendations:
description_html: "<strong>Las recomendaciones de cuentas ayudan a los nuevos usuarios a encontrar rápidamente contenido interesante</strong>. Cuando un usuario no ha interactuado con otros lo suficiente como para suscitar recomendaciones personalizadas de cuentas a las que seguir, en su lugar se le recomiendan estas cuentas. Se recalculan diariamente a partir de una mezcla de cuentas con el mayor número de interacciones recientes y con el mayor número de seguidores locales con un idioma determinado."
description_html: "<strong>Las recomendaciones de cuentas a las que seguir ayudan a los nuevos usuarios a encontrar rápidamente contenido interesante</strong>. Cuando un usuario no ha interactuado con otros lo suficiente como para formar recomendaciones personalizadas de seguimiento, estas cuentas se recomiendan en su lugar. Se recalculan diariamente a partir de una mezcla de cuentas con las interacciones más recientes y el mayor número de seguidores para un idioma determinado."
language: Para el idioma
status: Estado
suppress: Suprimir recomendación de cuentas
suppress: Eliminar recomendación de cuentas a las que seguir
suppressed: Suprimida
title: Recomendaciones de cuentas
unsuppress: Restaurar recomendaciones de cuentas
title: Recomendaciones de cuentas a las que seguir
unsuppress: Restaurar recomendaciones de cuentas a las que seguir
instances:
audit_log:
title: Registros de auditoría recientes

View File

@ -1672,6 +1672,7 @@ it:
disabled_account: Il tuo account attuale non sarà più pienamente utilizzabile. Tuttavia, avrai accesso all'esportazione dei dati e alla riattivazione.
followers: Questa azione sposterà tutti i follower dall'account attuale al nuovo account
only_redirect_html: In alternativa, puoi solo <a href="%{path}">impostare un reindirizzamento sul tuo profilo</a>.
other_data: Nessun altro dato verrà trasferito automaticamente (compresi i tuoi post e l'elenco degli account che segui)
redirect: Il profilo del tuo account corrente sarà aggiornato con un avviso di ridirezione e sarà escluso dalle ricerche
moderation:
title: Moderazione

View File

@ -65,7 +65,6 @@ an:
warn: Amagar lo conteniu filtrau dezaga d'una alvertencia mencionando lo titol d'o filtro
form_admin_settings:
activity_api_enabled: Conteyo de publicacions locals, usuarios activos y nuevos rechistros en periodos semanals
bootstrap_timeline_accounts: Estas cuentas amaneixerán en a parte superior d'as recomendacions d'os nuevos usuarios.
closed_registrations_message: Amostrau quan los rechistros son zarraus
custom_css: Puetz aplicar estilos personalizaus a la versión web de Mastodon.
mascot: Reemplaza la ilustración en a interficie web abanzada.

View File

@ -81,7 +81,6 @@ ar:
activity_api_enabled: عدد المنشورات المحلية و المستخدمين الناشطين و التسجيلات الأسبوعية الجديدة
app_icon: WEBP أو PNG أو GIF أو JPG. يتجاوز أيقونة التطبيق الافتراضية على الجوالات مع أيقونة مخصصة.
backups_retention_period: للمستخدمين القدرة على إنشاء أرشيفات لمنشوراتهم لتحميلها في وقت لاحق. عند التعيين إلى قيمة موجبة، سيتم حذف هذه الأرشيف تلقائياً من وحدة تخزينك بعد عدد الأيام المحدد.
bootstrap_timeline_accounts: سيتم تثبيت هذه الحسابات على قمة التوصيات للمستخدمين الجدد.
closed_registrations_message: ما سيعرض عند إغلاق التسجيلات
content_cache_retention_period: سيتم حذف جميع المنشورات من الخوادم الأخرى (بما في ذلك التعزيزات والردود) بعد عدد الأيام المحدد، دون أي تفاعل محلي للمستخدم مع هذه المنشورات. وهذا يشمل المنشورات التي قام المستخدم المحلي بوضع علامة عليها كإشارات مرجعية أو المفضلة. وسوف تختفي أيضا الإشارات الخاصة بين المستخدمين من المثيلات المختلفة ويستحيل استعادتها. والغرض من استخدام هذا الإعداد هو مثيلات الغرض الخاص ويفسد الكثير من توقعات المستخدمين عند تنفيذها للاستخدام لأغراض عامة.
custom_css: يمكنك تطبيق أساليب مخصصة على نسخة الويب من ماستدون.

View File

@ -79,6 +79,7 @@ be:
featured_tag:
name: 'Вось некаторыя з хэштэгаў, якімі вы нядаўна карысталіся:'
filters:
action: Выберыце, што трэба рабіць, калі допіс супадзе з фільтрам
actions:
blur: Схавайце медыя за знакам папярэджання, не хаваючы пры гэтым тэкст
hide: Поўнасцю схаваць адфільтраванае змесціва, дзейнічаць, нібы яго не існуе
@ -87,7 +88,7 @@ be:
activity_api_enabled: Падлік лакальна апублікаваных пастоў, актыўных карыстальнікаў і новых рэгістрацый у тыдзень
app_icon: WEBP, PNG, GIF ці JPG. Заменіце прадвызначаны значок праграмы на мабільных прыладах карыстальніцкім значком.
backups_retention_period: Карыстальнікі могуць ствараць архівы сваіх допісаў для наступнай запампоўкі. Пры станоўчай колькасці дзён гэтыя архівы будуць аўтаматычна выдаляцца са сховішча пасля заканчэння названай колькасці дзён.
bootstrap_timeline_accounts: Гэтыя ўліковыя запісы будуць замацаваны ў топе рэкамендацый для новых карыстальнікаў.
bootstrap_timeline_accounts: Гэтыя ўліковыя запісы будуць прымацаваныя наверсе рэкамендацый для новых карыстальнікаў. Дайце спіс уліковых запісаў, выкарыстоўваючы коску, каб раздзяліць іх.
closed_registrations_message: Паказваецца, калі рэгістрацыя закрытая
content_cache_retention_period: Усе допісы з іншых сервераў (разам з пашырэннямі і адказамі) будуць выдалены праз паказаную колькасць дзён, незалежна ад таго, як лакальны карыстальнік узаемадзейнічаў з гэтымі допісамі. Гэта датычыцца і тых допісаў, якія лакальны карыстальнік пазначыў у закладкі або ўпадабанае. Прыватныя згадванні паміж карыстальнікамі з розных экзэмпляраў сервераў таксама будуць страчаны і іх нельга будзе аднавіць. Выкарыстанне гэтай налады прызначана для экзэмпляраў сервераў спецыяльнага прызначэння і парушае многія чаканні карыстальнікаў пры выкарыстанні ў агульных мэтах.
custom_css: Вы можаце прымяняць карыстальніцкія стылі ў вэб-версіі Mastodon.

View File

@ -82,7 +82,6 @@ bg:
activity_api_enabled: Броят на местните публикувани публикации, дейни потребители и нови регистрации в седмични кофи
app_icon: WEBP, PNG, GIF или JPG. Заменя подразбиращата се икона на приложението в мобилни устройства с произволна икона.
backups_retention_period: Потребителите имат способността да пораждат архиви от публикациите си за по-късно изтегляне. Задавайки положителна стойност, тези архиви самодейно ще се изтрият от хранилището ви след определения брой дни.
bootstrap_timeline_accounts: Тези акаунти ще се закачат в горния край на препоръките за следване на нови потребители.
closed_registrations_message: Показва се, когато е затворено за регистрации
content_cache_retention_period: Всички публикации от други сървъри, включително подсилвания и отговори, ще се изтрият след посочения брой дни, без да се взема предвид каквото и да е взаимодействие на местния потребител с тези публикации. Това включва публикации, които местния потребител е означил като отметки или любими. Личните споменавания между потребители от различни инстанции също ще се загубят и невъзможно да се възстановят. Употребата на тази настройка е предназначена за случаи със специално предназначение и разбива очакванията на много потребители, когато се изпълнява за употреба с общо предназначение.
custom_css: Може да прилагате собствени стилове в уебверсията на Mastodon.

View File

@ -85,7 +85,6 @@ ca:
activity_api_enabled: Contador de tuts publicats localment, usuaris actius i registres nous en períodes setmanals
app_icon: WEBP, PNG, GIF o JPG. Canvia la icona per defecte de l'app en dispositius mòbils per una de personalitzada.
backups_retention_period: Els usuaris poden generar arxius de les seves publicacions per a baixar-los més endavant. Quan tingui un valor positiu, els arxius s'esborraran del vostre emmagatzematge després del nombre donat de dies.
bootstrap_timeline_accounts: Aquests comptes es fixaran en la part superior de les recomanacions de seguiment dels nous usuaris.
closed_registrations_message: Es mostra quan el registres estan tancats
content_cache_retention_period: S'esborraran totes les publicacions d'altres servidors (impulsos i respostes inclosos) passats els dies indicats, sense tenir en consideració les interaccions d'usuaris locals amb aquestes publicacions. Això inclou les publicacions que un usuari local hagi marcat com a favorites. També es perdran, i no es podran recuperar, les mencions privades entre usuaris d'instàncies diferents. Aquest paràmetre està pensat per a instàncies amb un propòsit especial i trencarà les expectatives dels usuaris si s'utilitza en una instància convencional.
custom_css: Pots aplicar estils personalitzats en la versió web de Mastodon.

View File

@ -88,7 +88,7 @@ cs:
activity_api_enabled: Počty lokálně zveřejnělých příspěvků, aktivních uživatelů a nových registrací v týdenních intervalech
app_icon: WEBP, PNG, GIF nebo JPG. Nahradí výchozí ikonu aplikace v mobilních zařízeních vlastní ikonou.
backups_retention_period: Uživatelé mají možnost vytvářet archivy svých příspěvků, které si mohou stáhnout později. Pokud je nastaveno na kladnou hodnotu, budou tyto archivy po zadaném počtu dní automaticky odstraněny z úložiště.
bootstrap_timeline_accounts: Tyto účty budou připnuty na vrchol nových uživatelů podle doporučení.
bootstrap_timeline_accounts: Tyto účty budou připnuty na vrcholu doporučení pro nové uživatele. Napište čárkou oddělený seznam účtů.
closed_registrations_message: Zobrazeno při zavření registrace
content_cache_retention_period: Všechny příspěvky z jiných serverů (včetně boostů a odpovědí) budou po uplynutí stanoveného počtu dní smazány bez ohledu na interakci místního uživatele s těmito příspěvky. To se týká i příspěvků, které místní uživatel přidal do záložek nebo oblíbených. Soukromé zmínky mezi uživateli z různých instancí budou rovněž ztraceny a nebude možné je obnovit. Použití tohoto nastavení je určeno pro instance pro speciální účely a při implementaci pro obecné použití porušuje mnohá očekávání uživatelů.
custom_css: Můžete použít vlastní styly ve verzi Mastodonu.

View File

@ -79,6 +79,7 @@ cy:
featured_tag:
name: 'Dyma rai or hashnodau a ddefnyddioch chi''n ddiweddar:'
filters:
action: Dewiswch pa weithred i'w gyflawni pan fydd postiad yn cyd-fynd â'r hidlydd
actions:
blur: Cuddio cyfryngau tu ôl i rybudd, heb guddio'r testun ei hun
hide: Cuddiwch y cynnwys wedi'i hidlo'n llwyr, gan ymddwyn fel pe na bai'n bodoli
@ -87,7 +88,7 @@ cy:
activity_api_enabled: Cyfrif o bostiadau a gyhoeddir yn lleol, defnyddwyr gweithredol, a chofrestriadau newydd mewn bwcedi wythnosol
app_icon: WEBP, PNG, GIF neu JPG. Yn diystyru'r eicon ap rhagosodedig ar ddyfeisiau symudol gydag eicon cyfaddas.
backups_retention_period: Mae gan ddefnyddwyr y gallu i gynhyrchu archifau o'u postiadau i'w llwytho i lawr yn ddiweddarach. Pan gânt eu gosod i werth positif, bydd yr archifau hyn yn cael eu dileu'n awtomatig o'ch storfa ar ôl y nifer penodedig o ddyddiau.
bootstrap_timeline_accounts: Bydd y cyfrifon hyn yn cael eu pinio i frig argymhellion dilynol defnyddwyr newydd.
bootstrap_timeline_accounts: Bydd y cyfrifon yma'n cael eu pinio i frig argymhellion dilyn defnyddwyr newydd. Darparwch restr cyfrifon wedi'u gwahanu gan gollnod.
closed_registrations_message: Yn cael eu dangos pan fydd cofrestriadau wedi cau
content_cache_retention_period: Bydd yr holl bostiadau gan weinyddion eraill (gan gynnwys hwb ac atebion) yn cael eu dileu ar ôl y nifer penodedig o ddyddiau, heb ystyried unrhyw ryngweithio defnyddiwr lleol â'r postiadau hynny. Mae hyn yn cynnwys postiadau lle mae defnyddiwr lleol wedi ei farcio fel nodau tudalen neu ffefrynnau. Bydd cyfeiriadau preifat rhwng defnyddwyr o wahanol achosion hefyd yn cael eu colli ac yn amhosibl eu hadfer. Mae'r defnydd o'r gosodiad hwn wedi'i fwriadu ar gyfer achosion pwrpas arbennig ac mae'n torri llawer o ddisgwyliadau defnyddwyr pan gaiff ei weithredu at ddibenion cyffredinol.
custom_css: Gallwch gymhwyso arddulliau cyfaddas ar fersiwn gwe Mastodon.

View File

@ -88,7 +88,7 @@ da:
activity_api_enabled: Antal lokalt opslåede indlæg, aktive brugere samt nye tilmeldinger i ugentlige opdelinger
app_icon: WEBP, PNG, GIF eller JPG. Tilsidesætter standard app-ikonet på mobilenheder med et tilpasset ikon.
backups_retention_period: Brugere har mulighed for at generere arkiver af deres indlæg til senere downloade. Når sat til positiv værdi, vil disse arkiver automatisk blive slettet fra lagerpladsen efter det angivne antal dage.
bootstrap_timeline_accounts: Disse konti fastgøres øverst på nye brugeres følg-anbefalinger.
bootstrap_timeline_accounts: Disse konti vil blive fastgjort til toppen af nye brugeres følg-anbefalinger. Angiv en kommasepareret liste over konti.
closed_registrations_message: Vises, når tilmeldinger er lukket
content_cache_retention_period: Alle indlæg fra andre servere (herunder fremhævelser og besvarelser) slettes efter det angivne antal dage uden hensyn til lokal brugerinteraktion med disse indlæg. Dette omfatter indlæg, hvor en lokal bruger har markeret dem som bogmærker eller favoritter. Private omtaler mellem brugere fra forskellige instanser vil også være tabt og umulige at gendanne. Brugen af denne indstilling er beregnet til særlige formål instanser og bryder mange brugerforventninger ved implementering til almindelig brug.
custom_css: Man kan anvende tilpassede stilarter på Mastodon-webversionen.

View File

@ -88,7 +88,7 @@ de:
activity_api_enabled: Anzahl der wöchentlichen Beiträge, aktiven Profile und Registrierungen auf diesem Server
app_icon: WEBP, PNG, GIF oder JPG. Überschreibt das Standard-App-Symbol auf mobilen Geräten mit einem eigenen Symbol.
backups_retention_period: Nutzer*innen haben die Möglichkeit, Archive ihrer Beiträge zu erstellen, die sie später herunterladen können. Wenn ein positiver Wert gesetzt ist, werden diese Archive nach der festgelegten Anzahl von Tagen automatisch aus deinem Speicher gelöscht.
bootstrap_timeline_accounts: Diese Konten werden bei den Follower-Empfehlungen für neu registrierte Nutzer*innen oben angeheftet.
bootstrap_timeline_accounts: Diese Konten werden an den Anfang der Follow-Empfehlungen für neue Nutzer angeheftet. Gib eine durch Kommata getrennte Liste von Konten an.
closed_registrations_message: Wird angezeigt, wenn Registrierungen deaktiviert sind
content_cache_retention_period: Sämtliche Beiträge von anderen Servern (einschließlich geteilte Beiträge und Antworten) werden, unabhängig von der Interaktion der lokalen Nutzer*innen mit diesen Beiträgen, nach der festgelegten Anzahl von Tagen gelöscht. Das betrifft auch Beiträge, die von lokalen Nutzer*innen favorisiert oder als Lesezeichen gespeichert wurden. Private Erwähnungen zwischen Nutzer*innen von verschiedenen Servern werden ebenfalls verloren gehen und können nicht wiederhergestellt werden. Diese Option richtet sich ausschließlich an Server mit speziellen Zwecken und wird die allgemeine Nutzungserfahrung beeinträchtigen, wenn sie für den allgemeinen Gebrauch aktiviert ist.
custom_css: Du kannst benutzerdefinierte Stile auf die Web-Version von Mastodon anwenden.

View File

@ -88,7 +88,7 @@ el:
activity_api_enabled: Καταμέτρηση τοπικά δημοσιευμένων δημοσιεύσεων, ενεργών χρηστών και νέων εγγραφών σε εβδομαδιαία πακέτα
app_icon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο εικονίδιο εφαρμογής σε κινητές συσκευές με προσαρμοσμένο εικονίδιο.
backups_retention_period: Οι χρήστες έχουν τη δυνατότητα να δημιουργήσουν αρχεία των αναρτήσεων τους για να κατεβάσουν αργότερα. Όταν οριστεί μια θετική τιμή, αυτά τα αρχεία θα διαγράφονται αυτόματα από τον αποθηκευτικό σου χώρο μετά τον καθορισμένο αριθμό ημερών.
bootstrap_timeline_accounts: Αυτοί οι λογαριασμοί θα καρφιτσωθούν στην κορυφή των νέων χρηστών που ακολουθούν τις συστάσεις.
bootstrap_timeline_accounts: Αυτοί οι λογαριασμοί θα καρφιτσωθούν στην κορυφή των προτεινόμενων ακολουθήσεων για νέους χρήστες. Παρέχετε μια λίστα λογαριασμών χωρισμένη με κόμμα.
closed_registrations_message: Εμφανίζεται όταν κλείνουν οι εγγραφές
content_cache_retention_period: Όλες οι αναρτήσεις από άλλους διακομιστές (συμπεριλαμβανομένων των ενισχύσεων και απαντήσεων) θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών, χωρίς να λαμβάνεται υπόψη οποιαδήποτε αλληλεπίδραση τοπικού χρήστη με αυτές τις αναρτήσεις. Αυτό περιλαμβάνει αναρτήσεις όπου ένας τοπικός χρήστης την έχει χαρακτηρίσει ως σελιδοδείκτη ή αγαπημένη. Θα χαθούν επίσης ιδιωτικές αναφορές μεταξύ χρηστών από διαφορετικές οντότητες και θα είναι αδύνατο να αποκατασταθούν. Η χρήση αυτής της ρύθμισης προορίζεται για οντότητες ειδικού σκοπού και χαλάει πολλές προσδοκίες του χρήστη όταν εφαρμόζεται για χρήση γενική σκοπού.
custom_css: Μπορείς να εφαρμόσεις προσαρμοσμένα στυλ στην έκδοση ιστοσελίδας του Mastodon.

View File

@ -82,7 +82,6 @@ en-GB:
activity_api_enabled: Counts of locally published posts, active users, and new registrations in weekly buckets
app_icon: WEBP, PNG, GIF or JPG. Overrides the default app icon on mobile devices with a custom icon.
backups_retention_period: Users have the ability to generate archives of their posts to download later. When set to a positive value, these archives will be automatically deleted from your storage after the specified number of days.
bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations.
closed_registrations_message: Displayed when sign-ups are closed
content_cache_retention_period: All posts from other servers (including boosts and replies) will be deleted after the specified number of days, without regard to any local user interaction with those posts. This includes posts where a local user has marked it as bookmarks or favorites. Private mentions between users from different instances will also be lost and impossible to restore. Use of this setting is intended for special purpose instances and breaks many user expectations when implemented for general purpose use.
custom_css: You can apply custom styles on the web version of Mastodon.

View File

@ -84,7 +84,6 @@ eo:
activity_api_enabled: Nombroj de loke publikigitaj afiŝoj, aktivaj uzantoj kaj novaj registradoj en semajnaj siteloj
app_icon: WEBP, PNG, GIF aŭ JPG. Anstataŭigas la defaŭltan aplikaĵan bildsimbolon sur porteblaj aparatoj kun propra bildsimbolo.
backups_retention_period: Uzantoj havas la kapablon generi arkivojn de siaj afiŝoj por elŝuti poste. Kiam estas agordita al pozitiva valoro, ĉi tiuj arkivoj estos aŭtomate forigitaj de via stokado post la specifita nombro da tagoj.
bootstrap_timeline_accounts: Ĉi tiuj kontoj pinglitas al la supro de sekvorekomendoj de novaj uzantoj.
closed_registrations_message: Montrita kiam registroj fermitas
content_cache_retention_period: Ĉiuj afiŝoj de aliaj serviloj (inkluzive de diskonigoj kaj respondoj) estos forigitaj post la specifita nombro da tagoj, sen konsidero al iu ajn loka uzantinterago kun tiuj afiŝoj. Ĉi tio inkluzivas afiŝojn, kie loka uzanto markis ĝin kiel legosignojn aŭ ŝatatajn. Privataj mencioj inter uzantoj de malsamaj nodoj ankaŭ estos perditaj kaj neeble restaŭreblaj. Uzo de ĉi tiu agordo estas celita por specialcelaj okazoj kaj rompas multajn uzantajn atendojn kiam efektivigita por ĝenerala uzo.
custom_css: Vi povas meti propajn stilojn en la retversio de Mastodon.

View File

@ -88,7 +88,7 @@ es-AR:
activity_api_enabled: Conteos de mensajes publicados localmente, cuentas activas y nuevos registros en tandas semanales
app_icon: WEBP, PNG, GIF o JPG. Reemplaza el ícono de aplicación predeterminado en dispositivos móviles con uno personalizado.
backups_retention_period: Los usuarios tienen la capacidad de generar archivos historiales de sus mensajes para descargar más adelante. Cuando se establece un valor positivo, estos archivos se eliminarán automáticamente de su almacenamiento después del número especificado de días.
bootstrap_timeline_accounts: Estas cuentas serán fijadas a la parte superior de las recomendaciones de cuentas a seguir para nuevos usuarios.
bootstrap_timeline_accounts: Estas cuentas se fijarán en la parte superior de las recomendaciones de seguimiento para los nuevos usuarios. Proporcioná una lista de cuentas separadas por comas.
closed_registrations_message: Mostrado cuando los registros están cerrados
content_cache_retention_period: Todos los mensajes de otros servidores (incluyendo adhesiones y respuestas) se eliminarán después del número de días especificado, sin tener en cuenta la interacción del usuario local con esos mensajes. Esto incluye mensajes que un usuario local haya agregado a marcadores o los haya marcado como favoritos. Las menciones privadas entre usuarios de diferentes servidores también se perderán y también serán imposibles de restaurar. El uso de esta configuración está destinado a servidores de propósito especial y rompe muchas expectativas de los usuarios cuando se implementa para uso general.
custom_css: Podés aplicar estilos personalizados a la versión web de Mastodon.

View File

@ -88,7 +88,7 @@ es-MX:
activity_api_enabled: Conteo de publicaciones publicadas localmente, usuarios activos, y nuevos registros en periodos semanales
app_icon: WEBP, PNG, GIF o JPG. Reemplaza el icono de aplicación predeterminado en dispositivos móviles con un icono personalizado.
backups_retention_period: Los usuarios tienen la posibilidad de generar archivos de sus mensajes para descargarlos más adelante. Cuando se establece en un valor positivo, estos archivos se eliminarán automáticamente del almacenamiento después del número especificado de días.
bootstrap_timeline_accounts: Estas cuentas aparecerán en la parte superior de las recomendaciones de los nuevos usuarios.
bootstrap_timeline_accounts: Estas cuentas se colocarán en la parte superior de las recomendaciones de cuentas a las que seguir para los nuevos usuarios. Proporciona una lista de cuentas separadas por comas.
closed_registrations_message: Mostrado cuando los registros están cerrados
content_cache_retention_period: Todas las publicaciones de otros servidores (incluyendo impuestos y respuestas) serán borrados después del número de días especificado, sin tener en cuenta cualquier interacción del usuario local con esas publicaciones. Esto incluye los mensajes que un usuario local haya marcado como favoritos. Las menciones privadas entre usuarios de diferentes instancias también se perderán y será imposible restaurarlas. El uso de esta configuración está pensado para instancias de propósito especial y rompe muchas expectativas de los usuarios cuando se implementa para uso general.
custom_css: Puedes aplicar estilos personalizados a la versión web de Mastodon.
@ -242,7 +242,7 @@ es-MX:
setting_default_privacy: Visibilidad de publicación
setting_default_quote_policy: Quién puede citar
setting_default_sensitive: Marcar siempre imágenes como sensibles
setting_delete_modal: Avisarme antes de borrar una publicación
setting_delete_modal: Avisarme antes de eliminar una publicación
setting_disable_hover_cards: Desactivar vista previa del perfil al pasar el cursor
setting_disable_swiping: Deshabilitar movimientos de deslizamiento
setting_display_media: Visualización multimedia
@ -252,7 +252,7 @@ es-MX:
setting_emoji_style: Estilo de emoji
setting_expand_spoilers: Siempre expandir las publicaciones marcadas con advertencias de contenido
setting_hide_network: Ocultar tu red
setting_missing_alt_text_modal: Avisarme antes de publicar multimedia sin descripción de texto
setting_missing_alt_text_modal: Avisarme antes de publicar contenido multimedia sin descripción de texto
setting_quick_boosting: Habilitar impulso rápido
setting_reduce_motion: Reducir el movimiento de las animaciones
setting_system_font_ui: Usar la fuente por defecto del sistema

View File

@ -88,7 +88,7 @@ es:
activity_api_enabled: Conteo de publicaciones publicadas localmente, usuarios activos y registros nuevos cada semana
app_icon: WEBP, PNG, GIF o JPG. Reemplaza el icono de aplicación predeterminado en dispositivos móviles con un icono personalizado.
backups_retention_period: Los usuarios tienen la capacidad de generar archivos de sus mensajes para descargar más adelante. Cuando se establece un valor positivo, estos archivos se eliminarán automáticamente del almacenamiento después del número de días especificado.
bootstrap_timeline_accounts: Estas cuentas aparecerán en la parte superior de las recomendaciones de los nuevos usuarios.
bootstrap_timeline_accounts: Estas cuentas se colocarán en la parte superior de las recomendaciones de cuentas a las que seguir para los nuevos usuarios. Proporciona una lista de cuentas separadas por comas.
closed_registrations_message: Mostrado cuando los registros están cerrados
content_cache_retention_period: Todas las publicaciones de otros servidores (incluso impulsos y respuestas) se eliminarán después del número de días especificado, sin tener en cuenta la interacción del usuario local con esos mensajes. Esto incluye mensajes donde un usuario local los ha marcado como marcadores o favoritos. Las menciones privadas entre usuarios de diferentes instancias también se perderán sin posibilidad de recuperación. El uso de esta configuración está destinado a instancias de propósito especial, y rompe muchas expectativas de los usuarios cuando se implementa para un uso de propósito general.
custom_css: Puedes aplicar estilos personalizados a la versión web de Mastodon.

View File

@ -88,7 +88,7 @@ et:
activity_api_enabled: Kohalike postituste, aktiivsete kasutajate ja uute registreerumistr arv nädala kaupa grupeeritult
app_icon: WEBP, PNG, GIF või JPG. Asendab mobiilsel seadmel äpi vaikeikooni kohandatud ikooniga.
backups_retention_period: Kasutajatel on võimalus genereerida oma postitustest hiljem allalaaditav arhiiv. Kui määrad positiivse arvu, siis kustutatakse need arhiivid serveri andmeruumist määratud arvu päevade järel automaatselt.
bootstrap_timeline_accounts: Need kasutajad kinnitatakse uute kasutajate jälgimissoovituste esiritta.
bootstrap_timeline_accounts: Järgnevad kasutajakontod kinnitatakse vaate ülaossa ning on mõeldud jälgitavate kontode soovituseks uutele kasutajatele. Sisend peab olema komadega eraldatud kasutajakontode loend.
closed_registrations_message: Kuvatakse, kui liitumised pole võimalikud
content_cache_retention_period: Kõik teiste serverite postitused (sealhulgas jagamised ja vastused) kustutatakse pärast määratud arvu päevade möödumist, sõltumata, kuidas kohalik kasutaja on nende postitustega interakteerunud. Hõlmatud on ka postitused, mille kohalik kasutaja on märkinud järjehoidjaks või lemmikuks. Ka eri instantside kasutajate vahelised privaatsed mainimised kaovad ja neid on võimatu taastada. See seadistus on mõeldud eriotstarbeliste instantside jaoks ja rikub paljude kasutajate ootusi, kui seda rakendatakse üldotstarbelise kasutuse puhul.
custom_css: Mastodoni veebiliideses on võimalik kasutada kohandatud stiile.

View File

@ -82,7 +82,6 @@ eu:
activity_api_enabled: Lokalki argitaratutako bidalketak, erabiltzaile aktiboak, eta izen-emateen kopuruak astero zenbatzen ditu
app_icon: WEBP, PNG, GIF edo JPG. Aplikazioaren ikono lehenetsia gainidazten du ikono pertsonalizatu batekin gailu mugikorretan.
backups_retention_period: Erabiltzaileek geroago deskarga dezaketen beren argitalpenen artxiboak sor ditzakete. Balio positibo bat ezartzean, artxibo hauek biltegiratzetik automatikoki ezabatuko dira zehazturiko egunen buruan.
bootstrap_timeline_accounts: Kontu hauek erabiltzaile berrien jarraitzeko gomendioen goiko aldean ainguratuko dira.
closed_registrations_message: Izen-ematea itxia dagoenean bistaratua
content_cache_retention_period: Beste zerbitzarietako argitalpen guztiak (bultzadak eta erantzunak barne) ezabatuko dira zehazturiko egunen buruan, argitalpen horiek izan ditzaketen erabiltzaile lokalaren interakzioa kontuan izanik gabe. Instantzia desberdinetako erabiltzaileen arteko aipamen pribatuak ere galdu egingo dira eta ezin izango dira berreskuratu. Ezarpen honen erabilera xede berezia duten instantziei zuzendua dago eta erabiltzaileen itxaropena hausten da orotariko erabilerarako inplementatzean.
custom_css: Estilo pertsonalizatuak aplikatu ditzakezu Mastodonen web bertsioan.

View File

@ -84,7 +84,6 @@ fa:
activity_api_enabled: تعداد بوق‌های منتشرهٔ محلی، کاربران فعال، و کاربران تازه در هر هفته
app_icon: WEBP، PNG، GIF یا JPG. با یک نماد سفارشی، نماد برنامه پیش‌فرض را در دستگاه‌های تلفن همراه لغو می‌کند.
backups_retention_period: کاربران می توانند بایگانی هایی از پست های خود ایجاد کنند تا بعدا دانلود کنند. وقتی روی مقدار مثبت تنظیم شود، این بایگانی‌ها پس از تعداد روزهای مشخص شده به‌طور خودکار از فضای ذخیره‌سازی شما حذف می‌شوند.
bootstrap_timeline_accounts: سنجاق کردنThese accounts will be pinned to the top of new users' follow recommendations.
closed_registrations_message: نمایش داده هنگام بسته بودن ثبت‌نام‌ها
content_cache_retention_period: همه پست‌های سرورهای دیگر (از جمله تقویت‌کننده‌ها و پاسخ‌ها) پس از چند روز مشخص شده، بدون توجه به هرگونه تعامل کاربر محلی با آن پست‌ها، حذف خواهند شد. این شامل پست هایی می شود که یک کاربر محلی آن را به عنوان نشانک یا موارد دلخواه علامت گذاری کرده است. ذکر خصوصی بین کاربران از نمونه های مختلف نیز از بین خواهد رفت و بازیابی آنها غیرممکن است. استفاده از این تنظیم برای موارد با هدف خاص در نظر گرفته شده است و بسیاری از انتظارات کاربر را هنگامی که برای استفاده عمومی اجرا می شود، از بین می برد.
custom_css: می‌توانیدروی نگارش وب ماستودون سبک‌های سفارشی اعمال کنید.

View File

@ -88,7 +88,7 @@ fi:
activity_api_enabled: Paikallisesti julkaistujen julkaisujen, aktiivisten käyttäjien ja rekisteröitymisten viikoittainen määrä
app_icon: WEBP, PNG, GIF tai JPG. Korvaa oletusarvoisen mobiililaitteiden sovelluskuvakkeen haluamallasi kuvakkeella.
backups_retention_period: Käyttäjillä on mahdollisuus arkistoida julkaisujaan myöhemmin ladattaviksi. Kun kentän arvo on positiivinen, nämä arkistot poistuvat automaattisesti, kun määritetty määrä päiviä on kulunut.
bootstrap_timeline_accounts: Nämä tilit kiinnitetään uusien käyttäjien seurantasuositusten alkuun.
bootstrap_timeline_accounts: Nämä tilit kiinnittyvät uusien käyttäjien seurantasuositusten alkuun. Syötä pilkuin eroteltu luettelo tilejä.
closed_registrations_message: Näkyy, kun rekisteröityminen on suljettu
content_cache_retention_period: Kaikki muiden palvelinten julkaisut (mukaan lukien tehostukset ja vastaukset) poistuvat, kun määritetty määrä päiviä on kulunut, lukuun ottamatta paikallisen käyttäjän vuorovaikutusta näiden julkaisujen kanssa. Tämä sisältää julkaisut, jotka paikallinen käyttäjä on merkinnyt kirjanmerkiksi tai suosikiksi. Myös yksityismaininnat eri palvelinten käyttäjien välillä menetetään, eikä niitä voi palauttaa. Tämä asetus on tarkoitettu käytettäväksi erityistapauksissa ja rikkoo monia käyttäjien odotuksia, kun sitä sovelletaan yleiskäyttöön.
custom_css: Voit käyttää mukautettuja tyylejä Mastodonin selainversiossa.

View File

@ -88,7 +88,7 @@ fo:
activity_api_enabled: Tal av lokalt útgivnum postum, virknum brúkarum og nýggjum skrásetingum býtt vikuliga
app_icon: WEBP, PNG, GIF ella JPG. Býtir vanligu ikonina á fartelefoneindum um við eina ser-ikon.
backups_retention_period: Brúkarar hava møguleika at gera trygdaravrit av teirra postum, sum tey kunnu taka niður seinni. Tá hetta er sett til eitt virði størri enn 0, so verða hesi trygdaravrit strikaði av sær sjálvum frá tínar goymslu eftir ásetta talið av døgum.
bootstrap_timeline_accounts: Hesar kontur verða festar ovast á listanum yvir brúkarar, sum tey nýggju verða mælt til at fylgja.
bootstrap_timeline_accounts: Hesar konturnar verða festar ovast í fylgjaratilmælunum hjá nýggjum brúkarum. Kom við einum komma-skildum lista av kontum.
closed_registrations_message: Víst tá stongt er fyri tilmeldingum
content_cache_retention_period: Allir postar frá øðrum ambætarum (íroknað stimbranir og svar) verða strikaði eftir ásetta talið av døgum, óansæð hvussu lokalir brúkarar hava samvirkað við hesar postar. Hetta fevnir eisini um postar, sum lokalir brúkarar hava bókamerkt ella yndismerkt. Privatar umrøður millum brúkarar frá ymiskum ambætarum verða eisini burturmistar og ómøguligar at endurskapa. Brúk av hesi stillingini er einans hugsað til serligar støður og oyðileggur nógv, sum brúkarar vænta av einum vanligum ambætara.
custom_css: Tú kanst seta títt egna snið upp í net-útgávuni av Mastodon.

View File

@ -82,7 +82,6 @@ fr-CA:
activity_api_enabled: Nombre de messages publiés localement, de comptes actifs et de nouvelles inscriptions par tranche hebdomadaire
app_icon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
backups_retention_period: Les utilisateur·rice·s ont la possibilité de générer des archives de leurs messages pour les télécharger plus tard. Lorsqu'elles sont définies à une valeur positive, ces archives seront automatiquement supprimées de votre stockage après le nombre de jours spécifié.
bootstrap_timeline_accounts: Ces comptes seront épinglés en tête de liste des recommandations pour les nouveaux utilisateurs.
closed_registrations_message: Affiché lorsque les inscriptions sont fermées
content_cache_retention_period: Tous les messages provenant d'autres serveurs (y compris les partages et les réponses) seront supprimés passé le nombre de jours spécifié, sans tenir compte de l'interaction de l'utilisateur·rice local·e avec ces messages. Cela inclut les messages qu'un·e utilisateur·rice aurait marqué comme signets ou comme favoris. Les mentions privées entre utilisateur·rice·s de différentes instances seront également perdues et impossibles à restaurer. L'utilisation de ce paramètre est destinée à des instances spécifiques et contrevient à de nombreuses attentes des utilisateurs lorsqu'elle est appliquée à des fins d'utilisation ordinaires.
custom_css: Vous pouvez appliquer des styles personnalisés sur la version Web de Mastodon.

View File

@ -82,7 +82,6 @@ fr:
activity_api_enabled: Nombre de messages publiés localement, de comptes actifs et de nouvelles inscriptions par tranche hebdomadaire
app_icon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée.
backups_retention_period: Les utilisateur·rice·s ont la possibilité de générer des archives de leurs messages pour les télécharger plus tard. Lorsqu'elles sont définies à une valeur positive, ces archives seront automatiquement supprimées de votre stockage après le nombre de jours spécifié.
bootstrap_timeline_accounts: Ces comptes seront épinglés en tête de liste des recommandations pour les nouveaux utilisateurs.
closed_registrations_message: Affiché lorsque les inscriptions sont fermées
content_cache_retention_period: Tous les messages provenant d'autres serveurs (y compris les partages et les réponses) seront supprimés passé le nombre de jours spécifié, sans tenir compte de l'interaction de l'utilisateur·rice local·e avec ces messages. Cela inclut les messages qu'un·e utilisateur·rice aurait marqué comme signets ou comme favoris. Les mentions privées entre utilisateur·rice·s de différentes instances seront également perdues et impossibles à restaurer. L'utilisation de ce paramètre est destinée à des instances spécifiques et contrevient à de nombreuses attentes des utilisateurs lorsqu'elle est appliquée à des fins d'utilisation ordinaires.
custom_css: Vous pouvez appliquer des styles personnalisés sur la version Web de Mastodon.

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