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

Merge upstream changes up to 9d664f87a0
pull/2861/head
Claire 2024-09-29 20:36:32 +02:00 committed by GitHub
commit 9bf624b44d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
240 changed files with 3054 additions and 1462 deletions

View File

@ -134,7 +134,7 @@ GEM
bindata (2.5.0)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.7)
blurhash (0.1.8)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (6.2.1)

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class Api::V1::DomainBlocks::PreviewsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }
before_action :require_user!
before_action :set_domain
before_action :set_domain_block_preview
def show
render json: @domain_block_preview, serializer: REST::DomainBlockPreviewSerializer
end
private
def set_domain
@domain = TagManager.instance.normalize_domain(params[:domain])
end
def set_domain_block_preview
@domain_block_preview = with_read_replica do
DomainBlockPreviewPresenter.new(
following_count: current_account.following.where(domain: @domain).count,
followers_count: current_account.followers.where(domain: @domain).count
)
end
end
end

View File

@ -10,6 +10,7 @@ module SettingsHelper
end
def featured_tags_hint(recently_used_tags)
recently_used_tags.present? &&
safe_join(
[
t('simple_form.hints.featured_tag.name'),

View File

@ -70,6 +70,7 @@ export async function apiRequest<ApiResponse = unknown>(
args: {
params?: RequestParamsOrData;
data?: RequestParamsOrData;
timeout?: number;
} = {},
) {
const { data } = await api().request<ApiResponse>({

View File

@ -0,0 +1,67 @@
import { useState, useCallback, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/Overlay';
import type {
OffsetValue,
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const AltTextBadge: React.FC<{
description: string;
}> = ({ description }) => {
const anchorRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const handleClick = useCallback(() => {
setOpen((v) => !v);
}, [setOpen]);
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
return (
<>
<button
ref={anchorRef}
className='media-gallery__alt__label'
onClick={handleClick}
>
ALT
</button>
<Overlay
rootClose
onHide={handleClose}
show={open}
target={anchorRef.current}
placement='top-end'
flip
offset={offset}
popperConfig={popperConfig}
>
{({ props }) => (
<div {...props} className='hover-card-controller'>
<div
className='media-gallery__alt__popover dropdown-animation'
role='tooltip'
>
<h4>
<FormattedMessage
id='alt_text_badge.title'
defaultMessage='Alt text'
/>
</h4>
<p>{description}</p>
</div>
</div>
)}
</Overlay>
</>
);
};

View File

@ -7,6 +7,7 @@ interface BaseProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
block?: boolean;
secondary?: boolean;
dangerous?: boolean;
}
interface PropsChildren extends PropsWithChildren<BaseProps> {
@ -26,6 +27,7 @@ export const Button: React.FC<Props> = ({
disabled,
block,
secondary,
dangerous,
className,
title,
text,
@ -46,6 +48,7 @@ export const Button: React.FC<Props> = ({
className={classNames('button', className, {
'button-secondary': secondary,
'button--block': block,
'button--dangerous': dangerous,
})}
disabled={disabled}
onClick={handleClick}

View File

@ -10,7 +10,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import { AltTextBadge } from 'flavours/glitch/components/alt_text_badge';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { formatTime } from 'flavours/glitch/features/video';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
@ -58,7 +60,7 @@ class Item extends PureComponent {
hoverToPlay () {
const { attachment } = this.props;
return !this.getAutoPlay() && attachment.get('type') === 'gifv';
return !this.getAutoPlay() && ['gifv', 'video'].includes(attachment.get('type'));
}
handleClick = (e) => {
@ -97,7 +99,7 @@ class Item extends PureComponent {
}
if (attachment.get('description')?.length > 0) {
badges.push(<span key='alt' className='media-gallery__alt__label'>ALT</span>);
badges.push(<AltTextBadge key='alt' description={attachment.get('description')} />);
}
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
@ -152,10 +154,15 @@ class Item extends PureComponent {
/>
</a>
);
} else if (attachment.get('type') === 'gifv') {
} else if (['gifv', 'video'].includes(attachment.get('type'))) {
const autoPlay = this.getAutoPlay();
const duration = attachment.getIn(['meta', 'original', 'duration']);
badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);
if (attachment.get('type') === 'gifv') {
badges.push(<span key='gif' className='media-gallery__alt__label media-gallery__alt__label--non-interactive'>GIF</span>);
} else {
badges.push(<span key='video' className='media-gallery__alt__label media-gallery__alt__label--non-interactive'>{formatTime(Math.floor(duration))}</span>);
}
thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
@ -169,6 +176,7 @@ class Item extends PureComponent {
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onLoadedData={this.handleImageLoad}
autoPlay={autoPlay}
playsInline
loop

View File

@ -4,22 +4,22 @@ import AccountNavigation from 'flavours/glitch/features/account/navigation';
import Trends from 'flavours/glitch/features/getting_started/containers/trends_container';
import { showTrends } from 'flavours/glitch/initial_state';
const DefaultNavigation: React.FC = () =>
showTrends ? (
<>
<div className='flex-spacer' />
<Trends />
</>
) : null;
const DefaultNavigation: React.FC = () => (showTrends ? <Trends /> : null);
export const NavigationPortal: React.FC = () => (
<div className='navigation-panel__portal'>
<Switch>
<Route path='/@:acct' exact component={AccountNavigation} />
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
<Route
path='/@:acct/tagged/:tagged?'
exact
component={AccountNavigation}
/>
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} />
</Switch>
</div>
);

View File

@ -648,6 +648,27 @@ class Status extends ImmutablePureComponent {
media={status.get('media_attachments')}
/>,
);
} else if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
media.push(
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={attachments}
lang={language}
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
hidden={isCollapsed || !isExpanded}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>,
);
mediaIcons.push('picture-o');
} else if (attachments.getIn([0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
@ -703,27 +724,6 @@ class Status extends ImmutablePureComponent {
</Bundle>,
);
mediaIcons.push('video-camera');
} else { // Media type is 'image' or 'gifv'
media.push(
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={attachments}
lang={language}
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
hidden={isCollapsed || !isExpanded}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>,
);
mediaIcons.push('picture-o');
}
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {

View File

@ -43,10 +43,7 @@ class AccountNavigation extends PureComponent {
}
return (
<>
<div className='flex-spacer' />
<FeaturedTags accountId={accountId} tagged={tagged} />
</>
);
}

View File

@ -1,158 +0,0 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AudiotrackIcon from '@/material-icons/400-24px/music_note.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
displayWidth: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
};
state = {
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
loaded: false,
};
handleImageLoad = () => {
this.setState({ loaded: true });
};
handleMouseEnter = e => {
if (this.hoverToPlay()) {
e.target.play();
}
};
handleMouseLeave = e => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
};
hoverToPlay () {
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
}
handleClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (this.state.visible) {
this.props.onOpenMedia(this.props.attachment);
} else {
this.setState({ visible: true });
}
}
};
render () {
const { attachment, displayWidth } = this.props;
const { visible, loaded } = this.state;
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
const height = width;
const status = attachment.get('status');
const title = status.get('spoiler_text') || attachment.get('description');
let thumbnail, label, icon, content;
if (!visible) {
icon = (
<span className='account-gallery__item__icons'>
<Icon id='eye-slash' icon={VisibilityOffIcon} />
</span>
);
} else {
if (['audio', 'video'].includes(attachment.get('type'))) {
content = (
<img
src={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')}
lang={status.get('language')}
onLoad={this.handleImageLoad}
/>
);
if (attachment.get('type') === 'audio') {
label = <Icon id='music' icon={AudiotrackIcon} />;
} else {
label = <Icon id='play' icon={PlayArrowIcon} />;
}
} else if (attachment.get('type') === 'image') {
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
content = (
<img
src={attachment.get('preview_url')}
alt={attachment.get('description')}
lang={status.get('language')}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (attachment.get('type') === 'gifv') {
content = (
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
title={attachment.get('description')}
lang={status.get('language')}
role='application'
src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlayGif}
playsInline
loop
muted
/>
);
label = 'GIF';
}
thumbnail = (
<div className='media-gallery__gifv'>
{content}
{label && (
<div className='media-gallery__item__badges'>
<span className='media-gallery__gifv__label'>{label}</span>
</div>
)}
</div>
);
}
return (
<div className='account-gallery__item' style={{ width, height }}>
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<Blurhash
hash={attachment.get('blurhash')}
className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
dummy={!useBlurhash}
/>
{visible ? thumbnail : icon}
</a>
</div>
);
}
}

View File

@ -0,0 +1,203 @@
import { useState, useCallback } from 'react';
import classNames from 'classnames';
import HeadphonesIcon from '@/material-icons/400-24px/headphones-fill.svg?react';
import MovieIcon from '@/material-icons/400-24px/movie-fill.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { AltTextBadge } from 'flavours/glitch/components/alt_text_badge';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { formatTime } from 'flavours/glitch/features/video';
import {
autoPlayGif,
displayMedia,
useBlurhash,
} from 'flavours/glitch/initial_state';
import type { Status, MediaAttachment } from 'flavours/glitch/models/status';
export const MediaItem: React.FC<{
attachment: MediaAttachment;
onOpenMedia: (arg0: MediaAttachment) => void;
}> = ({ attachment, onOpenMedia }) => {
const [visible, setVisible] = useState(
(displayMedia !== 'hide_all' &&
!attachment.getIn(['status', 'sensitive'])) ||
displayMedia === 'show_all',
);
const [loaded, setLoaded] = useState(false);
const handleImageLoad = useCallback(() => {
setLoaded(true);
}, [setLoaded]);
const handleMouseEnter = useCallback(
(e: React.MouseEvent<HTMLVideoElement>) => {
if (e.target instanceof HTMLVideoElement) {
void e.target.play();
}
},
[],
);
const handleMouseLeave = useCallback(
(e: React.MouseEvent<HTMLVideoElement>) => {
if (e.target instanceof HTMLVideoElement) {
e.target.pause();
e.target.currentTime = 0;
}
},
[],
);
const handleClick = useCallback(
(e: React.MouseEvent<HTMLAnchorElement>) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (visible) {
onOpenMedia(attachment);
} else {
setVisible(true);
}
}
},
[attachment, visible, onOpenMedia, setVisible],
);
const status = attachment.get('status') as Status;
const description = (attachment.getIn(['translation', 'description']) ||
attachment.get('description')) as string | undefined;
const previewUrl = attachment.get('preview_url') as string;
const fullUrl = attachment.get('url') as string;
const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
const lang = status.get('language') as string;
const blurhash = attachment.get('blurhash') as string;
const statusUrl = status.get('url') as string;
const type = attachment.get('type') as string;
let thumbnail;
const badges = [];
if (description && description.length > 0) {
badges.push(<AltTextBadge key='alt' description={description} />);
}
if (!visible) {
thumbnail = (
<div className='media-gallery__item__overlay'>
<Icon id='eye-slash' icon={VisibilityOffIcon} />
</div>
);
} else if (type === 'audio') {
thumbnail = (
<>
<img
src={previewUrl || avatarUrl}
alt={description}
title={description}
lang={lang}
onLoad={handleImageLoad}
/>
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
<Icon id='music' icon={HeadphonesIcon} />
</div>
</>
);
} else if (type === 'image') {
const focusX = (attachment.getIn(['meta', 'focus', 'x']) || 0) as number;
const focusY = (attachment.getIn(['meta', 'focus', 'y']) || 0) as number;
const x = (focusX / 2 + 0.5) * 100;
const y = (focusY / -2 + 0.5) * 100;
thumbnail = (
<img
src={previewUrl}
alt={description}
title={description}
lang={lang}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={handleImageLoad}
/>
);
} else if (['video', 'gifv'].includes(type)) {
const duration = attachment.getIn([
'meta',
'original',
'duration',
]) as number;
thumbnail = (
<div className='media-gallery__gifv'>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={description}
title={description}
lang={lang}
src={fullUrl}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onLoadedData={handleImageLoad}
autoPlay={autoPlayGif}
playsInline
loop
muted
/>
{type === 'video' && (
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
<Icon id='play' icon={MovieIcon} />
</div>
)}
</div>
);
if (type === 'gifv') {
badges.push(
<span
key='gif'
className='media-gallery__alt__label media-gallery__alt__label--non-interactive'
>
GIF
</span>,
);
} else {
badges.push(
<span
key='video'
className='media-gallery__alt__label media-gallery__alt__label--non-interactive'
>
{formatTime(Math.floor(duration))}
</span>,
);
}
}
return (
<div className='media-gallery__item media-gallery__item--square'>
<Blurhash
hash={blurhash}
className={classNames('media-gallery__preview', {
'media-gallery__preview--hidden': visible && loaded,
})}
dummy={!useBlurhash}
/>
<a
className='media-gallery__item-thumbnail'
href={statusUrl}
onClick={handleClick}
target='_blank'
rel='noopener noreferrer'
>
{thumbnail}
</a>
{badges.length > 0 && (
<div className='media-gallery__item__badges'>{badges}</div>
)}
</div>
);
};

View File

@ -20,7 +20,7 @@ import { expandAccountMediaTimeline } from '../../actions/timelines';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
import MediaItem from './components/media_item';
import { MediaItem } from './components/media_item';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);

View File

@ -21,11 +21,11 @@ const messages = defineMessages({
export const SensitiveButton = () => {
const intl = useIntl();
const spoilersAlwaysOn = useAppSelector((state) => state.getIn(['local_settings', 'always_show_spoilers_field']));
const spoilerText = useAppSelector((state) => state.getIn(['compose', 'spoiler_text']));
const sensitive = useAppSelector((state) => state.getIn(['compose', 'sensitive']));
const spoiler = useAppSelector((state) => state.getIn(['compose', 'spoiler']));
const mediaCount = useAppSelector((state) => state.getIn(['compose', 'media_attachments']).size);
const spoilersAlwaysOn = useAppSelector((state) => state.local_settings.getIn(['always_show_spoilers_field']));
const spoilerText = useAppSelector((state) => state.compose.get('spoiler_text'));
const sensitive = useAppSelector((state) => state.compose.get('sensitive'));
const spoiler = useAppSelector((state) => state.compose.get('spoiler'));
const mediaCount = useAppSelector((state) => state.compose.get('media_attachments').size);
const disabled = spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler;
const active = sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0);

View File

@ -1,81 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import spring from 'react-motion/lib/spring';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { undoUploadCompose, initMediaEditModal } from 'flavours/glitch/actions/compose';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import Motion from 'flavours/glitch/features/ui/util/optional_motion';
export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
const dispatch = useDispatch();
const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
const sensitive = useSelector(state => state.getIn(['compose', 'sensitive']));
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
}, [dispatch, id]);
const handleFocalPointClick = useCallback(() => {
dispatch(initMediaEditModal(id));
}, [dispatch, id]);
const handleDragStart = useCallback(() => {
onDragStart(id);
}, [onDragStart, id]);
const handleDragEnter = useCallback(() => {
onDragEnter(id);
}, [onDragEnter, id]);
if (!media) {
return null;
}
const focusX = media.getIn(['meta', 'focus', 'x']);
const focusY = media.getIn(['meta', 'focus', 'y']);
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
const missingDescription = (media.get('description') || '').length === 0;
return (
<div className='compose-form__upload' draggable onDragStart={handleDragStart} onDragEnter={handleDragEnter} onDragEnd={onDragEnd}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
{sensitive && <Blurhash
hash={media.get('blurhash')}
className='compose-form__upload__preview'
/>}
<div className='compose-form__upload__actions'>
<button type='button' className='icon-button compose-form__upload__delete' onClick={handleUndoClick}><Icon icon={CloseIcon} /></button>
<button type='button' className='icon-button' onClick={handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
</div>
<div className='compose-form__upload__warning'>
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
</div>
</div>
)}
</Motion>
</div>
);
};
Upload.propTypes = {
id: PropTypes.string,
onDragEnter: PropTypes.func,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
};

View File

@ -0,0 +1,130 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import {
undoUploadCompose,
initMediaEditModal,
} from 'flavours/glitch/actions/compose';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
export const Upload: React.FC<{
id: string;
dragging?: boolean;
overlay?: boolean;
tall?: boolean;
wide?: boolean;
}> = ({ id, dragging, overlay, tall, wide }) => {
const dispatch = useAppDispatch();
const media = useAppSelector(
(state) =>
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
.find((item: MediaAttachment) => item.get('id') === id) as // eslint-disable-line @typescript-eslint/no-unsafe-member-access
| MediaAttachment
| undefined,
);
const sensitive = useAppSelector(
(state) => state.compose.get('sensitive') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
}, [dispatch, id]);
const handleFocalPointClick = useCallback(() => {
dispatch(initMediaEditModal(id));
}, [dispatch, id]);
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
if (!media) {
return null;
}
const focusX = media.getIn(['meta', 'focus', 'x']) as number;
const focusY = media.getIn(['meta', 'focus', 'y']) as number;
const x = (focusX / 2 + 0.5) * 100;
const y = (focusY / -2 + 0.5) * 100;
const missingDescription =
((media.get('description') as string | undefined) ?? '').length === 0;
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
className={classNames('compose-form__upload media-gallery__item', {
dragging,
overlay,
'media-gallery__item--tall': tall,
'media-gallery__item--wide': wide,
})}
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
>
<div
className='compose-form__upload__thumbnail'
style={{
backgroundImage: !sensitive
? `url(${media.get('preview_url') as string})`
: undefined,
backgroundPosition: `${x}% ${y}%`,
}}
>
{sensitive && (
<Blurhash
hash={media.get('blurhash') as string}
className='compose-form__upload__preview'
/>
)}
<div className='compose-form__upload__actions'>
<button
type='button'
className='icon-button compose-form__upload__delete'
onClick={handleUndoClick}
>
<Icon id='close' icon={CloseIcon} />
</button>
<button
type='button'
className='icon-button'
onClick={handleFocalPointClick}
>
<Icon id='edit' icon={EditIcon} />{' '}
<FormattedMessage id='upload_form.edit' defaultMessage='Edit' />
</button>
</div>
<div className='compose-form__upload__warning'>
<button
type='button'
className={classNames('icon-button', {
active: missingDescription,
})}
onClick={handleFocalPointClick}
>
{missingDescription && <Icon id='warning' icon={WarningIcon} />} ALT
</button>
</div>
</div>
</div>
);
};

View File

@ -1,56 +0,0 @@
import { useRef, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { changeMediaOrder } from 'flavours/glitch/actions/compose';
import { SensitiveButton } from './sensitive_button';
import { Upload } from './upload';
import { UploadProgress } from './upload_progress';
export const UploadForm = () => {
const dispatch = useDispatch();
const mediaIds = useSelector(state => state.getIn(['compose', 'media_attachments']).map(item => item.get('id')));
const active = useSelector(state => state.getIn(['compose', 'is_uploading']));
const progress = useSelector(state => state.getIn(['compose', 'progress']));
const isProcessing = useSelector(state => state.getIn(['compose', 'is_processing']));
const dragItem = useRef();
const dragOverItem = useRef();
const handleDragStart = useCallback(id => {
dragItem.current = id;
}, [dragItem]);
const handleDragEnter = useCallback(id => {
dragOverItem.current = id;
}, [dragOverItem]);
const handleDragEnd = useCallback(() => {
dispatch(changeMediaOrder(dragItem.current, dragOverItem.current));
dragItem.current = null;
dragOverItem.current = null;
}, [dispatch, dragItem, dragOverItem]);
return (
<>
<UploadProgress active={active} progress={progress} isProcessing={isProcessing} />
{mediaIds.size > 0 && (
<div className='compose-form__uploads'>
{mediaIds.map(id => (
<Upload
key={id}
id={id}
onDragStart={handleDragStart}
onDragEnter={handleDragEnter}
onDragEnd={handleDragEnd}
/>
))}
</div>
)}
{!mediaIds.isEmpty() && <SensitiveButton />}
</>
);
};

View File

@ -0,0 +1,188 @@
import { useState, useCallback, useMemo } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import type { List } from 'immutable';
import type {
DragStartEvent,
DragEndEvent,
UniqueIdentifier,
Announcements,
ScreenReaderInstructions,
} from '@dnd-kit/core';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
rectSortingStrategy,
} from '@dnd-kit/sortable';
import { changeMediaOrder } from 'flavours/glitch/actions/compose';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { SensitiveButton } from './sensitive_button';
import { Upload } from './upload';
import { UploadProgress } from './upload_progress';
const messages = defineMessages({
screenReaderInstructions: {
id: 'upload_form.drag_and_drop.instructions',
defaultMessage:
'To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.',
},
onDragStart: {
id: 'upload_form.drag_and_drop.on_drag_start',
defaultMessage: 'Picked up media attachment {item}.',
},
onDragOver: {
id: 'upload_form.drag_and_drop.on_drag_over',
defaultMessage: 'Media attachment {item} was moved.',
},
onDragEnd: {
id: 'upload_form.drag_and_drop.on_drag_end',
defaultMessage: 'Media attachment {item} was dropped.',
},
onDragCancel: {
id: 'upload_form.drag_and_drop.on_drag_cancel',
defaultMessage:
'Dragging was cancelled. Media attachment {item} was dropped.',
},
});
export const UploadForm: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const mediaIds = useAppSelector(
(state) =>
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
.map((item: MediaAttachment) => item.get('id')) as List<string>, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
);
const active = useAppSelector(
(state) => state.compose.get('is_uploading') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const progress = useAppSelector(
(state) => state.compose.get('progress') as number, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const isProcessing = useAppSelector(
(state) => state.compose.get('is_processing') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragStart = useCallback(
(e: DragStartEvent) => {
const { active } = e;
setActiveId(active.id);
},
[setActiveId],
);
const handleDragEnd = useCallback(
(e: DragEndEvent) => {
const { active, over } = e;
if (over && active.id !== over.id) {
dispatch(changeMediaOrder(active.id, over.id));
}
setActiveId(null);
},
[dispatch, setActiveId],
);
const accessibility: {
screenReaderInstructions: ScreenReaderInstructions;
announcements: Announcements;
} = useMemo(
() => ({
screenReaderInstructions: {
draggable: intl.formatMessage(messages.screenReaderInstructions),
},
announcements: {
onDragStart({ active }) {
return intl.formatMessage(messages.onDragStart, { item: active.id });
},
onDragOver({ active }) {
return intl.formatMessage(messages.onDragOver, { item: active.id });
},
onDragEnd({ active }) {
return intl.formatMessage(messages.onDragEnd, { item: active.id });
},
onDragCancel({ active }) {
return intl.formatMessage(messages.onDragCancel, { item: active.id });
},
},
}),
[intl],
);
return (
<>
<UploadProgress
active={active}
progress={progress}
isProcessing={isProcessing}
/>
{mediaIds.size > 0 && (
<div
className={`compose-form__uploads media-gallery media-gallery--layout-${mediaIds.size}`}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
accessibility={accessibility}
>
<SortableContext
items={mediaIds.toArray()}
strategy={rectSortingStrategy}
>
{mediaIds.map((id, idx) => (
<Upload
key={id}
id={id}
dragging={id === activeId}
tall={mediaIds.size < 3 || (mediaIds.size === 3 && idx === 0)}
wide={mediaIds.size === 1}
/>
))}
</SortableContext>
<DragOverlay>
{activeId ? <Upload id={activeId as string} overlay /> : null}
</DragOverlay>
</DndContext>
</div>
)}
{!mediaIds.isEmpty() && <SensitiveButton />}
</>
);
};

View File

@ -195,6 +195,28 @@ export const DetailedStatus: React.FC<{
)
) {
media.push(<AttachmentList media={status.get('media_attachments')} />);
} else if (
['image', 'gifv'].includes(
status.getIn(['media_attachments', 0, 'type']) as string,
) ||
status.get('media_attachments').size > 1
) {
media.push(
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
lang={language}
height={300}
letterbox={letterboxMedia}
fullwidth={fullwidthMedia}
hidden={!expanded}
onOpenMedia={onOpenMedia}
visible={showMedia}
onToggleVisibility={onToggleMediaVisibility}
/>,
);
mediaIcons.push('picture-o');
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description =
@ -249,23 +271,6 @@ export const DetailedStatus: React.FC<{
/>,
);
mediaIcons.push('video-camera');
} else {
media.push(
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
lang={language}
height={300}
letterbox={letterboxMedia}
fullwidth={fullwidthMedia}
hidden={!expanded}
onOpenMedia={onOpenMedia}
visible={showMedia}
onToggleVisibility={onToggleMediaVisibility}
/>,
);
mediaIcons.push('picture-o');
}
} else if (status.get('spoiler_text').length === 0) {
media.push(

View File

@ -99,7 +99,7 @@ export const BlockModal = ({ accountId, acct }) => {
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</button>
<Button onClick={handleClick} autoFocus>
<Button onClick={handleClick} dangerous autoFocus>
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
</Button>
</div>

View File

@ -5,9 +5,9 @@ import { useRouteMatch, NavLink } from 'react-router-dom';
import { Icon } from 'flavours/glitch/components/icon';
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, onClick, href, method, badge, transparent, ...other }) => {
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, onClick, href, method, badge, transparent, optional, ...other }) => {
const match = useRouteMatch(to);
const className = classNames('column-link', { 'column-link--transparent': transparent });
const className = classNames('column-link', { 'column-link--transparent': transparent, 'column-link--optional': optional });
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon;
const activeIconElement = activeIcon ?? (activeIconComponent ? <Icon id={icon} icon={activeIconComponent} className='column-link__icon' /> : iconElement);
@ -58,6 +58,7 @@ ColumnLink.propTypes = {
method: PropTypes.string,
badge: PropTypes.node,
transparent: PropTypes.bool,
optional: PropTypes.bool,
};
export default ColumnLink;

View File

@ -1,106 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'flavours/glitch/actions/accounts';
import { blockDomain } from 'flavours/glitch/actions/domain_blocks';
import { closeModal } from 'flavours/glitch/actions/modal';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
export const DomainBlockModal = ({ domain, accountId, acct }) => {
const dispatch = useDispatch();
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockDomain(domain));
}, [dispatch, domain]);
const handleSecondaryClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={DomainDisabledIcon} />
</div>
<div>
<h1><FormattedMessage id='domain_block_modal.title' defaultMessage='Block domain?' /></h1>
<div>{domain}</div>
</div>
</div>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_wont_know' defaultMessage="They won't know they've been blocked." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_wont_see_posts' defaultMessage="You won't see posts or notifications from users on this server." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={PersonRemoveIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_will_lose_followers' defaultMessage='All your followers from this server will be removed.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_cant_follow' defaultMessage='Nobody from this server can follow you.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={HistoryIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_can_interact_with_old_posts' defaultMessage='People from this server can interact with your old posts.' /></div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<Button onClick={handleSecondaryClick} secondary>
<FormattedMessage id='domain_block_modal.block_account_instead' defaultMessage='Block @{name} instead' values={{ name: acct.split('@')[0] }} />
</Button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</button>
<Button onClick={handleClick} autoFocus>
<FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
</Button>
</div>
</div>
</div>
);
};
DomainBlockModal.propTypes = {
domain: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default DomainBlockModal;

View File

@ -0,0 +1,223 @@
import { useCallback, useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'flavours/glitch/actions/accounts';
import { blockDomain } from 'flavours/glitch/actions/domain_blocks';
import { closeModal } from 'flavours/glitch/actions/modal';
import { apiRequest } from 'flavours/glitch/api';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { useAppDispatch } from 'flavours/glitch/store';
interface DomainBlockPreviewResponse {
following_count: number;
followers_count: number;
}
export const DomainBlockModal: React.FC<{
domain: string;
accountId: string;
acct: string;
}> = ({ domain, accountId, acct }) => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
const [preview, setPreview] = useState<
DomainBlockPreviewResponse | 'error' | null
>(null);
const handleClick = useCallback(() => {
if (loading) {
return; // Prevent destructive action before the preview finishes loading or times out
}
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockDomain(domain));
}, [dispatch, loading, domain]);
const handleSecondaryClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
useEffect(() => {
setLoading(true);
apiRequest<DomainBlockPreviewResponse>('GET', 'v1/domain_blocks/preview', {
params: { domain },
timeout: 5000,
})
.then((data) => {
setPreview(data);
setLoading(false);
return '';
})
.catch(() => {
setPreview('error');
setLoading(false);
});
}, [setPreview, setLoading, domain]);
return (
<div className='modal-root__modal safety-action-modal' aria-live='polite'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon id='' icon={DomainDisabledIcon} />
</div>
<div>
<h1>
<FormattedMessage
id='domain_block_modal.title'
defaultMessage='Block domain?'
/>
</h1>
<div>{domain}</div>
</div>
</div>
<div className='safety-action-modal__bullet-points'>
{preview &&
preview !== 'error' &&
preview.followers_count + preview.following_count > 0 && (
<div>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={PersonRemoveIcon} />
</div>
<div>
<strong>
<FormattedMessage
id='domain_block_modal.you_will_lose_num_followers'
defaultMessage='You will lose {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} followers}} and {followingCount, plural, one {{followingCountDisplay} person you follow} other {{followingCountDisplay} people you follow}}.'
values={{
followersCount: preview.followers_count,
followersCountDisplay: (
<ShortNumber value={preview.followers_count} />
),
followingCount: preview.following_count,
followingCountDisplay: (
<ShortNumber value={preview.following_count} />
),
}}
/>
</strong>
</div>
</div>
)}
{preview === 'error' && (
<div>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={PersonRemoveIcon} />
</div>
<div>
<strong>
<FormattedMessage
id='domain_block_modal.you_will_lose_relationships'
defaultMessage='You will lose all followers and people you follow from this server.'
/>
</strong>
</div>
</div>
)}
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={CampaignIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_wont_know'
defaultMessage="They won't know they've been blocked."
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={VisibilityOffIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.you_wont_see_posts'
defaultMessage="You won't see posts or notifications from users on this server."
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={ReplyIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_cant_follow'
defaultMessage='Nobody from this server can follow you.'
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={HistoryIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_can_interact_with_old_posts'
defaultMessage='People from this server can interact with your old posts.'
/>
</div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<Button onClick={handleSecondaryClick} secondary>
<FormattedMessage
id='domain_block_modal.block_account_instead'
defaultMessage='Block @{name} instead'
values={{ name: acct.split('@')[0] }}
/>
</Button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'
/>
</button>
<Button onClick={handleClick} dangerous aria-busy={loading}>
{loading ? (
<LoadingIndicator />
) : (
<FormattedMessage
id='domain_block_modal.block'
defaultMessage='Block server'
/>
)}
</Button>
</div>
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default DomainBlockModal;

View File

@ -122,14 +122,17 @@ class NavigationPanel extends Component {
let banner = undefined;
if(transientSingleColumn)
banner = (<div className='switch-to-advanced'>
if (transientSingleColumn) {
banner = (
<div className='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}
{" "}
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>);
</div>
);
}
return (
<div className='navigation-panel'>
@ -139,6 +142,7 @@ class NavigationPanel extends Component {
</div>
}
<div className='navigation-panel__menu'>
{signedIn && (
<>
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
@ -178,8 +182,8 @@ class NavigationPanel extends Component {
{!!preferencesLink && <ColumnLink transparent href={preferencesLink} icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} />}
<ColumnLink transparent onClick={onOpenSettings} icon='cogs' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.app_settings)} />
{canManageReports(permissions) && <ColumnLink transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
{canViewAdminDashboard(permissions) && <ColumnLink transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
{canManageReports(permissions) && <ColumnLink optional transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
{canViewAdminDashboard(permissions) && <ColumnLink optional transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
</>
)}
@ -187,6 +191,9 @@ class NavigationPanel extends Component {
<hr />
<ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} />
</div>
</div>
<div className='flex-spacer' />
<NavigationPortal />
</div>

View File

@ -0,0 +1,2 @@
// Temporary until we type it correctly
export type MediaAttachment = Immutable.Map<string, unknown>;

View File

@ -10,3 +10,5 @@ export type Status = Immutable.Map<string, unknown>;
type CardShape = Required<ApiPreviewCardJSON>;
export type Card = RecordOf<CardShape>;
export type MediaAttachment = Immutable.Map<string, unknown>;

View File

@ -81,6 +81,18 @@
outline: $ui-button-icon-focus-outline;
}
&--dangerous {
background-color: var(--error-background-color);
color: var(--on-error-color);
&:active,
&:focus,
&:hover {
background-color: var(--error-active-background-color);
transition: none;
}
}
&--destructive {
&:active,
&:focus,
@ -230,6 +242,7 @@
flex: 0 0 auto;
a {
display: flex;
color: inherit;
text-decoration: none;
}
@ -640,19 +653,39 @@ body > [data-popper-placement] {
}
&__uploads {
display: flex;
gap: 8px;
padding: 0 12px;
flex-wrap: wrap;
align-self: stretch;
align-items: flex-start;
align-content: flex-start;
justify-content: center;
aspect-ratio: 3/2;
}
.media-gallery {
gap: 8px;
}
&__upload {
flex: 1 1 0;
min-width: calc(50% - 8px);
position: relative;
cursor: grab;
&.dragging {
opacity: 0;
}
&.overlay {
height: 100%;
border-radius: 8px;
pointer-events: none;
}
&__drag-handle {
position: absolute;
top: 50%;
inset-inline-start: 0;
transform: translateY(-50%);
color: $white;
background: transparent;
border: 0;
padding: 8px 3px;
cursor: grab;
}
&__actions {
display: flex;
@ -673,8 +706,7 @@ body > [data-popper-placement] {
&__thumbnail {
width: 100%;
height: 144px;
border-radius: 6px;
height: 100%;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
@ -3672,12 +3704,14 @@ $ui-header-logo-wordmark-width: 99px;
margin-top: 10px;
margin-bottom: 10px;
height: calc(100% - 20px);
overflow-y: auto;
overflow: hidden;
display: flex;
flex-direction: column;
& > a {
flex: 0 0 auto;
&__menu {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
.logo {
@ -3688,6 +3722,36 @@ $ui-header-logo-wordmark-width: 99px;
&__logo {
margin-bottom: 12px;
}
@media screen and (height <= 710px) {
&__portal {
display: none;
}
}
@media screen and (height <= 765px) {
&__portal .trends__item:nth-child(n + 3) {
display: none;
}
}
@media screen and (height <= 820px) {
&__portal .trends__item:nth-child(n + 4) {
display: none;
}
}
@media screen and (height <= 920px) {
.column-link.column-link--optional {
display: none;
}
}
@media screen and (height <= 1040px) {
.list-panel {
display: none;
}
}
}
.navigation-panel,
@ -4051,22 +4115,6 @@ $ui-header-logo-wordmark-width: 99px;
}
}
@media screen and (height <= 810px) {
.trends__item:nth-of-type(3) {
display: none;
}
}
@media screen and (height <= 720px) {
.trends__item:nth-of-type(2) {
display: none;
}
}
@media screen and (height <= 670px) {
display: none;
}
.trends__item {
border-bottom: 0;
padding: 10px;
@ -6259,6 +6307,7 @@ a.status-card {
.icon {
width: 24px;
height: 24px;
filter: var(--overlay-icon-shadow);
}
&:hover,
@ -6353,6 +6402,10 @@ a.status-card {
.icon-button {
color: $white;
.icon {
filter: var(--overlay-icon-shadow);
}
&:hover,
&:focus,
&:active {
@ -6411,6 +6464,7 @@ a.status-card {
.media-modal__page-dot {
flex: 0 0 auto;
background-color: $white;
filter: var(--overlay-icon-shadow);
opacity: 0.4;
height: 6px;
width: 6px;
@ -6687,6 +6741,14 @@ a.status-card {
display: flex;
gap: 16px;
align-items: center;
strong {
font-weight: 700;
}
}
&--deemphasized {
color: $secondary-text-color;
}
&__icon {
@ -7432,72 +7494,13 @@ img.modal-warning {
inset-inline-end: 8px;
display: flex;
gap: 2px;
&--layout-2 {
.media-gallery__item:nth-child(1) {
border-end-end-radius: 0;
border-start-end-radius: 0;
}
.media-gallery__item:nth-child(2) {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
}
&--layout-3 {
.media-gallery__item:nth-child(1) {
border-end-end-radius: 0;
border-start-end-radius: 0;
}
.media-gallery__item:nth-child(2) {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
}
.media-gallery__item:nth-child(3) {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-start-end-radius: 0;
}
}
&--layout-4 {
.media-gallery__item:nth-child(1) {
border-end-end-radius: 0;
border-start-end-radius: 0;
border-end-start-radius: 0;
}
.media-gallery__item:nth-child(2) {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
}
.media-gallery__item:nth-child(3) {
border-start-start-radius: 0;
border-start-end-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
}
.media-gallery__item:nth-child(4) {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-start-end-radius: 0;
}
}
}
.media-gallery__alt__label,
.media-gallery__gifv__label {
display: flex;
align-items: center;
justify-content: center;
.media-gallery__alt__label {
display: block;
text-align: center;
color: $white;
border: 0;
background: rgba($black, 0.65);
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
padding: 3px 8px;
@ -7505,8 +7508,41 @@ img.modal-warning {
font-size: 12px;
font-weight: 700;
z-index: 1;
pointer-events: none;
line-height: 20px;
cursor: pointer;
pointer-events: auto;
&--non-interactive {
pointer-events: none;
}
}
.media-gallery__alt__popover {
background: rgba($black, 0.65);
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
border-radius: 4px;
box-shadow: var(--dropdown-shadow);
padding: 16px;
min-width: 16em;
min-height: 2em;
max-width: 22em;
max-height: 30em;
overflow-y: auto;
h4 {
font-size: 15px;
line-height: 20px;
font-weight: 500;
color: $white;
margin-bottom: 8px;
}
p {
font-size: 15px;
line-height: 20px;
color: rgba($white, 0.85);
white-space: pre-line;
}
}
.attachment-list {
@ -7580,10 +7616,68 @@ img.modal-warning {
width: 100%;
min-height: 64px;
display: grid;
grid-template-columns: 50% 50%;
grid-template-rows: 50% 50%;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2px;
&--layout-2 {
& > .media-gallery__item:nth-child(1) {
border-end-end-radius: 0;
border-start-end-radius: 0;
}
& > .media-gallery__item:nth-child(2) {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
}
&--layout-3 {
& > .media-gallery__item:nth-child(1) {
border-end-end-radius: 0;
border-start-end-radius: 0;
}
& > .media-gallery__item:nth-child(2) {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
}
& > .media-gallery__item:nth-child(3) {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-start-end-radius: 0;
}
}
&--layout-4 {
& > .media-gallery__item:nth-child(1) {
border-end-end-radius: 0;
border-start-end-radius: 0;
border-end-start-radius: 0;
}
& > .media-gallery__item:nth-child(2) {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
}
& > .media-gallery__item:nth-child(3) {
border-start-start-radius: 0;
border-start-end-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
}
& > .media-gallery__item:nth-child(4) {
border-start-start-radius: 0;
border-end-start-radius: 0;
border-start-end-radius: 0;
}
}
@include fullwidth-gallery;
}
@ -7594,6 +7688,9 @@ img.modal-warning {
position: relative;
border-radius: 8px;
overflow: hidden;
outline: 1px solid var(--media-outline-color);
outline-offset: -1px;
z-index: 1;
&--tall {
grid-row: span 2;
@ -7610,15 +7707,44 @@ img.modal-warning {
&.letterbox {
background: $base-shadow-color;
}
&--square {
aspect-ratio: 1;
}
&__overlay {
position: absolute;
top: 0;
inset-inline-start: 0;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 100%;
height: 100%;
pointer-events: none;
padding: 8px;
z-index: 1;
&--corner {
align-items: flex-start;
justify-content: flex-end;
}
.icon {
color: $white;
filter: var(--overlay-icon-shadow);
}
}
}
.media-gallery__item-thumbnail {
cursor: zoom-in;
cursor: pointer;
display: block;
text-decoration: none;
color: $secondary-text-color;
position: relative;
z-index: 1;
z-index: -1;
&,
img {
@ -7640,7 +7766,7 @@ img.modal-warning {
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: 0;
z-index: -2;
background: $base-overlay-background;
&--hidden {
@ -7653,10 +7779,11 @@ img.modal-warning {
overflow: hidden;
position: relative;
width: 100%;
z-index: -1;
}
.media-gallery__item-gifv-thumbnail {
cursor: zoom-in;
cursor: pointer;
height: 100%;
width: 100%;
object-fit: contain;
@ -7668,13 +7795,6 @@ img.modal-warning {
}
}
.media-gallery__item-thumbnail-label {
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
position: absolute;
}
/* End Media Gallery */
.detailed,
@ -7697,6 +7817,8 @@ img.modal-warning {
border-radius: 8px;
padding-bottom: 44px;
width: 100%;
outline: 1px solid var(--media-outline-color);
outline-offset: -1px;
&.editable {
border-radius: 0;
@ -7753,6 +7875,7 @@ img.modal-warning {
.video-player__controls {
padding-top: 10px;
background: transparent;
z-index: 1;
}
}
@ -7766,16 +7889,15 @@ img.modal-warning {
color: $white;
display: flex;
align-items: center;
outline: 1px solid var(--media-outline-color);
outline-offset: -1px;
z-index: 2;
&.editable {
border-radius: 0;
height: 100% !important;
}
&:focus {
outline: 0;
}
.detailed-status & {
width: 100%;
height: 100%;
@ -7787,7 +7909,7 @@ img.modal-warning {
display: block;
max-width: 100vw;
max-height: 80vh;
z-index: 1;
z-index: -2;
position: relative;
}
@ -7814,7 +7936,7 @@ img.modal-warning {
&__controls {
position: absolute;
direction: ltr;
z-index: 2;
z-index: -1;
bottom: 0;
inset-inline-start: 0;
inset-inline-end: 0;
@ -8129,26 +8251,16 @@ img.modal-warning {
}
.account-gallery__container {
display: flex;
flex-wrap: wrap;
padding: 4px 2px;
}
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2px;
.account-gallery__item {
border: 0;
box-sizing: border-box;
display: block;
position: relative;
border-radius: 4px;
overflow: hidden;
margin: 2px;
.media-gallery__item {
border-radius: 0;
}
&__icons {
position: absolute;
top: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
.load-more {
grid-column: span 3;
}
}

View File

@ -313,6 +313,10 @@ code {
ul {
columns: 2;
@media screen and (max-width: $mobile-breakpoint) {
columns: 1;
}
}
}

View File

@ -56,7 +56,6 @@ table {
@supports not selector(::-webkit-scrollbar) {
html {
scrollbar-color: $action-button-color var(--background-border-color);
scrollbar-width: thin;
}
}

View File

@ -355,6 +355,10 @@ a.table-action-link {
@media screen and (max-width: $no-gap-breakpoint) {
border-top: 1px solid var(--background-border-color);
}
&--no-toolbar {
border-top: 1px solid var(--background-border-color);
}
}
@media screen and (width <= 870px) {

View File

@ -117,4 +117,9 @@ $dismiss-overlay-width: 4rem;
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
--on-surface-color: #{transparentize($ui-base-color, 0.5)};
--avatar-border-radius: 8px;
--media-outline-color: #{rgba(#fcf8ff, 0.15)};
--overlay-icon-shadow: drop-shadow(0 0 8px #{rgba($base-shadow-color, 0.25)});
--error-background-color: #{darken($error-red, 16%)};
--error-active-background-color: #{darken($error-red, 12%)};
--on-error-color: #fff;
}

View File

@ -70,6 +70,7 @@ export async function apiRequest<ApiResponse = unknown>(
args: {
params?: RequestParamsOrData;
data?: RequestParamsOrData;
timeout?: number;
} = {},
) {
const { data } = await api().request<ApiResponse>({

View File

@ -0,0 +1,67 @@
import { useState, useCallback, useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/Overlay';
import type {
OffsetValue,
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const AltTextBadge: React.FC<{
description: string;
}> = ({ description }) => {
const anchorRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const handleClick = useCallback(() => {
setOpen((v) => !v);
}, [setOpen]);
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
return (
<>
<button
ref={anchorRef}
className='media-gallery__alt__label'
onClick={handleClick}
>
ALT
</button>
<Overlay
rootClose
onHide={handleClose}
show={open}
target={anchorRef.current}
placement='top-end'
flip
offset={offset}
popperConfig={popperConfig}
>
{({ props }) => (
<div {...props} className='hover-card-controller'>
<div
className='media-gallery__alt__popover dropdown-animation'
role='tooltip'
>
<h4>
<FormattedMessage
id='alt_text_badge.title'
defaultMessage='Alt text'
/>
</h4>
<p>{description}</p>
</div>
</div>
)}
</Overlay>
</>
);
};

View File

@ -7,6 +7,7 @@ interface BaseProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
block?: boolean;
secondary?: boolean;
dangerous?: boolean;
}
interface PropsChildren extends PropsWithChildren<BaseProps> {
@ -26,6 +27,7 @@ export const Button: React.FC<Props> = ({
disabled,
block,
secondary,
dangerous,
className,
title,
text,
@ -46,6 +48,7 @@ export const Button: React.FC<Props> = ({
className={classNames('button', className, {
'button-secondary': secondary,
'button--block': block,
'button--dangerous': dangerous,
})}
disabled={disabled}
onClick={handleClick}

View File

@ -10,7 +10,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import { AltTextBadge } from 'mastodon/components/alt_text_badge';
import { Blurhash } from 'mastodon/components/blurhash';
import { formatTime } from 'mastodon/features/video';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
@ -57,7 +59,7 @@ class Item extends PureComponent {
hoverToPlay () {
const { attachment } = this.props;
return !this.getAutoPlay() && attachment.get('type') === 'gifv';
return !this.getAutoPlay() && ['gifv', 'video'].includes(attachment.get('type'));
}
handleClick = (e) => {
@ -96,7 +98,7 @@ class Item extends PureComponent {
}
if (attachment.get('description')?.length > 0) {
badges.push(<span key='alt' className='media-gallery__alt__label'>ALT</span>);
badges.push(<AltTextBadge key='alt' description={attachment.get('description')} />);
}
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
@ -150,10 +152,15 @@ class Item extends PureComponent {
/>
</a>
);
} else if (attachment.get('type') === 'gifv') {
} else if (['gifv', 'video'].includes(attachment.get('type'))) {
const autoPlay = this.getAutoPlay();
const duration = attachment.getIn(['meta', 'original', 'duration']);
badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);
if (attachment.get('type') === 'gifv') {
badges.push(<span key='gif' className='media-gallery__alt__label media-gallery__alt__label--non-interactive'>GIF</span>);
} else {
badges.push(<span key='video' className='media-gallery__alt__label media-gallery__alt__label--non-interactive'>{formatTime(Math.floor(duration))}</span>);
}
thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
@ -167,6 +174,7 @@ class Item extends PureComponent {
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onLoadedData={this.handleImageLoad}
autoPlay={autoPlay}
playsInline
loop

View File

@ -4,22 +4,22 @@ import AccountNavigation from 'mastodon/features/account/navigation';
import Trends from 'mastodon/features/getting_started/containers/trends_container';
import { showTrends } from 'mastodon/initial_state';
const DefaultNavigation: React.FC = () =>
showTrends ? (
<>
<div className='flex-spacer' />
<Trends />
</>
) : null;
const DefaultNavigation: React.FC = () => (showTrends ? <Trends /> : null);
export const NavigationPortal: React.FC = () => (
<div className='navigation-panel__portal'>
<Switch>
<Route path='/@:acct' exact component={AccountNavigation} />
<Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} />
<Route
path='/@:acct/tagged/:tagged?'
exact
component={AccountNavigation}
/>
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
<Route path='/@:acct/followers' exact component={AccountNavigation} />
<Route path='/@:acct/following' exact component={AccountNavigation} />
<Route path='/@:acct/media' exact component={AccountNavigation} />
<Route component={DefaultNavigation} />
</Switch>
</div>
);

View File

@ -449,7 +449,25 @@ class Status extends ImmutablePureComponent {
} else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={status.get('media_attachments')}
lang={language}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
@ -501,24 +519,6 @@ class Status extends ImmutablePureComponent {
)}
</Bundle>
);
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={status.get('media_attachments')}
lang={language}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
media = (

View File

@ -43,10 +43,7 @@ class AccountNavigation extends PureComponent {
}
return (
<>
<div className='flex-spacer' />
<FeaturedTags accountId={accountId} tagged={tagged} />
</>
);
}

View File

@ -1,158 +0,0 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AudiotrackIcon from '@/material-icons/400-24px/music_note.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
displayWidth: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
};
state = {
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
loaded: false,
};
handleImageLoad = () => {
this.setState({ loaded: true });
};
handleMouseEnter = e => {
if (this.hoverToPlay()) {
e.target.play();
}
};
handleMouseLeave = e => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
};
hoverToPlay () {
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
}
handleClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (this.state.visible) {
this.props.onOpenMedia(this.props.attachment);
} else {
this.setState({ visible: true });
}
}
};
render () {
const { attachment, displayWidth } = this.props;
const { visible, loaded } = this.state;
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
const height = width;
const status = attachment.get('status');
const title = status.get('spoiler_text') || attachment.get('description');
let thumbnail, label, icon, content;
if (!visible) {
icon = (
<span className='account-gallery__item__icons'>
<Icon id='eye-slash' icon={VisibilityOffIcon} />
</span>
);
} else {
if (['audio', 'video'].includes(attachment.get('type'))) {
content = (
<img
src={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')}
lang={status.get('language')}
onLoad={this.handleImageLoad}
/>
);
if (attachment.get('type') === 'audio') {
label = <Icon id='music' icon={AudiotrackIcon} />;
} else {
label = <Icon id='play' icon={PlayArrowIcon} />;
}
} else if (attachment.get('type') === 'image') {
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
content = (
<img
src={attachment.get('preview_url')}
alt={attachment.get('description')}
lang={status.get('language')}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (attachment.get('type') === 'gifv') {
content = (
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
title={attachment.get('description')}
lang={status.get('language')}
role='application'
src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlayGif}
playsInline
loop
muted
/>
);
label = 'GIF';
}
thumbnail = (
<div className='media-gallery__gifv'>
{content}
{label && (
<div className='media-gallery__item__badges'>
<span className='media-gallery__gifv__label'>{label}</span>
</div>
)}
</div>
);
}
return (
<div className='account-gallery__item' style={{ width, height }}>
<a className='media-gallery__item-thumbnail' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<Blurhash
hash={attachment.get('blurhash')}
className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
dummy={!useBlurhash}
/>
{visible ? thumbnail : icon}
</a>
</div>
);
}
}

View File

@ -0,0 +1,200 @@
import { useState, useCallback } from 'react';
import classNames from 'classnames';
import HeadphonesIcon from '@/material-icons/400-24px/headphones-fill.svg?react';
import MovieIcon from '@/material-icons/400-24px/movie-fill.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { AltTextBadge } from 'mastodon/components/alt_text_badge';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import { formatTime } from 'mastodon/features/video';
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
import type { Status, MediaAttachment } from 'mastodon/models/status';
export const MediaItem: React.FC<{
attachment: MediaAttachment;
onOpenMedia: (arg0: MediaAttachment) => void;
}> = ({ attachment, onOpenMedia }) => {
const [visible, setVisible] = useState(
(displayMedia !== 'hide_all' &&
!attachment.getIn(['status', 'sensitive'])) ||
displayMedia === 'show_all',
);
const [loaded, setLoaded] = useState(false);
const handleImageLoad = useCallback(() => {
setLoaded(true);
}, [setLoaded]);
const handleMouseEnter = useCallback(
(e: React.MouseEvent<HTMLVideoElement>) => {
if (e.target instanceof HTMLVideoElement) {
void e.target.play();
}
},
[],
);
const handleMouseLeave = useCallback(
(e: React.MouseEvent<HTMLVideoElement>) => {
if (e.target instanceof HTMLVideoElement) {
e.target.pause();
e.target.currentTime = 0;
}
},
[],
);
const handleClick = useCallback(
(e: React.MouseEvent<HTMLAnchorElement>) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (visible) {
onOpenMedia(attachment);
} else {
setVisible(true);
}
}
},
[attachment, visible, onOpenMedia, setVisible],
);
const status = attachment.get('status') as Status;
const description = (attachment.getIn(['translation', 'description']) ||
attachment.get('description')) as string | undefined;
const previewUrl = attachment.get('preview_url') as string;
const fullUrl = attachment.get('url') as string;
const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
const lang = status.get('language') as string;
const blurhash = attachment.get('blurhash') as string;
const statusId = status.get('id') as string;
const acct = status.getIn(['account', 'acct']) as string;
const type = attachment.get('type') as string;
let thumbnail;
const badges = [];
if (description && description.length > 0) {
badges.push(<AltTextBadge key='alt' description={description} />);
}
if (!visible) {
thumbnail = (
<div className='media-gallery__item__overlay'>
<Icon id='eye-slash' icon={VisibilityOffIcon} />
</div>
);
} else if (type === 'audio') {
thumbnail = (
<>
<img
src={previewUrl || avatarUrl}
alt={description}
title={description}
lang={lang}
onLoad={handleImageLoad}
/>
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
<Icon id='music' icon={HeadphonesIcon} />
</div>
</>
);
} else if (type === 'image') {
const focusX = (attachment.getIn(['meta', 'focus', 'x']) || 0) as number;
const focusY = (attachment.getIn(['meta', 'focus', 'y']) || 0) as number;
const x = (focusX / 2 + 0.5) * 100;
const y = (focusY / -2 + 0.5) * 100;
thumbnail = (
<img
src={previewUrl}
alt={description}
title={description}
lang={lang}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={handleImageLoad}
/>
);
} else if (['video', 'gifv'].includes(type)) {
const duration = attachment.getIn([
'meta',
'original',
'duration',
]) as number;
thumbnail = (
<div className='media-gallery__gifv'>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={description}
title={description}
lang={lang}
src={fullUrl}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onLoadedData={handleImageLoad}
autoPlay={autoPlayGif}
playsInline
loop
muted
/>
{type === 'video' && (
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
<Icon id='play' icon={MovieIcon} />
</div>
)}
</div>
);
if (type === 'gifv') {
badges.push(
<span
key='gif'
className='media-gallery__alt__label media-gallery__alt__label--non-interactive'
>
GIF
</span>,
);
} else {
badges.push(
<span
key='video'
className='media-gallery__alt__label media-gallery__alt__label--non-interactive'
>
{formatTime(Math.floor(duration))}
</span>,
);
}
}
return (
<div className='media-gallery__item media-gallery__item--square'>
<Blurhash
hash={blurhash}
className={classNames('media-gallery__preview', {
'media-gallery__preview--hidden': visible && loaded,
})}
dummy={!useBlurhash}
/>
<a
className='media-gallery__item-thumbnail'
href={`/@${acct}/${statusId}`}
onClick={handleClick}
target='_blank'
rel='noopener noreferrer'
>
{thumbnail}
</a>
{badges.length > 0 && (
<div className='media-gallery__item__badges'>{badges}</div>
)}
</div>
);
};

View File

@ -20,7 +20,7 @@ import { expandAccountMediaTimeline } from '../../actions/timelines';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
import MediaItem from './components/media_item';
import { MediaItem } from './components/media_item';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);

View File

@ -1,81 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import spring from 'react-motion/lib/spring';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { undoUploadCompose, initMediaEditModal } from 'mastodon/actions/compose';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import Motion from 'mastodon/features/ui/util/optional_motion';
export const Upload = ({ id, onDragStart, onDragEnter, onDragEnd }) => {
const dispatch = useDispatch();
const media = useSelector(state => state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id));
const sensitive = useSelector(state => state.getIn(['compose', 'spoiler']));
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
}, [dispatch, id]);
const handleFocalPointClick = useCallback(() => {
dispatch(initMediaEditModal(id));
}, [dispatch, id]);
const handleDragStart = useCallback(() => {
onDragStart(id);
}, [onDragStart, id]);
const handleDragEnter = useCallback(() => {
onDragEnter(id);
}, [onDragEnter, id]);
if (!media) {
return null;
}
const focusX = media.getIn(['meta', 'focus', 'x']);
const focusY = media.getIn(['meta', 'focus', 'y']);
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
const missingDescription = (media.get('description') || '').length === 0;
return (
<div className='compose-form__upload' draggable onDragStart={handleDragStart} onDragEnter={handleDragEnter} onDragEnd={onDragEnd}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
{sensitive && <Blurhash
hash={media.get('blurhash')}
className='compose-form__upload__preview'
/>}
<div className='compose-form__upload__actions'>
<button type='button' className='icon-button compose-form__upload__delete' onClick={handleUndoClick}><Icon icon={CloseIcon} /></button>
<button type='button' className='icon-button' onClick={handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
</div>
<div className='compose-form__upload__warning'>
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
</div>
</div>
)}
</Motion>
</div>
);
};
Upload.propTypes = {
id: PropTypes.string,
onDragEnter: PropTypes.func,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
};

View File

@ -0,0 +1,130 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import {
undoUploadCompose,
initMediaEditModal,
} from 'mastodon/actions/compose';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
export const Upload: React.FC<{
id: string;
dragging?: boolean;
overlay?: boolean;
tall?: boolean;
wide?: boolean;
}> = ({ id, dragging, overlay, tall, wide }) => {
const dispatch = useAppDispatch();
const media = useAppSelector(
(state) =>
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
.find((item: MediaAttachment) => item.get('id') === id) as // eslint-disable-line @typescript-eslint/no-unsafe-member-access
| MediaAttachment
| undefined,
);
const sensitive = useAppSelector(
(state) => state.compose.get('spoiler') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
}, [dispatch, id]);
const handleFocalPointClick = useCallback(() => {
dispatch(initMediaEditModal(id));
}, [dispatch, id]);
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
if (!media) {
return null;
}
const focusX = media.getIn(['meta', 'focus', 'x']) as number;
const focusY = media.getIn(['meta', 'focus', 'y']) as number;
const x = (focusX / 2 + 0.5) * 100;
const y = (focusY / -2 + 0.5) * 100;
const missingDescription =
((media.get('description') as string | undefined) ?? '').length === 0;
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
className={classNames('compose-form__upload media-gallery__item', {
dragging,
overlay,
'media-gallery__item--tall': tall,
'media-gallery__item--wide': wide,
})}
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
>
<div
className='compose-form__upload__thumbnail'
style={{
backgroundImage: !sensitive
? `url(${media.get('preview_url') as string})`
: undefined,
backgroundPosition: `${x}% ${y}%`,
}}
>
{sensitive && (
<Blurhash
hash={media.get('blurhash') as string}
className='compose-form__upload__preview'
/>
)}
<div className='compose-form__upload__actions'>
<button
type='button'
className='icon-button compose-form__upload__delete'
onClick={handleUndoClick}
>
<Icon id='close' icon={CloseIcon} />
</button>
<button
type='button'
className='icon-button'
onClick={handleFocalPointClick}
>
<Icon id='edit' icon={EditIcon} />{' '}
<FormattedMessage id='upload_form.edit' defaultMessage='Edit' />
</button>
</div>
<div className='compose-form__upload__warning'>
<button
type='button'
className={classNames('icon-button', {
active: missingDescription,
})}
onClick={handleFocalPointClick}
>
{missingDescription && <Icon id='warning' icon={WarningIcon} />} ALT
</button>
</div>
</div>
</div>
);
};

View File

@ -1,53 +0,0 @@
import { useRef, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { changeMediaOrder } from 'mastodon/actions/compose';
import { Upload } from './upload';
import { UploadProgress } from './upload_progress';
export const UploadForm = () => {
const dispatch = useDispatch();
const mediaIds = useSelector(state => state.getIn(['compose', 'media_attachments']).map(item => item.get('id')));
const active = useSelector(state => state.getIn(['compose', 'is_uploading']));
const progress = useSelector(state => state.getIn(['compose', 'progress']));
const isProcessing = useSelector(state => state.getIn(['compose', 'is_processing']));
const dragItem = useRef();
const dragOverItem = useRef();
const handleDragStart = useCallback(id => {
dragItem.current = id;
}, [dragItem]);
const handleDragEnter = useCallback(id => {
dragOverItem.current = id;
}, [dragOverItem]);
const handleDragEnd = useCallback(() => {
dispatch(changeMediaOrder(dragItem.current, dragOverItem.current));
dragItem.current = null;
dragOverItem.current = null;
}, [dispatch, dragItem, dragOverItem]);
return (
<>
<UploadProgress active={active} progress={progress} isProcessing={isProcessing} />
{mediaIds.size > 0 && (
<div className='compose-form__uploads'>
{mediaIds.map(id => (
<Upload
key={id}
id={id}
onDragStart={handleDragStart}
onDragEnter={handleDragEnter}
onDragEnd={handleDragEnd}
/>
))}
</div>
)}
</>
);
};

View File

@ -0,0 +1,185 @@
import { useState, useCallback, useMemo } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import type { List } from 'immutable';
import type {
DragStartEvent,
DragEndEvent,
UniqueIdentifier,
Announcements,
ScreenReaderInstructions,
} from '@dnd-kit/core';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
rectSortingStrategy,
} from '@dnd-kit/sortable';
import { changeMediaOrder } from 'mastodon/actions/compose';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { Upload } from './upload';
import { UploadProgress } from './upload_progress';
const messages = defineMessages({
screenReaderInstructions: {
id: 'upload_form.drag_and_drop.instructions',
defaultMessage:
'To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.',
},
onDragStart: {
id: 'upload_form.drag_and_drop.on_drag_start',
defaultMessage: 'Picked up media attachment {item}.',
},
onDragOver: {
id: 'upload_form.drag_and_drop.on_drag_over',
defaultMessage: 'Media attachment {item} was moved.',
},
onDragEnd: {
id: 'upload_form.drag_and_drop.on_drag_end',
defaultMessage: 'Media attachment {item} was dropped.',
},
onDragCancel: {
id: 'upload_form.drag_and_drop.on_drag_cancel',
defaultMessage:
'Dragging was cancelled. Media attachment {item} was dropped.',
},
});
export const UploadForm: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const mediaIds = useAppSelector(
(state) =>
state.compose // eslint-disable-line @typescript-eslint/no-unsafe-call
.get('media_attachments') // eslint-disable-line @typescript-eslint/no-unsafe-member-access
.map((item: MediaAttachment) => item.get('id')) as List<string>, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
);
const active = useAppSelector(
(state) => state.compose.get('is_uploading') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const progress = useAppSelector(
(state) => state.compose.get('progress') as number, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const isProcessing = useAppSelector(
(state) => state.compose.get('is_processing') as boolean, // eslint-disable-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
);
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragStart = useCallback(
(e: DragStartEvent) => {
const { active } = e;
setActiveId(active.id);
},
[setActiveId],
);
const handleDragEnd = useCallback(
(e: DragEndEvent) => {
const { active, over } = e;
if (over && active.id !== over.id) {
dispatch(changeMediaOrder(active.id, over.id));
}
setActiveId(null);
},
[dispatch, setActiveId],
);
const accessibility: {
screenReaderInstructions: ScreenReaderInstructions;
announcements: Announcements;
} = useMemo(
() => ({
screenReaderInstructions: {
draggable: intl.formatMessage(messages.screenReaderInstructions),
},
announcements: {
onDragStart({ active }) {
return intl.formatMessage(messages.onDragStart, { item: active.id });
},
onDragOver({ active }) {
return intl.formatMessage(messages.onDragOver, { item: active.id });
},
onDragEnd({ active }) {
return intl.formatMessage(messages.onDragEnd, { item: active.id });
},
onDragCancel({ active }) {
return intl.formatMessage(messages.onDragCancel, { item: active.id });
},
},
}),
[intl],
);
return (
<>
<UploadProgress
active={active}
progress={progress}
isProcessing={isProcessing}
/>
{mediaIds.size > 0 && (
<div
className={`compose-form__uploads media-gallery media-gallery--layout-${mediaIds.size}`}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
accessibility={accessibility}
>
<SortableContext
items={mediaIds.toArray()}
strategy={rectSortingStrategy}
>
{mediaIds.map((id, idx) => (
<Upload
key={id}
id={id}
dragging={id === activeId}
tall={mediaIds.size < 3 || (mediaIds.size === 3 && idx === 0)}
wide={mediaIds.size === 1}
/>
))}
</SortableContext>
<DragOverlay>
{activeId ? <Upload id={activeId as string} overlay /> : null}
</DragOverlay>
</DndContext>
</div>
)}
</>
);
};

View File

@ -151,7 +151,25 @@ export const DetailedStatus: React.FC<{
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder aspectRatio={attachmentAspectRatio} />;
} else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
if (
['image', 'gifv'].includes(
status.getIn(['media_attachments', 0, 'type']) as string,
) ||
status.get('media_attachments').size > 1
) {
media = (
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
lang={language}
height={300}
onOpenMedia={onOpenMedia}
visible={showMedia}
onToggleVisibility={onToggleMediaVisibility}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description =
attachment.getIn(['translation', 'description']) ||
@ -200,19 +218,6 @@ export const DetailedStatus: React.FC<{
onToggleVisibility={onToggleMediaVisibility}
/>
);
} else {
media = (
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
lang={language}
height={300}
onOpenMedia={onOpenMedia}
visible={showMedia}
onToggleVisibility={onToggleMediaVisibility}
/>
);
}
} else if (status.get('spoiler_text').length === 0) {
media = (

View File

@ -99,7 +99,7 @@ export const BlockModal = ({ accountId, acct }) => {
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</button>
<Button onClick={handleClick} autoFocus>
<Button onClick={handleClick} dangerous autoFocus>
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
</Button>
</div>

View File

@ -5,9 +5,9 @@ import { useRouteMatch, NavLink } from 'react-router-dom';
import { Icon } from 'mastodon/components/icon';
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, href, method, badge, transparent, ...other }) => {
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, href, method, badge, transparent, optional, ...other }) => {
const match = useRouteMatch(to);
const className = classNames('column-link', { 'column-link--transparent': transparent });
const className = classNames('column-link', { 'column-link--transparent': transparent, 'column-link--optional': optional });
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon;
const activeIconElement = activeIcon ?? (activeIconComponent ? <Icon id={icon} icon={activeIconComponent} className='column-link__icon' /> : iconElement);
@ -43,6 +43,7 @@ ColumnLink.propTypes = {
method: PropTypes.string,
badge: PropTypes.node,
transparent: PropTypes.bool,
optional: PropTypes.bool,
};
export default ColumnLink;

View File

@ -1,106 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'mastodon/actions/accounts';
import { blockDomain } from 'mastodon/actions/domain_blocks';
import { closeModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
export const DomainBlockModal = ({ domain, accountId, acct }) => {
const dispatch = useDispatch();
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockDomain(domain));
}, [dispatch, domain]);
const handleSecondaryClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={DomainDisabledIcon} />
</div>
<div>
<h1><FormattedMessage id='domain_block_modal.title' defaultMessage='Block domain?' /></h1>
<div>{domain}</div>
</div>
</div>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_wont_know' defaultMessage="They won't know they've been blocked." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_wont_see_posts' defaultMessage="You won't see posts or notifications from users on this server." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={PersonRemoveIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_will_lose_followers' defaultMessage='All your followers from this server will be removed.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_cant_follow' defaultMessage='Nobody from this server can follow you.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={HistoryIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_can_interact_with_old_posts' defaultMessage='People from this server can interact with your old posts.' /></div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<Button onClick={handleSecondaryClick} secondary>
<FormattedMessage id='domain_block_modal.block_account_instead' defaultMessage='Block @{name} instead' values={{ name: acct.split('@')[0] }} />
</Button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</button>
<Button onClick={handleClick} autoFocus>
<FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
</Button>
</div>
</div>
</div>
);
};
DomainBlockModal.propTypes = {
domain: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default DomainBlockModal;

View File

@ -0,0 +1,223 @@
import { useCallback, useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'mastodon/actions/accounts';
import { blockDomain } from 'mastodon/actions/domain_blocks';
import { closeModal } from 'mastodon/actions/modal';
import { apiRequest } from 'mastodon/api';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ShortNumber } from 'mastodon/components/short_number';
import { useAppDispatch } from 'mastodon/store';
interface DomainBlockPreviewResponse {
following_count: number;
followers_count: number;
}
export const DomainBlockModal: React.FC<{
domain: string;
accountId: string;
acct: string;
}> = ({ domain, accountId, acct }) => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
const [preview, setPreview] = useState<
DomainBlockPreviewResponse | 'error' | null
>(null);
const handleClick = useCallback(() => {
if (loading) {
return; // Prevent destructive action before the preview finishes loading or times out
}
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockDomain(domain));
}, [dispatch, loading, domain]);
const handleSecondaryClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
useEffect(() => {
setLoading(true);
apiRequest<DomainBlockPreviewResponse>('GET', 'v1/domain_blocks/preview', {
params: { domain },
timeout: 5000,
})
.then((data) => {
setPreview(data);
setLoading(false);
return '';
})
.catch(() => {
setPreview('error');
setLoading(false);
});
}, [setPreview, setLoading, domain]);
return (
<div className='modal-root__modal safety-action-modal' aria-live='polite'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon id='' icon={DomainDisabledIcon} />
</div>
<div>
<h1>
<FormattedMessage
id='domain_block_modal.title'
defaultMessage='Block domain?'
/>
</h1>
<div>{domain}</div>
</div>
</div>
<div className='safety-action-modal__bullet-points'>
{preview &&
preview !== 'error' &&
preview.followers_count + preview.following_count > 0 && (
<div>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={PersonRemoveIcon} />
</div>
<div>
<strong>
<FormattedMessage
id='domain_block_modal.you_will_lose_num_followers'
defaultMessage='You will lose {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} followers}} and {followingCount, plural, one {{followingCountDisplay} person you follow} other {{followingCountDisplay} people you follow}}.'
values={{
followersCount: preview.followers_count,
followersCountDisplay: (
<ShortNumber value={preview.followers_count} />
),
followingCount: preview.following_count,
followingCountDisplay: (
<ShortNumber value={preview.following_count} />
),
}}
/>
</strong>
</div>
</div>
)}
{preview === 'error' && (
<div>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={PersonRemoveIcon} />
</div>
<div>
<strong>
<FormattedMessage
id='domain_block_modal.you_will_lose_relationships'
defaultMessage='You will lose all followers and people you follow from this server.'
/>
</strong>
</div>
</div>
)}
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={CampaignIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_wont_know'
defaultMessage="They won't know they've been blocked."
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={VisibilityOffIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.you_wont_see_posts'
defaultMessage="You won't see posts or notifications from users on this server."
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={ReplyIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_cant_follow'
defaultMessage='Nobody from this server can follow you.'
/>
</div>
</div>
<div className='safety-action-modal__bullet-points--deemphasized'>
<div className='safety-action-modal__bullet-points__icon'>
<Icon id='' icon={HistoryIcon} />
</div>
<div>
<FormattedMessage
id='domain_block_modal.they_can_interact_with_old_posts'
defaultMessage='People from this server can interact with your old posts.'
/>
</div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<Button onClick={handleSecondaryClick} secondary>
<FormattedMessage
id='domain_block_modal.block_account_instead'
defaultMessage='Block @{name} instead'
values={{ name: acct.split('@')[0] }}
/>
</Button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'
/>
</button>
<Button onClick={handleClick} dangerous aria-busy={loading}>
{loading ? (
<LoadingIndicator />
) : (
<FormattedMessage
id='domain_block_modal.block'
defaultMessage='Block server'
/>
)}
</Button>
</div>
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default DomainBlockModal;

View File

@ -120,14 +120,17 @@ class NavigationPanel extends Component {
let banner = undefined;
if(transientSingleColumn)
banner = (<div className='switch-to-advanced'>
if (transientSingleColumn) {
banner = (
<div className='switch-to-advanced'>
{intl.formatMessage(messages.openedInClassicInterface)}
{" "}
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
{intl.formatMessage(messages.advancedInterface)}
</a>
</div>);
</div>
);
}
return (
<div className='navigation-panel'>
@ -141,6 +144,7 @@ class NavigationPanel extends Component {
</div>
}
<div className='navigation-panel__menu'>
{signedIn && (
<>
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
@ -179,8 +183,8 @@ class NavigationPanel extends Component {
<ColumnLink transparent href='/settings/preferences' icon='cog' iconComponent={SettingsIcon} text={intl.formatMessage(messages.preferences)} />
{canManageReports(permissions) && <ColumnLink transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
{canViewAdminDashboard(permissions) && <ColumnLink transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
{canManageReports(permissions) && <ColumnLink optional transparent href='/admin/reports' icon='flag' iconComponent={ModerationIcon} text={intl.formatMessage(messages.moderation)} />}
{canViewAdminDashboard(permissions) && <ColumnLink optional transparent href='/admin/dashboard' icon='tachometer' iconComponent={AdministrationIcon} text={intl.formatMessage(messages.administration)} />}
</>
)}
@ -188,6 +192,9 @@ class NavigationPanel extends Component {
<hr />
<ColumnLink transparent to='/about' icon='ellipsis-h' iconComponent={MoreHorizIcon} text={intl.formatMessage(messages.about)} />
</div>
</div>
<div className='flex-spacer' />
<NavigationPortal />
</div>

View File

@ -220,7 +220,6 @@
"domain_block_modal.they_cant_follow": "لا أحد من هذا الخادم يمكنه متابعتك.",
"domain_block_modal.they_wont_know": "لن يَعرف أنه قد تم حظره.",
"domain_block_modal.title": "أتريد حظر النطاق؟",
"domain_block_modal.you_will_lose_followers": "سيتم إزالة جميع متابعيك من هذا الخادم.",
"domain_block_modal.you_wont_see_posts": "لن ترى منشورات أو إشعارات من المستخدمين على هذا الخادم.",
"domain_pill.activitypub_lets_connect": "يتيح لك التواصل والتفاعل مع الناس ليس فقط على ماستدون، ولكن عبر تطبيقات اجتماعية مختلفة أيضا.",
"domain_pill.activitypub_like_language": "إنّ ActivityPub مثل لغة ماستدون التي يتحدث بها مع شبكات اجتماعية أخرى.",

View File

@ -219,7 +219,6 @@
"domain_block_modal.they_cant_follow": "Ніхто з гэтага сервера не зможа падпісацца на вас.",
"domain_block_modal.they_wont_know": "Карыстальнік не будзе ведаць пра блакіроўку.",
"domain_block_modal.title": "Заблакіраваць дамен?",
"domain_block_modal.you_will_lose_followers": "Усе падпісчыкі з гэтага сервера будуць выдаленыя.",
"domain_block_modal.you_wont_see_posts": "Вы не ўбачыце допісаў і апавяшчэнняў ад карыстальнікаў з гэтага сервера.",
"domain_pill.activitypub_lets_connect": "Ён дазваляе вам узаемадзейнічаць не толькі з карыстальнікамі Mastodon, але і розных іншых сацыяльных платформ.",
"domain_pill.activitypub_like_language": "ActivityPub — гэта мова, на якой Mastodon размаўляе з іншымі сацыяльнымі сеткамі.",

View File

@ -220,7 +220,6 @@
"domain_block_modal.they_cant_follow": "Никого от този сървър не може да ви последва.",
"domain_block_modal.they_wont_know": "Няма да узнаят, че са били блокирани.",
"domain_block_modal.title": "Блокирате ли домейн?",
"domain_block_modal.you_will_lose_followers": "Всичките ви последователи от този сървър ще се премахнат.",
"domain_block_modal.you_wont_see_posts": "Няма да виждате публикации или известия от потребителите на този сървър.",
"domain_pill.activitypub_lets_connect": "Позволява ви да се свързвате и взаимодействате с хора не само в Mastodon, но и през различни социални приложения.",
"domain_pill.activitypub_like_language": "ActivityPub е като език на Mastodon, говорещ с други социални мрежи.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Límit de freqüència",
"alert.unexpected.message": "S'ha produït un error inesperat.",
"alert.unexpected.title": "Vaja!",
"alt_text_badge.title": "Text alternatiu",
"announcement.announcement": "Anunci",
"attachments_list.unprocessed": "(sense processar)",
"audio.hide": "Amaga l'àudio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Ningú d'aquest servidor us pot seguir.",
"domain_block_modal.they_wont_know": "No sabran que són blocats.",
"domain_block_modal.title": "Bloquem el domini?",
"domain_block_modal.you_will_lose_followers": "S'eliminaran tots els vostres seguidors d'aquest servidor.",
"domain_block_modal.you_will_lose_num_followers": "Perdreu {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidors}} i {followingCount, plural, one {{followingCountDisplay} persona} other {{followingCountDisplay} persones}} que seguiu.",
"domain_block_modal.you_will_lose_relationships": "Perdreu seguidors i gent a qui seguiu d'aquest servidor.",
"domain_block_modal.you_wont_see_posts": "No veureu ni les publicacions ni les notificacions dels usuaris d'aquest servidor.",
"domain_pill.activitypub_lets_connect": "Us permet connectar i interactuar amb persones a Mastodon i també a d'altres apps socials.",
"domain_pill.activitypub_like_language": "ActivityPub és el llenguatge que Mastodon parla amb altres xarxes socials.",

View File

@ -220,7 +220,6 @@
"domain_block_modal.they_cant_follow": "Nikdo z tohoto serveru vás nemůže sledovat.",
"domain_block_modal.they_wont_know": "Nebude vědět, že je zablokován*a.",
"domain_block_modal.title": "Blokovat doménu?",
"domain_block_modal.you_will_lose_followers": "Všichni vaši sledující z tohoto serveru budou odstraněni.",
"domain_block_modal.you_wont_see_posts": "Neuvidíte příspěvky ani upozornění od uživatelů z tohoto serveru.",
"domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i s dalšími sociálními aplikacemi.",
"domain_pill.activitypub_like_language": "ActivityPub je jako jazyk, kterým Mastodon mluví s jinými sociálními sítěmi.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Ni all neb o'r gweinydd hwn eich dilyn.",
"domain_block_modal.they_wont_know": "Fyddan nhw ddim yn gwybod eu bod wedi cael eu blocio.",
"domain_block_modal.title": "Blocio parth?",
"domain_block_modal.you_will_lose_followers": "Bydd eich holl ddilynwyr o'r gweinydd hwn yn cael eu tynnu.",
"domain_block_modal.you_wont_see_posts": "Fyddwch chi ddim yn gweld postiadau na hysbysiadau gan ddefnyddwyr ar y gweinydd hwn.",
"domain_pill.activitypub_lets_connect": "Mae'n caniatáu ichi gysylltu a rhyngweithio â phobl nid yn unig ar Mastodon, ond ar draws gwahanol apiau cymdeithasol hefyd.",
"domain_pill.activitypub_like_language": "Mae ActivityPub fel yr iaith y mae Mastodon yn ei siarad â rhwydweithiau cymdeithasol eraill.",
@ -434,6 +433,8 @@
"lightbox.close": "Cau",
"lightbox.next": "Nesaf",
"lightbox.previous": "Blaenorol",
"lightbox.zoom_in": "Chwyddo i faint gwirioneddol",
"lightbox.zoom_out": "Chwyddo i ffitio",
"limited_account_hint.action": "Dangos y proffil beth bynnag",
"limited_account_hint.title": "Mae'r proffil hwn wedi cael ei guddio gan gymedrolwyr {domain}.",
"link_preview.author": "Gan {name}",
@ -785,6 +786,7 @@
"status.edit": "Golygu",
"status.edited": "Golygwyd ddiwethaf {date}",
"status.edited_x_times": "Golygwyd {count, plural, one {count} two {count} other {{count} gwaith}}",
"status.embed": "Cael y cod mewnblannu",
"status.favourite": "Ffafrio",
"status.favourites": "{count, plural, one {ffefryn} other {ffefryn}}",
"status.filter": "Hidlo'r postiad hwn",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Hastighedsbegrænset",
"alert.unexpected.message": "En uventet fejl opstod.",
"alert.unexpected.title": "Ups!",
"alt_text_badge.title": "Alt text",
"announcement.announcement": "Bekendtgørelse",
"attachments_list.unprocessed": "(ubehandlet)",
"audio.hide": "Skjul lyd",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Ingen fra denne server kan følge dig.",
"domain_block_modal.they_wont_know": "Vedkommende ser ikke den aktive blokering.",
"domain_block_modal.title": "Blokér domæne?",
"domain_block_modal.you_will_lose_followers": "Alle følgerne fra denne server fjernes.",
"domain_block_modal.you_will_lose_num_followers": "Man vil miste {followersCount, plural, one {{followersCountDisplay} følger} other {{followersCountDisplay} følgere}} og {followingCount, plural, one {{followingCountDisplay} person, man følger} other {{followingCountDisplay} personer, man følger}}.",
"domain_block_modal.you_will_lose_relationships": "Alle følgere og personer som følges på denne server mistes.",
"domain_block_modal.you_wont_see_posts": "Indlæg eller notifikationer fra brugere på denne server vises ikke.",
"domain_pill.activitypub_lets_connect": "Det muliggør at komme i forbindelse og interagere med folk ikke kun på Mastodon, men også på tværs af forskellige sociale apps.",
"domain_pill.activitypub_like_language": "ActivityPub er \"sproget\", Mastodon taler med andre sociale netværk.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Anfragelimit überschritten",
"alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
"alert.unexpected.title": "Oha!",
"alt_text_badge.title": "Bildbeschreibung",
"announcement.announcement": "Ankündigung",
"attachments_list.unprocessed": "(ausstehend)",
"audio.hide": "Audio ausblenden",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Niemand von diesem Server wird dir folgen können.",
"domain_block_modal.they_wont_know": "Es wird nicht erkennbar sein, dass diese Domain blockiert wurde.",
"domain_block_modal.title": "Domain blockieren?",
"domain_block_modal.you_will_lose_followers": "Alle Follower von diesem Server werden entfernt.",
"domain_block_modal.you_will_lose_num_followers": "Du wirst {followersCount, plural, one {{followersCountDisplay} Follower} other {{followersCountDisplay} Follower}} verlieren und {followingCount, plural, one {{followingCountDisplay} Profil} other {{followingCountDisplay} Profilen}} entfolgen.",
"domain_block_modal.you_will_lose_relationships": "Du wirst von diesem Server alle Follower und Profile, denen du dort folgst, verlieren.",
"domain_block_modal.you_wont_see_posts": "Du wirst keine Beiträge oder Benachrichtigungen von Profilen auf diesem Server sehen.",
"domain_pill.activitypub_lets_connect": "Somit kannst du dich nicht nur auf Mastodon mit Leuten verbinden und mit ihnen interagieren, sondern über alle sozialen Apps hinweg.",
"domain_pill.activitypub_like_language": "ActivityPub ist sozusagen die Sprache, die Mastodon mit anderen sozialen Netzwerken spricht.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Κανείς από αυτόν τον διακομιστή δεν μπορεί να σε ακολουθήσει.",
"domain_block_modal.they_wont_know": "Δεν θα ξέρουν ότι έχουν αποκλειστεί.",
"domain_block_modal.title": "Αποκλεισμός τομέα;",
"domain_block_modal.you_will_lose_followers": "Οι ακόλουθοί σου από αυτόν τον διακομιστή θα αφαιρεθούν.",
"domain_block_modal.you_wont_see_posts": "Δεν θα βλέπεις αναρτήσεις ή ειδοποιήσεις από χρήστες σε αυτόν το διακομιστή.",
"domain_pill.activitypub_lets_connect": "Σού επιτρέπει να συνδεθείς και να αλληλεπιδράσεις με τους ανθρώπους όχι μόνο στο Mastodon, αλλά και σε διαφορετικές κοινωνικές εφαρμογές.",
"domain_pill.activitypub_like_language": "Το ActivityPub είναι σαν τη γλώσσα Mastodon μιλάει με άλλα κοινωνικά δίκτυα.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.",
"alert.unexpected.title": "Oops!",
"alt_text_badge.title": "Alt text",
"announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Nobody from this server can follow you.",
"domain_block_modal.they_wont_know": "They won't know they've been blocked.",
"domain_block_modal.title": "Block domain?",
"domain_block_modal.you_will_lose_followers": "All your followers from this server will be removed.",
"domain_block_modal.you_will_lose_num_followers": "You will lose {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} followers}} and {followingCount, plural, one {{followingCountDisplay} person you follow} other {{followingCountDisplay} people you follow}}.",
"domain_block_modal.you_will_lose_relationships": "You will lose all followers and people you follow from this server.",
"domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.",
"domain_pill.activitypub_lets_connect": "It lets you connect and interact with people not just on Mastodon, but across different social apps too.",
"domain_pill.activitypub_like_language": "ActivityPub is like the language Mastodon speaks with other social networks.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.",
"alert.unexpected.title": "Oops!",
"alt_text_badge.title": "Alt text",
"announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Nobody from this server can follow you.",
"domain_block_modal.they_wont_know": "They won't know they've been blocked.",
"domain_block_modal.title": "Block domain?",
"domain_block_modal.you_will_lose_followers": "All your followers from this server will be removed.",
"domain_block_modal.you_will_lose_num_followers": "You will lose {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} followers}} and {followingCount, plural, one {{followingCountDisplay} person you follow} other {{followingCountDisplay} people you follow}}.",
"domain_block_modal.you_will_lose_relationships": "You will lose all followers and people you follow from this server.",
"domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.",
"domain_pill.activitypub_lets_connect": "It lets you connect and interact with people not just on Mastodon, but across different social apps too.",
"domain_pill.activitypub_like_language": "ActivityPub is like the language Mastodon speaks with other social networks.",
@ -850,6 +852,11 @@
"upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
"upload_form.description": "Describe for people who are blind or have low vision",
"upload_form.drag_and_drop.instructions": "To pick up a media attachment, press space or enter. While dragging, use the arrow keys to move the media attachment in any given direction. Press space or enter again to drop the media attachment in its new position, or press escape to cancel.",
"upload_form.drag_and_drop.on_drag_cancel": "Dragging was cancelled. Media attachment {item} was dropped.",
"upload_form.drag_and_drop.on_drag_end": "Media attachment {item} was dropped.",
"upload_form.drag_and_drop.on_drag_over": "Media attachment {item} was moved.",
"upload_form.drag_and_drop.on_drag_start": "Picked up media attachment {item}.",
"upload_form.edit": "Edit",
"upload_form.thumbnail": "Change thumbnail",
"upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Mesaĝkvante limigita",
"alert.unexpected.message": "Neatendita eraro okazis.",
"alert.unexpected.title": "Aj!",
"alt_text_badge.title": "Alt-teksto",
"announcement.announcement": "Anoncoj",
"attachments_list.unprocessed": "(neprilaborita)",
"audio.hide": "Kaŝi aŭdion",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Neniu el ĉi tiu servilo povas sekvi vin.",
"domain_block_modal.they_wont_know": "Ili ne scios, ke ili estas blokitaj.",
"domain_block_modal.title": "Ĉu bloki la domajnon?",
"domain_block_modal.you_will_lose_followers": "Ĉiuj viaj sekvantoj de ĉi tiu servilo estos forigitaj.",
"domain_block_modal.you_will_lose_num_followers": "Vi perdos {followersCount, plural, one {{followersCountDisplay} sekvanton} other {{followersCountDisplay} sekvantojn}} kaj {followingCount, plural, one {{followingCountDisplay} homon, kiu vi sekvas} other {{followingCountDisplay} homojn, kiuj vi sekvas}}.",
"domain_block_modal.you_will_lose_relationships": "Vi perdos ĉiujn sekvantojn kaj homojn, kiujn vi sekvas de ĉi tiu servilo.",
"domain_block_modal.you_wont_see_posts": "Vi ne vidos afiŝojn aŭ sciigojn de uzantoj sur ĉi tiu servilo.",
"domain_pill.activitypub_lets_connect": "Ĝi ebligas vin konekti kaj interagi kun homoj ne nur sur Mastodon, sed ankaŭ tra diversaj sociaj apoj.",
"domain_pill.activitypub_like_language": "ActivityPub estas kiel la lingvo kiun Mastodon parolas kun aliaj sociaj retoj.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Acción limitada",
"alert.unexpected.message": "Ocurrió un error.",
"alert.unexpected.title": "¡Epa!",
"alt_text_badge.title": "Texto alternativo",
"announcement.announcement": "Anuncio",
"attachments_list.unprocessed": "[sin procesar]",
"audio.hide": "Ocultar audio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Nadie de este servidor puede seguirte.",
"domain_block_modal.they_wont_know": "No sabrán que fueron bloqueados.",
"domain_block_modal.title": "¿Bloquear dominio?",
"domain_block_modal.you_will_lose_followers": "Se eliminarán todos tus seguidores de este servidor.",
"domain_block_modal.you_will_lose_num_followers": "Perderás {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} y {followingCount, plural, one {{followingCountDisplay} persona a la que sigues} other {{followingCountDisplay} personas a las que sigues}}.",
"domain_block_modal.you_will_lose_relationships": "Perderás a todos los seguidores y gente a la que sigas de este servidor.",
"domain_block_modal.you_wont_see_posts": "No verás mensajes ni notificaciones de usuarios en este servidor.",
"domain_pill.activitypub_lets_connect": "Te permite conectar e interactuar con cuentas no solo en Mastodon, sino también a través de diferentes aplicaciones sociales.",
"domain_pill.activitypub_like_language": "ActivityPub es como el idioma que Mastodon habla con otras redes sociales.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Tarifa limitada",
"alert.unexpected.message": "Hubo un error inesperado.",
"alert.unexpected.title": "¡Ups!",
"alt_text_badge.title": "Texto alternativo",
"announcement.announcement": "Anuncio",
"attachments_list.unprocessed": "(sin procesar)",
"audio.hide": "Ocultar audio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Nadie de este servidor puede seguirte.",
"domain_block_modal.they_wont_know": "No sabrán que han sido bloqueados.",
"domain_block_modal.title": "¿Bloquear dominio?",
"domain_block_modal.you_will_lose_followers": "Todos tus seguidores de este servidor serán eliminados.",
"domain_block_modal.you_will_lose_num_followers": "Vas a perder {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} y {followingCount, plural, one {{followingCountDisplay} persona a la que sigues} other {{followingCountDisplay} personas a las que sigas}}.",
"domain_block_modal.you_will_lose_relationships": "Perderás todos los seguidores y las personas que sigues de este servidor.",
"domain_block_modal.you_wont_see_posts": "No verás publicaciones ni notificaciones de usuarios en este servidor.",
"domain_pill.activitypub_lets_connect": "Te permite conectar e interactuar con personas no sólo en Mastodon, sino también a través de diferentes aplicaciones sociales.",
"domain_pill.activitypub_like_language": "ActivityPub es como el idioma que Mastodon habla con otras redes sociales.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Tráfico limitado",
"alert.unexpected.message": "Hubo un error inesperado.",
"alert.unexpected.title": "¡Ups!",
"alt_text_badge.title": "Texto alternativo",
"announcement.announcement": "Anuncio",
"attachments_list.unprocessed": "(sin procesar)",
"audio.hide": "Ocultar audio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Nadie de este servidor puede seguirte.",
"domain_block_modal.they_wont_know": "No sabrán que han sido bloqueados.",
"domain_block_modal.title": "¿Bloquear dominio?",
"domain_block_modal.you_will_lose_followers": "Se eliminarán todos tus seguidores de este servidor.",
"domain_block_modal.you_will_lose_num_followers": "Perderás {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} y {followingCount, plural, one {{followingCountDisplay} persona a la que sigues} other {{followingCountDisplay} personas a las que sigues}}.",
"domain_block_modal.you_will_lose_relationships": "Perderás a todos los seguidores y gente a la que sigas de este servidor.",
"domain_block_modal.you_wont_see_posts": "No verás mensajes ni notificaciones de usuarios en este servidor.",
"domain_pill.activitypub_lets_connect": "Te permite conectar e interactuar con personas no sólo en Mastodon, sino también a través de diferentes aplicaciones sociales.",
"domain_pill.activitypub_like_language": "ActivityPub es como el idioma que Mastodon habla con otras redes sociales.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Sellest serverist ei saa keegi sind jälgida.",
"domain_block_modal.they_wont_know": "Nad ei tea, et nad on blokeeritud.",
"domain_block_modal.title": "Blokeerida domeen?",
"domain_block_modal.you_will_lose_followers": "Kõik sinu sellest serverist pärit jälgijad eemaldatakse.",
"domain_block_modal.you_wont_see_posts": "Sa ei näe selle serveri kasutajate postitusi ega teavitusi.",
"domain_pill.activitypub_lets_connect": "See võimaldab sul ühenduda inimestega ja nendega suhelda mitte ainult Mastodonis, vaid ka teistes suhtlusrakendustes.",
"domain_pill.activitypub_like_language": "ActivityPub on nagu keel, mida Mastodon räägib teiste suhtlusvõrgustikega.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Abiadura mugatua",
"alert.unexpected.message": "Ustekabeko errore bat gertatu da.",
"alert.unexpected.title": "Ene!",
"alt_text_badge.title": "Testu alternatiboa",
"announcement.announcement": "Iragarpena",
"attachments_list.unprocessed": "(prozesatu gabe)",
"audio.hide": "Ezkutatu audioa",
@ -221,7 +222,6 @@
"domain_block_modal.they_cant_follow": "Zerbitzari honetako inork ezin zaitu jarraitu.",
"domain_block_modal.they_wont_know": "Ez dute jakingo blokeatuak izan direnik.",
"domain_block_modal.title": "Domeinua blokeatu nahi duzu?",
"domain_block_modal.you_will_lose_followers": "Zerbitzari honetako jarraitzaile guztiak kenduko dira.",
"domain_block_modal.you_wont_see_posts": "Ez dituzu zerbitzari honetako erabiltzaileen argitalpenik edota jakinarazpenik ikusiko.",
"domain_pill.activitypub_lets_connect": "Mastodon-en ez ezik, beste sare sozialen aplikazioetako jendearekin konektatzea eta harremanetan jartzea uzten dizu.",
"domain_pill.activitypub_like_language": "ActivityPub, Mastodon-ek beste sare sozialekin hitz egiteko erabiltzen duen hizkuntza bezalakoxea da.",

View File

@ -85,9 +85,11 @@
"alert.rate_limited.title": "محدودیت تعداد",
"alert.unexpected.message": "خطایی غیرمنتظره رخ داد.",
"alert.unexpected.title": "ای وای!",
"alt_text_badge.title": "متن جایگزین",
"announcement.announcement": "اعلامیه",
"attachments_list.unprocessed": "(پردازش نشده)",
"audio.hide": "نهفتن صدا",
"block_modal.remote_users_caveat": "ما از کارساز {domain} خواهیم خواست که به تصمیم شما احترام بگذارد. با این حال، تضمینی برای رعایت آن وجود ندارد زیرا برخی کارسازها ممکن است بلوک‌ها را به‌طور متفاوتی مدیریت کنند. فرسته‌های عمومی ممکن است همچنان برای کاربران که وارد نشده قابل مشاهده باشند.",
"block_modal.show_less": "نمایش کم‌تر",
"block_modal.show_more": "نمایش بیش‌تر",
"block_modal.they_cant_mention": "نمی‌توانند نامتان را برده یا پی‌تان بگیرند.",
@ -220,8 +222,10 @@
"domain_block_modal.they_cant_follow": "هیچ‌کسی از این کارساز نمی‌تواند پیتان بگیرد.",
"domain_block_modal.they_wont_know": "نخواهند دانست که مسدود شده‌اند.",
"domain_block_modal.title": "انسداد دامنه؟",
"domain_block_modal.you_will_lose_followers": "همهٔ پی‌گیرندگانتان از این کارساز برداشته خواهند شد.",
"domain_block_modal.you_will_lose_relationships": "شما تمام پیگیرکنندگان و افرادی که از این کارساز پیگیری می‌کنید را از دست خواهید داد.",
"domain_block_modal.you_wont_see_posts": "فرسته‌ها یا آگاهی‌ها از کاربران روی این کارساز را نخواهید دید.",
"domain_pill.activitypub_lets_connect": "این به شما اجازه می‌دهد تا نه تنها در ماستودون، بلکه در برنامه‌های اجتماعی مختلف نیز با افراد ارتباط برقرار کرده و تعامل داشته باشید.",
"domain_pill.activitypub_like_language": "ActivityPub مانند زبانی است که ماستودون با دیگر شبکه‌های اجتماعی صحبت می‌کند.",
"domain_pill.server": "کارساز",
"domain_pill.their_handle": "شناسه‌اش:",
"domain_pill.their_server": "خانهٔ رقمیش. جایی که همهٔ فرسته‌هایش می‌زیند.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Pyyntömäärää rajoitettu",
"alert.unexpected.message": "Tapahtui odottamaton virhe.",
"alert.unexpected.title": "Hups!",
"alt_text_badge.title": "Vaihtoehtoinen teksti",
"announcement.announcement": "Tiedote",
"attachments_list.unprocessed": "(käsittelemätön)",
"audio.hide": "Piilota ääni",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Kukaan tältä palvelimelta ei voi seurata sinua.",
"domain_block_modal.they_wont_know": "Hän ei saa tietää tulleensa estetyksi.",
"domain_block_modal.title": "Estetäänkö verkkotunnus?",
"domain_block_modal.you_will_lose_followers": "Kaikki seuraajasi tältä palvelimelta poistetaan.",
"domain_block_modal.you_will_lose_num_followers": "Menetät {followersCount, plural, one {{followersCountDisplay} seuraajasi} other {{followersCountDisplay} seuraajaasi}} ja {followingCount, plural, one {{followingCountDisplay} seurattusi} other {{followingCountDisplay} seurattuasi}}.",
"domain_block_modal.you_will_lose_relationships": "Menetät kaikki tämän palvelimen seuraajasi ja seurattusi.",
"domain_block_modal.you_wont_see_posts": "Et enää näe julkaisuja etkä ilmoituksia tämän palvelimen käyttäjiltä.",
"domain_pill.activitypub_lets_connect": "Sen avulla voit muodostaa yhteyden ja olla vuorovaikutuksessa ihmisten kanssa, ei vain Mastodonissa vaan myös muissa sosiaalisissa sovelluksissa.",
"domain_pill.activitypub_like_language": "ActivityPub on kuin kieli, jota Mastodon puhuu muiden sosiaalisten verkostojen kanssa.",

View File

@ -134,7 +134,6 @@
"dismissable_banner.public_timeline": "Ito ang mga pinakamakailang nakapublikong post mula sa mga taong nasa social web na sinusundan ng mga tao sa {domain}.",
"domain_block_modal.block": "Harangan ang serbiro",
"domain_block_modal.title": "Harangan ang domain?",
"domain_block_modal.you_will_lose_followers": "Mabubura ang iyong mga tagasunod mula sa serbirong ito.",
"domain_pill.server": "Serbiro",
"embed.instructions": "I-embed ang post na ito sa iyong pook-sapot sa pamamagitan ng pagsipi ng kodigo sa ilalim.",
"embed.preview": "Ito ang magiging itsura:",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Avmarkaður títtleiki",
"alert.unexpected.message": "Ein óvæntaður feilur kom fyri.",
"alert.unexpected.title": "Ups!",
"alt_text_badge.title": "Annar tekstur",
"announcement.announcement": "Kunngerð",
"attachments_list.unprocessed": "(óviðgjørt)",
"audio.hide": "Fjal ljóð",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Eingin frá hesum ambætara kann fylgja tær.",
"domain_block_modal.they_wont_know": "Tey vita ikki, at tey eru bannað.",
"domain_block_modal.title": "Banna økisnavni?",
"domain_block_modal.you_will_lose_followers": "Allir tínir fylgjarar á hesum ambætara hvørva.",
"domain_block_modal.you_will_lose_num_followers": "Tú missir {followersCount, plural, one {{followersCountDisplay} fylgjara} other {{followersCountDisplay} fylgjarar}} og {followingCount, plural, one {{followingCountDisplay} persón, sum tú fylgir} other {{followingCountDisplay} persónar, sum tú fylgir}}.",
"domain_block_modal.you_will_lose_relationships": "Tú fer at missa allar fylgjarar og øll tey, tú fylgir á hesum ambætaranum.",
"domain_block_modal.you_wont_see_posts": "Tú sært ongar postar ella boð frá brúkarum á hesum ambætara.",
"domain_pill.activitypub_lets_connect": "Tað letur teg fáa samband og samvirka við fólki ikki bara á Mastodon, men á øðrum sosialum appum eisini.",
"domain_pill.activitypub_like_language": "ActivityPub er málið, sum Mastodon tosar við onnur sosial netverk.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Personne de ce serveur ne peut vous suivre.",
"domain_block_modal.they_wont_know": "Il ne saura pas qu'il a été bloqué.",
"domain_block_modal.title": "Bloquer le domaine ?",
"domain_block_modal.you_will_lose_followers": "Tous vos abonnés de ce serveur seront supprimés.",
"domain_block_modal.you_wont_see_posts": "Vous ne verrez plus les publications ou les notifications des utilisateurs de ce serveur.",
"domain_pill.activitypub_lets_connect": "Cela vous permet de vous connecter et d'interagir avec les autres non seulement sur Mastodon, mais également sur d'autres applications de réseaux sociaux.",
"domain_pill.activitypub_like_language": "ActivityPub est comme une langue que Mastodon utilise pour communiquer avec les autres réseaux sociaux.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Personne de ce serveur ne peut vous suivre.",
"domain_block_modal.they_wont_know": "Il ne saura pas qu'il a été bloqué.",
"domain_block_modal.title": "Bloquer le domaine ?",
"domain_block_modal.you_will_lose_followers": "Tous vos abonnés de ce serveur seront supprimés.",
"domain_block_modal.you_wont_see_posts": "Vous ne verrez plus les publications ou les notifications des utilisateurs de ce serveur.",
"domain_pill.activitypub_lets_connect": "Cela vous permet de vous connecter et d'interagir avec les autres non seulement sur Mastodon, mais également sur d'autres applications de réseaux sociaux.",
"domain_pill.activitypub_like_language": "ActivityPub est comme une langue que Mastodon utilise pour communiquer avec les autres réseaux sociaux.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Net ien op dizze server kin jo folgje.",
"domain_block_modal.they_wont_know": "Se krije net te witten dat se blokkearre wurde.",
"domain_block_modal.title": "Domein blokkearje?",
"domain_block_modal.you_will_lose_followers": "Al jo folgers fan dizze server wurde ûntfolge.",
"domain_block_modal.you_wont_see_posts": "Jo sjogge gjin berjochten of meldingen mear fan brûkers op dizze server.",
"domain_pill.activitypub_lets_connect": "It soarget derfoar dat jo net allinnich mar ferbine en kommunisearje kinne mei minsken op Mastodon, mar ek mei oare sosjale apps.",
"domain_pill.activitypub_like_language": "ActivityPub is de taal dyt Mastodon mei oare sosjale netwurken sprekt.",

View File

@ -221,7 +221,7 @@
"domain_block_modal.they_cant_follow": "Ní féidir le duine ar bith ón bhfreastalaí seo tú a leanúint.",
"domain_block_modal.they_wont_know": "Ní bheidh a fhios acu go bhfuil bac orthu.",
"domain_block_modal.title": "Blocáil fearann?",
"domain_block_modal.you_will_lose_followers": "Bainfear do leantóirí go léir ón bhfreastalaí seo.",
"domain_block_modal.you_will_lose_num_followers": "Caillfidh tú {followersCount, plural, one {{followersCountDisplay} leantóir} two {{followersCountDisplay} leantóirí} few {{followersCountDisplay} leantóirí} many {{followersCountDisplay} leantóirí} other {{followersCountDisplay} leantóirí}} agus {followingCount, plural, one {{followingCountDisplay} duine atá á leanúint agat} two {{followingCountDisplay} daoine atá á leanúint agat} few {{followingCountDisplay} daoine atá á leanúint agat} many {{followingCountDisplay} daoine atá á leanúint agat} other {{followingCountDisplay} daoine atá á leanúint agat}}.",
"domain_block_modal.you_wont_see_posts": "Ní fheicfidh tú postálacha nó fógraí ó úsáideoirí ar an bhfreastalaí seo.",
"domain_pill.activitypub_lets_connect": "Ligeann sé duit ceangal agus idirghníomhú le daoine, ní hamháin ar Mastodon, ach thar aipeanna sóisialta éagsúla freisin.",
"domain_pill.activitypub_like_language": "Tá GníomhaíochtPub cosúil leis an teanga a labhraíonn Mastodon le líonraí sóisialta eile.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Chan urrainn do neach sam bith a th air an fhrithealaiche seo do leantainn.",
"domain_block_modal.they_wont_know": "Cha bhi fios aca gun deach am bacadh.",
"domain_block_modal.title": "A bheil thu airson an àrainn a bhacadh?",
"domain_block_modal.you_will_lose_followers": "Thèid a h-uile neach-leantainn agad a th air an fhrithealaiche seo a thoirt air falbh.",
"domain_block_modal.you_wont_see_posts": "Chan fhaic thu postaichean no brathan o chleachdaichean a th air an fhrithealaiche seo.",
"domain_pill.activitypub_lets_connect": "Leigidh e leat ceangal a dhèanamh ri daoine chan ann air Mastodon a-mhàin ach air feadh aplacaidean sòisealta eile cuideachd agus conaltradh leotha.",
"domain_pill.activitypub_like_language": "Tha ActivityPub coltach ri cànan a bhruidhneas Mastodon ri lìonraidhean sòisealta eile.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Límite de intentos",
"alert.unexpected.message": "Aconteceu un fallo non agardado.",
"alert.unexpected.title": "Vaites!",
"alt_text_badge.title": "Texto Alt",
"announcement.announcement": "Anuncio",
"attachments_list.unprocessed": "(sen procesar)",
"audio.hide": "Agochar audio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Ninguén deste servidor pode seguirte.",
"domain_block_modal.they_wont_know": "Non saberá que a bloqueaches.",
"domain_block_modal.title": "Bloquear dominio?",
"domain_block_modal.you_will_lose_followers": "Vanse eliminar todas as túas seguidoras deste servidor.",
"domain_block_modal.you_will_lose_num_followers": "Vas perder {followersCount, plural, one {{followersCountDisplay} seguidora} other {{followersCountDisplay} seguidoras}} e {followingCount, plural, one {{followingCountDisplay} persoa que segues} other {{followingCountDisplay} persoas que segues}}.",
"domain_block_modal.you_will_lose_relationships": "Vas perder todas as seguidoras e seguimentos a persoas deste servidor.",
"domain_block_modal.you_wont_see_posts": "Non verás publicacións ou notificacións das usuarias deste servidor.",
"domain_pill.activitypub_lets_connect": "Permíteche conectar e interactuar con persoas non só de Mastodon, se non tamén con outras sociais.",
"domain_pill.activitypub_like_language": "ActivityPub é algo así como o idioma que Mastodon fala con outras redes sociais.",
@ -527,7 +529,7 @@
"notification.poll": "Rematou a enquisa na que votaches",
"notification.reblog": "{name} compartiu a túa publicación",
"notification.reblog.name_and_others_with_link": "{name} e <a>{count, plural, one {# máis} other {# máis}}</a> promoveron a túa publicación",
"notification.relationships_severance_event": "Perdeuse a conexión con {name}",
"notification.relationships_severance_event": "Relacións perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "A administración de {from} suspendeu a {target}, o que significa que xa non vas recibir actualizacións de esa conta ou interactuar con ela.",
"notification.relationships_severance_event.domain_block": "A administración de {from} bloqueou a {target}, que inclúe a {followersCount} das túas seguidoras e a {followingCount, plural, one {# conta} other {# contas}} que sigues.",
"notification.relationships_severance_event.learn_more": "Saber máis",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "משתמש משרת זה לא יכול לעקוב אחריך.",
"domain_block_modal.they_wont_know": "הם לא ידעו כי נחסמו.",
"domain_block_modal.title": "לחסום שרת?",
"domain_block_modal.you_will_lose_followers": "כל עוקביך משרת זה יוסרו.",
"domain_block_modal.you_wont_see_posts": "לא תוכלו לראות הודעות ממשתמשים על שרת זה.",
"domain_pill.activitypub_lets_connect": "מאפשר לך להתחבר ולהתרועע עם אחרים לא רק במסטודון, אלא גם ביישומים חברתיים שונים אחרים.",
"domain_pill.activitypub_like_language": "אקטיביטיפאב היא למעשה השפה בה מסטודון מדבר עם רשתות חברתיות אחרות.",

View File

@ -221,7 +221,8 @@
"domain_block_modal.they_cant_follow": "Erről a kiszolgálóról senki sem követhet.",
"domain_block_modal.they_wont_know": "Nem fogja tudni, hogy letiltották.",
"domain_block_modal.title": "Letiltsuk a domaint?",
"domain_block_modal.you_will_lose_followers": "Az ezen a kiszolgálón lévő összes követődet törölni fogjuk.",
"domain_block_modal.you_will_lose_num_followers": "El fogsz veszíteni {followersCount, plural, one {{followersCountDisplay} követőt} other {{followersCountDisplay} követőt}} és {followingCount, plural, one {{followingCountDisplay} követett személyt} other {{followingCountDisplay} követett személyt}}.",
"domain_block_modal.you_will_lose_relationships": "Minden követőt és követett személyt el fogsz veszíteni erről a kiszolgálóról.",
"domain_block_modal.you_wont_see_posts": "Nem látsz majd bejegyzéseket vagy értesítéseket ennek a kiszolgálónak a felhasználóitól.",
"domain_pill.activitypub_lets_connect": "Lehetővé teszi, hogy kapcsolatba lépj nem csak a Mastodonon, hanem a más közösségi alkalmazásokon lévő emberekkel is.",
"domain_pill.activitypub_like_language": "Az ActivityPub olyan mint egy nyelv, amelyet a Mastodon a más közösségi hálózatokkal való kommunikációra használ.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Necuno de iste servitor pote sequer te.",
"domain_block_modal.they_wont_know": "Ille non sapera que ille ha essite blocate.",
"domain_block_modal.title": "Blocar dominio?",
"domain_block_modal.you_will_lose_followers": "Tote tu sequitores de iste servitor essera removite.",
"domain_block_modal.you_wont_see_posts": "Tu non videra messages e notificationes de usatores sur iste servitor.",
"domain_pill.activitypub_lets_connect": "Illo te permitte connecter e interager con personas non solmente sur Mastodon, ma tamben sur altere applicationes social.",
"domain_pill.activitypub_like_language": "ActivityPub es como le linguage commun que Mastodon parla con altere retes social.",

View File

@ -219,7 +219,6 @@
"domain_block_modal.they_cant_follow": "Tidak ada seorangpun dari server ini yang dapat mengikuti anda.",
"domain_block_modal.they_wont_know": "Mereka tidak akan tahu bahwa mereka diblokir.",
"domain_block_modal.title": "Blokir domain?",
"domain_block_modal.you_will_lose_followers": "Semua pengikut anda dari server ini akan dihapus.",
"domain_block_modal.you_wont_see_posts": "Anda tidak akan melihat postingan atau notifikasi dari pengguna di server ini.",
"domain_pill.activitypub_lets_connect": "Ini memungkinkan anda terhubung dan berinteraksi dengan orang-orang tidak hanya di Mastodon, tetapi juga di berbagai aplikasi sosial.",
"domain_pill.activitypub_like_language": "ActivityPub seperti bahasa yang digunakan Mastodon dengan jejaring sosial lainnya.",

View File

@ -206,7 +206,6 @@
"domain_block_modal.they_cant_follow": "Nequi de ti-ci servitor posse sequer te.",
"domain_block_modal.they_wont_know": "Ne va esser conscient pri li bloccada.",
"domain_block_modal.title": "Bloccar dominia?",
"domain_block_modal.you_will_lose_followers": "Omni tui sequitores de ti-ci servitor va esser efaciat.",
"domain_block_modal.you_wont_see_posts": "Tu ne va vider postas ni notificationes de usatores sur ti-ci servitor.",
"domain_pill.activitypub_lets_connect": "It possibilisa tui conexiones e interactiones con persones ne solmen sur Mastodon, ma anc tra diferent social aplis.",
"domain_pill.activitypub_like_language": "ActivityPub es li lingue usat de Mastodon por parlar con altri social retages.",

View File

@ -1,6 +1,7 @@
{
"about.blocks": "Jerata servili",
"about.contact": "Kontaktajo:",
"about.disclaimer": "Mastodon esas libera, publikfonta e komercmarko di Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Expliko nedisponebla",
"about.domain_blocks.preamble": "Mastodon generale permisas on vidar kontenajo e interagar kun uzanti de irga altra servilo en fediverso. Existas eceptioni quo facesis che ca partikulara servilo.",
"about.domain_blocks.silenced.explanation": "On generale ne vidar profili e kontenajo de ca servilo, se on ne reale trovar o voluntale juntar per sequar.",
@ -10,6 +11,7 @@
"about.not_available": "Ca informo ne igesis che ca servilo.",
"about.powered_by": "Necentraligita sociala ret quo povigesas da {mastodon}",
"about.rules": "Servilreguli",
"account.account_note_header": "Personala noto",
"account.add_or_remove_from_list": "Insertez o removez de listi",
"account.badges.bot": "Boto",
"account.badges.group": "Grupo",
@ -29,9 +31,12 @@
"account.featured_tags.last_status_never": "Nula posti",
"account.featured_tags.title": "Estalita hashtagi di {name}",
"account.follow": "Sequar",
"account.follow_back": "Anke sequez",
"account.followers": "Sequanti",
"account.followers.empty": "Nulu sequas ca uzanto til nun.",
"account.followers_counter": "{count, plural,one {{counter} sequanto} other {{counter} sequanti}}",
"account.following": "Sequata",
"account.following_counter": "{count, plural,one {{counter} sequato} other {{counter} sequati}}",
"account.follows.empty": "Ca uzanto ne sequa irgu til nun.",
"account.go_to_profile": "Irez al profilo",
"account.hide_reblogs": "Celez repeti de @{name}",
@ -47,6 +52,7 @@
"account.mute_notifications_short": "Silencigez avizi",
"account.mute_short": "Silencigez",
"account.muted": "Silencigata",
"account.mutual": "Mutuala",
"account.no_bio": "Deskriptajo ne provizesis.",
"account.open_original_page": "Apertez originala pagino",
"account.posts": "Mesaji",
@ -56,6 +62,7 @@
"account.requested_follow": "{name} demandis sequar tu",
"account.share": "Partigez profilo di @{name}",
"account.show_reblogs": "Montrez repeti de @{name}",
"account.statuses_counter": "{count, plural,one {{counter} mesajo} other {{counter} mesaji}}",
"account.unblock": "Desblokusar @{name}",
"account.unblock_domain": "Desblokusar {domain}",
"account.unblock_short": "Desblokusar",
@ -78,10 +85,21 @@
"alert.rate_limited.title": "Demandi limitizita",
"alert.unexpected.message": "Neexpektita eroro eventis.",
"alert.unexpected.title": "Problemo!",
"alt_text_badge.title": "Alternativa texto",
"announcement.announcement": "Anunco",
"attachments_list.unprocessed": "(neprocedita)",
"audio.hide": "Celez audio",
"block_modal.remote_users_caveat": "Ni questionos {domain} di la servilo por respektar vua decido. Publika posti forsan ankore estas videbla a neenirinta uzanti.",
"block_modal.show_less": "Montrar mine",
"block_modal.show_more": "Montrar plue",
"block_modal.they_cant_mention": "Oli ne povas mencionar o sequar vu.",
"block_modal.they_cant_see_posts": "Oli ne povas vidar vua mesaji e vu ne vidos vidar olia.",
"block_modal.they_will_know": "Oli povas vidar ke oli esas blokusita.",
"block_modal.title": "Blokusar uzanto?",
"block_modal.you_wont_see_mentions": "Vu ne vidos mesaji qua mencionas oli.",
"boost_modal.combo": "Vu povas pulsar {combo} por omisar co venontafoye",
"boost_modal.reblog": "Ka repetar posto?",
"boost_modal.undo_reblog": "Ka retrorepetar posto?",
"bundle_column_error.copy_stacktrace": "Kopierorraporto",
"bundle_column_error.error.body": "La demandita pagino ne povas strukturigesar. Forsan ol esas eroro en kodexo hike o vidilkoncilieblesproblemo.",
"bundle_column_error.error.title": "Ach!",
@ -138,30 +156,47 @@
"compose_form.lock_disclaimer.lock": "klefagesas",
"compose_form.placeholder": "Quo esas en tua spirito?",
"compose_form.poll.duration": "Votpostoduro",
"compose_form.poll.multiple": "Multopla selekteso",
"compose_form.poll.option_placeholder": "Selektato {number}",
"compose_form.poll.single": "Selektez un",
"compose_form.poll.switch_to_multiple": "Chanjez votposto por permisar multiselektaji",
"compose_form.poll.switch_to_single": "Chanjez votposto por permisar una selektajo",
"compose_form.poll.type": "Stilo",
"compose_form.publish": "Posto",
"compose_form.publish_form": "Publish",
"compose_form.reply": "Respondez",
"compose_form.save_changes": "Aktualigez",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Kontenajaverto (selektebla)",
"confirmation_modal.cancel": "Anulez",
"confirmations.block.confirm": "Restriktez",
"confirmations.delete.confirm": "Efacez",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.delete.title": "Ka efacar posto?",
"confirmations.delete_list.confirm": "Efacez",
"confirmations.delete_list.message": "Ka vu certe volas netempale efacar ca listo?",
"confirmations.delete_list.title": "Ka efacar listo?",
"confirmations.discard_edit_media.confirm": "Efacez",
"confirmations.discard_edit_media.message": "Vu havas nesparita chanji di mediodeskript o prevido, vu volas jus efacar?",
"confirmations.edit.confirm": "Modifikez",
"confirmations.edit.message": "Modifikar nun remplasos la mesajo quon vu nune skribas. Ka vu certe volas procedar?",
"confirmations.edit.title": "Ka remplasar posto?",
"confirmations.logout.confirm": "Ekirez",
"confirmations.logout.message": "Ka tu certe volas ekirar?",
"confirmations.logout.title": "Ka ekirar?",
"confirmations.mute.confirm": "Silencigez",
"confirmations.redraft.confirm": "Efacez e riskisez",
"confirmations.redraft.message": "Ka vu certe volas efacar ca posto e riskisigar ol? Favoriziti e repeti esos perdita, e respondi al posto originala esos orfanigita.",
"confirmations.redraft.title": "Ka efacar & riskisar posto?",
"confirmations.reply.confirm": "Respondez",
"confirmations.reply.message": "Respondar nun remplos mesajo quon vu nun igas. Ka vu certe volas durar?",
"confirmations.reply.title": "Ka remplasar posto?",
"confirmations.unfollow.confirm": "Desequez",
"confirmations.unfollow.message": "Ka vu certe volas desequar {name}?",
"confirmations.unfollow.title": "Ka retrosequar uzanto?",
"content_warning.hide": "Celez posto",
"content_warning.show": "Montrez nur",
"conversation.delete": "Efacez konverso",
"conversation.mark_as_read": "Markizez quale lektita",
"conversation.open": "Videz konverso",
@ -181,6 +216,28 @@
"dismissable_banner.explore_statuses": "Yen posti del tota reto sociala qui esas populara hodie. Posti plu nova kun plu repeti e favoriziti esas rangizita plu alte.",
"dismissable_banner.explore_tags": "Ca hashtagi bezonas plu famoza inter personi che ca e altra servili di la necentraligita situo nun.",
"dismissable_banner.public_timeline": "Yen la posti maxim recenta da personi che la reto sociala quin personi che {domain} sequas.",
"domain_block_modal.block": "Blokusez servilo",
"domain_block_modal.block_account_instead": "Blokusez @{name} vice",
"domain_block_modal.they_can_interact_with_old_posts": "Personi de ca servilo povas interagar kun vua desnova posti.",
"domain_block_modal.they_cant_follow": "Nulu de ca servilo povas sequar vu.",
"domain_block_modal.they_wont_know": "Lu ne savos ke lu blokusesis.",
"domain_block_modal.title": "Ka blokusar domeno?",
"domain_block_modal.you_will_lose_num_followers": "Vu desganos {followersCount, plural, one {{followersCountDisplay} sequanto} other {{followersCountDisplay} sequanti}} e {followingCount, plural, one {{followingCountDisplay} persono quan vu sequas} other {{followingCountDisplay} personi quan vu sequas}}.",
"domain_block_modal.you_will_lose_relationships": "Vu desganos omna sequanti e sequati de ca servilo.",
"domain_block_modal.you_wont_see_posts": "Vu ne vidos postoi o savigi de uzanti en ca servilo.",
"domain_pill.activitypub_lets_connect": "Ol povigas vu kuneskas e interagar kun personi ne nur sur Mastodon, o anke kun dessama socia softwari.",
"domain_pill.activitypub_like_language": "ActivityPub esas kam linguo quan Mastodon parolas kun altra socia reti.",
"domain_pill.server": "Servilo",
"domain_pill.their_handle": "Lua nomo:",
"domain_pill.their_server": "Lua komputala hemo, e havas omna lua posti.",
"domain_pill.their_username": "Lua unika identesilo sur lua servilo. Posible trovar uzanti kun sama uzantonomo sur dessama servili.",
"domain_pill.username": "Uzantonomo",
"domain_pill.whats_in_a_handle": "Quo esas nomo?",
"domain_pill.who_they_are": "Pro ke nomo esas deskripto di ulu, vu povas interagar kun personi tra socia interreto kun <button>ActivityPub-povizo</button>.",
"domain_pill.who_you_are": "Pro ke vua nomo esas deskripto pri vu e vua situo, personi povas interagar kun vu tra socia interreto kun <button>ActivityPub-povizo</button>.",
"domain_pill.your_handle": "Vua nomo:",
"domain_pill.your_server": "Vua komputerala hemo qua havas omna vua posti. On povas transferar a altra servili ulatempe e anke adportar vua sequanti.",
"domain_pill.your_username": "Vua unika identesilo sur lua servilo. Posible trovar uzanti kun sama uzantonomo sur dessama servili.",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Co esas quon ol semblos tale:",
"emoji_button.activity": "Ago",
@ -217,6 +274,7 @@
"empty_column.list": "There is nothing in this list yet.",
"empty_column.lists": "Vu ne havas irga listi til nun. Kande vu kreas talo, ol montresos hike.",
"empty_column.mutes": "Vu ne silencigis irga uzanti til nun.",
"empty_column.notification_requests": "Finis. Kande vu recevas nova savigi, oli aparos hike segun vua preferaji.",
"empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.",
"empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.",
"error.unexpected_crash.explanation": "Pro eroro en nia kodexo o vidilkonciliebloproblemo, ca pagino ne povas korekte montresar.",
@ -247,12 +305,30 @@
"filter_modal.select_filter.subtitle": "Usez disponebla grupo o kreez novajo",
"filter_modal.select_filter.title": "Filtragez ca posto",
"filter_modal.title.status": "Filtragez posto",
"filter_warning.matches_filter": "Sama kam filtrilo \"{title}\"",
"filtered_notifications_banner.pending_requests": "De {count, plural,=0 {nulu} one {1 persono} other {# personi}} quan vu forsan konocas",
"filtered_notifications_banner.title": "Filtrilita savigi",
"firehose.all": "Omno",
"firehose.local": "Ca servilo",
"firehose.remote": "Altra servili",
"follow_request.authorize": "Yurizar",
"follow_request.reject": "Refuzar",
"follow_requests.unlocked_explanation": "Quankam vua konto ne klefklozesis, la {domain} laborero pensas ke vu forsan volas kontralar sequodemandi de ca konti manuale.",
"follow_suggestions.curated_suggestion": "Selektato de jeranto",
"follow_suggestions.dismiss": "Ne montrez pluse",
"follow_suggestions.featured_longer": "Selektesis da la grupo di {domain}",
"follow_suggestions.friends_of_friends_longer": "Populara inter personi quan vu sequas",
"follow_suggestions.hints.featured": "Ca profilo selektesis da la grupo di {domain}.",
"follow_suggestions.hints.friends_of_friends": "Ca profilo esas populara inter personi quan vu sequas.",
"follow_suggestions.hints.most_followed": "Vua profilo esas un de maxim sequita sur {domain}.",
"follow_suggestions.hints.most_interactions": "Ca profilo recente populareskis sur {domain}.",
"follow_suggestions.hints.similar_to_recently_followed": "Ca profilo esas simila a la profili quan vu recente sequis.",
"follow_suggestions.personalized_suggestion": "Personalita sugestato",
"follow_suggestions.popular_suggestion": "Populara sugestato",
"follow_suggestions.popular_suggestion_longer": "Populara sur {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Simila a profili quan vu recente sequis",
"follow_suggestions.view_all": "Videz omno",
"follow_suggestions.who_to_follow": "Sequindo",
"followed_tags": "Hashtagi sequita",
"footer.about": "Pri co",
"footer.directory": "Profilcheflisto",
@ -279,6 +355,14 @@
"hashtag.follow": "Sequez hashtago",
"hashtag.unfollow": "Desequez hashtago",
"hashtags.and_other": "…e {count, plural, one {# plusa}other {# plusa}}",
"hints.profiles.followers_may_be_missing": "Sequanti di ca profilo forsan ne esas hike.",
"hints.profiles.follows_may_be_missing": "Sequati di ca profilo forsan ne esas hike.",
"hints.profiles.posts_may_be_missing": "Kelka posti sur ca profilo forsan ne esas hike.",
"hints.profiles.see_more_followers": "Vidar plu multa sequanti sur {domain}",
"hints.profiles.see_more_follows": "Vidar plu multa sequati sur {domain}",
"hints.profiles.see_more_posts": "Vidar plu multa posti sur {domain}",
"hints.threads.replies_may_be_missing": "Respondi de altra servili forsan ne esas hike.",
"hints.threads.see_more": "Vidar plu multa demandi sur {domain}",
"home.column_settings.show_reblogs": "Montrar repeti",
"home.column_settings.show_replies": "Montrar respondi",
"home.hide_announcements": "Celez anunci",
@ -286,6 +370,17 @@
"home.pending_critical_update.link": "Vidar aktualigaji",
"home.pending_critical_update.title": "Sekuresala aktualigajo gravega disponebla!",
"home.show_announcements": "Montrez anunci",
"ignore_notifications_modal.disclaimer": "Mastodon ne povas savigar uzanti ke vu ignoris olia savigi. Ignorar savigi ne cesos ipse mesaji sendesar.",
"ignore_notifications_modal.filter_instead": "Filtrar vice",
"ignore_notifications_modal.filter_to_act_users": "Vu povos aceptar, refuzar o raportar uzanti",
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtro helpas evitar posibla konfuzo",
"ignore_notifications_modal.filter_to_review_separately": "Vu povas kontrolar filtrita savigi separe",
"ignore_notifications_modal.ignore": "Ignorez savigi",
"ignore_notifications_modal.limited_accounts_title": "Ka ignorar savigi de jerita konti?",
"ignore_notifications_modal.new_accounts_title": "Ka ignorar savigi de nova konti?",
"ignore_notifications_modal.not_followers_title": "Ka ignorar savigi de personi qua ne sequas vu?",
"ignore_notifications_modal.not_following_title": "Ka ignorar savigi de personi quan vu ne sequas?",
"ignore_notifications_modal.private_mentions_title": "Ka ignorar savigi de nekonocita privata mencionii?",
"interaction_modal.description.favourite": "Kun konto che Mastodon, vu povas favorizar ca posto por savigar la autoro ke vu prizas ol e sparar ol por pose.",
"interaction_modal.description.follow": "Per konto che Mastodon, vu povas sequar {name} por ganar ola posti en vua hemniuzeto.",
"interaction_modal.description.reblog": "Per konto che Mastodon, vu povas repetar ca posti por dissemar lo a vua propra sequati.",
@ -341,9 +436,13 @@
"lightbox.close": "Klozar",
"lightbox.next": "Nexta",
"lightbox.previous": "Antea",
"lightbox.zoom_in": "Grandigez a reala grandeso",
"lightbox.zoom_out": "Grandigez por fitigar",
"limited_account_hint.action": "Jus montrez profilo",
"limited_account_hint.title": "Ca profilo celesas dal jereri di {domain}.",
"link_preview.author": "Da {name}",
"link_preview.more_from_author": "Plua de {name}",
"link_preview.shares": "{count, plural,one {{counter} posto} other {{counter} posti}}",
"lists.account.add": "Insertez a listo",
"lists.account.remove": "Efacez de listo",
"lists.delete": "Efacez listo",
@ -360,8 +459,19 @@
"lists.subheading": "Vua listi",
"load_pending": "{count, plural, one {# nova kozo} other {# nova kozi}}",
"loading_indicator.label": "Kargante…",
"media_gallery.hide": "Celez",
"moved_to_account_banner.text": "Vua konto {disabledAccount} es nune desaktiva pro ke vu movis a {movedToAccount}.",
"mute_modal.hide_from_notifications": "Celez de savigi",
"mute_modal.hide_options": "Celez preferaji",
"mute_modal.indefinite": "Til me retrosilencigas lu",
"mute_modal.show_options": "Montrez preferaji",
"mute_modal.they_can_mention_and_follow": "Lu povas mencionar e sequar vu, ma vu ne vidos lu.",
"mute_modal.they_wont_know": "Lu ne savos ke lu silencigesis.",
"mute_modal.title": "Ka silencigar uzanto?",
"mute_modal.you_wont_see_mentions": "Vu ne vidos posti qua mencionas lu.",
"mute_modal.you_wont_see_posts": "Lu ankore povas vidar vua posti, ma vu ne vidos lua.",
"navigation_bar.about": "Pri co",
"navigation_bar.administration": "Administro",
"navigation_bar.advanced_interface": "Apertez per retintervizajo",
"navigation_bar.blocks": "Blokusita uzeri",
"navigation_bar.bookmarks": "Libromarki",
@ -378,6 +488,7 @@
"navigation_bar.follows_and_followers": "Sequati e sequanti",
"navigation_bar.lists": "Listi",
"navigation_bar.logout": "Ekirar",
"navigation_bar.moderation": "Jero",
"navigation_bar.mutes": "Celita uzeri",
"navigation_bar.opened_in_classic_interface": "Posti, konti e altra pagini specifika apertesas en la retovidilo klasika.",
"navigation_bar.personal": "Personala",
@ -388,20 +499,71 @@
"navigation_bar.security": "Sekureso",
"not_signed_in_indicator.not_signed_in": "Vu mustas enirar por acesar ca moyeno.",
"notification.admin.report": "{name} raportizis {target}",
"notification.admin.report_account": "{name} raportis {count, plural,one {1 posto} other {# posti}} de {target} pro {category}",
"notification.admin.report_account_other": "{name} raportis {count, plural,one {1 posto} other {# posti}} de {target}",
"notification.admin.report_statuses": "{name} raportis {target} pro {category}",
"notification.admin.report_statuses_other": "{name} raportis {target}",
"notification.admin.sign_up": "{name} registresis",
"notification.admin.sign_up.name_and_others": "{name} e {count, plural,one {# altru} other {#altri}} enrejistris",
"notification.favourite": "{name} favorizis tua mesajo",
"notification.favourite.name_and_others_with_link": "{name} e <a>{count, plural,one {# altru} other {# altri}}</a> favorizis vua posto",
"notification.follow": "{name} sequeskis tu",
"notification.follow.name_and_others": "{name} e {count, plural,one {# altru} other {#altri}} sequis vu",
"notification.follow_request": "{name} demandas sequar vu",
"notification.follow_request.name_and_others": "{name} e {count, plural,one {# altru} other {# altri}} volas sequar vu",
"notification.label.mention": "Mencionez",
"notification.label.private_mention": "Privata menciono",
"notification.label.private_reply": "Privata respondo",
"notification.label.reply": "Respondez",
"notification.mention": "Mencionez",
"notification.moderation-warning.learn_more": "Lernez pluse",
"notification.moderation_warning": "Vu recevis jeraverto",
"notification.moderation_warning.action_delete_statuses": "Kelka vua posti efacesis.",
"notification.moderation_warning.action_disable": "Vua konto estas desaktivigita.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Kelka vua posti markizesis quale sentoza.",
"notification.moderation_warning.action_none": "Vua konto recevis jeraverto.",
"notification.moderation_warning.action_sensitive": "Vua posti markizesos quale sentoza pos nun.",
"notification.moderation_warning.action_silence": "Vua konto limitizesis.",
"notification.moderation_warning.action_suspend": "Vua konto restriktesis.",
"notification.own_poll": "Vua votposto finigis",
"notification.poll": "Votposto quan vu partoprenis finis",
"notification.reblog": "{name} repetis tua mesajo",
"notification.reblog.name_and_others_with_link": "{name} e <a>{count, plural,one {# altru} other {#altri}}</a> repetis vua posto",
"notification.relationships_severance_event": "Desganis konekteso kun {name}",
"notification.relationships_severance_event.account_suspension": "Administranto de {from} restriktis {target}, do vu ne povas plue recevar novaji de lu o interagar kun lu.",
"notification.relationships_severance_event.domain_block": "Administranto de {from} blokusis {target}, e anke {followersCount} de vua sequanti e {followingCount, plural, one {# konto} other {# konti}} quan vu sequas.",
"notification.relationships_severance_event.learn_more": "Lernez pluse",
"notification.relationships_severance_event.user_domain_block": "Vu blokusis {target}, do efacis {followersCount} de vua sequanti e {followingCount, plural, one {# konto} other {#konti}} quan vu sequis.",
"notification.status": "{name} nove postigis",
"notification.update": "{name} modifikis posto",
"notification_requests.accept": "Aceptez",
"notification_requests.accept_multiple": "{count, plural, one {Aceptar # demando…} other {Aceptar # demandi…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar demando} other {Aceptar demandi}}",
"notification_requests.confirm_accept_multiple.message": "Vu aceptos {count, plural, one {1 savigdemando} other {# savigdemandi}}. Ka vu certe volas durar?",
"notification_requests.confirm_accept_multiple.title": "Ka aceptar savigdemandi?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Ignorez demando} other {Ignorez demandi}}",
"notification_requests.confirm_dismiss_multiple.message": "Vu ignoros {count, plural, one {1 savigdemando} other {# savigdemandi}}. Vu ne povas facile ganar {count, plural, one {ol} other {oli}} pluse. Ka vu esas certe ke vu volas durar?",
"notification_requests.confirm_dismiss_multiple.title": "Ka ignorar savigdemandi?",
"notification_requests.dismiss": "Ignorez",
"notification_requests.dismiss_multiple": "{count, plural,one {Ignorez # demando…} other {Ignorez # demandi…}}",
"notification_requests.edit_selection": "Modifikez",
"notification_requests.exit_selection": "Finas",
"notification_requests.explainer_for_limited_account": "Savigi de ca konto filtresis pro ke la konto limitizesis da jeranto.",
"notification_requests.explainer_for_limited_remote_account": "Savigi de ca konto filtresis pro ke la konto o olua servilo limitizesis da jeranto.",
"notification_requests.maximize": "Parmontrez",
"notification_requests.minimize_banner": "Celez banero di filtrita savigi",
"notification_requests.notifications_from": "Savigi de {name}",
"notification_requests.title": "Filtrita savigi",
"notification_requests.view": "Videz savigi",
"notifications.clear": "Efacar savigi",
"notifications.clear_confirmation": "Ka tu esas certa, ke tu volas efacar omna tua savigi?",
"notifications.clear_title": "Ka efacar savigi?",
"notifications.column_settings.admin.report": "Nova raporti:",
"notifications.column_settings.admin.sign_up": "Nova registranti:",
"notifications.column_settings.alert": "Desktopavizi",
"notifications.column_settings.favourite": "Favoriziti:",
"notifications.column_settings.filter_bar.advanced": "Montrez omna kategorii",
"notifications.column_settings.filter_bar.category": "Rapidfiltrilbaro",
"notifications.column_settings.follow": "Nova sequanti:",
"notifications.column_settings.follow_request": "Nova sequodemandi:",
"notifications.column_settings.mention": "Mencioni:",
@ -427,6 +589,23 @@
"notifications.permission_denied": "Desktopavizi esas nedisplonebla pro antea refuzita vidilpermisdemando",
"notifications.permission_denied_alert": "Desktopavizi ne povas aktivigesar pro ke vidilpermiso refuzesis",
"notifications.permission_required": "Desktopavizi esas nedisplonebla pro ke bezonata permiso ne donesis.",
"notifications.policy.accept": "Aceptez",
"notifications.policy.accept_hint": "Montrez en savigi",
"notifications.policy.drop": "Ignorez",
"notifications.policy.drop_hint": "Nihiligez lu",
"notifications.policy.filter": "Filtrez",
"notifications.policy.filter_hint": "Sendez a enbuxo di filtrita savigi",
"notifications.policy.filter_limited_accounts_hint": "Limitizesis da serviljeranti",
"notifications.policy.filter_limited_accounts_title": "Jerita konti",
"notifications.policy.filter_new_accounts.hint": "Kreesis depos {days, plural, one {1 dio} other {# dii}}",
"notifications.policy.filter_new_accounts_title": "Nova konti",
"notifications.policy.filter_not_followers_hint": "Inkluzanta personi qua sequas vu min kam {days, plural, one {1 dio} other {# dii}}",
"notifications.policy.filter_not_followers_title": "Nesequanti",
"notifications.policy.filter_not_following_hint": "Til vu manuale aprobas lu",
"notifications.policy.filter_not_following_title": "Nesequati",
"notifications.policy.filter_private_mentions_hint": "Filtrita se ol ne esas respondo a vua sua menciono o se vu sequas la sendanto",
"notifications.policy.filter_private_mentions_title": "Nekonocita privata mencioni",
"notifications.policy.title": "Regular savigi de…",
"notifications_permission_banner.enable": "Aktivigez desktopavizi",
"notifications_permission_banner.how_to_control": "Por ganar avizi kande Mastodon ne esas apertita, aktivigez dekstopavizi. Vu povas precize regularar quale interakti facas deskstopavizi tra la supera {icon} butono pos oli aktivigesis.",
"notifications_permission_banner.title": "Irga kozo ne pasas vu",
@ -464,6 +643,10 @@
"onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"onboarding.tips.2fa": "<strong>Ka vu savas?</strong> Vu povas sekurigar vua konto per pozar 2-faktora verifiko en preferaji de vua konto. Telefonilnombro ne bezonesis!",
"onboarding.tips.accounts_from_other_servers": "<strong>Ka vu savas?</strong> Vu povas interagar kun profili sur altra servili senrupte!",
"onboarding.tips.migration": "<strong>Ka vu savas?</strong> Se vu sentas ke {domain} ne esas apta por vu en la futuro, vu povas transferar a altra servilo di Mastodon sen malganar vua sequanti!",
"onboarding.tips.verification": "<strong>Ka vu savas?</strong> Vu povas verifikar vua konto per pozi ligilo a vua profilo di Mastodon sur vua sua retsituo e adjuntar la retsituo a vua profilo. Senpage!",
"password_confirmation.exceeds_maxlength": "La konfirmo dil pasvorto superesas la limito pri longeso di pasvorti",
"password_confirmation.mismatching": "La konfirmo dil pasvorto ne egalesas",
"picture_in_picture.restore": "Retropozez",
@ -478,7 +661,15 @@
"poll_button.add_poll": "Insertez votposto",
"poll_button.remove_poll": "Efacez votposto",
"privacy.change": "Aranjar privateso di mesaji",
"privacy.direct.long": "Omnu quan mencionesis en la posto",
"privacy.direct.short": "Specifika personi",
"privacy.private.long": "Nur vua sequanti",
"privacy.private.short": "Sequanti",
"privacy.public.long": "Ulu de e ne de Mastodon",
"privacy.public.short": "Publike",
"privacy.unlisted.additional": "Co kondutas exakte kam publika, escepte la posto ne aparos en viva novajari o gretiketi, exploro, o sercho di Mastodon, mem se vu esas volunta totkonte.",
"privacy.unlisted.long": "Min multa algoritmoridikuli",
"privacy.unlisted.short": "Deslauta publiko",
"privacy_policy.last_updated": "Antea novajo ye {date}",
"privacy_policy.title": "Privatesguidilo",
"recommended": "Rekomendata",
@ -496,7 +687,9 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "hodie",
"reply_indicator.attachments": "{count, plural, one {# atachajo} other {# atachaji}}",
"reply_indicator.cancel": "Nihiligar",
"reply_indicator.poll": "Votposto",
"report.block": "Restriktez",
"report.block_explanation": "Vu ne vidos olia posti. Oli ne povas vidar vua posti o sequar vu. Oli savos ke oli restriktesis.",
"report.categories.legal": "Legala",
@ -539,9 +732,13 @@
"report.unfollow_explanation": "Vu sequas ca konto. Por ne vidar olia posti en vua hemniuzeto pluse, desequez oli.",
"report_notification.attached_statuses": "{count, plural,one {{count} posti} other {{count} posti}} adjuntesas",
"report_notification.categories.legal": "Legala",
"report_notification.categories.legal_sentence": "deslegala kontenajo",
"report_notification.categories.other": "Altra",
"report_notification.categories.other_sentence": "altra",
"report_notification.categories.spam": "Spamo",
"report_notification.categories.spam_sentence": "sendacho",
"report_notification.categories.violation": "Regulnesequo",
"report_notification.categories.violation_sentence": "regulviolaco",
"report_notification.open": "Apertez raporto",
"search.no_recent_searches": "Nula serchi recenta",
"search.placeholder": "Serchez",
@ -569,8 +766,11 @@
"server_banner.about_active_users": "Personi quo uzas ca servilo dum antea 30 dii (monate aktiva uzanti)",
"server_banner.active_users": "aktiva uzanti",
"server_banner.administered_by": "Administresis da:",
"server_banner.is_one_of_many": "{domain} esas 1 de multa sendependa servili di Mastodon quan vu povas uzar por partoprenar en la fediverso.",
"server_banner.server_stats": "Servilstatistiko:",
"sign_in_banner.create_account": "Kreez konto",
"sign_in_banner.follow_anyone": "On povas sequar ulu sur tota fediverso e vidar omno ye kronologia ordino. Nula algoritmi, reklami o klikatrapo omnaloke.",
"sign_in_banner.mastodon_is": "Mastodon esas la maxim bona voyo por saveskar eventi.",
"sign_in_banner.sign_in": "Enirez",
"sign_in_banner.sso_redirect": "Enirar o krear konto",
"status.admin_account": "Apertez jerintervizajo por @{name}",
@ -580,14 +780,18 @@
"status.bookmark": "Libromarko",
"status.cancel_reblog_private": "Desrepetez",
"status.cannot_reblog": "Ca posto ne povas repetesar",
"status.continued_thread": "Durigita postaro",
"status.copy": "Copy link to status",
"status.delete": "Efacar",
"status.detailed_status": "Detala konversvido",
"status.direct": "Private mencionez @{name}",
"status.direct_indicator": "Privata menciono",
"status.edit": "Modifikez",
"status.edited": "Recente modifikesis ye {date}",
"status.edited_x_times": "Modifikesis {count, plural, one {{count} foyo} other {{count} foyi}}",
"status.embed": "Ganez adherkodexo",
"status.favourite": "Favorizar",
"status.favourites": "{count, plural, one {favorizo} other {favorizi}}",
"status.filter": "Filtragez ca posto",
"status.history.created": "{name} kreis ye {date}",
"status.history.edited": "{name} modifikis ye {date}",
@ -606,9 +810,11 @@
"status.reblog": "Repetez",
"status.reblog_private": "Repetez kun originala videbleso",
"status.reblogged_by": "{name} repetis",
"status.reblogs": "{count, plural, one {repeto} other {repeti}}",
"status.reblogs.empty": "Nulu ja repetis ca posto. Kande ulu facas lo, lu montresos hike.",
"status.redraft": "Efacez e riskisigez",
"status.remove_bookmark": "Efacez libromarko",
"status.replied_in_thread": "Respondesis en postaro",
"status.replied_to": "Respondis a {name}",
"status.reply": "Respondar",
"status.replyAll": "Respondar a filo",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Enginn frá þessum netþjóni getur fylgst með þér.",
"domain_block_modal.they_wont_know": "Viðkomandi mun ekki vita að hann hafi verið útilokaður.",
"domain_block_modal.title": "Útiloka lén?",
"domain_block_modal.you_will_lose_followers": "Allir fylgjendur þínir af þessum netþjóni verða fjarlægðir.",
"domain_block_modal.you_wont_see_posts": "Þú munt ekki sjá neinar færslur eða tilkynningar frá notendum á þessum netþjóni.",
"domain_pill.activitypub_lets_connect": "Það gerir þér kleift að tengjast og eiga í samskiptum við fólk, ekki bara á Mastodon, heldur einnig á mörgum öðrum mismunandi samfélagsmiðlum.",
"domain_pill.activitypub_like_language": "ActivityPub er eins og tungumál sem Mastodon notar til að tala við önnur samfélagsnet.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Limitazione per eccesso di richieste",
"alert.unexpected.message": "Si è verificato un errore imprevisto.",
"alert.unexpected.title": "Oops!",
"alt_text_badge.title": "Testo alternativo",
"announcement.announcement": "Annuncio",
"attachments_list.unprocessed": "(non elaborato)",
"audio.hide": "Nascondi audio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Nessuno da questo server può seguirti.",
"domain_block_modal.they_wont_know": "Non sapranno di essere stati bloccati.",
"domain_block_modal.title": "Bloccare il dominio?",
"domain_block_modal.you_will_lose_followers": "Tutti i tuoi seguaci da questo server verranno rimossi.",
"domain_block_modal.you_will_lose_num_followers": "Perderai {followersCount, plural, one {{followersCountDisplay} seguace} other {{followersCountDisplay} seguaci}} e {followingCount, plural, one {{followingCountDisplay} persona che segui} other {{followingCountDisplay} persone che segui}}.",
"domain_block_modal.you_will_lose_relationships": "Perderai tutti i seguaci e le persone che segui da questo server.",
"domain_block_modal.you_wont_see_posts": "Non vedrai post o notifiche dagli utenti su questo server.",
"domain_pill.activitypub_lets_connect": "Ti consente di connetterti e interagire con le persone non solo su Mastodon, ma anche su diverse app social.",
"domain_pill.activitypub_like_language": "ActivityPub è come la lingua che Mastodon parla con altri social network.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "このサーバーのユーザーはあなたをフォローできなくなります。",
"domain_block_modal.they_wont_know": "ドメインブロックは相手からはわかりません。",
"domain_block_modal.title": "ドメインをブロックしますか?",
"domain_block_modal.you_will_lose_followers": "このサーバーのフォロワーはすべてフォロー解除されます。",
"domain_block_modal.you_wont_see_posts": "このサーバーのユーザーからの投稿や通知が閲覧できなくなります。",
"domain_pill.activitypub_lets_connect": "Mastodonからほかのソーシャルアプリのユーザーへ、そのまた別のアプリのユーザーへと、それぞれが互いにつながり関わり合うことをこのActivityPubの仕組みが実現しています。",
"domain_pill.activitypub_like_language": "ActivityPubとは、Mastodonがほかのサーバーと会話をするときにしゃべる「言葉」のようなものです。",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "이 서버의 누구도 나를 팔로우 할 수 없습니다.",
"domain_block_modal.they_wont_know": "내가 차단했다는 사실을 모를 것입니다.",
"domain_block_modal.title": "도메인을 차단할까요?",
"domain_block_modal.you_will_lose_followers": "이 서버에 있는 팔로워들이 모두 제거될 것입니다.",
"domain_block_modal.you_wont_see_posts": "이 서버 사용자의 게시물이나 알림을 보지 않게 됩니다.",
"domain_pill.activitypub_lets_connect": "이것은 마스토돈 뿐만이 아니라 다른 소셜 앱들을 넘나들며 사람들을 연결하고 상호작용 할 수 있게 합니다.",
"domain_pill.activitypub_like_language": "액티비티펍은 마스토돈이 다른 소셜 네트워크와 대화할 때 쓰는 언어 같은 것입니다.",

View File

@ -58,7 +58,6 @@
"disabled_account_banner.text": "Ratio tua {disabledAccount} debilitata est.",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"domain_block_modal.you_will_lose_followers": "Omnes sectatores tuī ex hoc servō removēbuntur.",
"domain_block_modal.you_wont_see_posts": "Nuntios aut notificātiōnēs ab usoribus in hōc servō nōn vidēbis.",
"domain_pill.activitypub_like_language": "ActivityPub est velut lingua quam Mastodon cum aliīs sociālibus rētibus loquitur.",
"domain_pill.your_handle": "Tuus nominulus:",

View File

@ -215,7 +215,6 @@
"domain_block_modal.they_cant_follow": "Dingun de este sirvidor puede segirte.",
"domain_block_modal.they_wont_know": "No savra ke tiene sido blokado.",
"domain_block_modal.title": "Bloka el domeno?",
"domain_block_modal.you_will_lose_followers": "Se efasaran todos tus suivantes de este sirvidor.",
"domain_block_modal.you_wont_see_posts": "No veras publikasyones ni avizos de utilizadores en este sirvidor.",
"domain_pill.server": "Sirvidor",
"domain_pill.their_handle": "Su alias:",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Sparta apribota.",
"alert.unexpected.message": "Įvyko netikėta klaida.",
"alert.unexpected.title": "Ups!",
"alt_text_badge.title": "Alternatyvus tekstas",
"announcement.announcement": "Skelbimas",
"attachments_list.unprocessed": "(neapdorotas)",
"audio.hide": "Slėpti garsą",
@ -98,7 +99,7 @@
"block_modal.you_wont_see_mentions": "Nematysi įrašus, kuriuose jie paminimi.",
"boost_modal.combo": "Galima paspausti {combo}, kad praleisti tai kitą kartą",
"boost_modal.reblog": "Pasidalinti įrašą?",
"boost_modal.undo_reblog": "Panaikinti pasidalintą įrašą?",
"boost_modal.undo_reblog": "Nebepasidalinti įrašo?",
"bundle_column_error.copy_stacktrace": "Kopijuoti klaidos ataskaitą",
"bundle_column_error.error.body": "Paprašytos puslapio nepavyko atvaizduoti. Tai gali būti dėl mūsų kodo klaidos arba naršyklės suderinamumo problemos.",
"bundle_column_error.error.title": "O, ne!",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Niekas iš šio serverio negali tavęs sekti.",
"domain_block_modal.they_wont_know": "Jie nežinos, kad buvo užblokuoti.",
"domain_block_modal.title": "Blokuoti serverį?",
"domain_block_modal.you_will_lose_followers": "Visi tavo sekėjai iš šio serverio bus pašalinti.",
"domain_block_modal.you_will_lose_num_followers": "Prarasi {followersCount, plural, one {{followersCountDisplay} sekėją} few {{followersCountDisplay} sekėjus} many {{followersCountDisplay} sekėjo} other {{followersCountDisplay} sekėjų}} ir {followingCount, plural, one {{followingCountDisplay} žmogų, kurį seki} few {{followingCountDisplay} žmones, kuriuos seki} many {{followingCountDisplay} žmonės, kuriuos seki} other {{followingCountDisplay} žmonių, kurių seki}}.",
"domain_block_modal.you_will_lose_relationships": "Prarasi visus sekėjus ir žmones, kuriuos seki iš šio serverio.",
"domain_block_modal.you_wont_see_posts": "Nematysi naudotojų įrašų ar pranešimų šiame serveryje.",
"domain_pill.activitypub_lets_connect": "Tai leidžia tau prisijungti ir bendrauti su žmonėmis ne tik „Mastodon“ platformoje, bet ir įvairiose socialinėse programėlėse.",
"domain_pill.activitypub_like_language": "„ActivityPub“ tai tarsi kalba, kuria „Mastodon“ kalba su kitais socialiniais tinklais.",
@ -766,7 +768,7 @@
"status.admin_status": "Atidaryti šį įrašą prižiūrėjimo sąsajoje",
"status.block": "Blokuoti @{name}",
"status.bookmark": "Pridėti į žymės",
"status.cancel_reblog_private": "Nebepakelti",
"status.cancel_reblog_private": "Nebepasidalinti",
"status.cannot_reblog": "Šis įrašas negali būti pakeltas.",
"status.continued_thread": "Tęsiama gijoje",
"status.copy": "Kopijuoti nuorodą į įrašą",

View File

@ -215,7 +215,6 @@
"domain_block_modal.they_cant_follow": "Neviens šajā serverī nevar Tev sekot.",
"domain_block_modal.they_wont_know": "Viņi nezinās, ka tikuši bloķēti.",
"domain_block_modal.title": "Bloķēt domēnu?",
"domain_block_modal.you_will_lose_followers": "Tiks noņemti visi tavi sekotāji no šī servera.",
"domain_pill.server": "Serveris",
"domain_pill.username": "Lietotājvārds",
"embed.instructions": "Iestrādā šo ziņu savā mājaslapā, kopējot zemāk redzamo kodu.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Dataverkeer beperkt",
"alert.unexpected.message": "Er deed zich een onverwachte fout voor",
"alert.unexpected.title": "Oeps!",
"alt_text_badge.title": "Alt-tekst",
"announcement.announcement": "Mededeling",
"attachments_list.unprocessed": "(niet verwerkt)",
"audio.hide": "Audio verbergen",
@ -216,12 +217,13 @@
"dismissable_banner.explore_tags": "Deze hashtags winnen aan populariteit op het sociale web (fediverse). Hashtags die door meer verschillende mensen worden gebruikt staan hoger.",
"dismissable_banner.public_timeline": "Dit zijn de meest recente openbare berichten van accounts op het sociale web (fediverse) die door mensen op {domain} worden gevolgd.",
"domain_block_modal.block": "Server blokkeren",
"domain_block_modal.block_account_instead": "In plaats hiervan {name} blokkeren",
"domain_block_modal.block_account_instead": "Alleen {name} blokkeren",
"domain_block_modal.they_can_interact_with_old_posts": "Mensen op deze server kunnen interactie hebben met jouw oude berichten.",
"domain_block_modal.they_cant_follow": "Niemand op deze server kan jou volgen.",
"domain_block_modal.they_wont_know": "Ze krijgen niet te weten dat ze worden geblokkeerd.",
"domain_block_modal.title": "Server blokkeren?",
"domain_block_modal.you_will_lose_followers": "Al jouw volgers van deze server worden ontvolgd.",
"domain_block_modal.you_will_lose_num_followers": "Je verliest {followersCount, plural, one {{followersCountDisplay} volger} other {{followersCountDisplay} volgers}} en {followingCount, plural, one {{followingCountDisplay} persoon die jij volgt} other {{followingCountDisplay} personen die jij volgt}}.",
"domain_block_modal.you_will_lose_relationships": "Je verliest alle volgers en mensen die je volgt van deze server.",
"domain_block_modal.you_wont_see_posts": "Je ziet geen berichten of meldingen meer van gebruikers op deze server.",
"domain_pill.activitypub_lets_connect": "Het zorgt ervoor dat je niet alleen maar kunt verbinden en communiceren met mensen op Mastodon, maar ook met andere sociale apps.",
"domain_pill.activitypub_like_language": "ActivityPub is de taal die Mastodon met andere sociale netwerken spreekt.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Ingen på denne tenaren kan fylgja deg.",
"domain_block_modal.they_wont_know": "Dei veit ikkje at dei er blokkerte.",
"domain_block_modal.title": "Blokker domenet?",
"domain_block_modal.you_will_lose_followers": "Alle fylgjarane dine frå denne tenaren blir fjerna.",
"domain_block_modal.you_wont_see_posts": "Du vil ikkje sjå innlegg eller varslingar frå brukarar på denne tenaren.",
"domain_pill.activitypub_lets_connect": "Den lar deg kople til og samhandle med folk ikkje berre på Mastodon, men òg på tvers av forskjellige sosiale appar.",
"domain_pill.activitypub_like_language": "ActivityPub er som språket Mastodon snakkar med andre sosiale nettverk.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Ingen fra denne serveren kan følge deg.",
"domain_block_modal.they_wont_know": "De kommer ikke til å få vite at du har valgt å blokkere dem.",
"domain_block_modal.title": "Blokker domenet?",
"domain_block_modal.you_will_lose_followers": "Alle dine følgere fra denne serveren vil bli fjernet.",
"domain_block_modal.you_wont_see_posts": "Du vil ikke se innlegg eller få varsler fra brukere på denne serveren.",
"domain_pill.activitypub_lets_connect": "Den lar deg koble til og samhandle med folk ikke bare på Mastodon, men også på tvers av forskjellige sosiale apper.",
"domain_pill.activitypub_like_language": "ActivityPub er liksom språket Mastodon snakker med andre sosiale nettverk.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Ograniczenie liczby zapytań",
"alert.unexpected.message": "Wystąpił nieoczekiwany błąd.",
"alert.unexpected.title": "Ups!",
"alt_text_badge.title": "Tekst alternatywny",
"announcement.announcement": "Ogłoszenie",
"attachments_list.unprocessed": "(nieprzetworzone)",
"audio.hide": "Ukryj dźwięk",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Nikt z tego serwera nie może Cię obserwować.",
"domain_block_modal.they_wont_know": "Użytkownik nie dowie się, że został zablokowany.",
"domain_block_modal.title": "Zablokować domenę?",
"domain_block_modal.you_will_lose_followers": "Wszyscy twoi obserwujący z tego serwera zostaną usunięci.",
"domain_block_modal.you_will_lose_num_followers": "Utracisz {followersCount, plural, one {jednego obserwującego} other {{followersCountDisplay} obserwujących}} i {followingCount, plural, one {jedną osobę którą obserwujesz} few {{followingCountDisplay} osoby które obserwujesz} other {{followingCountDisplay} osób które obserwujesz}}.",
"domain_block_modal.you_will_lose_relationships": "Utracisz wszystkich obserwujących z tego serwera i wszystkie osoby które obserwujesz na tym serwerze.",
"domain_block_modal.you_wont_see_posts": "Nie zobaczysz postów ani powiadomień od użytkowników na tym serwerze.",
"domain_pill.activitypub_lets_connect": "Pozwala połączyć się z ludźmi na Mastodonie, jak i na innych serwisach społecznościowych.",
"domain_pill.activitypub_like_language": "ActivityPub jest językiem używanym przez Mastodon do wymiany danych z innymi serwisami społecznościowymi.",

View File

@ -85,6 +85,7 @@
"alert.rate_limited.title": "Tentativas limitadas",
"alert.unexpected.message": "Ocorreu um erro inesperado.",
"alert.unexpected.title": "Eita!",
"alt_text_badge.title": "Texto alternativo",
"announcement.announcement": "Comunicados",
"attachments_list.unprocessed": "(não processado)",
"audio.hide": "Ocultar áudio",
@ -221,7 +222,8 @@
"domain_block_modal.they_cant_follow": "Ninguém deste servidor pode lhe seguir.",
"domain_block_modal.they_wont_know": "Eles não saberão que foram bloqueados.",
"domain_block_modal.title": "Dominio do bloco",
"domain_block_modal.you_will_lose_followers": "Todos os seus seguidores deste servidor serão removidos.",
"domain_block_modal.you_will_lose_num_followers": "Você perderá {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} e {followingCount, plural, one {{followingCountDisplay} pessoa que você segue} other {{followingCountDisplay} pessoas que você segue}}.",
"domain_block_modal.you_will_lose_relationships": "Você irá perder todos os seguidores e pessoas que você segue neste servidor.",
"domain_block_modal.you_wont_see_posts": "Você não verá postagens ou notificações de usuários neste servidor.",
"domain_pill.activitypub_lets_connect": "Ele permite que você se conecte e interaja com pessoas não apenas no Mastodon, mas também em diferentes aplicativos sociais.",
"domain_pill.activitypub_like_language": "ActivityPub é como a linguagem que o Mastodon fala com outras redes sociais.",

View File

@ -221,7 +221,6 @@
"domain_block_modal.they_cant_follow": "Ninguém deste servidor pode segui-lo.",
"domain_block_modal.they_wont_know": "Eles não saberão que foram bloqueados.",
"domain_block_modal.title": "Bloquear domínio?",
"domain_block_modal.you_will_lose_followers": "Todos os seus seguidores deste servidor serão removidos.",
"domain_block_modal.you_wont_see_posts": "Não verá publicações ou notificações de utilizadores neste servidor.",
"domain_pill.activitypub_lets_connect": "Permite-lhe conectar e interagir com pessoas não só no Mastodon, mas também em diferentes aplicações sociais.",
"domain_pill.activitypub_like_language": "O ActivityPub é como a linguagem que o Mastodon fala com outras redes sociais.",

View File

@ -220,7 +220,6 @@
"domain_block_modal.they_cant_follow": "Никто из этого сервера не может подписываться на вас.",
"domain_block_modal.they_wont_know": "Он не будет знать, что его заблокировали.",
"domain_block_modal.title": "Заблокировать домен?",
"domain_block_modal.you_will_lose_followers": "Все ваши подписчики с этого сервера будут удалены.",
"domain_block_modal.you_wont_see_posts": "Вы не будете видеть записи или уведомления от пользователей на этом сервере.",
"domain_pill.activitypub_lets_connect": "Это позволяет вам общаться и взаимодействовать с людьми не только на Mastodon, но и в различных социальных приложениях.",
"domain_pill.activitypub_like_language": "ActivityPub как язык Mastodon говорит с другими социальными сетями.",

View File

@ -34,7 +34,9 @@
"account.follow_back": "Sledovať späť",
"account.followers": "Sledovatelia",
"account.followers.empty": "Tento účet ešte nikto nesleduje.",
"account.followers_counter": "{count, plural, one {{counter} sledujúci} other {{counter} sledujúci}}",
"account.following": "Sledovaný účet",
"account.following_counter": "{count, plural, one {{counter} sledovaných} other {{counter} sledovaných}}",
"account.follows.empty": "Tento účet ešte nikoho nesleduje.",
"account.go_to_profile": "Prejsť na profil",
"account.hide_reblogs": "Skryť zdieľania od @{name}",
@ -60,6 +62,7 @@
"account.requested_follow": "{name} vás chce sledovať",
"account.share": "Zdieľaj profil @{name}",
"account.show_reblogs": "Zobrazovať zdieľania od @{name}",
"account.statuses_counter": "{count, plural, one {{counter} príspevok} other {{counter} príspevkov}}",
"account.unblock": "Odblokovať @{name}",
"account.unblock_domain": "Odblokovať doménu {domain}",
"account.unblock_short": "Odblokovať",
@ -215,7 +218,6 @@
"domain_block_modal.they_cant_follow": "Nikto z tohoto servera ťa nemôže nasledovať.",
"domain_block_modal.they_wont_know": "Nebude vedieť, že bol/a zablokovaný/á.",
"domain_block_modal.title": "Blokovať doménu?",
"domain_block_modal.you_will_lose_followers": "Všetci tvoji nasledovatelia z tohto servera budú odstránení.",
"domain_block_modal.you_wont_see_posts": "Neuvidíš príspevky, ani oboznámenia od užívateľov na tomto serveri.",
"domain_pill.activitypub_like_language": "ActivityPub je ako jazyk, ktorým Mastodon hovorí s ostatnými sociálnymi sieťami.",
"domain_pill.server": "Server",
@ -418,6 +420,7 @@
"lists.subheading": "Vaše zoznamy",
"load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položiek} other {# nových položiek}}",
"loading_indicator.label": "Načítavanie…",
"media_gallery.hide": "Skryť",
"moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálne deaktivovaný, pretože ste sa presunuli na {movedToAccount}.",
"mute_modal.hide_from_notifications": "Ukryť z upozornení",
"mute_modal.hide_options": "Skryť možnosti",

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