Merge commit '8a498f4e65ecf27cc2c992d02b47f890dafef20b' into glitch-soc/merge-upstream

This commit is contained in:
Claire committed 2024-05-01 14:50:16 +02:00
commit 3f61981f5d
80 files changed
+630 -463

No files matched your search

-152
View File
@@ -1,152 +0,0 @@
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import api from '../api';
import { compareId } from '../compare_id';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL';
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if (window.fetch && 'keepalive' in new Request('')) {
fetch('/api/v1/markers', {
keepalive: true,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
} else if (navigator && navigator.sendBeacon) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) {
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon('/api/v1/markers', formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}
};
const _buildParams = (state) => {
const params = {};
const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
api(getState).post('/api/v1/markers', params).then(() => {
dispatch(submitMarkersSuccess(params));
}).catch(() => {});
}, 300000, { leading: true, trailing: true });
export function submitMarkersSuccess({ home, notifications }) {
return {
type: MARKERS_SUBMIT_SUCCESS,
home: (home || {}).last_read_id,
notifications: (notifications || {}).last_read_id,
};
}
export function submitMarkers(params = {}) {
const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
if (params.immediate === true) {
debouncedSubmitMarkers.flush();
}
return result;
}
export const fetchMarkers = () => (dispatch, getState) => {
const params = { timeline: ['notifications'] };
dispatch(fetchMarkersRequest());
api(getState).get('/api/v1/markers', { params }).then(response => {
dispatch(fetchMarkersSuccess(response.data));
}).catch(error => {
dispatch(fetchMarkersFail(error));
});
};
export function fetchMarkersRequest() {
return {
type: MARKERS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchMarkersSuccess(markers) {
return {
type: MARKERS_FETCH_SUCCESS,
markers,
skipLoading: true,
};
}
export function fetchMarkersFail(error) {
return {
type: MARKERS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
};
}
+165
View File
@@ -0,0 +1,165 @@
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import type { MarkerJSON } from 'mastodon/api_types/markers';
import type { RootState } from 'mastodon/store';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
import api, { authorizationTokenFromState } from '../api';
import { compareId } from '../compare_id';
export const synchronouslySubmitMarkers = createAppAsyncThunk(
'markers/submit',
async (_args, { getState }) => {
const accessToken = authorizationTokenFromState(getState);
const params = buildPostMarkersParams(getState());
if (Object.keys(params).length === 0 || !accessToken) {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if ('fetch' in window && 'keepalive' in new Request('')) {
await fetch('/api/v1/markers', {
keepalive: true,
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if ('navigator' && 'sendBeacon' in navigator) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) {
if (value.last_read_id)
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon('/api/v1/markers', formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}
},
);
interface MarkerParam {
last_read_id?: string;
}
function getLastHomeId(state: RootState): string | undefined {
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
return (
state
// @ts-expect-error state.timelines is not yet typed
.getIn(['timelines', 'home', 'items'], ImmutableList())
// @ts-expect-error state.timelines is not yet typed
.find((item) => item !== null)
);
}
function getLastNotificationId(state: RootState): string | undefined {
// @ts-expect-error state.notifications is not yet typed
return state.getIn(['notifications', 'lastReadId']);
}
const buildPostMarkersParams = (state: RootState) => {
const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
const lastHomeId = getLastHomeId(state);
const lastNotificationId = getLastNotificationId(state);
if (lastHomeId && compareId(lastHomeId, state.markers.home) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
if (
lastNotificationId &&
compareId(lastNotificationId, state.markers.notifications) > 0
) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
export const submitMarkersAction = createAppAsyncThunk<{
home: string | undefined;
notifications: string | undefined;
}>('markers/submitAction', async (_args, { getState }) => {
const accessToken = authorizationTokenFromState(getState);
const params = buildPostMarkersParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return { home: undefined, notifications: undefined };
}
await api(getState).post<MarkerJSON>('/api/v1/markers', params);
return {
home: params.home?.last_read_id,
notifications: params.notifications?.last_read_id,
};
});
const debouncedSubmitMarkers = debounce(
(dispatch) => {
dispatch(submitMarkersAction());
},
300000,
{
leading: true,
trailing: true,
},
);
export const submitMarkers = createAppAsyncThunk(
'markers/submit',
(params: { immediate?: boolean }, { dispatch }) => {
debouncedSubmitMarkers(dispatch);
if (params.immediate) {
debouncedSubmitMarkers.flush();
}
},
);
export const fetchMarkers = createAppAsyncThunk(
'markers/fetch',
async (_args, { getState }) => {
const response = await api(getState).get<Record<string, MarkerJSON>>(
`/api/v1/markers`,
{ params: { timeline: ['notifications'] } },
);
return { markers: response.data };
},
);
@@ -1,46 +0,0 @@
// @ts-check
export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY';
export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
/**
* @typedef MediaProps
* @property {string} src
* @property {boolean} muted
* @property {number} volume
* @property {number} currentTime
* @property {string} poster
* @property {string} backgroundColor
* @property {string} foregroundColor
* @property {string} accentColor
*/
/**
* @param {string} statusId
* @param {string} accountId
* @param {string} playerType
* @param {MediaProps} props
* @returns {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
// @ts-expect-error
return (dispatch, getState) => {
// Do not open a player for a toot that does not exist
if (getState().hasIn(['statuses', statusId])) {
dispatch({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
}
};
};
/*
* @return {object}
*/
export const removePictureInPicture = () => ({
type: PICTURE_IN_PICTURE_REMOVE,
});
@@ -0,0 +1,31 @@
import { createAction } from '@reduxjs/toolkit';
import type { PIPMediaProps } from 'mastodon/reducers/picture_in_picture';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
interface DeployParams {
statusId: string;
accountId: string;
playerType: 'audio' | 'video';
props: PIPMediaProps;
}
export const removePictureInPicture = createAction('pip/remove');
export const deployPictureInPictureAction =
createAction<DeployParams>('pip/deploy');
export const deployPictureInPicture = createAppAsyncThunk(
'pip/deploy',
(args: DeployParams, { dispatch, getState }) => {
const { statusId } = args;
// Do not open a player for a toot that does not exist
// @ts-expect-error state.statuses is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (getState().hasIn(['statuses', statusId])) {
dispatch(deployPictureInPictureAction(args));
}
},
);
+7 -2
View File
@@ -29,9 +29,14 @@ const setCSRFHeader = () => {
void ready(setCSRFHeader);
export const authorizationTokenFromState = (getState?: GetState) => {
return (
getState && (getState().meta.get('access_token', '') as string | false)
);
};
const authorizationHeaderFromState = (getState?: GetState) => {
const accessToken =
getState && (getState().meta.get('access_token', '') as string);
const accessToken = authorizationTokenFromState(getState);
if (!accessToken) {
return {};
@@ -0,0 +1,7 @@
// See app/serializers/rest/account_serializer.rb
export interface MarkerJSON {
last_read_id: string;
version: string;
updated_at: string;
}
@@ -1,26 +1,26 @@
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react';
import classNames from 'classnames';
interface BaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
interface BaseProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
block?: boolean;
secondary?: boolean;
text?: JSX.Element;
}
interface PropsWithChildren extends BaseProps {
text?: never;
interface PropsChildren extends PropsWithChildren<BaseProps> {
text?: undefined;
}
interface PropsWithText extends BaseProps {
text: JSX.Element;
children: never;
text: JSX.Element | string;
children?: undefined;
}
type Props = PropsWithText | PropsWithChildren;
type Props = PropsWithText | PropsChildren;
export const Button: React.FC<Props> = ({
text,
type = 'button',
onClick,
disabled,
@@ -28,6 +28,7 @@ export const Button: React.FC<Props> = ({
secondary,
className,
title,
text,
children,
...props
}) => {
@@ -191,7 +191,7 @@ const timeRemainingString = (
interface Props {
intl: IntlShape;
timestamp: string;
year: number;
year?: number;
futureDate?: boolean;
short?: boolean;
}
@@ -203,11 +203,6 @@ class RelativeTimestamp extends Component<Props, States> {
now: Date.now(),
};
static defaultProps = {
year: new Date().getFullYear(),
short: true,
};
_timer: number | undefined;
shouldComponentUpdate(nextProps: Props, nextState: States) {
@@ -257,7 +252,13 @@ class RelativeTimestamp extends Component<Props, States> {
}
render() {
const { timestamp, intl, year, futureDate, short } = this.props;
const {
timestamp,
intl,
futureDate,
year = new Date().getFullYear(),
short = true,
} = this.props;
const timeGiven = timestamp.includes('T');
const date = new Date(timestamp);
@@ -81,7 +81,7 @@ class StatusActionBar extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
relationship: ImmutablePropTypes.record,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
@@ -262,7 +262,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
},
deployPictureInPicture (status, type, mediaProps) {
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps}));
},
onInteractionModal (type, status) {
@@ -82,7 +82,7 @@ class GettingStarted extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.map,
myAccount: ImmutablePropTypes.record,
multiColumn: PropTypes.bool,
fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number,
@@ -210,4 +210,4 @@ class Footer extends ImmutablePureComponent {
}
export default withRouter(connect(makeMapStateToProps)(injectIntl(Footer)));
export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer)));
@@ -1,51 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const mapStateToProps = (state, { accountId }) => ({
account: state.getIn(['accounts', accountId]),
});
class Header extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
statusId: PropTypes.string.isRequired,
account: ImmutablePropTypes.record.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { account, statusId, onClose, intl } = this.props;
return (
<div className='picture-in-picture__header'>
<Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'>
<Avatar account={account} size={36} />
<DisplayName account={account} />
</Link>
<IconButton icon='times' iconComponent={CloseIcon} onClick={onClose} title={intl.formatMessage(messages.close)} />
</div>
);
}
}
export default connect(mapStateToProps)(injectIntl(Header));
@@ -0,0 +1,46 @@
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import { useAppSelector } from 'mastodon/store';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
interface Props {
accountId: string;
statusId: string;
onClose: () => void;
}
export const Header: React.FC<Props> = ({ accountId, statusId, onClose }) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
const intl = useIntl();
if (!account) return null;
return (
<div className='picture-in-picture__header'>
<Link
to={`/@${account.get('acct')}/${statusId}`}
className='picture-in-picture__header__account'
>
<Avatar account={account} size={36} />
<DisplayName account={account} />
</Link>
<IconButton
icon='times'
iconComponent={CloseIcon}
onClick={onClose}
title={intl.formatMessage(messages.close)}
/>
</div>
);
};
@@ -1,89 +0,0 @@
import PropTypes from 'prop-types';
import { Component } from 'react';
import { connect } from 'react-redux';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import Audio from 'mastodon/features/audio';
import Video from 'mastodon/features/video';
import Footer from './components/footer';
import Header from './components/header';
const mapStateToProps = state => ({
...state.get('picture_in_picture'),
});
class PictureInPicture extends Component {
static propTypes = {
statusId: PropTypes.string,
accountId: PropTypes.string,
type: PropTypes.string,
src: PropTypes.string,
muted: PropTypes.bool,
volume: PropTypes.number,
currentTime: PropTypes.number,
poster: PropTypes.string,
backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string,
accentColor: PropTypes.string,
dispatch: PropTypes.func.isRequired,
};
handleClose = () => {
const { dispatch } = this.props;
dispatch(removePictureInPicture());
};
render () {
const { type, src, currentTime, accountId, statusId } = this.props;
if (!currentTime) {
return null;
}
let player;
if (type === 'video') {
player = (
<Video
src={src}
currentTime={this.props.currentTime}
volume={this.props.volume}
muted={this.props.muted}
autoPlay
inline
alwaysVisible
/>
);
} else if (type === 'audio') {
player = (
<Audio
src={src}
currentTime={this.props.currentTime}
volume={this.props.volume}
muted={this.props.muted}
poster={this.props.poster}
backgroundColor={this.props.backgroundColor}
foregroundColor={this.props.foregroundColor}
accentColor={this.props.accentColor}
autoPlay
/>
);
}
return (
<div className='picture-in-picture'>
<Header accountId={accountId} statusId={statusId} onClose={this.handleClose} />
{player}
<Footer statusId={statusId} />
</div>
);
}
}
export default connect(mapStateToProps)(PictureInPicture);
@@ -0,0 +1,79 @@
import { useCallback } from 'react';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import Audio from 'mastodon/features/audio';
import Video from 'mastodon/features/video';
import { useAppDispatch, useAppSelector } from 'mastodon/store/typed_functions';
import Footer from './components/footer';
import { Header } from './components/header';
export const PictureInPicture: React.FC = () => {
const dispatch = useAppDispatch();
const handleClose = useCallback(() => {
dispatch(removePictureInPicture());
}, [dispatch]);
const pipState = useAppSelector((s) => s.picture_in_picture);
if (pipState.type === null) {
return null;
}
const {
type,
src,
currentTime,
accountId,
statusId,
volume,
muted,
poster,
backgroundColor,
foregroundColor,
accentColor,
} = pipState;
let player;
switch (type) {
case 'video':
player = (
<Video
src={src}
currentTime={currentTime}
volume={volume}
muted={muted}
autoPlay
inline
alwaysVisible
/>
);
break;
case 'audio':
player = (
<Audio
src={src}
currentTime={currentTime}
volume={volume}
muted={muted}
poster={poster}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
accentColor={accentColor}
autoPlay
/>
);
}
return (
<div className='picture-in-picture'>
<Header accountId={accountId} statusId={statusId} onClose={handleClose} />
{player}
<Footer statusId={statusId} />
</div>
);
};
@@ -74,7 +74,7 @@ class ActionBar extends PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
relationship: ImmutablePropTypes.record,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
@@ -14,7 +14,7 @@ import { HotKeys } from 'react-hotkeys';
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import PictureInPicture from 'mastodon/features/picture_in_picture';
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
+2
View File
@@ -471,6 +471,8 @@
"notification.own_poll": "Анкетата ви приключи",
"notification.poll": "Анкета, в която гласувахте, приключи",
"notification.reblog": "{name} подсили ваша публикация",
"notification.relationships_severance_event": "Изгуби се връзката с {name}",
"notification.relationships_severance_event.learn_more": "Научете повече",
"notification.status": "{name} току-що публикува",
"notification.update": "{name} промени публикация",
"notification_requests.accept": "Приемам",
+1 -1
View File
@@ -472,7 +472,7 @@
"notification.own_poll": "La teva enquesta ha finalitzat",
"notification.poll": "Ha finalitzat una enquesta en què has votat",
"notification.reblog": "{name} t'ha impulsat",
"notification.relationships_severance_event": "Connexions perdudes amb {name}",
"notification.relationships_severance_event": "S'han perdut les connexions amb {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspès {target}; això vol dir que ja no en podreu rebre actualitzacions o interactuar-hi.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} ha blocat {target}, incloent-hi {followersCount} dels vostres seguidors i {followingCount, plural, one {# compte} other {# comptes}} que seguiu.",
"notification.relationships_severance_event.learn_more": "Per a saber-ne més",
@@ -298,6 +298,7 @@
"filter_modal.select_filter.title": "Filtrar esta publicación",
"filter_modal.title.status": "Filtrar una publicación",
"filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer",
"filtered_notifications_banner.private_mentions": "{count, plural, one {mención privada} other {menciones privadas}}",
"filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todas",
"firehose.local": "Este servidor",
+1
View File
@@ -298,6 +298,7 @@
"filter_modal.select_filter.title": "Filtrar esta publicación",
"filter_modal.title.status": "Filtrar una publicación",
"filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer",
"filtered_notifications_banner.private_mentions": "{count, plural, one {mención privada} other {menciones privadas}}",
"filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todas",
"firehose.local": "Este servidor",
+4
View File
@@ -472,7 +472,11 @@
"notification.own_poll": "Äänestyksesi on päättynyt",
"notification.poll": "Kysely, johon osallistuit, on päättynyt",
"notification.reblog": "{name} tehosti julkaisuasi",
"notification.relationships_severance_event": "Menetettiin yhteydet palvelimeen {name}",
"notification.relationships_severance_event.account_suspension": "Palvelimen {from} ylläpitäjä on jäädyttänyt verkkotunnuksen {target}, minkä takia et voi enää vastaanottaa heidän päivityksiään tai olla vuorovaikutuksessa heidän kanssaan.",
"notification.relationships_severance_event.domain_block": "Palvelimen {from} ylläpitäjä on estänyt verkkotunnuksen {target}, mukaan lukien {followersCount} seuraajistasi ja {followingCount, plural, one {# seuratuistasi} other {# seuratuistasi}}.",
"notification.relationships_severance_event.learn_more": "Lue lisää",
"notification.relationships_severance_event.user_domain_block": "Olet estänyt verkkotunnuksen {target}, mikä poisti {followersCount} seuraajistasi ja {followingCount, plural, one {# seuratuistasi} other {# seuratuistasi}}.",
"notification.status": "{name} julkaisi juuri",
"notification.update": "{name} muokkasi julkaisua",
"notification_requests.accept": "Hyväksy",
+6
View File
@@ -298,6 +298,7 @@
"filter_modal.select_filter.title": "Filtrar esta publicación",
"filter_modal.title.status": "Filtrar unha publicación",
"filtered_notifications_banner.pending_requests": "Notificacións de {count, plural, =0 {ninguén} one {unha persoa} other {# persoas}} que poderías coñecer",
"filtered_notifications_banner.private_mentions": "{count, plural, one {mención privada} other {mencións privadas}}",
"filtered_notifications_banner.title": "Notificacións filtradas",
"firehose.all": "Todo",
"firehose.local": "Este servidor",
@@ -471,6 +472,11 @@
"notification.own_poll": "A túa enquisa rematou",
"notification.poll": "Rematou a enquisa na que votaches",
"notification.reblog": "{name} compartiu a túa publicación",
"notification.relationships_severance_event": "Perdeuse a conexión 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",
"notification.relationships_severance_event.user_domain_block": "Bloqueaches a {target}, eliminando a {followersCount} das túas seguidoras e a {followingCount, plural, one {# conta} other {# contas}} que sigues.",
"notification.status": "{name} publicou",
"notification.update": "{name} editou unha publicación",
"notification_requests.accept": "Aceptar",
+15 -15
View File
@@ -124,7 +124,7 @@
"column.domain_blocks": "Letiltott domainek",
"column.favourites": "Kedvencek",
"column.firehose": "Hírfolyamok",
"column.follow_requests": "Követési kérelmek",
"column.follow_requests": "Követési kérések",
"column.home": "Kezdőlap",
"column.lists": "Listák",
"column.mutes": "Némított felhasználók",
@@ -133,8 +133,8 @@
"column.public": "Föderációs idővonal",
"column_back_button.label": "Vissza",
"column_header.hide_settings": "Beállítások elrejtése",
"column_header.moveLeft_settings": "Oszlop elmozdítása balra",
"column_header.moveRight_settings": "Oszlop elmozdítása jobbra",
"column_header.moveLeft_settings": "Oszlop áthelyezése balra",
"column_header.moveRight_settings": "Oszlop áthelyezése jobbra",
"column_header.pin": "Kitűzés",
"column_header.show_settings": "Beállítások megjelenítése",
"column_header.unpin": "Kitűzés eltávolítása",
@@ -143,7 +143,7 @@
"community.column_settings.media_only": "Csak média",
"community.column_settings.remote_only": "Csak távoli",
"compose.language.change": "Nyelv megváltoztatása",
"compose.language.search": "Nyelv keresése...",
"compose.language.search": "Nyelvek keresése…",
"compose.published.body": "A bejegyzés publikálásra került.",
"compose.published.open": "Megnyitás",
"compose.saved.body": "A bejegyzés mentve.",
@@ -473,8 +473,8 @@
"notification.poll": "Egy szavazás, melyben részt vettél, véget ért",
"notification.reblog": "{name} megtolta a bejegyzésedet",
"notification.relationships_severance_event": "Elvesztek a kapcsolatok vele: {name}",
"notification.relationships_severance_event.account_suspension": "Egy admin a {from} kiszolgálóról felfüggesztette {target} fiókot, ami azt jelenti, hogy mostantól nem tudsz vele interaktálni vagy tőle értesítéseket kapni.",
"notification.relationships_severance_event.domain_block": "Egy admin a {from} kiszolgálón letiltotta {target} domaint, beleértve {followersCount} követődet és {followingCount, plural, one {#} other {#}} általad követett személyt.",
"notification.relationships_severance_event.account_suspension": "Egy admin a(z) {from} kiszolgálóról felfüggesztette {target} fiókját, ami azt jelenti, hogy mostantól nem fogsz róla értesítést kapni, és nem fogsz tudni vele kapcsolatba lépni.",
"notification.relationships_severance_event.domain_block": "Egy admin a(z) {from} kiszolgálón letiltotta {target} domaint, beleértve {followersCount} követőt és {followingCount, plural, one {#} other {#}} követett fiókot.",
"notification.relationships_severance_event.learn_more": "További információk",
"notification.relationships_severance_event.user_domain_block": "Letiltottad a(z) {target} domaint, ezzel eltávolítva {followersCount} követőt és {followingCount, plural, one {#} other {#}} követett fiókot.",
"notification.status": "{name} bejegyzést tett közzé",
@@ -492,7 +492,7 @@
"notifications.column_settings.filter_bar.advanced": "Minden kategória megjelenítése",
"notifications.column_settings.filter_bar.category": "Gyorsszűrő sáv",
"notifications.column_settings.follow": "Új követők:",
"notifications.column_settings.follow_request": "Új követési kérelmek:",
"notifications.column_settings.follow_request": "Új követési kérések:",
"notifications.column_settings.mention": "Megemlítések:",
"notifications.column_settings.poll": "Szavazási eredmények:",
"notifications.column_settings.push": "Leküldéses értesítések",
@@ -552,14 +552,14 @@
"onboarding.share.next_steps": "Lehetséges következő lépések:",
"onboarding.share.title": "Profil megosztása",
"onboarding.start.lead": "Az új Mastodon-fiók használatra kész. Így hozhatod ki belőle a legtöbbet:",
"onboarding.start.skip": "Szeretnél előreugrani?",
"onboarding.start.skip": "Nincs szükséged segítségre a kezdéshez?",
"onboarding.start.title": "Ez sikerült!",
"onboarding.steps.follow_people.body": "A Mastodon az érdekes emberek követéséről szól.",
"onboarding.steps.follow_people.title": "{count, plural, one {egy ember} other {# ember}} követése",
"onboarding.steps.publish_status.body": "Üdvözöljük a világot.",
"onboarding.steps.follow_people.title": "Szabd személyre a kezdőlapodat",
"onboarding.steps.publish_status.body": "Köszöntsd a világot szöveggel, fotókkal, videókkal vagy szavazásokkal {emoji}",
"onboarding.steps.publish_status.title": "Az első bejegyzés létrehozása",
"onboarding.steps.setup_profile.body": "Mások nagyobb valószínűséggel lépnek kapcsolatba veled egy kitöltött profil esetén.",
"onboarding.steps.setup_profile.title": "Profilod testreszabása",
"onboarding.steps.setup_profile.body": "Növeld az interakciók számát a profilod részletesebb kitöltésével.",
"onboarding.steps.setup_profile.title": "Szabd személyre a profilodat",
"onboarding.steps.share_profile.body": "Tudasd az ismerőseiddel, hogyan találhatnak meg a Mastodonon",
"onboarding.steps.share_profile.title": "Oszd meg a Mastodon profilodat",
"onboarding.tips.2fa": "<strong>Tudtad?</strong> A fiókod biztonságossá teheted, ha a fiók beállításaiban beállítod a kétlépcsős hitelesítést. Bármilyen választott TOTP alkalmazással működik, nincs szükség telefonszámra!",
@@ -787,9 +787,9 @@
"upload_modal.hint": "Kattints vagy húzd a kört az előnézetben arra a fókuszpontra, mely minden bélyegképen látható kell, hogy legyen.",
"upload_modal.preparing_ocr": "OCR előkészítése…",
"upload_modal.preview_label": "Előnézet ({ratio})",
"upload_progress.label": "Feltöltés...",
"upload_progress.label": "Feltöltés…",
"upload_progress.processing": "Feldolgozás…",
"username.taken": "Ez a felhasználónév foglalt. Válassz másikat",
"username.taken": "Ez a felhasználónév foglalt. Válassz másikat.",
"video.close": "Videó bezárása",
"video.download": "Fájl letöltése",
"video.exit_fullscreen": "Kilépés teljes képernyőből",
@@ -799,5 +799,5 @@
"video.mute": "Hang némítása",
"video.pause": "Szünet",
"video.play": "Lejátszás",
"video.unmute": "Hang némításának vége"
"video.unmute": "Hang némításának feloldása"
}
+3
View File
@@ -473,7 +473,10 @@
"notification.poll": "Könnun sem þú tókst þátt í er lokið",
"notification.reblog": "{name} endurbirti færsluna þína",
"notification.relationships_severance_event": "Missti tengingar við {name}",
"notification.relationships_severance_event.account_suspension": "Stjórnandi á {from} hefur fryst {target}, sem þýðir að þú færð ekki lengur skilaboð frá viðkomandi né átt í samskiptum við viðkomandi.",
"notification.relationships_severance_event.domain_block": "Stjórnandi á {from} hefur lokað á {target} og þar með {followersCount} fylgjendur þína auk {followingCount, plural, one {# aðgangs} other {# aðganga}} sem þú fylgist með.",
"notification.relationships_severance_event.learn_more": "Kanna nánar",
"notification.relationships_severance_event.user_domain_block": "Þú hefur lokað á {target} og þar með fjarlægt {followersCount} fylgjendur þína auk {followingCount, plural, one {# aðgangs} other {# aðganga}} sem þú fylgist með.",
"notification.status": "{name} sendi inn rétt í þessu",
"notification.update": "{name} breytti færslu",
"notification_requests.accept": "Samþykkja",
+3
View File
@@ -298,6 +298,7 @@
"filter_modal.select_filter.title": "Filtra questo post",
"filter_modal.title.status": "Filtra un post",
"filtered_notifications_banner.pending_requests": "Notifiche da {count, plural, =0 {nessuno} one {una persona} other {# persone}} che potresti conoscere",
"filtered_notifications_banner.private_mentions": "{count, plural,one {menzione privata} other {menzioni private}}",
"filtered_notifications_banner.title": "Notifiche filtrate",
"firehose.all": "Tutto",
"firehose.local": "Questo server",
@@ -473,7 +474,9 @@
"notification.reblog": "{name} ha rebloggato il tuo post",
"notification.relationships_severance_event": "Connessioni perse con {name}",
"notification.relationships_severance_event.account_suspension": "Un amministratore da {from} ha sospeso {target}, il che significa che non puoi più ricevere aggiornamenti da loro o interagire con loro.",
"notification.relationships_severance_event.domain_block": "Un amministratore da {from} ha bloccato {target}, inclusi {followersCount} dei tuoi seguaci e {followingCount, plural, one {# account} other {# account}} che segui.",
"notification.relationships_severance_event.learn_more": "Scopri di più",
"notification.relationships_severance_event.user_domain_block": "Tu hai bloccato {target}, rimuovendo {followersCount} dei tuoi seguaci e {followingCount, plural, one {# account} other {# account}} che segui.",
"notification.status": "{name} ha appena pubblicato un post",
"notification.update": "{name} ha modificato un post",
"notification_requests.accept": "Accetta",
+1
View File
@@ -177,6 +177,7 @@
"confirmations.delete_list.message": "本当にこのリストを完全に削除しますか?",
"confirmations.discard_edit_media.confirm": "破棄",
"confirmations.discard_edit_media.message": "メディアの説明またはプレビューに保存されていない変更があります。それでも破棄しますか?",
"confirmations.domain_block.confirm": "サーバーをブロック",
"confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。公開タイムラインにそのドメインのコンテンツが表示されなくなり、通知も届かなくなります。そのドメインのフォロワーはアンフォローされます。",
"confirmations.edit.confirm": "編集",
"confirmations.edit.message": "今編集すると現在作成中のメッセージが上書きされます。本当に実行しますか?",
+5
View File
@@ -221,6 +221,7 @@
"filter_modal.select_filter.prompt_new": "Taggayt tamaynutt : {name}",
"filter_modal.select_filter.search": "Nadi neɣ snulfu-d",
"filter_modal.select_filter.title": "Sizdeg tassufeɣt-a",
"filter_modal.title.status": "Sizdeg tassufeɣt",
"firehose.all": "Akk",
"firehose.local": "Deg uqeddac-ayi",
"firehose.remote": "Iqeddacen nniḍen",
@@ -335,6 +336,7 @@
"mute_modal.show_options": "Sken-d tinefrunin",
"mute_modal.title": "Sgugem aseqdac?",
"navigation_bar.about": "Ɣef",
"navigation_bar.advanced_interface": "Ldi deg ugrudem n web leqqayen",
"navigation_bar.blocks": "Iseqdacen yettusḥebsen",
"navigation_bar.bookmarks": "Ticraḍ",
"navigation_bar.community_timeline": "Tasuddemt tadigant",
@@ -364,6 +366,7 @@
"notification.own_poll": "Tafrant-ik·im tfuk",
"notification.poll": "Tfukk tefrant ideg tettekkaḍ",
"notification.reblog": "{name} yebḍa tajewwiqt-ik i tikelt-nniḍen",
"notification.relationships_severance_event.learn_more": "Issin ugar",
"notification.status": "{name} akken i d-yessufeɣ",
"notification_requests.accept": "Qbel",
"notification_requests.dismiss": "Agi",
@@ -372,6 +375,8 @@
"notifications.clear_confirmation": "Tebɣiḍ s tidet ad tekkseḍ akk tilɣa-inek·em i lebda?",
"notifications.column_settings.alert": "Tilɣa n tnarit",
"notifications.column_settings.favourite": "Imenyafen:",
"notifications.column_settings.filter_bar.advanced": "Sken-d akk taggayin",
"notifications.column_settings.filter_bar.category": "Iri n usizdeg uzrib",
"notifications.column_settings.follow": "Imeḍfaṛen imaynuten:",
"notifications.column_settings.follow_request": "Isuturen imaynuten n teḍfeṛt:",
"notifications.column_settings.mention": "Abdar:",
+7
View File
@@ -298,6 +298,7 @@
"filter_modal.select_filter.title": "이 게시물을 필터",
"filter_modal.title.status": "게시물 필터",
"filtered_notifications_banner.pending_requests": "알 수도 있는 {count, plural, =0 {0 명} one {한 명} other {# 명}}의 사람들로부터의 알림",
"filtered_notifications_banner.private_mentions": "{count, plural, other {개인적인 멘션}}",
"filtered_notifications_banner.title": "걸러진 알림",
"firehose.all": "모두",
"firehose.local": "이 서버",
@@ -471,6 +472,11 @@
"notification.own_poll": "설문을 마침",
"notification.poll": "참여한 설문이 종료됨",
"notification.reblog": "{name} 님이 부스트했습니다",
"notification.relationships_severance_event": "{name} 님과의 연결이 끊어졌습니다",
"notification.relationships_severance_event.account_suspension": "{from}의 관리자가 {target}를 정지시켰기 때문에 그들과 더이상 상호작용 할 수 없고 정보를 받아볼 수 없습니다.",
"notification.relationships_severance_event.domain_block": "{from}의 관리자가 {target}를 차단하였고 여기에는 나의 {followersCount} 명의 팔로워와 {followingCount, plural, other {#}} 명의 팔로우가 포함되었습니다.",
"notification.relationships_severance_event.learn_more": "더 알아보기",
"notification.relationships_severance_event.user_domain_block": "내가 {target}를 차단하여 {followersCount} 명의 팔로워와 {followingCount, plural, other {#}} 명의 팔로우가 제거되었습니다.",
"notification.status": "{name} 님이 방금 게시물을 올렸습니다",
"notification.update": "{name} 님이 게시물을 수정했습니다",
"notification_requests.accept": "수락",
@@ -484,6 +490,7 @@
"notifications.column_settings.alert": "데스크탑 알림",
"notifications.column_settings.favourite": "좋아요:",
"notifications.column_settings.filter_bar.advanced": "모든 범주 표시",
"notifications.column_settings.filter_bar.category": "빠른 필터 막대",
"notifications.column_settings.follow": "새 팔로워:",
"notifications.column_settings.follow_request": "새 팔로우 요청:",
"notifications.column_settings.mention": "멘션:",
@@ -89,8 +89,12 @@
"announcement.announcement": "Comunicados",
"attachments_list.unprocessed": "(não processado)",
"audio.hide": "Ocultar áudio",
"block_modal.remote_users_caveat": "Pediremos ao servidor {domínio} que respeite sua decisão. No entanto, a conformidade não é garantida pois alguns servidores podem lidar com os blocos de maneira diferente. As postagens públicas ainda podem estar visíveis para usuários não logados.",
"block_modal.show_less": "Mostrar menos",
"block_modal.show_more": "Mostrar mais",
"block_modal.they_cant_mention": "Eles não podem mencionar ou seguir você.",
"block_modal.they_cant_see_posts": "Eles não podem ver suas postagens e você não verá as deles.",
"block_modal.they_will_know": "Eles podem ver que estão bloqueados.",
"block_modal.title": "Bloquear usuário?",
"block_modal.you_wont_see_mentions": "Você não verá publicações que os mencionem.",
"boost_modal.combo": "Pressione {combo} para pular isso na próxima vez",
@@ -173,6 +177,7 @@
"confirmations.delete_list.message": "Você tem certeza de que deseja excluir esta lista?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Há mudanças não salvas na descrição ou pré-visualização da mídia. Descartar assim mesmo?",
"confirmations.domain_block.confirm": "Servidor de blocos",
"confirmations.domain_block.message": "Você tem certeza de que deseja bloquear tudo de {domain}? Você não verá mais o conteúdo desta instância em nenhuma linha do tempo pública ou nas suas notificações. Seus seguidores desta instância serão removidos.",
"confirmations.edit.confirm": "Editar",
"confirmations.edit.message": "Editar agora irá substituir a mensagem que está sendo criando. Tem certeza de que deseja continuar?",
@@ -204,8 +209,27 @@
"dismissable_banner.explore_statuses": "Estas são postagens de toda a rede social que estão ganhando tração hoje. Postagens mais recentes com mais impulsos e favoritos têm classificações mais altas.",
"dismissable_banner.explore_tags": "Estas hashtags estão ganhando popularidade no momento entre as pessoas deste e de outros servidores da rede descentralizada.",
"dismissable_banner.public_timeline": "Estas são as publicações públicas mais recentes de pessoas na rede social que pessoas em {domain} seguem.",
"domain_block_modal.block": "Servidor de blocos.",
"domain_block_modal.block_account_instead": "Bloco @(nome)",
"domain_block_modal.they_can_interact_with_old_posts": "Pessoas deste servidor podem interagir com suas publicações antigas.",
"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_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.",
"domain_pill.server": "Servidor",
"domain_pill.their_handle": "Seu identificador:",
"domain_pill.their_server": "Sua casa digital, onde ficam todas as suas postagens.",
"domain_pill.their_username": "Seu identificador exclusivo em seu servidor. É possível encontrar usuários com o mesmo nome de usuário em servidores diferentes.",
"domain_pill.username": "Nome de usuário",
"domain_pill.whats_in_a_handle": "O que há em uma alça?",
"domain_pill.who_they_are": "Como os identificadores indicam quem alguém é e onde está, você pode interagir com pessoas na web social de <button>plataformas alimentadas pelo ActivityPub</button>.",
"domain_pill.who_you_are": "Como seu identificador indica quem você é e onde está, as pessoas podem interagir com você nas redes sociais das <button>plataformas alimentadas pelo ActivityPub</button>.",
"domain_pill.your_handle": "Seu identificador:",
"domain_pill.your_server": "Sua casa digital, onde ficam todas as suas postagens. Não gosta deste? Transfira servidores a qualquer momento e traga seus seguidores também.",
"domain_pill.your_username": "Seu identificador exclusivo neste servidor. É possível encontrar usuários com o mesmo nome de usuário em servidores diferentes.",
"embed.instructions": "Incorpore este toot no seu site ao copiar o código abaixo.",
"embed.preview": "Aqui está como vai ficar:",
"emoji_button.activity": "Atividade",
@@ -273,6 +297,8 @@
"filter_modal.select_filter.subtitle": "Use uma categoria existente ou crie uma nova",
"filter_modal.select_filter.title": "Filtrar esta publicação",
"filter_modal.title.status": "Filtrar uma publicação",
"filtered_notifications_banner.pending_requests": "Notificações de {count, plural, =0 {no one} one {one person} other {# people}} que você talvez conheça",
"filtered_notifications_banner.private_mentions": "{count, plural, one {private mention} other {private mentions}}",
"filtered_notifications_banner.title": "Notificações filtradas",
"firehose.all": "Tudo",
"firehose.local": "Este servidor",
@@ -402,7 +428,9 @@
"loading_indicator.label": "Carregando…",
"media_gallery.toggle_visible": "{number, plural, one {Ocultar mídia} other {Ocultar mídias}}",
"moved_to_account_banner.text": "Sua conta {disabledAccount} está desativada porque você a moveu para {movedToAccount}.",
"mute_modal.hide_from_notifications": "Ocultar das notificações",
"mute_modal.hide_options": "Ocultar opções",
"mute_modal.indefinite": "Até que eu os ative",
"mute_modal.show_options": "Mostrar opções",
"mute_modal.they_can_mention_and_follow": "Eles podem mencionar e seguir você, mas você não os verá.",
"mute_modal.they_wont_know": "Eles não saberão que foram silenciados.",
@@ -444,6 +472,11 @@
"notification.own_poll": "Sua enquete terminou",
"notification.poll": "Uma enquete que você votou terminou",
"notification.reblog": "{name} deu boost no teu toot",
"notification.relationships_severance_event": "Conexões perdidas com {name}",
"notification.relationships_severance_event.account_suspension": "Um administrador de {from} suspendeu {target}, o que significa que você não pode mais receber atualizações deles ou interagir com eles.",
"notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.relationships_severance_event.learn_more": "Saber mais",
"notification.relationships_severance_event.user_domain_block": "You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.status": "{name} acabou de tootar",
"notification.update": "{name} editou uma publicação",
"notification_requests.accept": "Aceitar",
@@ -456,6 +489,8 @@
"notifications.column_settings.admin.sign_up": "Novas inscrições:",
"notifications.column_settings.alert": "Notificações no computador",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Exibir todas as categorias",
"notifications.column_settings.filter_bar.category": "Barra de filtro rápido",
"notifications.column_settings.follow": "Seguidores:",
"notifications.column_settings.follow_request": "Seguidores pendentes:",
"notifications.column_settings.mention": "Menções:",
@@ -481,7 +516,9 @@
"notifications.permission_denied": "Navegador não tem permissão para ativar notificações no computador.",
"notifications.permission_denied_alert": "Verifique a permissão do navegador para ativar notificações no computador.",
"notifications.permission_required": "Ativar notificações no computador exige permissão do navegador.",
"notifications.policy.filter_new_accounts.hint": "Created within the past {days, plural, one {one day} other {# days}}",
"notifications.policy.filter_new_accounts_title": "Novas contas",
"notifications.policy.filter_not_followers_hint": "Including people who have been following you fewer than {days, plural, one {one day} other {# days}}",
"notifications.policy.filter_not_followers_title": "Pessoas que não estão te seguindo",
"notifications.policy.filter_not_following_hint": "Até que você os aprove manualmente",
"notifications.policy.filter_not_following_title": "Pessoas que você não segue",
@@ -569,6 +606,7 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "hoje",
"reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}",
"reply_indicator.cancel": "Cancelar",
"reply_indicator.poll": "Enquete",
"report.block": "Bloquear",
@@ -667,6 +705,7 @@
"status.edited_x_times": "Editado {count, plural, one {{count} hora} other {{count} vezes}}",
"status.embed": "Incorporar",
"status.favourite": "Favorita",
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
"status.filter": "Filtrar esta publicação",
"status.filtered": "Filtrado",
"status.hide": "Ocultar publicação",
@@ -687,6 +726,7 @@
"status.reblog": "Dar boost",
"status.reblog_private": "Dar boost para o mesmo público",
"status.reblogged_by": "{name} deu boost",
"status.reblogs": "{count, plural, one {boost} other {boosts}}",
"status.reblogs.empty": "Nada aqui. Quando alguém der boost, o usuário aparecerá aqui.",
"status.redraft": "Excluir e rascunhar",
"status.remove_bookmark": "Remover do Salvos",
@@ -298,6 +298,7 @@
"filter_modal.select_filter.title": "Filtrar esta publicação",
"filter_modal.title.status": "Filtrar uma publicação",
"filtered_notifications_banner.pending_requests": "Notificações de {count, plural, =0 {ninguém} one {uma pessoa} other {# pessoas}} que talvez conheça",
"filtered_notifications_banner.private_mentions": "{count, plural,one {menção privada} other {menções privadas}}",
"filtered_notifications_banner.title": "Notificações filtradas",
"firehose.all": "Todas",
"firehose.local": "Este servidor",
@@ -471,6 +472,11 @@
"notification.own_poll": "A sua votação terminou",
"notification.poll": "Uma votação em que participaste chegou ao fim",
"notification.reblog": "{name} reforçou a tua publicação",
"notification.relationships_severance_event": "Perdeu as ligações com {name}",
"notification.relationships_severance_event.account_suspension": "Um administrador de {from} suspendeu {target}, o que significa que já não pode receber atualizações dele ou interagir com ele.",
"notification.relationships_severance_event.domain_block": "Um administrador de {from} bloqueou {target}, incluindo {followersCount} dos seus seguidores e {followingCount, plural, one {# conta} other {# contas}} que segue.",
"notification.relationships_severance_event.learn_more": "Saber mais",
"notification.relationships_severance_event.user_domain_block": "Bloqueou {target}, removendo {followersCount} dos seus seguidores e {followingCount, plural, one {# conta} other {# contas}} que segue.",
"notification.status": "{name} acabou de publicar",
"notification.update": "{name} editou uma publicação",
"notification_requests.accept": "Aceitar",
+6
View File
@@ -298,6 +298,7 @@
"filter_modal.select_filter.title": "Filtriraj to objavo",
"filter_modal.title.status": "Filtrirajte objavo",
"filtered_notifications_banner.pending_requests": "Obvestila od {count, plural, =0 {nikogar, ki bi ga} one {# človeka, ki bi ga} two {# ljudi, ki bi ju} few {# ljudi, ki bi jih} other {# ljudi, ki bi jih}} lahko poznali",
"filtered_notifications_banner.private_mentions": "{count, plural, one {zasebna omemba} two {zasebni omembi} few {zasebne omembe} other {zasebnih omemb}}",
"filtered_notifications_banner.title": "Filtrirana obvestila",
"firehose.all": "Vse",
"firehose.local": "Ta strežnik",
@@ -471,6 +472,11 @@
"notification.own_poll": "Vaša anketa je zaključena",
"notification.poll": "Anketa, v kateri ste sodelovali, je zaključena",
"notification.reblog": "{name} je izpostavila/a vašo objavo",
"notification.relationships_severance_event": "Povezave z {name} prekinjene",
"notification.relationships_severance_event.account_suspension": "Skrbnik na {from} je suspendiral račun {target}, kar pomeni, da od računa ne morete več prejemati posodobitev ali imeti z njim interakcij.",
"notification.relationships_severance_event.domain_block": "Skrbnik na {from} je blokiral domeno {target}, vključno z vašimi sledilci ({followersCount}) in {followingCount, plural, one {# računom, ki mu sledite} two {# računoma, ki jima sledite} few {# računi, ki jim sledite} other {# računi, ki jim sledite}}.",
"notification.relationships_severance_event.learn_more": "Več o tem",
"notification.relationships_severance_event.user_domain_block": "Blokirali ste domeno {target}, vključno z vašimi sledilci ({followersCount}) in {followingCount, plural, one {# računom, ki mu sledite} two {# računoma, ki jima sledite} few {# računi, ki jim sledite} other {# računi, ki jim sledite}}.",
"notification.status": "{name} je pravkar objavil/a",
"notification.update": "{name} je uredil(a) objavo",
"notification_requests.accept": "Sprejmi",
+2
View File
@@ -462,6 +462,7 @@
"notification.own_poll": "Ваша анкета је завршена",
"notification.poll": "Завршена је анкета у којој сте гласали",
"notification.reblog": "{name} је подржао вашу објаву",
"notification.relationships_severance_event.learn_more": "Сазнајте више",
"notification.status": "{name} је управо објавио",
"notification.update": "{name} је уредио објаву",
"notification_requests.accept": "Прихвати",
@@ -474,6 +475,7 @@
"notifications.column_settings.admin.sign_up": "Нове рагистрације:",
"notifications.column_settings.alert": "Обавештења на радној површини",
"notifications.column_settings.favourite": "Омиљено:",
"notifications.column_settings.filter_bar.advanced": "Прикажи све категорије",
"notifications.column_settings.follow": "Нови пратиоци:",
"notifications.column_settings.follow_request": "Нови захтеви за праћење:",
"notifications.column_settings.mention": "Помињања:",
+4 -4
View File
@@ -21,14 +21,14 @@ import history from './history';
import listAdder from './list_adder';
import listEditor from './list_editor';
import lists from './lists';
import markers from './markers';
import { markersReducer } from './markers';
import media_attachments from './media_attachments';
import meta from './meta';
import { modalReducer } from './modal';
import { notificationPolicyReducer } from './notification_policy';
import { notificationRequestsReducer } from './notification_requests';
import notifications from './notifications';
import picture_in_picture from './picture_in_picture';
import { pictureInPictureReducer } from './picture_in_picture';
import polls from './polls';
import push_notifications from './push_notifications';
import { relationshipsReducer } from './relationships';
@@ -77,8 +77,8 @@ const reducers = {
suggestions,
polls,
trends,
markers,
picture_in_picture,
markers: markersReducer,
picture_in_picture: pictureInPictureReducer,
history,
tags,
followed_tags,
@@ -1,26 +0,0 @@
import { Map as ImmutableMap } from 'immutable';
import {
MARKERS_SUBMIT_SUCCESS,
} from '../actions/markers';
const initialState = ImmutableMap({
home: '0',
notifications: '0',
});
export default function markers(state = initialState, action) {
switch(action.type) {
case MARKERS_SUBMIT_SUCCESS:
if (action.home) {
state = state.set('home', action.home);
}
if (action.notifications) {
state = state.set('notifications', action.notifications);
}
return state;
default:
return state;
}
}
@@ -0,0 +1,18 @@
import { createReducer } from '@reduxjs/toolkit';
import { submitMarkersAction } from 'mastodon/actions/markers';
const initialState = {
home: '0',
notifications: '0',
};
export const markersReducer = createReducer(initialState, (builder) => {
builder.addCase(
submitMarkersAction.fulfilled,
(state, { payload: { home, notifications } }) => {
if (home) state.home = home;
if (notifications) state.notifications = notifications;
},
);
});
@@ -13,7 +13,7 @@ import {
unfocusApp,
} from '../actions/app';
import {
MARKERS_FETCH_SUCCESS,
fetchMarkers,
} from '../actions/markers';
import {
notificationsUpdate,
@@ -255,8 +255,8 @@ const recountUnread = (state, last_read_id) => {
export default function notifications(state = initialState, action) {
switch(action.type) {
case MARKERS_FETCH_SUCCESS:
return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state;
case fetchMarkers.fulfilled.type:
return action.payload.markers.notifications ? recountUnread(state, action.payload.markers.notifications.last_read_id) : state;
case NOTIFICATIONS_MOUNT:
return updateMounted(state);
case NOTIFICATIONS_UNMOUNT:
@@ -1,26 +0,0 @@
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture';
import { TIMELINE_DELETE } from '../actions/timelines';
const initialState = {
statusId: null,
accountId: null,
type: null,
src: null,
muted: false,
volume: 0,
currentTime: 0,
};
export default function pictureInPicture(state = initialState, action) {
switch(action.type) {
case PICTURE_IN_PICTURE_DEPLOY:
return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props };
case PICTURE_IN_PICTURE_REMOVE:
return { ...initialState };
case TIMELINE_DELETE:
return (state.statusId === action.id) ? { ...initialState } : state;
default:
return state;
}
}
@@ -0,0 +1,56 @@
import type { Reducer } from '@reduxjs/toolkit';
import {
deployPictureInPictureAction,
removePictureInPicture,
} from 'mastodon/actions/picture_in_picture';
import { TIMELINE_DELETE } from '../actions/timelines';
export interface PIPMediaProps {
src: string;
muted: boolean;
volume: number;
currentTime: number;
poster: string;
backgroundColor: string;
foregroundColor: string;
accentColor: string;
}
interface PIPStateWithValue extends Partial<PIPMediaProps> {
statusId: string;
accountId: string;
type: 'audio' | 'video';
}
interface PIPStateEmpty extends Partial<PIPMediaProps> {
type: null;
}
type PIPState = PIPStateWithValue | PIPStateEmpty;
const initialState = {
type: null,
muted: false,
volume: 0,
currentTime: 0,
};
export const pictureInPictureReducer: Reducer<PIPState> = (
state = initialState,
action,
) => {
if (deployPictureInPictureAction.match(action))
return {
statusId: action.payload.statusId,
accountId: action.payload.accountId,
type: action.payload.playerType,
...action.payload.props,
};
else if (removePictureInPicture.match(action)) return initialState;
else if (action.type === TIMELINE_DELETE)
if (state.type && state.statusId === action.id) return initialState;
return state;
};
+1 -1
View File
@@ -60,7 +60,7 @@ export const makeGetStatus = () => {
export const makeGetPictureInPicture = () => {
return createSelector([
(state, { id }) => state.get('picture_in_picture').statusId === id,
(state, { id }) => state.picture_in_picture.statusId === id,
(state) => state.getIn(['meta', 'layout']) !== 'mobile',
], (inUse, available) => ImmutableMap({
inUse: inUse && available,
@@ -2612,6 +2612,7 @@ a.account__display-name {
}
$ui-header-height: 55px;
$ui-header-logo-wordmark-width: 99px;
.ui__header {
display: none;
@@ -2627,6 +2628,10 @@ $ui-header-height: 55px;
&__logo {
display: inline-flex;
padding: 15px;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
container: header-logo / inline-size;
.logo {
height: $ui-header-height - 30px;
@@ -2637,7 +2642,7 @@ $ui-header-height: 55px;
display: none;
}
@media screen and (width >= 320px) {
@container header-logo (min-width: #{$ui-header-logo-wordmark-width}) {
.logo--wordmark {
display: block;
}
@@ -2654,6 +2659,7 @@ $ui-header-height: 55px;
gap: 10px;
padding: 0 10px;
overflow: hidden;
flex-shrink: 0;
.button {
flex: 0 0 auto;
@@ -5084,6 +5090,7 @@ a.status-card {
.language-dropdown__dropdown {
box-shadow: var(--dropdown-shadow);
background: var(--dropdown-background-color);
backdrop-filter: var(--background-filter);
border: 1px solid var(--dropdown-border-color);
padding: 4px;
border-radius: 4px;