Merge tag 'v3.3.0' into instance_only_statuses

This commit is contained in:
Renato "Lond" Cerqueira
2020-12-27 11:00:43 +01:00
877 changed files with 35407 additions and 11128 deletions

View File

@ -1,6 +1,5 @@
import api, { getLinks } from '../api';
import openDB from '../storage/db';
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
import { importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
@ -74,45 +73,13 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
function getFromDB(dispatch, getState, index, id) {
return new Promise((resolve, reject) => {
const request = index.get(id);
request.onerror = reject;
request.onsuccess = () => {
if (!request.result) {
reject();
return;
}
dispatch(importAccount(request.result));
resolve(request.result.moved && getFromDB(dispatch, getState, index, request.result.moved));
};
});
}
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
if (getState().getIn(['accounts', id], null) !== null) {
return;
}
dispatch(fetchAccountRequest(id));
openDB().then(db => getFromDB(
dispatch,
getState,
db.transaction('accounts', 'read').objectStore('accounts').index('id'),
id,
).then(() => db.close(), error => {
db.close();
throw error;
})).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(importFetchedAccount(response.data));
})).then(() => {
dispatch(fetchAccountSuccess());
}).catch(error => {
dispatch(fetchAccountFail(id, error));
@ -142,14 +109,14 @@ export function fetchAccountFail(id, error) {
};
};
export function followAccount(id, reblogs = true) {
export function followAccount(id, options = { reblogs: true }) {
return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
const locked = getState().getIn(['accounts', id, 'locked'], false);
dispatch(followAccountRequest(id, locked));
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => {
dispatch(followAccountFail(error, locked));
@ -290,11 +257,11 @@ export function unblockAccountFail(error) {
};
export function muteAccount(id, notifications) {
export function muteAccount(id, notifications, duration=0) {
return (dispatch, getState) => {
dispatch(muteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => {
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {

View File

@ -8,3 +8,10 @@ export const focusApp = () => ({
export const unfocusApp = () => ({
type: APP_UNFOCUS,
});
export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE';
export const changeLayout = layout => ({
type: APP_LAYOUT_CHANGE,
layout,
});

View File

@ -154,9 +154,7 @@ export function submitCompose(routerHistory) {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
}).then(function (response) {
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
routerHistory.push('/timelines/direct');
} else if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) {
routerHistory.goBack();
}

View File

@ -150,10 +150,10 @@ export const createListFail = error => ({
error,
});
export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
dispatch(updateListRequest(id));
api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
dispatch(updateListSuccess(data));
if (shouldReset) {

View File

@ -1,8 +1,10 @@
import api from '../api';
import { debounce } from 'lodash';
import compareId from '../compare_id';
import { showAlertForError } from './alerts';
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) => {
@ -26,15 +28,19 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
},
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;
}
@ -57,8 +63,8 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const _buildParams = (state) => {
const params = {};
const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]);
const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']);
const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
params.home = {
@ -82,11 +88,9 @@ const debouncedSubmitMarkers = debounce((dispatch, getState) => {
return;
}
api().post('/api/v1/markers', params).then(() => {
api(getState).post('/api/v1/markers', params).then(() => {
dispatch(submitMarkersSuccess(params));
}).catch(error => {
dispatch(showAlertForError(error));
});
}).catch(() => {});
}, 300000, { leading: true, trailing: true });
export function submitMarkersSuccess({ home, notifications }) {
@ -97,6 +101,48 @@ export function submitMarkersSuccess({ home, notifications }) {
};
};
export function submitMarkers() {
return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
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,
};
};

View File

@ -13,6 +13,7 @@ export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
export function fetchMutes() {
return (dispatch, getState) => {
@ -104,3 +105,12 @@ export function toggleHideNotifications() {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}
export function changeMuteDuration(duration) {
return dispatch => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
};
}

View File

@ -16,6 +16,7 @@ import { getFiltersRegex } from '../selectors';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import compareId from 'mastodon/compare_id';
import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer';
import { requestNotificationPermission } from '../utils/notifications';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
@ -33,6 +34,12 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
@ -59,7 +66,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
let filtered = false;
if (notification.type === 'mention') {
if (['mention', 'status'].includes(notification.type)) {
const dropRegex = filters[0];
const regex = filters[1];
const searchIndex = searchTextFromRawStatus(notification.status);
@ -232,3 +239,47 @@ export const mountNotifications = () => ({
export const unmountNotifications = () => ({
type: NOTIFICATIONS_UNMOUNT,
});
export const markNotificationsAsRead = () => ({
type: NOTIFICATIONS_MARK_AS_READ,
});
// Browser support
export function setupBrowserNotifications() {
return dispatch => {
dispatch(setBrowserSupport('Notification' in window));
if ('Notification' in window) {
dispatch(setBrowserPermission(Notification.permission));
}
if ('Notification' in window && 'permissions' in navigator) {
navigator.permissions.query({ name: 'notifications' }).then((status) => {
status.onchange = () => dispatch(setBrowserPermission(Notification.permission));
}).catch(console.warn);
}
};
}
export function requestBrowserPermission(callback = noOp) {
return dispatch => {
requestNotificationPermission((permission) => {
dispatch(setBrowserPermission(permission));
callback(permission);
});
};
};
export function setBrowserSupport (value) {
return {
type: NOTIFICATIONS_SET_BROWSER_SUPPORT,
value,
};
}
export function setBrowserPermission (value) {
return {
type: NOTIFICATIONS_SET_BROWSER_PERMISSION,
value,
};
}

View File

@ -1,8 +1,21 @@
import { changeSetting, saveSettings } from './settings';
import { requestBrowserPermission } from './notifications';
export const INTRODUCTION_VERSION = 20181216044202;
export const closeOnboarding = () => dispatch => {
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
dispatch(saveSettings());
dispatch(requestBrowserPermission((permission) => {
if (permission === 'granted') {
dispatch(changeSetting(['notifications', 'alerts', 'follow'], true));
dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true));
dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true));
dispatch(changeSetting(['notifications', 'alerts', 'mention'], true));
dispatch(changeSetting(['notifications', 'alerts', 'poll'], true));
dispatch(changeSetting(['notifications', 'alerts', 'status'], true));
dispatch(saveSettings());
}
}));
};

View File

@ -0,0 +1,38 @@
// @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
* @return {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
/*
* @return {object}
*/
export const removePictureInPicture = () => ({
type: PICTURE_IN_PICTURE_REMOVE,
});

View File

@ -1,9 +1,7 @@
import api from '../api';
import openDB from '../storage/db';
import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer';
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { ensureComposeIsVisible } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
@ -40,48 +38,6 @@ export function fetchStatusRequest(id, skipLoading) {
};
};
function getFromDB(dispatch, getState, accountIndex, index, id) {
return new Promise((resolve, reject) => {
const request = index.get(id);
request.onerror = reject;
request.onsuccess = () => {
const promises = [];
if (!request.result) {
reject();
return;
}
dispatch(importStatus(request.result));
if (getState().getIn(['accounts', request.result.account], null) === null) {
promises.push(new Promise((accountResolve, accountReject) => {
const accountRequest = accountIndex.get(request.result.account);
accountRequest.onerror = accountReject;
accountRequest.onsuccess = () => {
if (!request.result) {
accountReject();
return;
}
dispatch(importAccount(accountRequest.result));
accountResolve();
};
}));
}
if (request.result.reblog && getState().getIn(['statuses', request.result.reblog], null) === null) {
promises.push(getFromDB(dispatch, getState, accountIndex, index, request.result.reblog));
}
resolve(Promise.all(promises));
};
});
}
export function fetchStatus(id) {
return (dispatch, getState) => {
const skipLoading = getState().getIn(['statuses', id], null) !== null;
@ -94,23 +50,10 @@ export function fetchStatus(id) {
dispatch(fetchStatusRequest(id, skipLoading));
openDB().then(db => {
const transaction = db.transaction(['accounts', 'statuses'], 'read');
const accountIndex = transaction.objectStore('accounts').index('id');
const index = transaction.objectStore('statuses').index('id');
return getFromDB(dispatch, getState, accountIndex, index, id).then(() => {
db.close();
}, error => {
db.close();
throw error;
});
}).then(() => {
dispatch(fetchStatusSuccess(skipLoading));
}, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(skipLoading));
})).catch(error => {
}).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading));
});
};
@ -152,7 +95,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
dispatch(deleteStatusRequest(id));
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
evictStatus(id);
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account));

View File

@ -1,3 +1,5 @@
// @ts-check
import { connectStream } from '../stream';
import {
updateTimeline,
@ -19,24 +21,59 @@ import { getLocale } from '../locales';
const { messages } = getLocale();
export function connectTimelineStream (timelineId, path, pollingRefresh = null, accept = null) {
/**
* @param {number} max
* @return {number}
*/
const randomUpTo = max =>
Math.floor(Math.random() * Math.floor(max));
return connectStream (path, pollingRefresh, (dispatch, getState) => {
/**
* @param {string} timelineId
* @param {string} channelName
* @param {Object.<string, string>} params
* @param {Object} options
* @param {function(Function, Function): void} [options.fallback]
* @param {function(object): boolean} [options.accept]
* @return {function(): void}
*/
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) =>
connectStream(channelName, params, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);
let pollingId;
/**
* @param {function(Function, Function): void} fallback
*/
const useFallback = fallback => {
fallback(dispatch, () => {
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
});
};
return {
onConnect() {
dispatch(connectTimeline(timelineId));
if (pollingId) {
clearTimeout(pollingId);
pollingId = null;
}
},
onDisconnect() {
dispatch(disconnectTimeline(timelineId));
if (options.fallback) {
pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
}
},
onReceive (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
@ -63,17 +100,59 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
},
};
});
}
/**
* @param {Function} dispatch
* @param {function(): void} done
*/
const refreshHomeTimelineAndNotification = (dispatch, done) => {
dispatch(expandHomeTimeline({}, () =>
dispatch(expandNotifications({}, () =>
dispatch(fetchAnnouncements(done))))));
};
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
export const connectHashtagStream = (id, tag, local, accept) => connectTimelineStream(`hashtag:${id}${local ? ':local' : ''}`, `hashtag${local ? ':local' : ''}&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
/**
* @return {function(): void}
*/
export const connectUserStream = () =>
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification });
/**
* @param {Object} options
* @param {boolean} [options.onlyMedia]
* @return {function(): void}
*/
export const connectCommunityStream = ({ onlyMedia } = {}) =>
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
/**
* @param {Object} options
* @param {boolean} [options.onlyMedia]
* @param {boolean} [options.onlyRemote]
* @return {function(): void}
*/
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
/**
* @param {string} columnId
* @param {string} tagName
* @param {boolean} onlyLocal
* @param {function(object): boolean} accept
* @return {function(): void}
*/
export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) =>
connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept });
/**
* @return {function(): void}
*/
export const connectDirectStream = () =>
connectTimelineStream('direct', 'direct');
/**
* @param {string} listId
* @return {function(): void}
*/
export const connectListStream = listId =>
connectTimelineStream(`list:${listId}`, 'list', { list: listId });

View File

@ -0,0 +1,112 @@
const DIGIT_CHARACTERS = [
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'#',
'$',
'%',
'*',
'+',
',',
'-',
'.',
':',
';',
'=',
'?',
'@',
'[',
']',
'^',
'_',
'{',
'|',
'}',
'~',
];
export const decode83 = (str) => {
let value = 0;
let c, digit;
for (let i = 0; i < str.length; i++) {
c = str[i];
digit = DIGIT_CHARACTERS.indexOf(c);
value = value * 83 + digit;
}
return value;
};
export const intToRGB = int => ({
r: Math.max(0, (int >> 16)),
g: Math.max(0, (int >> 8) & 255),
b: Math.max(0, (int & 255)),
});
export const getAverageFromBlurhash = blurhash => {
if (!blurhash) {
return null;
}
return intToRGB(decode83(blurhash.slice(2, 6)));
};

View File

@ -4,13 +4,6 @@ exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] =
<button
className="button button-secondary"
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
@ -18,13 +11,6 @@ exports[`<Button /> renders a button element 1`] = `
<button
className="button"
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
@ -33,13 +19,6 @@ exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
className="button"
disabled={true}
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
@ -47,13 +26,6 @@ exports[`<Button /> renders class="button--block" if props.block given 1`] = `
<button
className="button button--block"
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
/>
`;
@ -61,13 +33,6 @@ exports[`<Button /> renders the children 1`] = `
<button
className="button"
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
<p>
children
@ -79,13 +44,6 @@ exports[`<Button /> renders the given text 1`] = `
<button
className="button"
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
foo
</button>
@ -95,13 +53,6 @@ exports[`<Button /> renders the props.text instead of children 1`] = `
<button
className="button"
onClick={[Function]}
style={
Object {
"height": "36px",
"lineHeight": "36px",
"padding": "0 16px",
}
}
>
foo
</button>

View File

@ -8,6 +8,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state';
import RelativeTimestamp from './relative_timestamp';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -107,11 +108,17 @@ class Account extends ImmutablePureComponent {
}
}
let mute_expires_at;
if (account.get('mute_expires_at')) {
mute_expires_at = <div><RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></div>;
}
return (
<div className='account'>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
{mute_expires_at}
<DisplayName account={account} />
</Permalink>

View File

@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { reduceMotion } from 'mastodon/initial_state';
const obfuscatedCount = count => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return '1+';
}
};
export default class AnimatedNumber extends React.PureComponent {
static propTypes = {
value: PropTypes.number.isRequired,
obfuscate: PropTypes.bool,
};
state = {
@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent {
}
render () {
const { value } = this.props;
const { value, obfuscate } = this.props;
const { direction } = this.state;
if (reduceMotion) {
return <FormattedNumber value={value} />;
return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />;
}
const styles = [{
@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent {
{items => (
<span className='animated-number'>
{items.map(({ key, data, style }) => (
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}><FormattedNumber value={data} /></span>
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
))}
</span>
)}

View File

@ -1,8 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
const assetHost = process.env.CDN_HOST || '';
import { assetHost } from 'mastodon/utils/config';
export default class AutosuggestEmoji extends React.PureComponent {

View File

@ -4,7 +4,6 @@ import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
@ -189,11 +188,6 @@ export default class AutosuggestInput extends ImmutablePureComponent {
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
if (isRtl(value)) {
style.direction = 'rtl';
}
return (
<div className='autosuggest-input'>
@ -212,7 +206,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={style}
dir='auto'
aria-autocomplete='list'
id={id}
className={className}

View File

@ -4,7 +4,6 @@ import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import classNames from 'classnames';
@ -195,11 +194,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
if (isRtl(value)) {
style.direction = 'rtl';
}
return [
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
@ -220,7 +214,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style}
dir='auto'
aria-autocomplete='list'
/>
</label>

View File

@ -10,17 +10,11 @@ export default class Button extends React.PureComponent {
disabled: PropTypes.bool,
block: PropTypes.bool,
secondary: PropTypes.bool,
size: PropTypes.number,
className: PropTypes.string,
title: PropTypes.string,
style: PropTypes.object,
children: PropTypes.node,
};
static defaultProps = {
size: 36,
};
handleClick = (e) => {
if (!this.props.disabled) {
this.props.onClick(e);
@ -36,13 +30,6 @@ export default class Button extends React.PureComponent {
}
render () {
const style = {
padding: `0 ${this.props.size / 2.25}px`,
height: `${this.props.size}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
};
const className = classNames('button', this.props.className, {
'button-secondary': this.props.secondary,
'button--block': this.props.block,
@ -54,7 +41,6 @@ export default class Button extends React.PureComponent {
disabled={this.props.disabled}
onClick={this.handleClick}
ref={this.setRef}
style={style}
title={this.props.title}
>
{this.props.text || this.props.children}

View File

@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import detectPassiveEvents from 'detect-passive-events';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollTop } from '../scroll';
export default class Column extends React.PureComponent {
@ -35,9 +35,9 @@ export default class Column extends React.PureComponent {
componentDidMount () {
if (this.props.bindToDocument) {
document.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
} else {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
}

View File

@ -34,6 +34,7 @@ class ColumnHeader extends React.PureComponent {
onMove: PropTypes.func,
onClick: PropTypes.func,
appendContent: PropTypes.node,
collapseIssues: PropTypes.bool,
};
state = {
@ -83,7 +84,7 @@ class ColumnHeader extends React.PureComponent {
}
render () {
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent } = this.props;
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
@ -145,7 +146,20 @@ class ColumnHeader extends React.PureComponent {
}
if (children || (multiColumn && this.props.onPin)) {
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
collapseButton = (
<button
className={collapsibleButtonClassName}
title={formatMessage(collapsed ? messages.show : messages.hide)}
aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
aria-pressed={collapsed ? 'false' : 'true'}
onClick={this.handleToggleClick}
>
<i className='icon-with-badge'>
<Icon id='sliders' />
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
</i>
</button>
);
}
const hasTitle = icon && title;

View File

@ -5,9 +5,9 @@ import IconButton from './icon_button';
import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import detectPassiveEvents from 'detect-passive-events';
import { supportsPassiveEvents } from 'detect-passive-events';
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
let id = 0;
class DropdownMenu extends React.PureComponent {
@ -205,7 +205,7 @@ export default class Dropdown extends React.PureComponent {
handleClose = () => {
if (this.activeElement) {
this.activeElement.focus();
this.activeElement.focus({ preventScroll: true });
this.activeElement = null;
}
this.props.onClose(this.state.id);

View File

@ -66,17 +66,31 @@ export default class ErrorBoundary extends React.PureComponent {
}
render() {
const { hasError, copied } = this.state;
const { hasError, copied, errorMessage } = this.state;
if (!hasError) {
return this.props.children;
}
const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError');
return (
<div className='error-boundary'>
<div>
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
<p className='error-boundary__error'>
{ likelyBrowserAddonIssue ? (
<FormattedMessage id='error.unexpected_crash.explanation_addons' defaultMessage='This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.' />
) : (
<FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' />
)}
</p>
<p>
{ likelyBrowserAddonIssue ? (
<FormattedMessage id='error.unexpected_crash.next_steps_addons' defaultMessage='Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
) : (
<FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
)}
</p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div>
</div>

View File

@ -54,8 +54,6 @@ export default class GIFV extends React.PureComponent {
<video
src={src}
width={width}
height={height}
role='button'
tabIndex='0'
aria-label={alt}

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import AnimatedNumber from 'mastodon/components/animated_number';
export default class IconButton extends React.PureComponent {
@ -24,6 +25,8 @@ export default class IconButton extends React.PureComponent {
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
counter: PropTypes.number,
obfuscateCount: PropTypes.bool,
};
static defaultProps = {
@ -97,6 +100,8 @@ export default class IconButton extends React.PureComponent {
pressed,
tabIndex,
title,
counter,
obfuscateCount,
} = this.props;
const {
@ -111,8 +116,13 @@ export default class IconButton extends React.PureComponent {
activate,
deactivate,
overlayed: overlay,
'icon-button--with-counter': typeof counter !== 'undefined',
});
if (typeof counter !== 'undefined') {
style.width = 'auto';
}
return (
<button
aria-label={title}
@ -128,7 +138,7 @@ export default class IconButton extends React.PureComponent {
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
</button>
);
}

View File

@ -4,16 +4,18 @@ import Icon from 'mastodon/components/icon';
const formatNumber = num => num > 40 ? '40+' : num;
const IconWithBadge = ({ id, count, className }) => (
const IconWithBadge = ({ id, count, issueBadge, className }) => (
<i className='icon-with-badge'>
<Icon id={id} fixedWidth className={className} />
{count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>}
{issueBadge && <i className='icon-with-badge__issue-badge' />}
</i>
);
IconWithBadge.propTypes = {
id: PropTypes.string.isRequired,
count: PropTypes.number.isRequired,
issueBadge: PropTypes.bool,
className: PropTypes.string,
};

View File

@ -2,10 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
import { is } from 'immutable';
// Diff these props in the "rendered" state
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
// Diff these props in the "unrendered" state
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
@ -33,9 +30,12 @@ export default class IntersectionObserverArticle extends React.Component {
// If we're going from rendered to unrendered (or vice versa) then update
return true;
}
// Otherwise, diff based on props
const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
// If we are and remain hidden, diff based on props
if (isUnrendered) {
return !updateOnPropsForUnrendered.every(prop => nextProps[prop] === this.props[prop]);
}
// Else, assume the children have changed
return true;
}
componentDidMount () {

View File

@ -1,19 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import 'wicg-inert';
import { multiply } from 'color-blend';
export default class ModalRoot extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
onClose: PropTypes.func.isRequired,
backgroundColor: PropTypes.shape({
r: PropTypes.number,
g: PropTypes.number,
b: PropTypes.number,
}),
};
state = {
revealed: !!this.props.children,
};
activeElement = this.state.revealed ? document.activeElement : null;
activeElement = this.props.children ? document.activeElement : null;
handleKeyUp = (e) => {
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
@ -53,8 +55,6 @@ export default class ModalRoot extends React.PureComponent {
this.activeElement = document.activeElement;
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
} else if (!nextProps.children) {
this.setState({ revealed: false });
}
}
@ -68,14 +68,7 @@ export default class ModalRoot extends React.PureComponent {
Promise.resolve().then(() => {
this.activeElement.focus({ preventScroll: true });
this.activeElement = null;
}).catch((error) => {
console.error(error);
});
}
if (this.props.children) {
requestAnimationFrame(() => {
this.setState({ revealed: true });
});
}).catch(console.error);
}
}
@ -94,7 +87,6 @@ export default class ModalRoot extends React.PureComponent {
render () {
const { children, onClose } = this.props;
const { revealed } = this.state;
const visible = !!children;
if (!visible) {
@ -103,10 +95,16 @@ export default class ModalRoot extends React.PureComponent {
);
}
let backgroundColor = null;
if (this.props.backgroundColor) {
backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
}
return (
<div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}>
<div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' onClick={onClose} />
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.7)` : null }} />
<div role='dialog' className='modal-root__container'>{children}</div>
</div>
</div>

View File

@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { FormattedMessage } from 'react-intl';
export default @connect()
class PictureInPicturePlaceholder extends React.PureComponent {
static propTypes = {
width: PropTypes.number,
dispatch: PropTypes.func.isRequired,
};
state = {
width: this.props.width,
height: this.props.width && (this.props.width / (16/9)),
};
handleClick = () => {
const { dispatch } = this.props;
dispatch(removePictureInPicture());
}
setRef = c => {
this.node = c;
if (this.node) {
this._setDimensions();
}
}
_setDimensions () {
const width = this.node.offsetWidth;
const height = width / (16/9);
this.setState({ width, height });
}
componentDidMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}
handleResize = debounce(() => {
if (this.node) {
this._setDimensions();
}
}, 250, {
trailing: true,
});
render () {
const { height } = this.state;
return (
<div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex='0' onClick={this.handleClick}>
<Icon id='window-restore' />
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
</div>
);
}
}

View File

@ -17,6 +17,7 @@ import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
@ -95,6 +96,11 @@ class Status extends ImmutablePureComponent {
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
scrollKey: PropTypes.string,
deployPictureInPicture: PropTypes.func,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
};
// Avoid checking props that are functions (and whose equality will always
@ -104,6 +110,8 @@ class Status extends ImmutablePureComponent {
'account',
'muted',
'hidden',
'unread',
'pictureInPicture',
];
state = {
@ -184,8 +192,13 @@ class Status extends ImmutablePureComponent {
return <div className='audio-player' style={{ height: '110px' }} />;
}
handleOpenVideo = (media, options) => {
this.props.onOpenVideo(media, options);
handleOpenVideo = (options) => {
const status = this._properStatus();
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options);
}
handleOpenMedia = (media, index) => {
this.props.onOpenMedia(this._properStatus().get('id'), media, index);
}
handleHotkeyOpenMedia = e => {
@ -195,16 +208,21 @@ class Status extends ImmutablePureComponent {
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
onOpenMedia(status.get('media_attachments'), 0);
onOpenMedia(status.get('id'), status.get('media_attachments'), 0);
}
}
}
handleDeployPictureInPicture = (type, mediaProps) => {
const { deployPictureInPicture } = this.props;
const status = this._properStatus();
deployPictureInPicture(status, type, mediaProps);
}
handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history);
@ -265,7 +283,7 @@ class Status extends ImmutablePureComponent {
let media = null;
let statusAvatar, prepend, rebloggedByText;
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey } = this.props;
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, pictureInPicture } = this.props;
let { status, account, ...other } = this.props;
@ -336,7 +354,9 @@ class Status extends ImmutablePureComponent {
status = status.get('reblog');
}
if (status.get('media_attachments').size > 0) {
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
} else if (status.get('media_attachments').size > 0) {
if (this.props.muted) {
media = (
<AttachmentList
@ -361,6 +381,7 @@ class Status extends ImmutablePureComponent {
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
/>
)}
</Bundle>
@ -373,6 +394,7 @@ class Status extends ImmutablePureComponent {
{Component => (
<Component
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
@ -382,6 +404,7 @@ class Status extends ImmutablePureComponent {
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
@ -396,7 +419,7 @@ class Status extends ImmutablePureComponent {
media={status.get('media_attachments')}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.props.onOpenMedia}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
@ -409,7 +432,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
media = (
<Card
onOpenMedia={this.props.onOpenMedia}
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
cacheWidth={this.props.cacheMediaWidth}
@ -438,14 +461,16 @@ class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />
</a>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>

View File

@ -21,7 +21,7 @@ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
local_only: { id: 'status.local_only', defaultMessage: 'This post is only visible by other users of your instance' },
@ -44,16 +44,6 @@ const messages = defineMessages({
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
});
const obfuscatedCount = count => {
if (count < 0) {
return 0;
} else if (count <= 1) {
return count;
} else {
return '1+';
}
};
const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
@ -331,9 +321,10 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton}
<div className='status__action-bar-dropdown'>

View File

@ -1,7 +1,6 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
@ -186,17 +185,12 @@ export default class StatusContent extends React.PureComponent {
const content = { __html: status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };
const directionStyle = { direction: 'ltr' };
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': renderReadMore,
});
if (isRtl(status.get('search_index'))) {
directionStyle.direction = 'rtl';
}
const showThreadButton = (
<button className='status__content__read-more-button' onClick={this.props.onClick}>
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
@ -225,7 +219,7 @@ export default class StatusContent extends React.PureComponent {
}
return (
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} />
{' '}
@ -234,7 +228,7 @@ export default class StatusContent extends React.PureComponent {
{mentionsPlaceholder}
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} dangerouslySetInnerHTML={content} />
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
@ -243,8 +237,8 @@ export default class StatusContent extends React.PureComponent {
);
} else if (this.props.onClick) {
const output = [
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
<div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
<div className='status__content__text status__content__text--visible' dangerouslySetInnerHTML={content} />
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
@ -259,8 +253,8 @@ export default class StatusContent extends React.PureComponent {
return output;
} else {
return (
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}>
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
<div className={classNames} ref={this.setRef} tabIndex='0'>
<div className='status__content__text status__content__text--visible' dangerouslySetInnerHTML={content} />
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}

View File

@ -30,6 +30,7 @@ export default class MediaContainer extends PureComponent {
media: null,
index: null,
time: null,
backgroundColor: null,
};
handleOpenMedia = (media, index) => {
@ -52,7 +53,16 @@ export default class MediaContainer extends PureComponent {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = 0;
this.setState({ media: null, index: null, time: null });
this.setState({
media: null,
index: null,
time: null,
backgroundColor: null,
});
}
setBackgroundColor = color => {
this.setState({ backgroundColor: color });
}
render () {
@ -85,13 +95,14 @@ export default class MediaContainer extends PureComponent {
);
})}
<ModalRoot onClose={this.handleCloseMedia}>
<ModalRoot backgroundColor={this.state.backgroundColor} onClose={this.handleCloseMedia}>
{this.state.media && (
<MediaModal
media={this.state.media}
index={this.state.index || 0}
time={this.state.time}
onClose={this.handleCloseMedia}
onChangeBackgroundColor={this.setBackgroundColor}
/>
)}
</ModalRoot>

View File

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import Status from '../components/status';
import { makeGetStatus } from '../selectors';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
import {
replyCompose,
mentionCompose,
@ -37,6 +37,7 @@ import { initMuteModal } from '../actions/mutes';
import { initBlockModal } from '../actions/blocks';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { deployPictureInPicture } from '../actions/picture_in_picture';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state';
import { showAlertForError } from '../actions/alerts';
@ -53,9 +54,11 @@ const messages = defineMessages({
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const mapStateToProps = (state, props) => ({
status: getStatus(state, props),
pictureInPicture: getPictureInPicture(state, props),
});
return mapStateToProps;
@ -146,12 +149,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(mentionCompose(account, router));
},
onOpenMedia (media, index) {
dispatch(openModal('MEDIA', { media, index }));
onOpenMedia (statusId, media, index) {
dispatch(openModal('MEDIA', { statusId, media, index }));
},
onOpenVideo (media, options) {
dispatch(openModal('VIDEO', { media, options }));
onOpenVideo (statusId, media, options) {
dispatch(openModal('VIDEO', { statusId, media, options }));
},
onBlock (status) {
@ -207,6 +210,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(unblockDomain(domain));
},
deployPictureInPicture (status, type, mediaProps) {
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

View File

@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
import Avatar from 'mastodon/components/avatar';
import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number';
@ -35,6 +36,8 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
@ -66,6 +69,17 @@ class Header extends ImmutablePureComponent {
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onNotifyToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
};
@ -130,8 +144,11 @@ class Header extends ImmutablePureComponent {
return null;
}
const suspended = account.get('suspended');
let info = [];
let actionBtn = '';
let bellBtn = '';
let lockedIcon = '';
let menu = [];
@ -147,13 +164,17 @@ class Header extends ImmutablePureComponent {
info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
}
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
}
if (me !== account.get('id')) {
if (!account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
@ -258,7 +279,7 @@ class Header extends ImmutablePureComponent {
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
<div className='account__header__image'>
<div className='account__header__info'>
{info}
{!suspended && info}
</div>
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
@ -272,11 +293,14 @@ class Header extends ImmutablePureComponent {
<div className='spacer' />
<div className='account__header__tabs__buttons'>
{actionBtn}
{!suspended && (
<div className='account__header__tabs__buttons'>
{actionBtn}
{bellBtn}
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
</div>
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
</div>
)}
</div>
<div className='account__header__tabs__name'>
@ -288,7 +312,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__extra'>
<div className='account__header__bio'>
{ (fields.size > 0 || identity_proofs.size > 0) && (
{(fields.size > 0 || identity_proofs.size > 0) && (
<div className='account__header__fields'>
{identity_proofs.map((proof, i) => (
<dl key={i}>
@ -314,33 +338,35 @@ class Header extends ImmutablePureComponent {
</div>
)}
{account.get('id') !== me && <AccountNoteContainer account={account} />}
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
</div>
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber
value={account.get('statuses_count')}
renderer={counterRenderer('statuses')}
/>
</NavLink>
{!suspended && (
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber
value={account.get('statuses_count')}
renderer={counterRenderer('statuses')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<ShortNumber
value={account.get('following_count')}
renderer={counterRenderer('following')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<ShortNumber
value={account.get('following_count')}
renderer={counterRenderer('following')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<ShortNumber
value={account.get('followers_count')}
renderer={counterRenderer('followers')}
/>
</NavLink>
</div>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<ShortNumber
value={account.get('followers_count')}
renderer={counterRenderer('followers')}
/>
</NavLink>
</div>
)}
</div>
</div>
</div>

View File

@ -122,7 +122,7 @@ export default class MediaItem extends ImmutablePureComponent {
<div className='media-gallery__gifv'>
{content}
<span className='media-gallery__gifv__label'>{label}</span>
{label && <span className='media-gallery__gifv__label'>{label}</span>}
</div>
);
}

View File

@ -15,12 +15,15 @@ import { ScrollContainer } from 'react-router-scroll-4';
import LoadMore from 'mastodon/components/load_more';
import MissingIndicator from 'mastodon/components/missing_indicator';
import { openModal } from 'mastodon/actions/modal';
import { FormattedMessage } from 'react-intl';
const mapStateToProps = (state, props) => ({
isAccount: !!state.getIn(['accounts', props.params.accountId]),
attachments: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false),
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
});
class LoadMoreMedia extends ImmutablePureComponent {
@ -56,6 +59,8 @@ class AccountGallery extends ImmutablePureComponent {
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
isAccount: PropTypes.bool,
blockedBy: PropTypes.bool,
suspended: PropTypes.bool,
multiColumn: PropTypes.bool,
};
@ -100,15 +105,18 @@ class AccountGallery extends ImmutablePureComponent {
}
handleOpenMedia = attachment => {
const { dispatch } = this.props;
const statusId = attachment.getIn(['status', 'id']);
if (attachment.get('type') === 'video') {
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } }));
} else if (attachment.get('type') === 'audio') {
this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } }));
} else {
const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') }));
dispatch(openModal('MEDIA', { media, index, statusId }));
}
}
@ -119,7 +127,7 @@ class AccountGallery extends ImmutablePureComponent {
}
render () {
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props;
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
const { width } = this.state;
if (!isAccount) {
@ -144,6 +152,14 @@ class AccountGallery extends ImmutablePureComponent {
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
}
let emptyMessage;
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
}
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
@ -152,15 +168,21 @@ class AccountGallery extends ImmutablePureComponent {
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} />
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
{attachments.map((attachment, index) => attachment === null ? (
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
) : (
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
))}
{(suspended || blockedBy) ? (
<div className='empty-column-indicator'>
{emptyMessage}
</div>
) : (
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
{attachments.map((attachment, index) => attachment === null ? (
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
) : (
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
))}
{loadOlder}
</div>
{loadOlder}
</div>
)}
{isLoading && attachments.size === 0 && (
<div className='scrollable__append'>

View File

@ -23,7 +23,6 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
};
@ -56,6 +55,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onReblogToggle(this.props.account);
}
handleNotifyToggle = () => {
this.props.onNotifyToggle(this.props.account);
}
handleMute = () => {
this.props.onMute(this.props.account);
}
@ -107,6 +110,7 @@ export default class Header extends ImmutablePureComponent {
onMention={this.handleMention}
onDirect={this.handleDirect}
onReblogToggle={this.handleReblogToggle}
onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}

View File

@ -76,9 +76,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onReblogToggle (account) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
dispatch(followAccount(account.get('id'), false));
dispatch(followAccount(account.get('id'), { reblogs: false }));
} else {
dispatch(followAccount(account.get('id'), true));
dispatch(followAccount(account.get('id'), { reblogs: true }));
}
},
@ -90,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onNotifyToggle (account) {
if (account.getIn(['relationship', 'notifying'])) {
dispatch(followAccount(account.get('id'), { notify: false }));
} else {
dispatch(followAccount(account.get('id'), { notify: true }));
}
},
onReport (account) {
dispatch(initReport(account));
},

View File

@ -31,6 +31,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
};
@ -57,6 +58,7 @@ class AccountTimeline extends ImmutablePureComponent {
withReplies: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
@ -113,7 +115,7 @@ class AccountTimeline extends ImmutablePureComponent {
}
render () {
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn, remote, remoteUrl } = this.props;
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
@ -134,7 +136,9 @@ class AccountTimeline extends ImmutablePureComponent {
let emptyMessage;
if (blockedBy) {
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
@ -153,7 +157,7 @@ class AccountTimeline extends ImmutablePureComponent {
alwaysPrepend
append={remoteMessage}
scrollKey='account_timeline'
statusIds={blockedBy ? emptyList : statusIds}
statusIds={(suspended || blockedBy) ? emptyList : statusIds}
featuredStatusIds={featuredStatusIds}
isLoading={isLoading}
hasMore={hasMore}

View File

@ -37,7 +37,11 @@ class Audio extends React.PureComponent {
backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string,
accentColor: PropTypes.string,
currentTime: PropTypes.number,
autoPlay: PropTypes.bool,
volume: PropTypes.number,
muted: PropTypes.bool,
deployPictureInPicture: PropTypes.func,
};
state = {
@ -64,6 +68,19 @@ class Audio extends React.PureComponent {
}
}
_pack() {
return {
src: this.props.src,
volume: this.audio.volume,
muted: this.audio.muted,
currentTime: this.audio.currentTime,
poster: this.props.poster,
backgroundColor: this.props.backgroundColor,
foregroundColor: this.props.foregroundColor,
accentColor: this.props.accentColor,
};
}
_setDimensions () {
const width = this.player.offsetWidth;
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
@ -112,6 +129,10 @@ class Audio extends React.PureComponent {
componentWillUnmount () {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
if (!this.state.paused && this.audio && this.props.deployPictureInPicture) {
this.props.deployPictureInPicture('audio', this._pack());
}
}
togglePlay = () => {
@ -225,7 +246,7 @@ class Audio extends React.PureComponent {
handleTimeUpdate = () => {
this.setState({
currentTime: this.audio.currentTime,
duration: Math.floor(this.audio.duration),
duration: this.audio.duration,
});
}
@ -248,7 +269,13 @@ class Audio extends React.PureComponent {
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
this.setState({ paused: true }, () => this.audio.pause());
this.audio.pause();
if (this.props.deployPictureInPicture) {
this.props.deployPictureInPicture('audio', this._pack());
}
this.setState({ paused: true });
}
}, 150, { trailing: true });
@ -261,10 +288,22 @@ class Audio extends React.PureComponent {
}
handleLoadedData = () => {
const { autoPlay } = this.props;
const { autoPlay, currentTime, volume, muted } = this.props;
if (currentTime) {
this.audio.currentTime = currentTime;
}
if (volume !== undefined) {
this.audio.volume = volume;
}
if (muted !== undefined) {
this.audio.muted = muted;
}
if (autoPlay) {
this.audio.play();
this.togglePlay();
}
}
@ -347,13 +386,59 @@ class Audio extends React.PureComponent {
return this.props.foregroundColor || '#ffffff';
}
seekBy (time) {
const currentTime = this.audio.currentTime + time;
if (!isNaN(currentTime)) {
this.setState({ currentTime }, () => {
this.audio.currentTime = currentTime;
});
}
}
handleAudioKeyDown = e => {
// On the audio element or the seek bar, we can safely use the space bar
// for playback control because there are no buttons to press
if (e.key === ' ') {
e.preventDefault();
e.stopPropagation();
this.togglePlay();
}
}
handleKeyDown = e => {
switch(e.key) {
case 'k':
e.preventDefault();
e.stopPropagation();
this.togglePlay();
break;
case 'm':
e.preventDefault();
e.stopPropagation();
this.toggleMute();
break;
case 'j':
e.preventDefault();
e.stopPropagation();
this.seekBy(-10);
break;
case 'l':
e.preventDefault();
e.stopPropagation();
this.seekBy(10);
break;
}
}
render () {
const { src, intl, alt, editable, autoPlay } = this.props;
const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state;
const progress = (currentTime / duration) * 100;
const progress = Math.min((currentTime / duration) * 100, 100);
return (
<div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex='0' onKeyDown={this.handleKeyDown}>
<audio
src={src}
ref={this.setAudioRef}
@ -367,12 +452,14 @@ class Audio extends React.PureComponent {
<canvas
role='button'
tabIndex='0'
className='audio-player__canvas'
width={this.state.width}
height={this.state.height}
style={{ width: '100%', position: 'absolute', top: 0, left: 0 }}
ref={this.setCanvasRef}
onClick={this.togglePlay}
onKeyDown={this.handleAudioKeyDown}
title={alt}
aria-label={alt}
/>
@ -393,20 +480,21 @@ class Audio extends React.PureComponent {
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }}
onKeyDown={this.handleAudioKeyDown}
/>
</div>
<div className='video-player__controls active'>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
<div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} />
<span
className={classNames('video-player__volume__handle')}
className='video-player__volume__handle'
tabIndex='0'
style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }}
/>
@ -415,12 +503,14 @@ class Audio extends React.PureComponent {
<span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
<span className='video-player__time-total'>{formatTime(Math.floor(this.state.duration || this.props.duration))}</span>
</span>
</div>
<div className='video-player__buttons right'>
<button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} onClick={this.handleDownload}><Icon id='download' fixedWidth /></button>
<a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>
</div>
</div>
</div>

View File

@ -79,6 +79,18 @@ class ComposeForm extends ImmutablePureComponent {
}
}
getFulltextForCharacterCounting = () => {
return [this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text)].join('');
}
canSubmit = () => {
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia));
}
handleSubmit = () => {
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
@ -86,11 +98,7 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onChange(this.autosuggestTextarea.textarea.value);
}
// Submit disabled:
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
if (!this.canSubmit()) {
return;
}
@ -180,10 +188,8 @@ class ComposeForm extends ImmutablePureComponent {
}
render () {
const { intl, onPaste, showSearch, anyMedia } = this.props;
const { intl, onPaste, showSearch } = this.props;
const disabled = this.props.isSubmitting;
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
@ -246,11 +252,11 @@ class ComposeForm extends ImmutablePureComponent {
<SpoilerButtonContainer />
<FederationDropdownContainer />
</div>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} /></div>
</div>
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabledButton} block /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={!this.canSubmit()} block /></div>
</div>
</div>
);

View File

@ -5,8 +5,9 @@ import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'
import Overlay from 'react-overlays/lib/Overlay';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import detectPassiveEvents from 'detect-passive-events';
import { supportsPassiveEvents } from 'detect-passive-events';
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
import { assetHost } from 'mastodon/utils/config';
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@ -25,11 +26,10 @@ const messages = defineMessages({
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
});
const assetHost = process.env.CDN_HOST || '';
let EmojiPicker, Emoji; // load asynchronously
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
class ModifierPickerMenu extends React.PureComponent {

View File

@ -5,7 +5,7 @@ import IconButton from '../../../components/icon_button';
import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import detectPassiveEvents from 'detect-passive-events';
import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
@ -21,7 +21,7 @@ const messages = defineMessages({
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
});
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
class PrivacyDropdownMenu extends React.PureComponent {
@ -179,7 +179,7 @@ class PrivacyDropdown extends React.PureComponent {
} else {
const { top } = target.getBoundingClientRect();
if (this.state.open && this.activeElement) {
this.activeElement.focus();
this.activeElement.focus({ preventScroll: true });
}
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
this.setState({ open: !this.state.open });
@ -220,7 +220,7 @@ class PrivacyDropdown extends React.PureComponent {
handleClose = () => {
if (this.state.open && this.activeElement) {
this.activeElement.focus();
this.activeElement.focus({ preventScroll: true });
}
this.setState({ open: false });
}

View File

@ -6,7 +6,6 @@ import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { isRtl } from '../../../rtl';
import AttachmentList from 'mastodon/components/attachment_list';
const messages = defineMessages({
@ -45,9 +44,6 @@ class ReplyIndicator extends ImmutablePureComponent {
}
const content = { __html: status.get('contentHtml') };
const style = {
direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
};
return (
<div className='reply-indicator'>
@ -60,7 +56,7 @@ class ReplyIndicator extends ImmutablePureComponent {
</a>
</div>
<div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
<div className='reply-indicator__content' dangerouslySetInnerHTML={content} />
{status.get('media_attachments').size > 0 && (
<AttachmentList

View File

@ -97,16 +97,6 @@ class SearchResults extends ImmutablePureComponent {
<div className='search-results__section'>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
<div className='search-results__info'>
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching toots by their content is not enabled on this Mastodon server.' />
</div>
</div>
);
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
statuses = (
<div className='search-results__section'>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
<div className='search-results__info'>
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching toots by their content is not enabled on this Mastodon server.' />
</div>

View File

@ -6,13 +6,20 @@ import { changeComposeSensitivity } from 'mastodon/actions/compose';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
const messages = defineMessages({
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' },
marked: {
id: 'compose_form.sensitive.marked',
defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}',
},
unmarked: {
id: 'compose_form.sensitive.unmarked',
defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}',
},
});
const mapStateToProps = state => ({
active: state.getIn(['compose', 'sensitive']),
disabled: state.getIn(['compose', 'spoiler']),
mediaCount: state.getIn(['compose', 'media_attachments']).size,
});
const mapDispatchToProps = dispatch => ({
@ -28,16 +35,17 @@ class SensitiveButton extends React.PureComponent {
static propTypes = {
active: PropTypes.bool,
disabled: PropTypes.bool,
mediaCount: PropTypes.number,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { active, disabled, onClick, intl } = this.props;
const { active, disabled, mediaCount, onClick, intl } = this.props;
return (
<div className='compose-form__sensitive-button'>
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}>
<input
name='mark-sensitive'
type='checkbox'
@ -48,7 +56,11 @@ class SensitiveButton extends React.PureComponent {
<span className={classNames('checkbox', { active })} />
<FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
<FormattedMessage
id='compose_form.sensitive.hide'
defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}'
values={{ count: mediaCount }}
/>
</label>
</div>
);

View File

@ -5,7 +5,30 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { me } from '../../../initial_state';
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
const buildHashtagRE = () => {
try {
const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
const ALPHA = '\\p{L}\\p{M}';
const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
return new RegExp(
'(?:^|[^\\/\\)\\w])#((' +
'[' + WORD + '_]' +
'[' + WORD + HASHTAG_SEPARATORS + ']*' +
'[' + ALPHA + HASHTAG_SEPARATORS + ']' +
'[' + WORD + HASHTAG_SEPARATORS +']*' +
'[' + WORD + '_]' +
')|(' +
'[' + WORD + '_]*' +
'[' + ALPHA + ']' +
'[' + WORD + '_]*' +
'))', 'iu',
);
} catch {
return /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
}
};
const APPROX_HASHTAG_RE = buildHashtagRE();
const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),

View File

@ -1,18 +1,17 @@
import { autoPlayGif } from '../../initial_state';
import unicodeMapping from './emoji_unicode_mapping_light';
import { assetHost } from 'mastodon/utils/config';
import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeMapping));
const assetHost = process.env.CDN_HOST || '';
// Convert to file names from emojis. (For different variation selector emojis)
const emojiFilenames = (emojis) => {
return emojis.map(v => unicodeMapping[v].filename);
};
// Emoji requiring extra borders depending on theme
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴']);
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺']);
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
const emojiFilename = (filename) => {

View File

@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import Icon from 'mastodon/components/icon';
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
import { autoPlayGif, reduceMotion } from 'mastodon/initial_state';
import { autoPlayGif, reduceMotion, disableSwiping } from 'mastodon/initial_state';
import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
import { mascot } from 'mastodon/initial_state';
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
@ -15,6 +15,7 @@ import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_pick
import AnimatedNumber from 'mastodon/components/animated_number';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
import { assetHost } from 'mastodon/utils/config';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -153,8 +154,6 @@ class Content extends ImmutablePureComponent {
}
const assetHost = process.env.CDN_HOST || '';
class Emoji extends React.PureComponent {
static propTypes = {
@ -397,7 +396,7 @@ class Announcements extends ImmutablePureComponent {
_markAnnouncementAsRead () {
const { dismissAnnouncement, announcements } = this.props;
const { index } = this.state;
const announcement = announcements.get(index);
const announcement = announcements.get(announcements.size - 1 - index);
if (!announcement.get('read')) dismissAnnouncement(announcement.get('id'));
}
@ -436,8 +435,9 @@ class Announcements extends ImmutablePureComponent {
removeReaction={this.props.removeReaction}
intl={intl}
selected={index === idx}
disabled={disableSwiping}
/>
))}
)).reverse()}
</ReactSwipeableViews>
{announcements.size > 1 && (

View File

@ -10,7 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, profile_directory, showTrends } from '../../initial_state';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { List as ImmutableList } from 'immutable';
import NavigationBar from '../compose/components/navigation_bar';
import NavigationContainer from '../compose/containers/navigation_container';
import Icon from 'mastodon/components/icon';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
import TrendsContainer from './containers/trends_container';
@ -40,6 +40,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
myAccount: state.getIn(['accounts', me]),
columns: state.getIn(['settings', 'columns']),
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
});
@ -89,60 +90,66 @@ class GettingStarted extends ImmutablePureComponent {
}
render () {
const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
const { intl, myAccount, columns, multiColumn, unreadFollowRequests } = this.props;
const navItems = [];
let i = 1;
let height = (multiColumn) ? 0 : 60;
if (multiColumn) {
navItems.push(
<ColumnSubheading key={i++} text={intl.formatMessage(messages.discover)} />,
<ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
<ColumnLink key={i++} icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
<ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />,
<ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
<ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
);
height += 34 + 48*2;
if (profile_directory) {
navItems.push(
<ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
<ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
);
height += 48;
}
navItems.push(
<ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />,
<ColumnSubheading key='header-personal' text={intl.formatMessage(messages.personal)} />,
);
height += 34;
} else if (profile_directory) {
navItems.push(
<ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
<ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
);
height += 48;
}
if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) {
navItems.push(
<ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />,
);
height += 48;
}
navItems.push(
<ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
<ColumnLink key={i++} icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
<ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
<ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
<ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
<ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
);
height += 48*4;
if (myAccount.get('locked') || unreadFollowRequests > 0) {
navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
height += 48;
}
if (!multiColumn) {
navItems.push(
<ColumnSubheading key={i++} text={intl.formatMessage(messages.settings_subheading)} />,
<ColumnLink key={i++} icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
<ColumnSubheading key='header-settings' text={intl.formatMessage(messages.settings_subheading)} />,
<ColumnLink key='preferences' icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
);
height += 34 + 48;
@ -161,7 +168,7 @@ class GettingStarted extends ImmutablePureComponent {
<div className='getting-started'>
<div className='getting-started__wrapper' style={{ height }}>
{!multiColumn && <NavigationBar account={myAccount} />}
{!multiColumn && <NavigationContainer />}
{navItems}
</div>

View File

@ -9,6 +9,7 @@ import screenHello from '../../../images/screen_hello.svg';
import screenFederation from '../../../images/screen_federation.svg';
import screenInteractions from '../../../images/screen_interactions.svg';
import logoTransparent from '../../../images/logo_transparent.svg';
import { disableSwiping } from 'mastodon/initial_state';
const FrameWelcome = ({ domain, onNext }) => (
<div className='introduction__frame'>
@ -171,7 +172,7 @@ class Introduction extends React.PureComponent {
return (
<div className='introduction'>
<ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='introduction__pager'>
<ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} disabled={disableSwiping} className='introduction__pager'>
{pages.map((page, i) => (
<div key={i} className={classNames('introduction__frame-wrapper', { 'active': i === currentIndex })}>{page}</div>
))}

View File

@ -10,15 +10,19 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { connectListStream } from '../../actions/streaming';
import { expandListTimeline } from '../../actions/timelines';
import { fetchList, deleteList } from '../../actions/lists';
import { fetchList, deleteList, updateList } from '../../actions/lists';
import { openModal } from '../../actions/modal';
import MissingIndicator from '../../components/missing_indicator';
import LoadingIndicator from '../../components/loading_indicator';
import Icon from 'mastodon/components/icon';
import RadioButton from 'mastodon/components/radio_button';
const messages = defineMessages({
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
});
const mapStateToProps = (state, props) => ({
@ -131,11 +135,18 @@ class ListTimeline extends React.PureComponent {
}));
}
handleRepliesPolicyChange = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateList(id, undefined, false, target.value));
}
render () {
const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list } = this.props;
const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list, intl } = this.props;
const { id } = this.props.params;
const pinned = !!columnId;
const title = list ? list.get('title') : id;
const replies_policy = list ? list.get('replies_policy') : undefined;
if (typeof list === 'undefined') {
return (
@ -166,7 +177,7 @@ class ListTimeline extends React.PureComponent {
pinned={pinned}
multiColumn={multiColumn}
>
<div className='column-header__links'>
<div className='column-settings__row column-header__links'>
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
<Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
</button>
@ -175,6 +186,19 @@ class ListTimeline extends React.PureComponent {
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
</button>
</div>
{ replies_policy !== undefined && (
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
</span>
<div className='column-settings__row'>
{ ['none', 'list', 'followed'].map(policy => (
<RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
))}
</div>
</div>
)}
</ColumnHeader>
<StatusListContainer

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button';
import SettingToggle from './setting_toggle';
export default class ColumnSettings extends React.PureComponent {
@ -12,6 +13,10 @@ export default class ColumnSettings extends React.PureComponent {
pushSettings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onRequestNotificationPermission: PropTypes.func,
alertsEnabled: PropTypes.bool,
browserSupport: PropTypes.bool,
browserPermission: PropTypes.bool,
};
onPushChange = (path, checked) => {
@ -19,7 +24,7 @@ export default class ColumnSettings extends React.PureComponent {
}
render () {
const { settings, pushSettings, onChange, onClear } = this.props;
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
@ -32,6 +37,20 @@ export default class ColumnSettings extends React.PureComponent {
return (
<div>
{alertsEnabled && browserSupport && browserPermission === 'denied' && (
<div className='column-settings__row column-settings__row--with-margin'>
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
</div>
)}
{alertsEnabled && browserSupport && browserPermission === 'default' && (
<div className='column-settings__row column-settings__row--with-margin'>
<span className='warning-hint'>
<FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
</span>
</div>
)}
<div className='column-settings__row'>
<ClearColumnButton onClick={onClear} />
</div>
@ -40,6 +59,7 @@ export default class ColumnSettings extends React.PureComponent {
<span id='notifications-filter-bar' className='column-settings__section'>
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
</span>
<div className='column-settings__row'>
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
@ -50,7 +70,7 @@ export default class ColumnSettings extends React.PureComponent {
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
@ -61,7 +81,7 @@ export default class ColumnSettings extends React.PureComponent {
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
@ -72,7 +92,7 @@ export default class ColumnSettings extends React.PureComponent {
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
@ -83,7 +103,7 @@ export default class ColumnSettings extends React.PureComponent {
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
@ -94,7 +114,7 @@ export default class ColumnSettings extends React.PureComponent {
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
@ -105,12 +125,23 @@ export default class ColumnSettings extends React.PureComponent {
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'poll']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-status'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New toots:' /></span>
<div className='column-settings__row'>
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'status']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
</div>
</div>
</div>
);
}

View File

@ -9,6 +9,7 @@ const tooltips = defineMessages({
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
});
export default @injectIntl
@ -87,6 +88,13 @@ class FilterBar extends React.PureComponent {
>
<Icon id='tasks' fixedWidth />
</button>
<button
className={selectedFilter === 'status' ? 'active' : ''}
onClick={this.onClick('status')}
title={intl.formatMessage(tooltips.statuses)}
>
<Icon id='home' fixedWidth />
</button>
<button
className={selectedFilter === 'follow' ? 'active' : ''}
onClick={this.onClick('follow')}

View File

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
export default class GrantPermissionButton extends React.PureComponent {
static propTypes = {
onClick: PropTypes.func.isRequired,
};
render () {
return (
<button className='text-btn column-header__permission-btn' tabIndex='0' onClick={this.props.onClick}>
<FormattedMessage id='notifications.grant_permission' defaultMessage='Grant permission.' />
</button>
);
}
}

View File

@ -10,6 +10,7 @@ import AccountContainer from 'mastodon/containers/account_container';
import FollowRequestContainer from '../containers/follow_request_container';
import Icon from 'mastodon/components/icon';
import Permalink from 'mastodon/components/permalink';
import classNames from 'classnames';
const messages = defineMessages({
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
@ -17,6 +18,7 @@ const messages = defineMessages({
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
});
const notificationForScreenReader = (intl, message, timestamp) => {
@ -49,6 +51,7 @@ class Notification extends ImmutablePureComponent {
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
unread: PropTypes.bool,
};
handleMoveUp = () => {
@ -113,11 +116,11 @@ class Notification extends ImmutablePureComponent {
}
renderFollow (notification, account, link) {
const { intl } = this.props;
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
<div className={classNames('notification notification-follow focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user-plus' fixedWidth />
@ -135,11 +138,11 @@ class Notification extends ImmutablePureComponent {
}
renderFollowRequest (notification, account, link) {
const { intl } = this.props;
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
<div className={classNames('notification notification-follow-request focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user' fixedWidth />
@ -169,16 +172,17 @@ class Notification extends ImmutablePureComponent {
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
unread={this.props.unread}
/>
);
}
renderFavourite (notification, link) {
const { intl } = this.props;
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className={classNames('notification notification-favourite focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='star' className='star-icon' fixedWidth />
@ -206,11 +210,11 @@ class Notification extends ImmutablePureComponent {
}
renderReblog (notification, link) {
const { intl } = this.props;
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className={classNames('notification notification-reblog focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='retweet' fixedWidth />
@ -237,14 +241,46 @@ class Notification extends ImmutablePureComponent {
);
}
renderStatus (notification, link) {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-status focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='home' fixedWidth />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.status' defaultMessage='{name} just posted' values={{ name: link }} />
</span>
</div>
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
muted
withDismiss
hidden={this.props.hidden}
getScrollPosition={this.props.getScrollPosition}
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
);
}
renderPoll (notification, account) {
const { intl } = this.props;
const { intl, unread } = this.props;
const ownPoll = me === account.get('id');
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
<div className={classNames('notification notification-poll focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='tasks' fixedWidth />
@ -292,6 +328,8 @@ class Notification extends ImmutablePureComponent {
return this.renderFavourite(notification, link);
case 'reblog':
return this.renderReblog(notification, link);
case 'status':
return this.renderStatus(notification, link);
case 'poll':
return this.renderPoll(notification, account);
}

View File

@ -0,0 +1,48 @@
import React from 'react';
import Icon from 'mastodon/components/icon';
import Button from 'mastodon/components/button';
import IconButton from 'mastodon/components/icon_button';
import { requestBrowserPermission } from 'mastodon/actions/notifications';
import { changeSetting } from 'mastodon/actions/settings';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
export default @connect()
@injectIntl
class NotificationsPermissionBanner extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.dispatch(requestBrowserPermission());
}
handleClose = () => {
this.props.dispatch(changeSetting(['notifications', 'dismissPermissionBanner'], true));
}
render () {
const { intl } = this.props;
return (
<div className='notifications-permission-banner'>
<div className='notifications-permission-banner__close'>
<IconButton icon='times' onClick={this.handleClose} title={intl.formatMessage(messages.close)} />
</div>
<h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2>
<p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' /> }} /></p>
<Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button>
</div>
);
}
}

View File

@ -12,6 +12,7 @@ export default class SettingToggle extends React.PureComponent {
label: PropTypes.node.isRequired,
onChange: PropTypes.func.isRequired,
defaultValue: PropTypes.bool,
disabled: PropTypes.bool,
}
onChange = ({ target }) => {
@ -19,12 +20,12 @@ export default class SettingToggle extends React.PureComponent {
}
render () {
const { prefix, settings, settingPath, label, defaultValue } = this.props;
const { prefix, settings, settingPath, label, defaultValue, disabled } = this.props;
const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
return (
<div className='setting-toggle'>
<Toggle id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
<Toggle disabled={disabled} id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
</div>
);

View File

@ -3,28 +3,55 @@ import { defineMessages, injectIntl } from 'react-intl';
import ColumnSettings from '../components/column_settings';
import { changeSetting } from '../../../actions/settings';
import { setFilter } from '../../../actions/notifications';
import { clearNotifications } from '../../../actions/notifications';
import { clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
import { openModal } from '../../../actions/modal';
import { showAlert } from '../../../actions/alerts';
const messages = defineMessages({
clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' },
});
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'notifications']),
pushSettings: state.get('push_notifications'),
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
browserSupport: state.getIn(['notifications', 'browserSupport']),
browserPermission: state.getIn(['notifications', 'browserPermission']),
});
const mapDispatchToProps = (dispatch, { intl }) => ({
onChange (path, checked) {
if (path[0] === 'push') {
dispatch(changePushNotifications(path.slice(1), checked));
if (checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
dispatch(requestBrowserPermission((permission) => {
if (permission === 'granted') {
dispatch(changePushNotifications(path.slice(1), checked));
} else {
dispatch(showAlert(undefined, messages.permissionDenied));
}
}));
} else {
dispatch(changePushNotifications(path.slice(1), checked));
}
} else if (path[0] === 'quickFilter') {
dispatch(changeSetting(['notifications', ...path], checked));
dispatch(setFilter('all'));
} else if (path[0] === 'alerts' && checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
if (checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') {
dispatch(requestBrowserPermission((permission) => {
if (permission === 'granted') {
dispatch(changeSetting(['notifications', ...path], checked));
} else {
dispatch(showAlert(undefined, messages.permissionDenied));
}
}));
} else {
dispatch(changeSetting(['notifications', ...path], checked));
}
} else {
dispatch(changeSetting(['notifications', ...path], checked));
}
@ -38,6 +65,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}));
},
onRequestNotificationPermission () {
dispatch(requestBrowserPermission());
},
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));

View File

@ -4,7 +4,15 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { expandNotifications, scrollTopNotifications, loadPending, mountNotifications, unmountNotifications } from '../../actions/notifications';
import {
expandNotifications,
scrollTopNotifications,
loadPending,
mountNotifications,
unmountNotifications,
markNotificationsAsRead,
} from '../../actions/notifications';
import { submitMarkers } from '../../actions/markers';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import NotificationContainer from './containers/notification_container';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@ -15,15 +23,25 @@ import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import ScrollableList from '../../components/scrollable_list';
import LoadGap from '../../components/load_gap';
import Icon from 'mastodon/components/icon';
import compareId from 'mastodon/compare_id';
import NotificationsPermissionBanner from './components/notifications_permission_banner';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' },
});
const getExcludedTypes = createSelector([
state => state.getIn(['settings', 'notifications', 'shows']),
], (shows) => {
return ImmutableList(shows.filter(item => !item).keys());
});
const getNotifications = createSelector([
state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
getExcludedTypes,
state => state.getIn(['notifications', 'items']),
], (showFilterBar, allowedType, excludedTypes, notifications) => {
if (!showFilterBar || allowedType === 'all') {
@ -32,7 +50,7 @@ const getNotifications = createSelector([
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
}
return notifications.filter(item => item !== null && allowedType === item.get('type'));
return notifications.filter(item => item === null || allowedType === item.get('type'));
});
const mapStateToProps = state => ({
@ -42,6 +60,9 @@ const mapStateToProps = state => ({
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
hasMore: state.getIn(['notifications', 'hasMore']),
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
lastReadId: state.getIn(['notifications', 'readMarkerId']),
canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']),
});
export default @connect(mapStateToProps)
@ -60,6 +81,9 @@ class Notifications extends React.PureComponent {
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
numPending: PropTypes.number,
lastReadId: PropTypes.string,
canMarkAsRead: PropTypes.bool,
needsNotificationPermission: PropTypes.bool,
};
static defaultProps = {
@ -146,8 +170,13 @@ class Notifications extends React.PureComponent {
}
}
handleMarkAsRead = () => {
this.props.dispatch(markNotificationsAsRead());
this.props.dispatch(submitMarkers({ immediate: true }));
};
render () {
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props;
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
@ -174,6 +203,7 @@ class Notifications extends React.PureComponent {
accountId={item.get('account')}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
unread={lastReadId !== '0' && compareId(item.get('id'), lastReadId) > 0}
/>
));
} else {
@ -190,6 +220,8 @@ class Notifications extends React.PureComponent {
showLoading={isLoading && notifications.size === 0}
hasMore={hasMore}
numPending={numPending}
prepend={needsNotificationPermission && <NotificationsPermissionBanner />}
alwaysPrepend
emptyMessage={emptyMessage}
onLoadMore={this.handleLoadOlder}
onLoadPending={this.handleLoadPending}
@ -202,6 +234,21 @@ class Notifications extends React.PureComponent {
</ScrollableList>
);
let extraButton = null;
if (canMarkAsRead) {
extraButton = (
<button
aria-label={intl.formatMessage(messages.markAsRead)}
title={intl.formatMessage(messages.markAsRead)}
onClick={this.handleMarkAsRead}
className='column-header__button'
>
<Icon id='check' />
</button>
);
}
return (
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
@ -213,6 +260,7 @@ class Notifications extends React.PureComponent {
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
extraButton={extraButton}
>
<ColumnSettingsContainer />
</ColumnHeader>

View File

@ -0,0 +1,159 @@
import React from 'react';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import classNames from 'classnames';
import { me, boostModal } from 'mastodon/initial_state';
import { defineMessages, injectIntl } from 'react-intl';
import { replyCompose } from 'mastodon/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
import { makeGetStatus } from 'mastodon/selectors';
import { openModal } from 'mastodon/actions/modal';
const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, { statusId }) => ({
status: getStatus(state, { id: statusId }),
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
});
return mapStateToProps;
};
export default @connect(makeMapStateToProps)
@injectIntl
class Footer extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
statusId: PropTypes.string.isRequired,
status: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
askReplyConfirmation: PropTypes.bool,
withOpenButton: PropTypes.bool,
onClose: PropTypes.func,
};
_performReply = () => {
const { dispatch, status, onClose } = this.props;
const { router } = this.context;
if (onClose) {
onClose();
}
dispatch(replyCompose(status, router.history));
};
handleReplyClick = () => {
const { dispatch, askReplyConfirmation, intl } = this.props;
if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: this._performReply,
}));
} else {
this._performReply();
}
};
handleFavouriteClick = () => {
const { dispatch, status } = this.props;
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
};
_performReblog = () => {
const { dispatch, status } = this.props;
dispatch(reblog(status));
}
handleReblogClick = e => {
const { dispatch, status } = this.props;
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else if ((e && e.shiftKey) || !boostModal) {
this._performReblog();
} else {
dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
}
};
handleOpenClick = e => {
const { router } = this.context;
if (e.button !== 0 || !router) {
return;
}
const { status } = this.props;
router.history.push(`/statuses/${status.get('id')}`);
}
render () {
const { status, intl, withOpenButton } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let replyIcon, replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll);
}
let reblogTitle = '';
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
}
return (
<div className='picture-in-picture__footer'>
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} />}
</div>
);
}
}

View File

@ -0,0 +1,47 @@
import React from 'react';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import { Link } from 'react-router-dom';
import Avatar from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const mapStateToProps = (state, { accountId }) => ({
account: state.getIn(['accounts', accountId]),
});
export default @connect(mapStateToProps)
@injectIntl
class Header extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
statusId: PropTypes.string.isRequired,
account: ImmutablePropTypes.map.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={`/statuses/${statusId}`} className='picture-in-picture__header__account'>
<Avatar account={account} size={36} />
<DisplayName account={account} />
</Link>
<IconButton icon='times' onClick={onClose} title={intl.formatMessage(messages.close)} />
</div>
);
}
}

View File

@ -0,0 +1,85 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Video from 'mastodon/features/video';
import Audio from 'mastodon/features/audio';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import Header from './components/header';
import Footer from './components/footer';
const mapStateToProps = state => ({
...state.get('picture_in_picture'),
});
export default @connect(mapStateToProps)
class PictureInPicture extends React.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>
);
}
}

View File

@ -15,7 +15,7 @@ const messages = defineMessages({
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },

View File

@ -15,6 +15,7 @@ import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import AnimatedNumber from 'mastodon/components/animated_number';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
@ -41,6 +42,10 @@ class DetailedStatus extends ImmutablePureComponent {
domain: PropTypes.string.isRequired,
compact: PropTypes.bool,
showMedia: PropTypes.bool,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
onToggleMediaVisibility: PropTypes.func,
};
@ -57,8 +62,8 @@ class DetailedStatus extends ImmutablePureComponent {
e.stopPropagation();
}
handleOpenVideo = (media, options) => {
this.props.onOpenVideo(media, options);
handleOpenVideo = (options) => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
}
handleExpandedToggle = () => {
@ -101,7 +106,7 @@ class DetailedStatus extends ImmutablePureComponent {
render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' };
const { intl, compact } = this.props;
const { intl, compact, pictureInPicture } = this.props;
if (!status) {
return null;
@ -118,7 +123,9 @@ class DetailedStatus extends ImmutablePureComponent {
outerStyle.height = `${this.state.height}px`;
}
if (status.get('media_attachments').size > 0) {
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder />;
} else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
@ -140,6 +147,7 @@ class DetailedStatus extends ImmutablePureComponent {
media = (
<Video
preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}

View File

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import DetailedStatus from '../components/detailed_status';
import { makeGetStatus } from '../../../selectors';
import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
import {
replyCompose,
mentionCompose,
@ -40,10 +40,12 @@ const messages = defineMessages({
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const mapStateToProps = (state, props) => ({
status: getStatus(state, props),
domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, props),
});
return mapStateToProps;

View File

@ -43,7 +43,7 @@ import {
import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks';
import { initReport } from '../../actions/reports';
import { makeGetStatus } from '../../selectors';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { ScrollContainer } from 'react-router-scroll-4';
import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header';
@ -72,6 +72,7 @@ const messages = defineMessages({
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([
(_, { id }) => id,
@ -129,11 +130,12 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List();
if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
}
@ -143,6 +145,7 @@ const makeMapStateToProps = () => {
descendantsIds,
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
};
};
@ -167,6 +170,10 @@ class Status extends ImmutablePureComponent {
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
domain: PropTypes.string.isRequired,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
};
state = {
@ -274,22 +281,20 @@ class Status extends ImmutablePureComponent {
}
handleOpenMedia = (media, index) => {
this.props.dispatch(openModal('MEDIA', { media, index }));
this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index }));
}
handleOpenVideo = (media, options) => {
this.props.dispatch(openModal('VIDEO', { media, options }));
this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options }));
}
handleHotkeyOpenMedia = e => {
const status = this._properStatus();
const { status } = this.props;
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
@ -492,7 +497,7 @@ class Status extends ImmutablePureComponent {
render () {
let ancestors, descendants;
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props;
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
if (status === null) {
@ -550,6 +555,7 @@ class Status extends ImmutablePureComponent {
domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
/>
<ActionBar

View File

@ -4,13 +4,11 @@ import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { previewState } from './video_modal';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { status }) => ({
account: state.getIn(['accounts', status.get('account')]),
const mapStateToProps = (state, { statusId }) => ({
accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
});
export default @connect(mapStateToProps)
@ -18,12 +16,13 @@ class AudioModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map,
statusId: PropTypes.string.isRequired,
accountStaticAvatar: PropTypes.string.isRequired,
options: PropTypes.shape({
autoPlay: PropTypes.bool,
}),
account: ImmutablePropTypes.map,
onClose: PropTypes.func.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
};
static contextTypes = {
@ -52,15 +51,8 @@ class AudioModal extends ImmutablePureComponent {
}
}
handleStatusClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}
}
render () {
const { media, status, account } = this.props;
const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {};
return (
@ -71,7 +63,7 @@ class AudioModal extends ImmutablePureComponent {
alt={media.get('description')}
duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150}
poster={media.get('preview_url') || account.get('avatar_static')}
poster={media.get('preview_url') || accountStaticAvatar}
backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])}
@ -79,11 +71,9 @@ class AudioModal extends ImmutablePureComponent {
/>
</div>
{status && (
<div className={classNames('media-modal__meta')}>
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
</div>
)}
<div className='media-modal__overlay'>
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
</div>
</div>
);
}

View File

@ -75,9 +75,10 @@ class BoostModal extends ImmutablePureComponent {
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>

View File

@ -8,6 +8,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
import TabsBar, { links, getIndex, getLink } from './tabs_bar';
import { Link } from 'react-router-dom';
import { disableSwiping } from 'mastodon/initial_state';
import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
@ -29,7 +31,7 @@ import Icon from 'mastodon/components/icon';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
import detectPassiveEvents from 'detect-passive-events';
import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
const componentMap = {
@ -73,12 +75,14 @@ class ColumnsArea extends ImmutablePureComponent {
}
componentWillReceiveProps() {
this.setState({ shouldAnimate: false });
if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) {
this.setState({ shouldAnimate: false });
}
}
componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
this.lastIndex = getIndex(this.context.router.history.location.pathname);
@ -95,10 +99,15 @@ class ColumnsArea extends ImmutablePureComponent {
componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
}
const newIndex = getIndex(this.context.router.history.location.pathname);
if (this.lastIndex !== newIndex) {
this.lastIndex = newIndex;
this.setState({ shouldAnimate: true });
}
this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true });
}
componentWillUnmount () {
@ -185,7 +194,7 @@ class ColumnsArea extends ImmutablePureComponent {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
const content = columnIndex !== -1 ? (
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}>
{links.map(this.renderView)}
</ReactSwipeableViews>
) : (

View File

@ -18,6 +18,11 @@ import { length } from 'stringz';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import GIFV from 'mastodon/components/gifv';
import { me } from 'mastodon/initial_state';
// eslint-disable-next-line import/no-extraneous-dependencies
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
// eslint-disable-next-line import/extensions
import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
import { assetHost } from 'mastodon/utils/config';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -48,8 +53,6 @@ const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
.replace(/\n/g, ' ')
.replace(/\*\*\*\*\*\*/g, '\n\n');
const assetHost = process.env.CDN_HOST || '';
class ImageLoader extends React.PureComponent {
static propTypes = {
@ -104,6 +107,7 @@ class FocalPointModal extends ImmutablePureComponent {
dirty: false,
progress: 0,
loading: true,
ocrStatus: '',
};
componentWillMount () {
@ -219,11 +223,18 @@ class FocalPointModal extends ImmutablePureComponent {
this.setState({ detecting: true });
fetchTesseract().then(({ TesseractWorker }) => {
const worker = new TesseractWorker({
workerPath: `${assetHost}/packs/ocr/worker.min.js`,
corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`,
langPath: `${assetHost}/ocr/lang-data`,
fetchTesseract().then(({ createWorker }) => {
const worker = createWorker({
workerPath: tesseractWorkerPath,
corePath: tesseractCorePath,
langPath: assetHost,
logger: ({ status, progress }) => {
if (status === 'recognizing text') {
this.setState({ ocrStatus: 'detecting', progress });
} else {
this.setState({ ocrStatus: 'preparing', progress });
}
},
});
let media_url = media.get('url');
@ -236,12 +247,18 @@ class FocalPointModal extends ImmutablePureComponent {
}
}
worker.recognize(media_url)
.progress(({ progress }) => this.setState({ progress }))
.finally(() => worker.terminate())
.then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
.catch(() => this.setState({ detecting: false }));
}).catch(() => this.setState({ detecting: false }));
(async () => {
await worker.load();
await worker.loadLanguage('eng');
await worker.initialize('eng');
const { data: { text } } = await worker.recognize(media_url);
this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false });
await worker.terminate();
})();
}).catch((e) => {
console.error(e);
this.setState({ detecting: false });
});
}
handleThumbnailChange = e => {
@ -261,7 +278,7 @@ class FocalPointModal extends ImmutablePureComponent {
render () {
const { media, intl, account, onClose, isUploadingThumbnail } = this.props;
const { x, y, dragging, description, dirty, detecting, progress } = this.state;
const { x, y, dragging, description, dirty, detecting, progress, ocrStatus } = this.state;
const width = media.getIn(['meta', 'original', 'width']) || null;
const height = media.getIn(['meta', 'original', 'height']) || null;
@ -282,6 +299,13 @@ class FocalPointModal extends ImmutablePureComponent {
descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />;
}
let ocrMessage = '';
if (ocrStatus === 'detecting') {
ocrMessage = <FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />;
} else {
ocrMessage = <FormattedMessage id='upload_modal.preparing_ocr' defaultMessage='Preparing OCR…' />;
}
return (
<div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
<div className='report-modal__target'>
@ -333,7 +357,7 @@ class FocalPointModal extends ImmutablePureComponent {
/>
<div className='setting-text__modifiers'>
<UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={<FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />} />
<UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={ocrMessage} />
</div>
</div>
@ -364,6 +388,7 @@ class FocalPointModal extends ImmutablePureComponent {
{media.get('type') === 'video' && (
<Video
preview={media.get('preview_url')}
frameRate={media.getIn(['meta', 'original', 'frame_rate'])}
blurhash={media.get('blurhash')}
src={media.get('url')}
detailed

View File

@ -13,6 +13,7 @@ export default class ImageLoader extends React.PureComponent {
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool,
}
static defaultProps = {
@ -151,6 +152,9 @@ export default class ImageLoader extends React.PureComponent {
alt={alt}
src={src}
onClick={onClick}
width={width}
height={height}
zoomButtonHidden={this.props.zoomButtonHidden}
/>
)}
</div>

View File

@ -4,12 +4,15 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Video from 'mastodon/features/video';
import classNames from 'classnames';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImageLoader from './image_loader';
import Icon from 'mastodon/components/icon';
import GIFV from 'mastodon/components/gifv';
import { disableSwiping } from 'mastodon/initial_state';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -24,10 +27,11 @@ class MediaModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,
status: ImmutablePropTypes.map,
statusId: PropTypes.string,
index: PropTypes.number.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
};
static contextTypes = {
@ -37,23 +41,40 @@ class MediaModal extends ImmutablePureComponent {
state = {
index: null,
navigationHidden: false,
zoomButtonHidden: false,
};
handleSwipe = (index) => {
this.setState({ index: index % this.props.media.size });
}
handleTransitionEnd = () => {
this.setState({
zoomButtonHidden: false,
});
}
handleNextClick = () => {
this.setState({ index: (this.getIndex() + 1) % this.props.media.size });
this.setState({
index: (this.getIndex() + 1) % this.props.media.size,
zoomButtonHidden: true,
});
}
handlePrevClick = () => {
this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size });
this.setState({
index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
zoomButtonHidden: true,
});
}
handleChangeIndex = (e) => {
const index = Number(e.currentTarget.getAttribute('data-index'));
this.setState({ index: index % this.props.media.size });
this.setState({
index: index % this.props.media.size,
zoomButtonHidden: true,
});
}
handleKeyDown = (e) => {
@ -83,6 +104,25 @@ class MediaModal extends ImmutablePureComponent {
this.props.onClose();
});
}
this._sendBackgroundColor();
}
componentDidUpdate (prevProps, prevState) {
if (prevState.index !== this.state.index) {
this._sendBackgroundColor();
}
}
_sendBackgroundColor () {
const { media, onChangeBackgroundColor } = this.props;
const index = this.getIndex();
const blurhash = media.getIn([index, 'blurhash']);
if (blurhash) {
const backgroundColor = getAverageFromBlurhash(blurhash);
onChangeBackgroundColor(backgroundColor);
}
}
componentWillUnmount () {
@ -95,6 +135,8 @@ class MediaModal extends ImmutablePureComponent {
this.context.router.history.goBack();
}
}
this.props.onChangeBackgroundColor(null);
}
getIndex () {
@ -110,30 +152,19 @@ class MediaModal extends ImmutablePureComponent {
handleStatusClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
this.context.router.history.push(`/statuses/${this.props.statusId}`);
}
}
render () {
const { media, status, intl, onClose } = this.props;
const { media, statusId, intl, onClose } = this.props;
const { navigationHidden } = this.state;
const index = this.getIndex();
let pagination = [];
const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>;
const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>;
if (media.size > 1) {
pagination = media.map((item, i) => {
const classes = ['media-modal__button'];
if (i === index) {
classes.push('media-modal__button--active');
}
return (<li className='media-modal__page-dot' key={i}><button tabIndex='0' className={classes.join(' ')} onClick={this.handleChangeIndex} data-index={i}>{i + 1}</button></li>);
});
}
const content = media.map((image) => {
const width = image.getIn(['meta', 'original', 'width']) || null;
const height = image.getIn(['meta', 'original', 'height']) || null;
@ -148,6 +179,7 @@ class MediaModal extends ImmutablePureComponent {
alt={image.get('description')}
key={image.get('url')}
onClick={this.toggleNavigation}
zoomButtonHidden={this.state.zoomButtonHidden}
/>
);
} else if (image.get('type') === 'video') {
@ -160,7 +192,7 @@ class MediaModal extends ImmutablePureComponent {
src={image.get('url')}
width={image.get('width')}
height={image.get('height')}
startTime={time || 0}
currentTime={time || 0}
onCloseVideo={onClose}
detailed
alt={image.get('description')}
@ -200,18 +232,26 @@ class MediaModal extends ImmutablePureComponent {
'media-modal__navigation--hidden': navigationHidden,
});
let pagination;
if (media.size > 1) {
pagination = media.map((item, i) => (
<button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
{i + 1}
</button>
));
}
return (
<div className='modal-root__modal media-modal'>
<div
className='media-modal__closer'
role='presentation'
onClick={onClose}
>
<div className='media-modal__closer' role='presentation' onClick={onClose} >
<ReactSwipeableViews
style={swipeableViewsStyle}
containerStyle={containerStyle}
onChangeIndex={this.handleSwipe}
onTransitionEnd={this.handleTransitionEnd}
index={index}
disabled={disableSwiping}
>
{content}
</ReactSwipeableViews>
@ -223,15 +263,10 @@ class MediaModal extends ImmutablePureComponent {
{leftNav}
{rightNav}
{status && (
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
</div>
)}
<ul className='media-modal__pagination'>
{pagination}
</ul>
<div className='media-modal__overlay'>
{pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
</div>
</div>
</div>
);

View File

@ -45,6 +45,10 @@ export default class ModalRoot extends React.PureComponent {
onClose: PropTypes.func.isRequired,
};
state = {
backgroundColor: null,
};
getSnapshotBeforeUpdate () {
return { visible: !!this.props.type };
}
@ -59,6 +63,10 @@ export default class ModalRoot extends React.PureComponent {
}
}
setBackgroundColor = color => {
this.setState({ backgroundColor: color });
}
renderLoading = modalId => () => {
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
}
@ -71,13 +79,14 @@ export default class ModalRoot extends React.PureComponent {
render () {
const { type, props, onClose } = this.props;
const { backgroundColor } = this.state;
const visible = !!type;
return (
<Base onClose={onClose}>
<Base backgroundColor={backgroundColor} onClose={onClose}>
{visible && (
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={onClose} />}
</BundleContainer>
)}
</Base>

View File

@ -1,25 +1,32 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import Button from '../../../components/button';
import { closeModal } from '../../../actions/modal';
import { muteAccount } from '../../../actions/accounts';
import { toggleHideNotifications } from '../../../actions/mutes';
import { toggleHideNotifications, changeMuteDuration } from '../../../actions/mutes';
const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
indefinite: { id: 'mute_modal.indefinite', defaultMessage: 'Indefinite' },
});
const mapStateToProps = state => {
return {
account: state.getIn(['mutes', 'new', 'account']),
notifications: state.getIn(['mutes', 'new', 'notifications']),
muteDuration: state.getIn(['mutes', 'new', 'duration']),
};
};
const mapDispatchToProps = dispatch => {
return {
onConfirm(account, notifications) {
dispatch(muteAccount(account.get('id'), notifications));
onConfirm(account, notifications, muteDuration) {
dispatch(muteAccount(account.get('id'), notifications, muteDuration));
},
onClose() {
@ -29,6 +36,10 @@ const mapDispatchToProps = dispatch => {
onToggleNotifications() {
dispatch(toggleHideNotifications());
},
onChangeMuteDuration(e) {
dispatch(changeMuteDuration(e.target.value));
},
};
};
@ -43,6 +54,8 @@ class MuteModal extends React.PureComponent {
onConfirm: PropTypes.func.isRequired,
onToggleNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
muteDuration: PropTypes.number.isRequired,
onChangeMuteDuration: PropTypes.func.isRequired,
};
componentDidMount() {
@ -51,7 +64,7 @@ class MuteModal extends React.PureComponent {
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account, this.props.notifications);
this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
}
handleCancel = () => {
@ -66,8 +79,12 @@ class MuteModal extends React.PureComponent {
this.props.onToggleNotifications();
}
changeMuteDuration = (e) => {
this.props.onChangeMuteDuration(e);
}
render () {
const { account, notifications } = this.props;
const { account, notifications, muteDuration, intl } = this.props;
return (
<div className='modal-root__modal mute-modal'>
@ -91,6 +108,21 @@ class MuteModal extends React.PureComponent {
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
</label>
</div>
<div>
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
<select value={muteDuration} onChange={this.changeMuteDuration}>
<option value={0}>{intl.formatMessage(messages.indefinite)}</option>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
</select>
</div>
</div>
<div className='mute-modal__action-bar'>

View File

@ -3,9 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Video from 'mastodon/features/video';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash';
export const previewState = 'previewVideoModal';
@ -13,13 +12,14 @@ export default class VideoModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map,
statusId: PropTypes.string,
options: PropTypes.shape({
startTime: PropTypes.number,
autoPlay: PropTypes.bool,
defaultVolume: PropTypes.number,
}),
onClose: PropTypes.func.isRequired,
onChangeBackgroundColor: PropTypes.func.isRequired,
};
static contextTypes = {
@ -27,36 +27,35 @@ export default class VideoModal extends ImmutablePureComponent {
};
componentDidMount () {
if (this.context.router) {
const history = this.context.router.history;
const { router } = this.context;
const { media, onChangeBackgroundColor, onClose } = this.props;
history.push(history.location.pathname, previewState);
if (router) {
router.history.push(router.history.location.pathname, previewState);
this.unlistenHistory = router.history.listen(() => onClose());
}
this.unlistenHistory = history.listen(() => {
this.props.onClose();
});
const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
if (backgroundColor) {
onChangeBackgroundColor(backgroundColor);
}
}
componentWillUnmount () {
if (this.context.router) {
const { router } = this.context;
if (router) {
this.unlistenHistory();
if (this.context.router.history.location.state === previewState) {
this.context.router.history.goBack();
if (router.history.location.state === previewState) {
router.history.goBack();
}
}
}
handleStatusClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}
}
render () {
const { media, status, onClose } = this.props;
const { media, statusId, onClose } = this.props;
const options = this.props.options || {};
return (
@ -64,22 +63,21 @@ export default class VideoModal extends ImmutablePureComponent {
<div className='video-modal__container'>
<Video
preview={media.get('preview_url')}
frameRate={media.getIn(['meta', 'original', 'frame_rate'])}
blurhash={media.get('blurhash')}
src={media.get('url')}
startTime={options.startTime}
currentTime={options.startTime}
autoPlay={options.autoPlay}
defaultVolume={options.defaultVolume}
volume={options.defaultVolume}
onCloseVideo={onClose}
detailed
alt={media.get('description')}
/>
</div>
{status && (
<div className={classNames('media-modal__meta')}>
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
</div>
)}
<div className='media-modal__overlay'>
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
</div>
</div>
);
}

View File

@ -1,8 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
compress: { id: 'lightbox.compress', defaultMessage: 'Compress image view box' },
expand: { id: 'lightbox.expand', defaultMessage: 'Expand image view box' },
});
const MIN_SCALE = 1;
const MAX_SCALE = 4;
const NAV_BAR_HEIGHT = 66;
const getMidpoint = (p1, p2) => ({
x: (p1.clientX + p2.clientX) / 2,
@ -14,7 +22,77 @@ const getDistance = (p1, p2) =>
const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
export default class ZoomableImage extends React.PureComponent {
// Normalizing mousewheel speed across browsers
// copy from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
const normalizeWheel = event => {
// Reasonable defaults
const PIXEL_STEP = 10;
const LINE_HEIGHT = 40;
const PAGE_HEIGHT = 800;
let sX = 0,
sY = 0, // spinX, spinY
pX = 0,
pY = 0; // pixelX, pixelY
// Legacy
if ('detail' in event) {
sY = event.detail;
}
if ('wheelDelta' in event) {
sY = -event.wheelDelta / 120;
}
if ('wheelDeltaY' in event) {
sY = -event.wheelDeltaY / 120;
}
if ('wheelDeltaX' in event) {
sX = -event.wheelDeltaX / 120;
}
// side scrolling on FF with DOMMouseScroll
if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
sX = sY;
sY = 0;
}
pX = sX * PIXEL_STEP;
pY = sY * PIXEL_STEP;
if ('deltaY' in event) {
pY = event.deltaY;
}
if ('deltaX' in event) {
pX = event.deltaX;
}
if ((pX || pY) && event.deltaMode) {
if (event.deltaMode === 1) { // delta in LINE units
pX *= LINE_HEIGHT;
pY *= LINE_HEIGHT;
} else { // delta in PAGE units
pX *= PAGE_HEIGHT;
pY *= PAGE_HEIGHT;
}
}
// Fall-back if spin cannot be determined
if (pX && !sX) {
sX = (pX < 1) ? -1 : 1;
}
if (pY && !sY) {
sY = (pY < 1) ? -1 : 1;
}
return {
spinX: sX,
spinY: sY,
pixelX: pX,
pixelY: pY,
};
};
export default @injectIntl
class ZoomableImage extends React.PureComponent {
static propTypes = {
alt: PropTypes.string,
@ -22,6 +100,8 @@ export default class ZoomableImage extends React.PureComponent {
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool,
intl: PropTypes.object.isRequired,
}
static defaultProps = {
@ -32,6 +112,26 @@ export default class ZoomableImage extends React.PureComponent {
state = {
scale: MIN_SCALE,
zoomMatrix: {
type: null, // 'width' 'height'
fullScreen: null, // bool
rate: null, // full screen scale rate
clientWidth: null,
clientHeight: null,
offsetWidth: null,
offsetHeight: null,
clientHeightFixed: null,
scrollTop: null,
scrollLeft: null,
translateX: null,
translateY: null,
},
zoomState: 'expand', // 'expand' 'compress'
navigationHidden: false,
dragPosition: { top: 0, left: 0, x: 0, y: 0 },
dragged: false,
lockScroll: { x: 0, y: 0 },
lockTranslate: { x: 0, y: 0 },
}
removers = [];
@ -49,17 +149,105 @@ export default class ZoomableImage extends React.PureComponent {
// https://www.chromestatus.com/features/5093566007214080
this.container.addEventListener('touchmove', handler, { passive: false });
this.removers.push(() => this.container.removeEventListener('touchend', handler));
handler = this.mouseDownHandler;
this.container.addEventListener('mousedown', handler);
this.removers.push(() => this.container.removeEventListener('mousedown', handler));
handler = this.mouseWheelHandler;
this.container.addEventListener('wheel', handler);
this.removers.push(() => this.container.removeEventListener('wheel', handler));
// Old Chrome
this.container.addEventListener('mousewheel', handler);
this.removers.push(() => this.container.removeEventListener('mousewheel', handler));
// Old Firefox
this.container.addEventListener('DOMMouseScroll', handler);
this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler));
this.initZoomMatrix();
}
componentWillUnmount () {
this.removeEventListeners();
}
componentDidUpdate () {
this.setState({ zoomState: this.state.scale >= this.state.zoomMatrix.rate ? 'compress' : 'expand' });
if (this.state.scale === MIN_SCALE) {
this.container.style.removeProperty('cursor');
}
}
UNSAFE_componentWillReceiveProps () {
// reset when slide to next image
if (this.props.zoomButtonHidden) {
this.setState({
scale: MIN_SCALE,
lockTranslate: { x: 0, y: 0 },
}, () => {
this.container.scrollLeft = 0;
this.container.scrollTop = 0;
});
}
}
removeEventListeners () {
this.removers.forEach(listeners => listeners());
this.removers = [];
}
mouseWheelHandler = e => {
e.preventDefault();
const event = normalizeWheel(e);
if (this.state.zoomMatrix.type === 'width') {
// full width, scroll vertical
this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y);
} else {
// full height, scroll horizontal
this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelY, this.state.lockScroll.x);
}
// lock horizontal scroll
this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelX, this.state.lockScroll.x);
}
mouseDownHandler = e => {
this.container.style.cursor = 'grabbing';
this.container.style.userSelect = 'none';
this.setState({ dragPosition: {
left: this.container.scrollLeft,
top: this.container.scrollTop,
// Get the current mouse position
x: e.clientX,
y: e.clientY,
} });
this.image.addEventListener('mousemove', this.mouseMoveHandler);
this.image.addEventListener('mouseup', this.mouseUpHandler);
}
mouseMoveHandler = e => {
const dx = e.clientX - this.state.dragPosition.x;
const dy = e.clientY - this.state.dragPosition.y;
this.container.scrollLeft = Math.max(this.state.dragPosition.left - dx, this.state.lockScroll.x);
this.container.scrollTop = Math.max(this.state.dragPosition.top - dy, this.state.lockScroll.y);
this.setState({ dragged: true });
}
mouseUpHandler = () => {
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
this.image.removeEventListener('mousemove', this.mouseMoveHandler);
this.image.removeEventListener('mouseup', this.mouseUpHandler);
}
handleTouchStart = e => {
if (e.touches.length !== 2) return;
@ -80,7 +268,8 @@ export default class ZoomableImage extends React.PureComponent {
const distance = getDistance(...e.touches);
const midpoint = getMidpoint(...e.touches);
const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance);
const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate);
const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance);
this.zoom(scale, midpoint);
@ -89,7 +278,7 @@ export default class ZoomableImage extends React.PureComponent {
}
zoom(nextScale, midpoint) {
const { scale } = this.state;
const { scale, zoomMatrix } = this.state;
const { scrollLeft, scrollTop } = this.container;
// math memo:
@ -104,14 +293,105 @@ export default class ZoomableImage extends React.PureComponent {
this.setState({ scale: nextScale }, () => {
this.container.scrollLeft = nextScrollLeft;
this.container.scrollTop = nextScrollTop;
// reset the translateX/Y constantly
if (nextScale < zoomMatrix.rate) {
this.setState({
lockTranslate: {
x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
},
});
}
});
}
handleClick = e => {
// don't propagate event to MediaModal
e.stopPropagation();
const dragged = this.state.dragged;
this.setState({ dragged: false });
if (dragged) return;
const handler = this.props.onClick;
if (handler) handler();
this.setState({ navigationHidden: !this.state.navigationHidden });
}
handleMouseDown = e => {
e.preventDefault();
}
initZoomMatrix = () => {
const { width, height } = this.props;
const { clientWidth, clientHeight } = this.container;
const { offsetWidth, offsetHeight } = this.image;
const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT;
const type = width / height < clientWidth / clientHeightFixed ? 'width' : 'height';
const fullScreen = type === 'width' ? width > clientWidth : height > clientHeightFixed;
const rate = type === 'width' ? Math.min(clientWidth, width) / offsetWidth : Math.min(clientHeightFixed, height) / offsetHeight;
const scrollTop = type === 'width' ? (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2;
const scrollLeft = (clientWidth - offsetWidth) / 2;
const translateX = type === 'width' ? (width - offsetWidth) / (2 * rate) : 0;
const translateY = type === 'height' ? (height - offsetHeight) / (2 * rate) : 0;
this.setState({
zoomMatrix: {
type: type,
fullScreen: fullScreen,
rate: rate,
clientWidth: clientWidth,
clientHeight: clientHeight,
offsetWidth: offsetWidth,
offsetHeight: offsetHeight,
clientHeightFixed: clientHeightFixed,
scrollTop: scrollTop,
scrollLeft: scrollLeft,
translateX: translateX,
translateY: translateY,
},
});
}
handleZoomClick = e => {
e.preventDefault();
e.stopPropagation();
const { scale, zoomMatrix } = this.state;
if ( scale >= zoomMatrix.rate ) {
this.setState({
scale: MIN_SCALE,
lockScroll: {
x: 0,
y: 0,
},
lockTranslate: {
x: 0,
y: 0,
},
}, () => {
this.container.scrollLeft = 0;
this.container.scrollTop = 0;
});
} else {
this.setState({
scale: zoomMatrix.rate,
lockScroll: {
x: zoomMatrix.scrollLeft,
y: zoomMatrix.scrollTop,
},
lockTranslate: {
x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX,
y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY,
},
}, () => {
this.container.scrollLeft = zoomMatrix.scrollLeft;
this.container.scrollTop = zoomMatrix.scrollTop;
});
}
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
}
setContainerRef = c => {
@ -123,29 +403,47 @@ export default class ZoomableImage extends React.PureComponent {
}
render () {
const { alt, src } = this.props;
const { scale } = this.state;
const overflow = scale === 1 ? 'hidden' : 'scroll';
const { alt, src, width, height, intl } = this.props;
const { scale, lockTranslate } = this.state;
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
const zoomButtonTitle = this.state.zoomState === 'compress' ? intl.formatMessage(messages.compress) : intl.formatMessage(messages.expand);
return (
<div
className='zoomable-image'
ref={this.setContainerRef}
style={{ overflow }}
>
<img
role='presentation'
ref={this.setImageRef}
alt={alt}
title={alt}
src={src}
<React.Fragment>
<IconButton
className={`media-modal__zoom-button ${zoomButtonShouldHide}`}
title={zoomButtonTitle}
icon={this.state.zoomState}
onClick={this.handleZoomClick}
size={40}
style={{
transform: `scale(${scale})`,
transformOrigin: '0 0',
fontSize: '30px', /* Fontawesome's fa-compress fa-expand is larger than fa-close */
}}
onClick={this.handleClick}
/>
</div>
<div
className='zoomable-image'
ref={this.setContainerRef}
style={{ overflow }}
>
<img
role='presentation'
ref={this.setImageRef}
alt={alt}
title={alt}
src={src}
width={width}
height={height}
style={{
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
transformOrigin: '0 0',
}}
draggable={false}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
/>
</div>
</React.Fragment>
);
}

View File

@ -8,19 +8,20 @@ import PropTypes from 'prop-types';
import NotificationsContainer from './containers/notifications_container';
import LoadingBarContainer from './containers/loading_bar_container';
import ModalContainer from './containers/modal_container';
import { isMobile } from '../../is_mobile';
import { layoutFromWindow } from 'mastodon/is_mobile';
import { debounce } from 'lodash';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { expandHomeTimeline } from '../../actions/timelines';
import { expandNotifications } from '../../actions/notifications';
import { fetchFilters } from '../../actions/filters';
import { clearHeight } from '../../actions/height_cache';
import { focusApp, unfocusApp } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers } from 'mastodon/actions/markers';
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container';
import DocumentTitle from './components/document_title';
import PictureInPicture from 'mastodon/features/picture_in_picture';
import {
Compose,
Status,
@ -51,7 +52,7 @@ import {
Search,
Directory,
} from './util/async-components';
import { me, forceSingleColumn } from '../../initial_state';
import { me } from '../../initial_state';
import { previewState as previewMediaState } from './components/media_modal';
import { previewState as previewVideoState } from './components/video_modal';
@ -64,6 +65,7 @@ const messages = defineMessages({
});
const mapStateToProps = state => ({
layout: state.getIn(['meta', 'layout']),
isComposing: state.getIn(['compose', 'is_composing']),
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
@ -109,17 +111,11 @@ class SwitchingColumnsArea extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired,
};
state = {
mobile: isMobile(window.innerWidth),
mobile: PropTypes.bool,
};
componentWillMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
if (this.state.mobile || forceSingleColumn) {
if (this.props.mobile) {
document.body.classList.toggle('layout-single-column', true);
document.body.classList.toggle('layout-multiple-columns', false);
} else {
@ -128,44 +124,21 @@ class SwitchingColumnsArea extends React.PureComponent {
}
}
componentDidUpdate (prevProps, prevState) {
componentDidUpdate (prevProps) {
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
this.node.handleChildrenContentChange();
}
if (prevState.mobile !== this.state.mobile && !forceSingleColumn) {
document.body.classList.toggle('layout-single-column', this.state.mobile);
document.body.classList.toggle('layout-multiple-columns', !this.state.mobile);
if (prevProps.mobile !== this.props.mobile) {
document.body.classList.toggle('layout-single-column', this.props.mobile);
document.body.classList.toggle('layout-multiple-columns', !this.props.mobile);
}
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}
shouldUpdateScroll (_, { location }) {
return location.state !== previewMediaState && location.state !== previewVideoState;
}
handleLayoutChange = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.onLayoutChange();
}, 500, {
trailing: true,
})
handleResize = () => {
const mobile = isMobile(window.innerWidth);
if (mobile !== this.state.mobile) {
this.handleLayoutChange.cancel();
this.props.onLayoutChange();
this.setState({ mobile });
} else {
this.handleLayoutChange();
}
}
setRef = c => {
if (c) {
this.node = c.getWrappedInstance();
@ -173,13 +146,11 @@ class SwitchingColumnsArea extends React.PureComponent {
}
render () {
const { children } = this.props;
const { mobile } = this.state;
const singleColumn = forceSingleColumn || mobile;
const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
const { children, mobile } = this.props;
const redirect = mobile ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />;
return (
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
<WrappedSwitch>
{redirect}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
@ -243,6 +214,7 @@ class UI extends React.PureComponent {
location: PropTypes.object,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
layout: PropTypes.string.isRequired,
};
state = {
@ -265,17 +237,13 @@ class UI extends React.PureComponent {
handleWindowFocus = () => {
this.props.dispatch(focusApp());
this.props.dispatch(submitMarkers({ immediate: true }));
}
handleWindowBlur = () => {
this.props.dispatch(unfocusApp());
}
handleLayoutChange = () => {
// The cached heights are no longer accurate, invalidate
this.props.dispatch(clearHeight());
}
handleDragEnter = (e) => {
e.preventDefault();
@ -349,10 +317,28 @@ class UI extends React.PureComponent {
}
}
componentWillMount () {
handleLayoutChange = debounce(() => {
this.props.dispatch(clearHeight()); // The cached heights are no longer accurate, invalidate
}, 500, {
trailing: true,
});
handleResize = () => {
const layout = layoutFromWindow();
if (layout !== this.props.layout) {
this.handleLayoutChange.cancel();
this.props.dispatch(changeLayout(layout));
} else {
this.handleLayoutChange();
}
}
componentDidMount () {
window.addEventListener('focus', this.handleWindowFocus, false);
window.addEventListener('blur', this.handleWindowBlur, false);
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
window.addEventListener('resize', this.handleResize, { passive: true });
document.addEventListener('dragenter', this.handleDragEnter, false);
document.addEventListener('dragover', this.handleDragOver, false);
@ -364,19 +350,14 @@ class UI extends React.PureComponent {
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
}
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
}
this.props.dispatch(fetchMarkers());
this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications());
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
}
componentDidMount () {
this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName) && !e.altKey;
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
};
}
@ -384,6 +365,7 @@ class UI extends React.PureComponent {
window.removeEventListener('focus', this.handleWindowFocus);
window.removeEventListener('blur', this.handleWindowBlur);
window.removeEventListener('beforeunload', this.handleBeforeUnload);
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('dragenter', this.handleDragEnter);
document.removeEventListener('dragover', this.handleDragOver);
@ -514,7 +496,7 @@ class UI extends React.PureComponent {
render () {
const { draggingOver } = this.state;
const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
const { children, isComposing, location, dropdownMenuIsOpen, layout } = this.props;
const handlers = {
help: this.handleHotkeyToggleHelp,
@ -541,10 +523,11 @@ class UI extends React.PureComponent {
return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}>
<SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'}>
{children}
</SwitchingColumnsArea>
{layout !== 'mobile' && <PictureInPicture />}
<NotificationsContainer />
<LoadingBarContainer className='loading-bar' />
<ModalContainer />

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS, is } from 'immutable';
import { is } from 'immutable';
import { throttle, debounce } from 'lodash';
import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
@ -99,25 +99,32 @@ class Video extends React.PureComponent {
static propTypes = {
preview: PropTypes.string,
frameRate: PropTypes.string,
src: PropTypes.string.isRequired,
alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
sensitive: PropTypes.bool,
startTime: PropTypes.number,
currentTime: PropTypes.number,
onOpenVideo: PropTypes.func,
onCloseVideo: PropTypes.func,
detailed: PropTypes.bool,
inline: PropTypes.bool,
editable: PropTypes.bool,
alwaysVisible: PropTypes.bool,
cacheWidth: PropTypes.func,
visible: PropTypes.bool,
onToggleVisibility: PropTypes.func,
deployPictureInPicture: PropTypes.func,
intl: PropTypes.object.isRequired,
blurhash: PropTypes.string,
link: PropTypes.node,
autoPlay: PropTypes.bool,
defaultVolume: PropTypes.number,
volume: PropTypes.number,
muted: PropTypes.bool,
};
static defaultProps = {
frameRate: 25,
};
state = {
@ -195,7 +202,7 @@ class Video extends React.PureComponent {
handleTimeUpdate = () => {
this.setState({
currentTime: this.video.currentTime,
duration: Math.floor(this.video.duration),
duration:this.video.duration,
});
}
@ -263,6 +270,81 @@ class Video extends React.PureComponent {
}
}, 15);
seekBy (time) {
const currentTime = this.video.currentTime + time;
if (!isNaN(currentTime)) {
this.setState({ currentTime }, () => {
this.video.currentTime = currentTime;
});
}
}
handleVideoKeyDown = e => {
// On the video element or the seek bar, we can safely use the space bar
// for playback control because there are no buttons to press
if (e.key === ' ') {
e.preventDefault();
e.stopPropagation();
this.togglePlay();
}
}
handleKeyDown = e => {
const frameTime = 1 / this.getFrameRate();
switch(e.key) {
case 'k':
e.preventDefault();
e.stopPropagation();
this.togglePlay();
break;
case 'm':
e.preventDefault();
e.stopPropagation();
this.toggleMute();
break;
case 'f':
e.preventDefault();
e.stopPropagation();
this.toggleFullscreen();
break;
case 'j':
e.preventDefault();
e.stopPropagation();
this.seekBy(-10);
break;
case 'l':
e.preventDefault();
e.stopPropagation();
this.seekBy(10);
break;
case ',':
e.preventDefault();
e.stopPropagation();
this.seekBy(-frameTime);
break;
case '.':
e.preventDefault();
e.stopPropagation();
this.seekBy(frameTime);
break;
}
// If we are in fullscreen mode, we don't want any hotkeys
// interacting with the UI that's not visible
if (this.state.fullscreen) {
e.preventDefault();
e.stopPropagation();
if (e.key === 'Escape') {
exitFullscreen();
}
}
}
togglePlay = () => {
if (this.state.paused) {
this.setState({ paused: false }, () => this.video.play());
@ -297,6 +379,15 @@ class Video extends React.PureComponent {
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
if (!this.state.paused && this.video && this.props.deployPictureInPicture) {
this.props.deployPictureInPicture('video', {
src: this.props.src,
currentTime: this.video.currentTime,
muted: this.video.muted,
volume: this.video.volume,
});
}
}
componentWillReceiveProps (nextProps) {
@ -328,7 +419,18 @@ class Video extends React.PureComponent {
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
this.setState({ paused: true }, () => this.video.pause());
this.video.pause();
if (this.props.deployPictureInPicture) {
this.props.deployPictureInPicture('video', {
src: this.props.src,
currentTime: this.video.currentTime,
muted: this.video.muted,
volume: this.video.volume,
});
}
this.setState({ paused: true });
}
}, 150, { trailing: true })
@ -361,15 +463,21 @@ class Video extends React.PureComponent {
}
handleLoadedData = () => {
if (this.props.startTime) {
this.video.currentTime = this.props.startTime;
const { currentTime, volume, muted, autoPlay } = this.props;
if (currentTime) {
this.video.currentTime = currentTime;
}
if (this.props.defaultVolume !== undefined) {
this.video.volume = this.props.defaultVolume;
if (volume !== undefined) {
this.video.volume = volume;
}
if (this.props.autoPlay) {
if (muted !== undefined) {
this.video.muted = muted;
}
if (autoPlay) {
this.video.play();
}
}
@ -387,25 +495,13 @@ class Video extends React.PureComponent {
}
handleOpenVideo = () => {
const { src, preview, width, height, alt } = this.props;
this.video.pause();
const media = fromJS({
type: 'video',
url: src,
preview_url: preview,
description: alt,
width,
height,
});
const options = {
this.props.onOpenVideo({
startTime: this.video.currentTime,
autoPlay: !this.state.paused,
defaultVolume: this.state.volume,
};
this.video.pause();
this.props.onOpenVideo(media, options);
});
}
handleCloseVideo = () => {
@ -413,10 +509,21 @@ class Video extends React.PureComponent {
this.props.onCloseVideo();
}
getFrameRate () {
if (this.props.frameRate && isNaN(this.props.frameRate)) {
// The frame rate is returned as a fraction string so we
// need to convert it to a number
return this.props.frameRate.split('/').reduce((p, c) => p / c);
}
return this.props.frameRate;
}
render () {
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable, blurhash } = this.props;
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = (currentTime / duration) * 100;
const progress = Math.min((currentTime / duration) * 100, 100);
const playerStyle = {};
let { width, height } = this.props;
@ -430,7 +537,7 @@ class Video extends React.PureComponent {
let preload;
if (startTime || fullscreen || dragging) {
if (this.props.currentTime || fullscreen || dragging) {
preload = 'auto';
} else if (detailed) {
preload = 'metadata';
@ -455,6 +562,7 @@ class Video extends React.PureComponent {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onClick={this.handleClickRoot}
onKeyDown={this.handleKeyDown}
tabIndex={0}
>
<Blurhash
@ -478,6 +586,7 @@ class Video extends React.PureComponent {
height={height}
volume={volume}
onClick={this.togglePlay}
onKeyDown={this.handleVideoKeyDown}
onPlay={this.handlePlay}
onPause={this.handlePause}
onLoadedData={this.handleLoadedData}
@ -500,13 +609,14 @@ class Video extends React.PureComponent {
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
style={{ left: `${progress}%` }}
onKeyDown={this.handleVideoKeyDown}
/>
</div>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
<div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} />
@ -522,18 +632,16 @@ class Video extends React.PureComponent {
<span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(duration)}</span>
<span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span>
</span>
)}
{link && <span className='video-player__link'>{link}</span>}
</div>
<div className='video-player__buttons right'>
{(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
{(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} className='player-button' onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
</div>
</div>
</div>

View File

@ -25,5 +25,6 @@ export const usePendingItems = getMeta('use_pending_items');
export const showTrends = getMeta('trends');
export const title = getMeta('title');
export const cropImages = getMeta('crop_images');
export const disableSwiping = getMeta('disable_swiping');
export default initialState;

View File

@ -1,27 +1,32 @@
import detectPassiveEvents from 'detect-passive-events';
import { supportsPassiveEvents } from 'detect-passive-events';
import { forceSingleColumn } from 'mastodon/initial_state';
const LAYOUT_BREAKPOINT = 630;
export function isMobile(width) {
return width <= LAYOUT_BREAKPOINT;
export const isMobile = width => width <= LAYOUT_BREAKPOINT;
export const layoutFromWindow = () => {
if (isMobile(window.innerWidth)) {
return 'mobile';
} else if (forceSingleColumn) {
return 'single-column';
} else {
return 'multi-column';
}
};
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
let userTouching = false;
let listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
let listenerOptions = supportsPassiveEvents ? { passive: true } : false;
function touchListener() {
const touchListener = () => {
userTouching = true;
window.removeEventListener('touchstart', touchListener, listenerOptions);
}
};
window.addEventListener('touchstart', touchListener, listenerOptions);
export function isUserTouching() {
return userTouching;
}
export const isUserTouching = () => userTouching;
export function isIOS() {
return iOS;
};
export const isIOS = () => iOS;

View File

@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "تصفح المزيد على الملف التعريفي الأصلي",
"account.cancel_follow_request": "إلغاء طلب المتابَعة",
"account.direct": "رسالة خاصة إلى @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "النطاق مخفي",
"account.edit_profile": "تعديل الملف الشخصي",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "أوصِ به على صفحتك",
"account.follow": "تابِع",
"account.followers": "مُتابِعون",
@ -147,6 +149,7 @@
"emoji_button.search_results": "نتائج البحث",
"emoji_button.symbols": "رموز",
"emoji_button.travel": "الأماكن والسفر",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "ليس هناك تبويقات!",
"empty_column.account_unavailable": "الملف التعريفي غير متوفر",
"empty_column.blocks": "لم تقم بحظر أي مستخدِم بعد.",
@ -166,7 +169,9 @@
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
"empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات",
"error.unexpected_crash.explanation": "نظرا لوجود خطأ في التعليمات البرمجية أو مشكلة توافق مع المتصفّح، تعذر عرض هذه الصفحة بشكل صحيح.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "حاول إعادة إنعاش الصفحة. إن لم تُحلّ المشكلة ، يمكنك دائمًا استخدام ماستدون عبر متصفّح آخر أو تطبيق أصلي.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "انسخ تتبع الارتباطات إلى الحافظة",
"errors.unexpected_crash.report_issue": "الإبلاغ عن خلل",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "لإلغاء التركيز على حقل النص أو نافذة البحث",
"keyboard_shortcuts.up": "للانتقال إلى أعلى القائمة",
"lightbox.close": "إغلاق",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "التالي",
"lightbox.previous": "العودة",
"lightbox.view_context": "اعرض السياق",
"lists.account.add": "أضف إلى القائمة",
"lists.account.remove": "احذف من القائمة",
"lists.delete": "احذف القائمة",
@ -265,6 +271,10 @@
"lists.edit.submit": "تعديل العنوان",
"lists.new.create": "إنشاء قائمة",
"lists.new.title_placeholder": "عنوان القائمة الجديدة",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
"lists.subheading": "قوائمك",
"load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "غير موجود",
"missing_indicator.sublabel": "تعذر العثور على هذا المورد",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "تطبيقات الأجهزة المحمولة",
"navigation_bar.blocks": "الحسابات المحجوبة",
"navigation_bar.bookmarks": "الفواصل المرجعية",
@ -303,6 +315,7 @@
"notification.own_poll": "انتهى استطلاعك للرأي",
"notification.poll": "لقد إنتها تصويت شاركت فيه",
"notification.reblog": "{name} قام بترقية تبويقك",
"notification.status": "{name} just posted",
"notifications.clear": "امسح الإخطارات",
"notifications.clear_confirmation": "أمتأكد من أنك تود مسح جل الإخطارات الخاصة بك و المتلقاة إلى حد الآن ؟",
"notifications.column_settings.alert": "إشعارات سطح المكتب",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "الترقيّات:",
"notifications.column_settings.show": "اعرِضها في عمود",
"notifications.column_settings.sound": "أصدر صوتا",
"notifications.column_settings.status": "تبويقات جديدة:",
"notifications.filter.all": "الكل",
"notifications.filter.boosts": "الترقيات",
"notifications.filter.favourites": "المفضلة",
"notifications.filter.follows": "يتابِع",
"notifications.filter.mentions": "الإشارات",
"notifications.filter.polls": "نتائج استطلاع الرأي",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} إشعارات",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "انتهى",
"poll.refresh": "تحديث",
"poll.total_people": "{count, plural, one {# شخص} two {# شخصين} few {# أشخاص} many {# أشخاص} other {# أشخاص}}",
@ -436,7 +460,7 @@
"units.short.million": "{count} مليون",
"units.short.thousand": "{count} ألف",
"upload_area.title": "اسحب ثم أفلت للرفع",
"upload_button.label": "إضافة وسائط ({formats})",
"upload_button.label": "إضافة وسائط",
"upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.",
"upload_error.poll": "لا يمكن إدراج ملفات في استطلاعات الرأي.",
"upload_form.audio_description": "وصف للأشخاص ذي قِصر السمع",
@ -452,6 +476,7 @@
"upload_modal.detect_text": "اكتشف النص مِن الصورة",
"upload_modal.edit_media": "تعديل الوسائط",
"upload_modal.hint": "اضغط أو اسحب الدائرة على خانة المعاينة لاختيار نقطة التركيز التي ستُعرَض دائمًا على كل المصغرات.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "معاينة ({ratio})",
"upload_progress.label": "يرفع...",
"video.close": "إغلاق الفيديو",

View File

@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Encaboxar la solicitú de siguimientu",
"account.direct": "Unviar un mensaxe direutu a @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Dominiu anubríu",
"account.edit_profile": "Editar el perfil",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Destacar nel perfil",
"account.follow": "Siguir",
"account.followers": "Siguidores",
@ -96,9 +98,9 @@
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Barritar",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
"compose_form.spoiler.marked": "El testu nun va anubrise darrera d'una alvertencia",
"compose_form.spoiler.unmarked": "El testu nun va anubrise",
"compose_form.spoiler_placeholder": "Escribi equí l'alvertencia",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viaxes y llugares",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "¡Equí nun hai barritos!",
"empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "Entá nun bloquiesti a nunengún usuariu.",
@ -166,7 +169,9 @@
"empty_column.notifications": "Entá nun tienes nunengún avisu. Interactúa con otros p'aniciar la conversación.",
"empty_column.public": "¡Equí nun hai nada! Escribi daqué público o sigui a usuarios d'otros sirvidores pa rellenar esto",
"error.unexpected_crash.explanation": "Pola mor d'un fallu nel códigu o un problema de compatibilidá del restolador, esta páxina nun pudo amosase correutamente.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "pa desenfocar l'área de composición/gueta",
"keyboard_shortcuts.up": "pa xubir na llista",
"lightbox.close": "Close",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Siguiente",
"lightbox.previous": "Previous",
"lightbox.view_context": "View context",
"lists.account.add": "Amestar a la llista",
"lists.account.remove": "Desaniciar de la llista",
"lists.delete": "Desaniciar la llista",
@ -265,6 +271,10 @@
"lists.edit.submit": "Change title",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "Títulu nuevu de la llista",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "Guetar ente la xente que sigues",
"lists.subheading": "Les tos llistes",
"load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Alternar la visibilidá",
"missing_indicator.label": "Nun s'alcontró",
"missing_indicator.sublabel": "Esti recursu nun pudo alcontrase",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "¿Anubrir los avisos d'esti usuariu?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "Aplicaciones pa móviles",
"navigation_bar.blocks": "Usuarios bloquiaos",
"navigation_bar.bookmarks": "Marcadores",
@ -303,6 +315,7 @@
"notification.own_poll": "Your poll has ended",
"notification.poll": "Finó una encuesta na que votesti",
"notification.reblog": "{name} compartió'l to estáu",
"notification.status": "{name} just posted",
"notifications.clear": "Llimpiar avisos",
"notifications.clear_confirmation": "¿De xuru que quies llimpiar dafechu tolos avisos?",
"notifications.column_settings.alert": "Avisos d'escritoriu",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Barritos compartíos:",
"notifications.column_settings.show": "Amosar en columna",
"notifications.column_settings.sound": "Reproducir un soníu",
"notifications.column_settings.status": "New toots:",
"notifications.filter.all": "Too",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Menciones",
"notifications.filter.polls": "Poll results",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} avisos",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Acabó",
"poll.refresh": "Refresh",
"poll.total_people": "{count, plural, one {# persona} other {# persones}}",
@ -451,6 +475,7 @@
"upload_modal.detect_text": "Deteutar el testu de la semeya",
"upload_modal.edit_media": "Edición",
"upload_modal.hint": "Calca o arrastra'l círculu de la previsualización pa escoyer el puntu d'enfoque que va amosase siempres en toles miniatures.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "Previsualización ({ratio})",
"upload_progress.label": "Xubiendo…",
"video.close": "Zarrar el videu",

View File

@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Откажи искането за следване",
"account.direct": "Direct Message @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Скрит домейн",
"account.edit_profile": "Редактирай профила си",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Характеристика на профила",
"account.follow": "Последвай",
"account.followers": "Последователи",
@ -96,9 +98,9 @@
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Раздумай",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Content warning",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "No toots here!",
"empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "You haven't blocked any users yet.",
@ -166,7 +169,9 @@
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Затвори",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lightbox.view_context": "View context",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.delete": "Delete list",
@ -265,6 +271,10 @@
"lists.edit.submit": "Change title",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Hide {number, plural, one {image} other {images}}",
"missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Hide notifications from this user?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "Mobile apps",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.bookmarks": "Bookmarks",
@ -303,6 +315,7 @@
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} сподели твоята публикация",
"notification.status": "{name} just posted",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.alert": "Десктоп известия",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Споделяния:",
"notifications.column_settings.show": "Покажи в колона",
"notifications.column_settings.sound": "Play sound",
"notifications.column_settings.status": "New toots:",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.polls": "Poll results",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} notifications",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Closed",
"poll.refresh": "Refresh",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
@ -395,7 +419,7 @@
"status.pinned": "Pinned toot",
"status.read_more": "Read more",
"status.reblog": "Споделяне",
"status.reblog_private": "Boost to original audience",
"status.reblog_private": "Boost with original visibility",
"status.reblogged_by": "{name} сподели",
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft",
@ -452,6 +476,7 @@
"upload_modal.detect_text": "Detect text from picture",
"upload_modal.edit_media": "Edit media",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Uploading…",
"video.close": "Close video",

View File

@ -1,22 +1,24 @@
{
"account.account_note_header": "Note",
"account.account_note_header": "নোট",
"account.add_or_remove_from_list": "তালিকাতে যুক্ত বা অপসারণ করুন",
"account.badges.bot": "বট",
"account.badges.group": "Group",
"account.badges.group": "গ্রুপ",
"account.block": "@{name} কে ব্লক করুন",
"account.block_domain": "{domain} থেকে সব আড়াল করুন",
"account.blocked": "অবরুদ্ধ",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.browse_more_on_origin_server": "মূল প্রোফাইলটিতে আরও ব্রাউজ করুন",
"account.cancel_follow_request": "অনুসরণ অনুরোধ বাতিল করুন",
"account.direct": "@{name} কে সরাসরি বার্তা",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "ডোমেন গোপন করুন",
"account.edit_profile": "প্রোফাইল পরিবর্তন করুন",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "নিজের পাতায় দেখান",
"account.follow": "অনুসরণ করুন",
"account.followers": "অনুসরণকারী",
"account.followers.empty": "এই সদস্যকে এখনো কেউ অনুসরণ করে না।.",
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
"account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
"account.followers_counter": "{count, plural,one {{counter} জন অনুসরণকারী } other {{counter} জন অনুসরণকারী}}",
"account.following_counter": "{count, plural,one {{counter} জনকে অনুসরণ} other {{counter} জনকে অনুসরণ}}",
"account.follows.empty": "এই সদস্য কাওকে এখনো অনুসরণ করেন না.",
"account.follows_you": "আপনাকে অনুসরণ করে",
"account.hide_reblogs": "@{name}'র সমর্থনগুলি লুকিয়ে ফেলুন",
@ -36,19 +38,19 @@
"account.requested": "অনুমতির অপেক্ষা। অনুসরণ করার অনুরোধ বাতিল করতে এখানে ক্লিক করুন",
"account.share": "@{name} র প্রোফাইল অন্যদের দেখান",
"account.show_reblogs": "@{name} র সমর্থনগুলো দেখান",
"account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
"account.statuses_counter": "{count, plural,one {{counter} টুট} other {{counter} টুট}}",
"account.unblock": "@{name} র কার্যকলাপ দেখুন",
"account.unblock_domain": "{domain} কে আবার দেখুন",
"account.unendorse": "আপনার নিজের পাতায় এটা দেখবেন না",
"account.unfollow": "অনুসরণ না করতে",
"account.unmute": "@{name} র কার্যকলাপ আবার দেখুন",
"account.unmute_notifications": "@{name} র প্রজ্ঞাপন দেখুন",
"account_note.placeholder": "Click to add a note",
"account_note.placeholder": "নোট যোগ করতে ক্লিক করুন",
"alert.rate_limited.message": "{retry_time, time, medium} -এর পরে আবার প্রচেষ্টা করুন।",
"alert.rate_limited.title": "হার সীমিত",
"alert.unexpected.message": "সমস্যা অপ্রত্যাশিত.",
"alert.unexpected.title": "ওহো!",
"announcement.announcement": "Announcement",
"announcement.announcement": "ঘোষণা",
"autosuggest_hashtag.per_week": "প্রতি সপ্তাহে {count}",
"boost_modal.combo": "পরেরবার আপনি {combo} টিপলে এটি আর আসবে না",
"bundle_column_error.body": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।.",
@ -58,7 +60,7 @@
"bundle_modal_error.message": "এই অংশটি দেখাতে যেয়ে কোনো সমস্যা হয়েছে।.",
"bundle_modal_error.retry": "আবার চেষ্টা করুন",
"column.blocks": "যাদের ব্লক করা হয়েছে",
"column.bookmarks": "Bookmarks",
"column.bookmarks": "বুকমার্ক",
"column.community": "স্থানীয় সময়সারি",
"column.direct": "সরাসরি লেখা",
"column.directory": "প্রোফাইল ব্রাউজ করুন",
@ -79,9 +81,9 @@
"column_header.show_settings": "সেটিং দেখান",
"column_header.unpin": "পিন খুলুন",
"column_subheading.settings": "সেটিং",
"community.column_settings.local_only": "Local only",
"community.column_settings.local_only": "শুধুমাত্র স্থানীয়",
"community.column_settings.media_only": "শুধুমাত্র ছবি বা ভিডিও",
"community.column_settings.remote_only": "Remote only",
"community.column_settings.remote_only": "শুধুমাত্র দূরবর্তী",
"compose_form.direct_message_warning": "শুধুমাত্র যাদেরকে উল্লেখ করা হয়েছে তাদেরকেই এই টুটটি পাঠানো হবে ।",
"compose_form.direct_message_warning_learn_more": "আরো জানুন",
"compose_form.hashtag_warning": "কোনো হ্যাশট্যাগের ভেতরে এই টুটটি থাকবেনা কারণ এটি তালিকাবহির্ভূত। শুধুমাত্র প্রকাশ্য ঠোটগুলো হ্যাশট্যাগের ভেতরে খুঁজে পাওয়া যাবে।",
@ -92,8 +94,8 @@
"compose_form.poll.duration": "ভোটগ্রহনের সময়",
"compose_form.poll.option_placeholder": "বিকল্প {number}",
"compose_form.poll.remove_option": "এই বিকল্পটি মুছে ফেলুন",
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.poll.switch_to_multiple": "একাধিক পছন্দ অনুমতি দেওয়ার জন্য পোল পরিবর্তন করুন",
"compose_form.poll.switch_to_single": "একটি একক পছন্দের অনুমতি দেওয়ার জন্য পোল পরিবর্তন করুন",
"compose_form.publish": "টুট",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে",
@ -147,10 +149,11 @@
"emoji_button.search_results": "খোঁজার ফলাফল",
"emoji_button.symbols": "প্রতীক",
"emoji_button.travel": "ভ্রমণ এবং স্থান",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "এখানে কোনো টুট নেই!",
"empty_column.account_unavailable": "নিজস্ব পাতা নেই",
"empty_column.blocks": "আপনি কোনো ব্যবহারকারীদের ব্লক করেন নি।",
"empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
"empty_column.bookmarked_statuses": "আপনার কাছে এখনও কোনও বুকমার্কড টুট নেই। আপনি যখন একটি বুকমার্ক করেন, এটি এখানে প্রদর্শিত হবে।",
"empty_column.community": "স্থানীয় সময়রেখাতে কিছু নেই। প্রকাশ্যভাবে কিছু লিখে লেখালেখির উদ্বোধন করে ফেলুন!",
"empty_column.direct": "আপনার কাছে সরাসরি পাঠানো কোনো লেখা নেই। যদি কেও পাঠায়, সেটা এখানে দেখা যাবে।",
"empty_column.domain_blocks": "এখনও কোনও লুকানো ডোমেন নেই।",
@ -166,13 +169,15 @@
"empty_column.notifications": "আপনার এখনো কোনো প্রজ্ঞাপন নেই। কথোপকথন শুরু করতে, অন্যদের সাথে মেলামেশা করতে পারেন।",
"empty_column.public": "এখানে এখনো কিছু নেই! প্রকাশ্য ভাবে কিছু লিখুন বা অন্য সার্ভার থেকে কাওকে অনুসরণ করে এই জায়গা ভরে ফেলুন",
"error.unexpected_crash.explanation": "আমাদের কোড বা ব্রাউজারের সামঞ্জস্য ইস্যুতে একটি বাগের কারণে এই পৃষ্ঠাটি সঠিকভাবে প্রদর্শিত করা যায় নি।",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "পাতাটি রিফ্রেশ করে চেষ্টা করুন। তবুও যদি না হয়, তবে আপনি অন্য একটি ব্রাউজার অথবা আপনার ডিভাইসের জন্যে এপের মাধ্যমে মাস্টডন ব্যাবহার করতে পারবেন।.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "স্টেকট্রেস ক্লিপবোর্ডে কপি করুন",
"errors.unexpected_crash.report_issue": "সমস্যার প্রতিবেদন করুন",
"follow_request.authorize": "অনুমতি দিন",
"follow_request.reject": "প্রত্যাখ্যান করুন",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"generic.saved": "Saved",
"follow_requests.unlocked_explanation": "আপনার অ্যাকাউন্টটি লক না থাকলেও, {domain} কর্মীরা ভেবেছিলেন যে আপনি এই অ্যাকাউন্টগুলি থেকে ম্যানুয়ালি অনুসরণের অনুরোধগুলি পর্যালোচনা করতে চাইতে পারেন।",
"generic.saved": "সংরক্ষণ হয়েছে",
"getting_started.developers": "তৈরিকারকদের জন্য",
"getting_started.directory": "নিজস্ব-পাতাগুলির তালিকা",
"getting_started.documentation": "নথিপত্র",
@ -193,8 +198,8 @@
"home.column_settings.basic": "সাধারণ",
"home.column_settings.show_reblogs": "সমর্থনগুলো দেখান",
"home.column_settings.show_replies": "মতামত দেখান",
"home.hide_announcements": "Hide announcements",
"home.show_announcements": "Show announcements",
"home.hide_announcements": "ঘোষণা লুকান",
"home.show_announcements": "ঘোষণা দেখান",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}",
"intervals.full.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}}",
@ -236,13 +241,13 @@
"keyboard_shortcuts.muted": "বন্ধ করা ব্যবহারকারীদের তালিকা খুলতে",
"keyboard_shortcuts.my_profile": "আপনার নিজের পাতা দেখতে",
"keyboard_shortcuts.notifications": "প্রজ্ঞাপনের কলাম খুলতে",
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.open_media": "মিডিয়া খলার জন্য",
"keyboard_shortcuts.pinned": "পিন দেওয়া টুটের তালিকা খুলতে",
"keyboard_shortcuts.profile": "লেখকের পাতা দেখতে",
"keyboard_shortcuts.reply": "মতামত দিতে",
"keyboard_shortcuts.requests": "অনুসরণ অনুরোধের তালিকা দেখতে",
"keyboard_shortcuts.search": "খোঁজার অংশে ফোকাস করতে",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.spoilers": "CW ক্ষেত্র দেখাবার/লুকবার জন্য",
"keyboard_shortcuts.start": "\"প্রথম শুরুর\" কলাম বের করতে",
"keyboard_shortcuts.toggle_hidden": "CW লেখা দেখতে বা লুকাতে",
"keyboard_shortcuts.toggle_sensitivity": "ভিডিও/ছবি দেখতে বা বন্ধ করতে",
@ -250,9 +255,10 @@
"keyboard_shortcuts.unfocus": "লেখা বা খোঁজার জায়গায় ফোকাস না করতে",
"keyboard_shortcuts.up": "তালিকার উপরের দিকে যেতে",
"lightbox.close": "বন্ধ",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "পরবর্তী",
"lightbox.previous": "পূর্ববর্তী",
"lightbox.view_context": "প্রসঙ্গটি দেখতে",
"lists.account.add": "তালিকাতে যুক্ত করতে",
"lists.account.remove": "তালিকা থেকে বাদ দিতে",
"lists.delete": "তালিকা মুছে ফেলতে",
@ -260,6 +266,10 @@
"lists.edit.submit": "শিরোনাম সম্পাদনা করতে",
"lists.new.create": "তালিকাতে যুক্ত করতে",
"lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
"lists.subheading": "আপনার তালিকা",
"load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
@ -267,10 +277,12 @@
"media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
"missing_indicator.label": "খুঁজে পাওয়া যায়নি",
"missing_indicator.sublabel": "জিনিসটা খুঁজে পাওয়া যায়নি",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "এই ব্যবহারকারীর প্রজ্ঞাপন বন্ধ করবেন ?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "মোবাইলের আপ্প",
"navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
"navigation_bar.bookmarks": "Bookmarks",
"navigation_bar.bookmarks": "বুকমার্ক",
"navigation_bar.community_timeline": "স্থানীয় সময়রেখা",
"navigation_bar.compose": "নতুন টুট লিখুন",
"navigation_bar.direct": "সরাসরি লেখাগুলি",
@ -293,11 +305,12 @@
"navigation_bar.security": "নিরাপত্তা",
"notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন",
"notification.follow": "{name} আপনাকে অনুসরণ করেছেন",
"notification.follow_request": "{name} has requested to follow you",
"notification.follow_request": "{name} আপনাকে অনুসরণ করার জন্য অনুরধ করেছে",
"notification.mention": "{name} আপনাকে উল্লেখ করেছেন",
"notification.own_poll": "আপনার পোল শেষ হয়েছে",
"notification.poll": "আপনি ভোট দিয়েছিলেন এমন এক নির্বাচনের ভোটের সময় শেষ হয়েছে",
"notification.reblog": "{name} আপনার কার্যক্রমে সমর্থন দেখিয়েছেন",
"notification.status": "{name} just posted",
"notifications.clear": "প্রজ্ঞাপনগুলো মুছে ফেলতে",
"notifications.clear_confirmation": "আপনি কি নির্চিত প্রজ্ঞাপনগুলো মুছে ফেলতে চান ?",
"notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপনগুলি",
@ -306,20 +319,31 @@
"notifications.column_settings.filter_bar.category": "সংক্ষিপ্ত ছাঁকনি অংশ",
"notifications.column_settings.filter_bar.show": "দেখানো",
"notifications.column_settings.follow": "নতুন অনুসরণকারীরা:",
"notifications.column_settings.follow_request": "New follow requests:",
"notifications.column_settings.follow_request": "অনুসরণের অনুরোধগুলি:",
"notifications.column_settings.mention": "প্রজ্ঞাপনগুলো:",
"notifications.column_settings.poll": "নির্বাচনের ফলাফল:",
"notifications.column_settings.push": "পুশ প্রজ্ঞাপনগুলি",
"notifications.column_settings.reblog": "সমর্থনগুলো:",
"notifications.column_settings.show": "কলামে দেখানো",
"notifications.column_settings.sound": "শব্দ বাজানো",
"notifications.column_settings.status": "New toots:",
"notifications.filter.all": "সব",
"notifications.filter.boosts": "সমর্থনগুলো",
"notifications.filter.favourites": "পছন্দের গুলো",
"notifications.filter.follows": "অনুসরণের",
"notifications.filter.mentions": "উল্লেখিত",
"notifications.filter.polls": "নির্বাচনের ফলাফল",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} প্রজ্ঞাপন",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "বন্ধ",
"poll.refresh": "বদলেছে কিনা দেখতে",
"poll.total_people": "{count, plural, one {# ব্যক্তি} other {# ব্যক্তি}}",
@ -345,7 +369,7 @@
"relative_time.just_now": "এখন",
"relative_time.minutes": "{number}মিঃ",
"relative_time.seconds": "{number} সেকেন্ড",
"relative_time.today": "today",
"relative_time.today": "আজ",
"reply_indicator.cancel": "বাতিল করতে",
"report.forward": "এটা আরো পাঠান {target} তে",
"report.forward_hint": "এই নিবন্ধনটি অন্য একটি সার্ভারে। অপ্রকাশিতনামাভাবে রিপোর্টের কপি সেখানেও কি পাঠাতে চান ?",
@ -368,7 +392,7 @@
"status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন",
"status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন",
"status.block": "@{name} কে ব্লক করুন",
"status.bookmark": "Bookmark",
"status.bookmark": "বুকমার্ক",
"status.cancel_reblog_private": "সমর্থন বাতিল করতে",
"status.cannot_reblog": "এটিতে সমর্থন দেওয়া যাবেনা",
"status.copy": "লেখাটির লিংক কপি করতে",
@ -393,7 +417,7 @@
"status.reblogged_by": "{name} সমর্থন দিয়েছে",
"status.reblogs.empty": "এখনো কেও এটাতে সমর্থন দেয়নি। যখন কেও দেয়, সেটা তখন এখানে দেখা যাবে।",
"status.redraft": "মুছে আবার নতুন করে লিখতে",
"status.remove_bookmark": "Remove bookmark",
"status.remove_bookmark": "বুকমার্ক সরান",
"status.reply": "মতামত জানাতে",
"status.replyAll": "লেখাযুক্ত সবার কাছে মতামত জানাতে",
"status.report": "@{name} কে রিপোর্ট করতে",
@ -419,33 +443,34 @@
"time_remaining.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}} বাকি আছে",
"time_remaining.moments": "সময় বাকি আছে",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} talking",
"timeline_hint.remote_resource_not_displayed": "অন্য সার্ভারগুলি থেকে {resource} দেখাচ্ছে না। ",
"timeline_hint.resources.followers": "অনুসরকারীরা",
"timeline_hint.resources.follows": "অনুসরণ করে",
"timeline_hint.resources.statuses": "পুরনো টুটগুলি",
"trends.counter_by_accounts": "{count, plural,one {{counter} জন ব্যক্তি} other {{counter} জন লোক}} কথা বলছে",
"trends.trending_now": "বর্তমানে জনপ্রিয়",
"ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"units.short.billion": "{count}বিলিয়ন",
"units.short.million": "{count}মিলিওন",
"units.short.thousand": "{count}হাজার",
"upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে",
"upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।",
"upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।",
"upload_form.audio_description": "Describe for people with hearing loss",
"upload_form.audio_description": "শ্রবণশক্তি লোকদের জন্য বর্ণনা করুন",
"upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে",
"upload_form.edit": "সম্পাদন",
"upload_form.thumbnail": "Change thumbnail",
"upload_form.thumbnail": "থাম্বনেল পরিবর্তন করুন",
"upload_form.undo": "মুছে ফেলতে",
"upload_form.video_description": "Describe for people with hearing loss or visual impairment",
"upload_form.video_description": "শ্রবণশক্তি হ্রাস বা চাক্ষুষ প্রতিবন্ধী ব্যক্তিদের জন্য বর্ণনা করুন",
"upload_modal.analyzing_picture": "চিত্র বিশ্লেষণ করা হচ্ছে…",
"upload_modal.apply": "প্রয়োগ করুন",
"upload_modal.choose_image": "Choose image",
"upload_modal.choose_image": "ছবি নির্বাচন করুন",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
"upload_modal.detect_text": "ছবি থেকে পাঠ্য সনাক্ত করুন",
"upload_modal.edit_media": "মিডিয়া সম্পাদনা করুন",
"upload_modal.hint": "একটি দৃশ্যমান পয়েন্ট নির্বাচন করুন ক্লিক অথবা টানার মাধ্যমে যেটি সবময় সব থাম্বনেলে দেখা যাবে।",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "পূর্বরূপ({ratio})",
"upload_progress.label": "যুক্ত করতে পাঠানো হচ্ছে...",
"video.close": "ভিডিওটি বন্ধ করতে",

View File

@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Nullañ ar bedadenn heuliañ",
"account.direct": "Kas ur gemennadenn da @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Domani berzet",
"account.edit_profile": "Aozañ ar profil",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Lakaat war-wel war ar profil",
"account.follow": "Heuliañ",
"account.followers": "Heulier·ezed·ien",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Disoc'hoù an enklask",
"emoji_button.symbols": "Arouezioù",
"emoji_button.travel": "Lec'hioù ha Beajoù",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "Toud ebet amañ!",
"empty_column.account_unavailable": "Profil dihegerz",
"empty_column.blocks": "N'eus ket bet berzet implijer·ez ganeoc'h c'hoazh.",
@ -166,7 +169,9 @@
"empty_column.notifications": "N'ho peus kemenn ebet c'hoazh. Grit gant implijer·ezed·ien all evit loc'hañ ar gomz.",
"empty_column.public": "N'eus netra amañ! Skrivit un dra bennak foran pe heuilhit implijer·ien·ezed eus dafariadoù all evit leuniañ",
"error.unexpected_crash.explanation": "Abalamour d'ur beug en hor c'hod pe d'ur gudenn geverlec'hded n'hallomp ket skrammañ ar bajenn-mañ en un doare dereat.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Klaskit azbevaat ar bajenn. Ma n'a ket en-dro e c'hallit klask ober gant Mastodon dre ur merdeer disheñvel pe dre an arload genidik.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Eilañ ar roudoù diveugañ er golver",
"errors.unexpected_crash.report_issue": "Danevellañ ur fazi",
"follow_request.authorize": "Aotren",
@ -250,9 +255,10 @@
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Serriñ",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Da-heul",
"lightbox.previous": "A-raok",
"lightbox.view_context": "Diskouez ar c'hemperzh",
"lists.account.add": "Ouzhpennañ d'al listenn",
"lists.account.remove": "Lemel kuit eus al listenn",
"lists.delete": "Dilemel al listenn",
@ -260,6 +266,10 @@
"lists.edit.submit": "Cheñch an titl",
"lists.new.create": "Ouzhpennañ ul listenn",
"lists.new.title_placeholder": "Titl nevez al listenn",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "Search among people you follow",
"lists.subheading": "Ho listennoù",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
@ -267,7 +277,9 @@
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Digavet",
"missing_indicator.sublabel": "This resource could not be found",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Hide notifications from this user?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "Arloadoù pellgomz",
"navigation_bar.blocks": "Implijer·ezed·ien berzet",
"navigation_bar.bookmarks": "Sinedoù",
@ -292,12 +304,13 @@
"navigation_bar.public_timeline": "Red-amzer kevreet",
"navigation_bar.security": "Diogelroez",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.follow": "heuliañ a ra {name} ac'hanoc'h",
"notification.follow_request": "{name} has requested to follow you",
"notification.mention": "{name} mentioned you",
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your status",
"notification.status": "{name} just posted",
"notifications.clear": "Skarzhañ ar c'hemennoù",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.alert": "Kemennoù war ar burev",
@ -313,13 +326,24 @@
"notifications.column_settings.reblog": "Skignadennoù:",
"notifications.column_settings.show": "Diskouez er bann",
"notifications.column_settings.sound": "Seniñ",
"notifications.column_settings.status": "New toots:",
"notifications.filter.all": "Pep tra",
"notifications.filter.boosts": "Skignadennoù",
"notifications.filter.favourites": "Muiañ-karet",
"notifications.filter.follows": "Heuliañ",
"notifications.filter.mentions": "Menegoù",
"notifications.filter.polls": "Disoc'hoù ar sontadegoù",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} a gemennoù",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Serret",
"poll.refresh": "Azbevaat",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
@ -389,7 +413,7 @@
"status.pinned": "Toud spilhennet",
"status.read_more": "Lenn muioc'h",
"status.reblog": "Skignañ",
"status.reblog_private": "Boost to original audience",
"status.reblog_private": "Boost with original visibility",
"status.reblogged_by": "{name} boosted",
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft",
@ -430,7 +454,7 @@
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Ouzhpennañ ur media ({formats})",
"upload_button.label": "Ouzhpennañ ur media",
"upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people with hearing loss",
@ -446,6 +470,7 @@
"upload_modal.detect_text": "Dinoiñ testenn diouzh ar skeudenn",
"upload_modal.edit_media": "Embann ar media",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "Rakwel ({ratio})",
"upload_progress.label": "O pellgargañ...",
"video.close": "Serriñ ar video",

View File

@ -1,5 +1,5 @@
{
"account.account_note_header": "La teva nota per a @{name}",
"account.account_note_header": "Nota",
"account.add_or_remove_from_list": "Afegir o Treure de les llistes",
"account.badges.bot": "Bot",
"account.badges.group": "Grup",
@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Navega més en el perfil original",
"account.cancel_follow_request": "Anul·la la sol·licitud de seguiment",
"account.direct": "Missatge directe @{name}",
"account.disable_notifications": "Deixa de notificar-me els tuts de @{name}",
"account.domain_blocked": "Domini ocult",
"account.edit_profile": "Edita el perfil",
"account.enable_notifications": "Notificam els tuts de @{name}",
"account.endorse": "Recomana en el teu perfil",
"account.follow": "Segueix",
"account.followers": "Seguidors",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Resultats de la cerca",
"emoji_button.symbols": "Símbols",
"emoji_button.travel": "Viatges i Llocs",
"empty_column.account_suspended": "Compte suspès",
"empty_column.account_timeline": "No hi ha tuts aquí!",
"empty_column.account_unavailable": "Perfil no disponible",
"empty_column.blocks": "Encara no has bloquejat cap usuari.",
@ -166,7 +169,9 @@
"empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
"empty_column.public": "No hi ha res aquí! Escriu públicament alguna cosa o manualment segueix usuaris d'altres servidors per omplir-ho",
"error.unexpected_crash.explanation": "A causa d'un bug en el nostre codi o un problema de compatibilitat del navegador, aquesta pàgina podria no ser mostrada correctament.",
"error.unexpected_crash.next_steps": "Prova recarregant la pàgina. Si això no ajuda, encara podries ser capaç d'utilitzar Mastodon a través d'un navegador diferent o amb una app nativa.",
"error.unexpected_crash.explanation_addons": "Aquesta pàgina podria no mostrar-se correctament. Aquest error és possiblement causat per una extensió del navegador o per eienes automàtiques de traducció.",
"error.unexpected_crash.next_steps": "Prova recarregant la pàgina. Si això no ajuda, encara podries ser capaç d'utilitzar Mastodon a través d'un navegador diferent o amb una aplicació nativa.",
"error.unexpected_crash.next_steps_addons": "Prova de desactivar-les i refrescant la pàgina. Si això no ajuda, encara pots ser capaç dutilitzar Mastodon amb un altre navegador o aplicació nativa.",
"errors.unexpected_crash.copy_stacktrace": "Còpia stacktrace al porta-retalls",
"errors.unexpected_crash.report_issue": "Informa d'un problema",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "descentrar l'àrea de composició de text/cerca",
"keyboard_shortcuts.up": "moure amunt en la llista",
"lightbox.close": "Tancar",
"lightbox.compress": "Quadre de visualització dimatge comprimida",
"lightbox.expand": "Amplia el quadre de visualització de limatge",
"lightbox.next": "Següent",
"lightbox.previous": "Anterior",
"lightbox.view_context": "Veure el context",
"lists.account.add": "Afegir a la llista",
"lists.account.remove": "Treure de la llista",
"lists.delete": "Esborrar llista",
@ -265,6 +271,10 @@
"lists.edit.submit": "Canvi de títol",
"lists.new.create": "Afegir llista",
"lists.new.title_placeholder": "Nova llista",
"lists.replies_policy.followed": "Qualsevol usuari seguit",
"lists.replies_policy.list": "Membres de la llista",
"lists.replies_policy.none": "Ningú",
"lists.replies_policy.title": "Mostra respostes a:",
"lists.search": "Cercar entre les persones que segueixes",
"lists.subheading": "Les teves llistes",
"load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Alternar visibilitat",
"missing_indicator.label": "No trobat",
"missing_indicator.sublabel": "Aquest recurs no pot ser trobat",
"mute_modal.duration": "Durada",
"mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?",
"mute_modal.indefinite": "Indefinit",
"navigation_bar.apps": "Apps mòbils",
"navigation_bar.blocks": "Usuaris bloquejats",
"navigation_bar.bookmarks": "Marcadors",
@ -303,6 +315,7 @@
"notification.own_poll": "La teva enquesta ha finalitzat",
"notification.poll": "Ha finalitzat una enquesta en la que has votat",
"notification.reblog": "{name} ha impulsat el teu estat",
"notification.status": "ha publicat {name}",
"notifications.clear": "Netejar notificacions",
"notifications.clear_confirmation": "Estàs segur que vols esborrar permanentment totes les teves notificacions?",
"notifications.column_settings.alert": "Notificacions d'escriptori",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostra en la columna",
"notifications.column_settings.sound": "Reproduir so",
"notifications.column_settings.status": "Nous tuts:",
"notifications.filter.all": "Tots",
"notifications.filter.boosts": "Impulsos",
"notifications.filter.favourites": "Favorits",
"notifications.filter.follows": "Seguiments",
"notifications.filter.mentions": "Mencions",
"notifications.filter.polls": "Resultats de l'enquesta",
"notifications.filter.statuses": "Actualitzacions de gent que segueixes",
"notifications.grant_permission": "Concedir permís.",
"notifications.group": "{count} notificacions",
"notifications.mark_as_read": "Marca cada notificació com a llegida",
"notifications.permission_denied": "No sha pogut activar les notificacions descriptori perquè sha denegat el permís.",
"notifications.permission_denied_alert": "No es poden activar les notificacions del escriptori perquè el permís del navegador ha estat denegat abans",
"notifications.permission_required": "Les notificacions d'escriptori no estan disponibles perquè el permís requerit no ha estat concedit.",
"notifications_permission_banner.enable": "Activar les notificacions descriptori",
"notifications_permission_banner.how_to_control": "Per a rebre notificacions quan Mastodon no està obert cal activar les notificacions descriptori. Pots controlar amb precisió quins tipus dinteraccions generen notificacions descriptori després dactivar el botó {icon} de dalt.",
"notifications_permission_banner.title": "Mai et perdis res",
"picture_in_picture.restore": "Retornal",
"poll.closed": "Finalitzada",
"poll.refresh": "Actualitza",
"poll.total_people": "{count, plural, one {# persona} other {# persones}}",
@ -448,10 +472,11 @@
"upload_modal.analyzing_picture": "Analitzant imatge…",
"upload_modal.apply": "Aplica",
"upload_modal.choose_image": "Tria imatge",
"upload_modal.description_placeholder": "Uns salts ràpids de guineu marró sobre el gos gandul",
"upload_modal.description_placeholder": "Jove xef, porti whisky amb quinze glaçons dhidrogen, coi!",
"upload_modal.detect_text": "Detecta el text de l'imatge",
"upload_modal.edit_media": "Editar multimèdia",
"upload_modal.hint": "Fes clic o arrossega el cercle en la previsualització per escollir el punt focal que sempre serà visible de totes les miniatures.",
"upload_modal.preparing_ocr": "Preparant OCR…",
"upload_modal.preview_label": "Previsualitza ({ratio})",
"upload_progress.label": "Pujant...",
"video.close": "Tancar el vídeo",

View File

@ -1,5 +1,5 @@
{
"account.account_note_header": "A vostra nota per @{name}",
"account.account_note_header": "Nota",
"account.add_or_remove_from_list": "Aghjunghje o toglie da e liste",
"account.badges.bot": "Bot",
"account.badges.group": "Gruppu",
@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Vede di più nant'à u prufile uriginale",
"account.cancel_follow_request": "Annullà a dumanda d'abbunamentu",
"account.direct": "Missaghju direttu @{name}",
"account.disable_notifications": "Ùn mi nutificate più quandu @{name} pubblica qualcosa",
"account.domain_blocked": "Duminiu piattatu",
"account.edit_profile": "Mudificà u prufile",
"account.enable_notifications": "Nutificate mi quandu @{name} pubblica qualcosa",
"account.endorse": "Fà figurà nant'à u prufilu",
"account.follow": "Siguità",
"account.followers": "Abbunati",
@ -96,9 +98,9 @@
"compose_form.poll.switch_to_single": "Cambià u scandagliu per ùn accittà ch'una scelta",
"compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Indicà u media cum'è sensibile",
"compose_form.sensitive.marked": "Media indicatu cum'è sensibile",
"compose_form.sensitive.unmarked": "Media micca indicatu cum'è sensibile",
"compose_form.sensitive.hide": "{count, plural, one {Indicà u media cum'è sensibile} other {Indicà i media cum'è sensibili}}",
"compose_form.sensitive.marked": "{count, plural, one {Media indicatu cum'è sensibile} other {Media indicati cum'è sensibili}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Media micca indicatu cum'è sensibile} other {Media micca indicati cum'è sensibili}}",
"compose_form.spoiler.marked": "Testu piattatu daret'à un'avertimentu",
"compose_form.spoiler.unmarked": "Testu micca piattatu",
"compose_form.spoiler_placeholder": "Scrive u vostr'avertimentu quì",
@ -132,7 +134,7 @@
"directory.new_arrivals": "Ultimi arrivi",
"directory.recently_active": "Attività ricente",
"embed.instructions": "Integrà stu statutu à u vostru situ cù u codice quì sottu.",
"embed.preview": "Assumiglierà à qualcosa cusì:",
"embed.preview": "Hà da parè à quessa:",
"emoji_button.activity": "Attività",
"emoji_button.custom": "Persunalizati",
"emoji_button.flags": "Bandere",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Risultati di a cerca",
"emoji_button.symbols": "Simbuli",
"emoji_button.travel": "Lochi è Viaghju",
"empty_column.account_suspended": "Contu suspesu",
"empty_column.account_timeline": "Nisun statutu quì!",
"empty_column.account_unavailable": "Prufile micca dispunibule",
"empty_column.blocks": "Per avà ùn avete bluccatu manc'un utilizatore.",
@ -166,7 +169,9 @@
"empty_column.notifications": "Ùn avete ancu nisuna nutificazione. Interact with others to start the conversation.",
"empty_column.public": "Ùn c'hè nunda quì! Scrivete qualcosa in pubblicu o seguitate utilizatori d'altri servori per empie a linea pubblica",
"error.unexpected_crash.explanation": "In ragione d'un bug indè u nostru codice o un prublemu di cumpatibilità cù quessu navigatore, sta pagina ùn hè micca pussuta esse affissata currettamente.",
"error.unexpected_crash.explanation_addons": "Sta pagina ùn hè micca pussuta esse affissata currettamente, prubabilmente per via d'un'estenzione di navigatore o d'un lugiziale di traduzione.",
"error.unexpected_crash.next_steps": "Pruvate d'attualizà sta pagina. S'ellu persiste u prublemu, pudete forse sempre accede à Mastodon dapoi un'alltru navigatore o applicazione.",
"error.unexpected_crash.next_steps_addons": "Pruvate di disattivà quelli è poi attualizà sta pagina. S'ellu persiste u prublemu, pudete forse sempre accede à Mastodon dapoi un'alltru navigatore o applicazione.",
"errors.unexpected_crash.copy_stacktrace": "Cupià stacktrace nant'à u fermacarta",
"errors.unexpected_crash.report_issue": "Palisà prublemu",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "ùn fucalizà più l'area di testu",
"keyboard_shortcuts.up": "cullà indè a lista",
"lightbox.close": "Chjudà",
"lightbox.compress": "Cumprime a finestra d'affissera di i ritratti",
"lightbox.expand": "Ingrandà a finestra d'affissera di i ritratti",
"lightbox.next": "Siguente",
"lightbox.previous": "Pricidente",
"lightbox.view_context": "Vede u cuntestu",
"lists.account.add": "Aghjunghje à a lista",
"lists.account.remove": "Toglie di a lista",
"lists.delete": "Toglie a lista",
@ -265,14 +271,20 @@
"lists.edit.submit": "Cambià u titulu",
"lists.new.create": "Aghjunghje",
"lists.new.title_placeholder": "Titulu di a lista",
"lists.replies_policy.followed": "Tutti i vostri abbunamenti",
"lists.replies_policy.list": "Membri di a lista",
"lists.replies_policy.none": "Nimu",
"lists.replies_policy.title": "Vede e risposte à:",
"lists.search": "Circà indè i vostr'abbunamenti",
"lists.subheading": "E vo liste",
"load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
"loading_indicator.label": "Caricamentu...",
"media_gallery.toggle_visible": "Cambià a visibilità",
"media_gallery.toggle_visible": "Piattà {number, plural, one {ritrattu} other {ritratti}}",
"missing_indicator.label": "Micca trovu",
"missing_indicator.sublabel": "Ùn era micca pussivule di truvà sta risorsa",
"mute_modal.duration": "Durata",
"mute_modal.hide_notifications": "Piattà nutificazione da st'utilizatore?",
"mute_modal.indefinite": "Indifinita",
"navigation_bar.apps": "Applicazione per u telefuninu",
"navigation_bar.blocks": "Utilizatori bluccati",
"navigation_bar.bookmarks": "Segnalibri",
@ -303,6 +315,7 @@
"notification.own_poll": "U vostru scandagliu hè compiu",
"notification.poll": "Un scandagliu induve avete vutatu hè finitu",
"notification.reblog": "{name} hà spartutu u vostru statutu",
"notification.status": "{name} hà appena pubblicatu",
"notifications.clear": "Purgà e nutificazione",
"notifications.clear_confirmation": "Site sicuru·a che vulete toglie tutte ste nutificazione?",
"notifications.column_settings.alert": "Nutificazione nant'à l'urdinatore",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Spartere:",
"notifications.column_settings.show": "Mustrà indè a colonna",
"notifications.column_settings.sound": "Sunà",
"notifications.column_settings.status": "Statuti novi:",
"notifications.filter.all": "Tuttu",
"notifications.filter.boosts": "Spartere",
"notifications.filter.favourites": "Favuriti",
"notifications.filter.follows": "Abbunamenti",
"notifications.filter.mentions": "Minzione",
"notifications.filter.polls": "Risultati di u scandagliu",
"notifications.filter.statuses": "Messe à ghjornu di e persone chì siguitate",
"notifications.grant_permission": "Auturizà.",
"notifications.group": "{count} nutificazione",
"notifications.mark_as_read": "Marcà tutte e nutificazione cum'è lette",
"notifications.permission_denied": "Ùn si po micca attivà e nutificazione desktop perchè l'auturizazione hè stata ricusata",
"notifications.permission_denied_alert": "Ùn pudete micca attivà e nutificazione nant'à l'urdinatore, perchè avete digià ricusatu a dumanda d'auturizazione di u navigatore",
"notifications.permission_required": "Ùn si po micca attivà e nutificazione desktop perchè a l'auturizazione richiesta ùn hè micca stata data.",
"notifications_permission_banner.enable": "Attivà e nutificazione nant'à l'urdinatore",
"notifications_permission_banner.how_to_control": "Per riceve nutificazione quandu Mastodon ùn hè micca aperta, attivate e nutificazione nant'à l'urdinatore. Pudete decide quali tippi d'interazione anu da mandà ste nutificazione cù u buttone {icon} quì sopra quandu saranu attivate.",
"notifications_permission_banner.title": "Ùn mancate mai nunda",
"picture_in_picture.restore": "Rimette in piazza",
"poll.closed": "Chjosu",
"poll.refresh": "Attualizà",
"poll.total_people": "{count, plural, one {# persona} other {# persone}}",
@ -357,7 +381,7 @@
"report.hint": "U signalamentu sarà mandatu à i muderatori di u servore. Pudete spiegà perchè avete palisatu stu contu quì sottu:",
"report.placeholder": "Altri cummenti",
"report.submit": "Mandà",
"report.target": "Signalamentu",
"report.target": "Signalamentu di {target}",
"search.placeholder": "Circà",
"search_popout.search_format": "Ricerca avanzata",
"search_popout.tips.full_text": "I testi simplici rimandanu i statuti ch'avete scritti, aghjunti à i vostri favuriti, spartuti o induve quelli site mintuvatu·a, è ancu i cugnomi, nomi pubblichi è hashtag chì currispondenu.",
@ -436,7 +460,7 @@
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Drag & drop per caricà un fugliale",
"upload_button.label": "Aghjunghje un media ({formats})",
"upload_button.label": "Aghjunghje un media",
"upload_error.limit": "Limita di caricamentu di fugliali trapassata.",
"upload_error.poll": "Ùn si pò micca caricà fugliali cù i scandagli.",
"upload_form.audio_description": "Discrizzione per i ciochi",
@ -452,6 +476,7 @@
"upload_modal.detect_text": "Ditettà testu da u ritrattu",
"upload_modal.edit_media": "Cambià media",
"upload_modal.hint": "Cliccate o sguillate u chjerchju nant'à a vista per sceglie u puntu fucale chì sarà sempre in vista indè tutte e miniature.",
"upload_modal.preparing_ocr": "Priparazione di l'OCR…",
"upload_modal.preview_label": "Vista ({ratio})",
"upload_progress.label": "Caricamentu...",
"video.close": "Chjudà a video",

View File

@ -1,5 +1,5 @@
{
"account.account_note_header": "Note",
"account.account_note_header": "Poznámka",
"account.add_or_remove_from_list": "Přidat nebo odstranit ze seznamů",
"account.badges.bot": "Robot",
"account.badges.group": "Skupina",
@ -9,13 +9,15 @@
"account.browse_more_on_origin_server": "Více na původním profilu",
"account.cancel_follow_request": "Zrušit žádost o sledování",
"account.direct": "Poslat uživateli @{name} přímou zprávu",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Doména skryta",
"account.edit_profile": "Upravit profil",
"account.enable_notifications": "Oznámit mě na příspěvky @{name}",
"account.endorse": "Zvýraznit na profilu",
"account.follow": "Sledovat",
"account.followers": "Sledující",
"account.followers.empty": "Tohoto uživatele ještě nikdo nesleduje.",
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
"account.followers_counter": "{count, plural, one {{counter} sledující} few {{counter} sledující} many {{counter} sledujících} other {{counter} sledujících}}",
"account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
"account.follows.empty": "Tento uživatel ještě nikoho nesleduje.",
"account.follows_you": "Sleduje vás",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Výsledky hledání",
"emoji_button.symbols": "Symboly",
"emoji_button.travel": "Cestování a místa",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "Nejsou tu žádné tooty!",
"empty_column.account_unavailable": "Profil nedostupný",
"empty_column.blocks": "Ještě jste nezablokovali žádného uživatele.",
@ -166,7 +169,9 @@
"empty_column.notifications": "Ještě nemáte žádná oznámení. Začněte s někým konverzaci.",
"empty_column.public": "Tady nic není! Napište něco veřejně, nebo začněte ručně sledovat uživatele z jiných serverů, aby tu něco přibylo",
"error.unexpected_crash.explanation": "Kvůli chybě v našem kódu nebo problému s kompatibilitou prohlížeče nemohla být tato stránka načtena správně.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Zkuste stránku načíst znovu. Pokud to nepomůže, zkuste Mastodon používat pomocí jiného prohlížeče nebo nativní aplikace.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Zkopírovat stacktrace do schránky",
"errors.unexpected_crash.report_issue": "Nahlásit problém",
"federation.change": "Adjust status federation",
@ -177,7 +182,7 @@
"follow_request.authorize": "Autorizovat",
"follow_request.reject": "Odmítnout",
"follow_requests.unlocked_explanation": "Přestože váš účet není uzamčen, {domain} si myslí, že budete chtít následující požadavky na sledování zkontrolovat ručně.",
"generic.saved": "Saved",
"generic.saved": "Uloženo",
"getting_started.developers": "Vývojáři",
"getting_started.directory": "Adresář profilů",
"getting_started.documentation": "Dokumentace",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "zrušení zaměření na psací prostor/hledání",
"keyboard_shortcuts.up": "posunutí nahoru v seznamu",
"lightbox.close": "Zavřít",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Další",
"lightbox.previous": "Předchozí",
"lightbox.view_context": "Zobrazit kontext",
"lists.account.add": "Přidat do seznamu",
"lists.account.remove": "Odebrat ze seznamu",
"lists.delete": "Smazat seznam",
@ -265,6 +271,10 @@
"lists.edit.submit": "Změnit název",
"lists.new.create": "Přidat seznam",
"lists.new.title_placeholder": "Název nového seznamu",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "Hledejte mezi lidmi, které sledujete",
"lists.subheading": "Vaše seznamy",
"load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Přepnout viditelnost",
"missing_indicator.label": "Nenalezeno",
"missing_indicator.sublabel": "Tento zdroj se nepodařilo najít",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Skrýt oznámení od tohoto uživatele?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "Mobilní aplikace",
"navigation_bar.blocks": "Blokovaní uživatelé",
"navigation_bar.bookmarks": "Záložky",
@ -303,6 +315,7 @@
"notification.own_poll": "Vaše anketa skončila",
"notification.poll": "Anketa, ve které jste hlasovali, skončila",
"notification.reblog": "Uživatel {name} boostnul váš toot",
"notification.status": "{name} just posted",
"notifications.clear": "Smazat oznámení",
"notifications.clear_confirmation": "Opravdu chcete trvale smazat všechna vaše oznámení?",
"notifications.column_settings.alert": "Oznámení na počítači",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Boosty:",
"notifications.column_settings.show": "Zobrazit ve sloupci",
"notifications.column_settings.sound": "Přehrát zvuk",
"notifications.column_settings.status": "Nové tooty:",
"notifications.filter.all": "Vše",
"notifications.filter.boosts": "Boosty",
"notifications.filter.favourites": "Oblíbení",
"notifications.filter.follows": "Sledování",
"notifications.filter.mentions": "Zmínky",
"notifications.filter.polls": "Výsledky anket",
"notifications.filter.statuses": "Aktuality od lidí, které sledujete",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} oznámení",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Uzavřeno",
"poll.refresh": "Obnovit",
"poll.total_people": "{count, plural, one {# člověk} few {# lidé} many {# lidí} other {# lidí}}",
@ -436,13 +460,13 @@
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Nahrajte přetažením",
"upload_button.label": "Přidat média ({formats})",
"upload_button.label": "Přidat média",
"upload_error.limit": "Byl překročen limit nahraných souborů.",
"upload_error.poll": "U anket není nahrávání souborů povoleno.",
"upload_form.audio_description": "Popis pro sluchově postižené",
"upload_form.description": "Popis pro zrakově postižené",
"upload_form.edit": "Upravit",
"upload_form.thumbnail": "Change thumbnail",
"upload_form.thumbnail": "Změnit miniaturu",
"upload_form.undo": "Smazat",
"upload_form.video_description": "Popis pro sluchově či zrakově postižené",
"upload_modal.analyzing_picture": "Analyzuji obrázek…",
@ -452,6 +476,7 @@
"upload_modal.detect_text": "Detekovat text z obrázku",
"upload_modal.edit_media": "Upravit média",
"upload_modal.hint": "Kliknutím na nebo přetáhnutím kruhu na náhledu vyberte oblast, která bude na všech náhledech vždy zobrazen.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "Náhled ({ratio})",
"upload_progress.label": "Nahrávání…",
"video.close": "Zavřít video",

View File

@ -1,22 +1,24 @@
{
"account.account_note_header": "Note",
"account.account_note_header": "Nodyn",
"account.add_or_remove_from_list": "Ychwanegu neu Dileu o'r rhestrau",
"account.badges.bot": "Bot",
"account.badges.group": "Grŵp",
"account.block": "Blocio @{name}",
"account.block_domain": "Cuddio popeth rhag {domain}",
"account.blocked": "Blociwyd",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.browse_more_on_origin_server": "Pori mwy ar y proffil gwreiddiol",
"account.cancel_follow_request": "Canslo cais dilyn",
"account.direct": "Neges breifat @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Parth wedi ei guddio",
"account.edit_profile": "Golygu proffil",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Arddangos ar fy mhroffil",
"account.follow": "Dilyn",
"account.followers": "Dilynwyr",
"account.followers.empty": "Nid oes neb yn dilyn y defnyddiwr hwn eto.",
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
"account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
"account.followers_counter": "{count, plural, one {{counter} Ddilynwr} other {{counter} o Ddilynwyr}}",
"account.following_counter": "{count, plural, one {{counter} yn Dilyn} other {{counter} yn Dilyn}}",
"account.follows.empty": "Nid yw'r defnyddiwr hwn yn dilyn unrhyw un eto.",
"account.follows_you": "Yn eich dilyn chi",
"account.hide_reblogs": "Cuddio bwstiau o @{name}",
@ -36,14 +38,14 @@
"account.requested": "Aros am gymeradwyaeth. Cliciwch er mwyn canslo cais dilyn",
"account.share": "Rhannwch broffil @{name}",
"account.show_reblogs": "Dangos bwstiau o @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
"account.statuses_counter": "{count, plural, one {{counter} t} other {{counter} o Dŵtiau}}",
"account.unblock": "Dadflocio @{name}",
"account.unblock_domain": "Dadguddio {domain}",
"account.unendorse": "Peidio a'i arddangos ar fy mhroffil",
"account.unfollow": "Dad-ddilyn",
"account.unmute": "Dad-dawelu @{name}",
"account.unmute_notifications": "Dad-dawelu hysbysiadau o @{name}",
"account_note.placeholder": "Click to add a note",
"account_note.placeholder": "Clicio i ychwanegu nodyn",
"alert.rate_limited.message": "Ceisiwch eto ar ôl {retry_time, time, medium}.",
"alert.rate_limited.title": "Cyfradd gyfyngedig",
"alert.unexpected.message": "Digwyddodd gwall annisgwyl.",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Canlyniadau chwilio",
"emoji_button.symbols": "Symbolau",
"emoji_button.travel": "Teithio & Llefydd",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "Dim tŵtiau fama!",
"empty_column.account_unavailable": "Proffil ddim ar gael",
"empty_column.blocks": "Nid ydych wedi blocio unrhyw ddefnyddwyr eto.",
@ -166,13 +169,15 @@
"empty_column.notifications": "Nid oes gennych unrhyw hysbysiadau eto. Rhyngweithiwch ac eraill i ddechrau'r sgwrs.",
"empty_column.public": "Does dim byd yma! Ysgrifennwch rhywbeth yn gyhoeddus, neu dilynwch ddefnyddwyr o achosion eraill i'w lenwi",
"error.unexpected_crash.explanation": "Oherwydd gwall yn ein cod neu oherwydd problem cysondeb porwr, nid oedd y dudalen hon gallu cael ei dangos yn gywir.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Ceisiwch ail-lwytho y dudalen. Os nad yw hyn yn eich helpu, efallai gallech defnyddio Mastodon trwy borwr neu ap brodorol gwahanol.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copïo'r olrhain stac i'r clipfwrdd",
"errors.unexpected_crash.report_issue": "Rhoi gwybod am broblem",
"follow_request.authorize": "Caniatau",
"follow_request.reject": "Gwrthod",
"follow_requests.unlocked_explanation": "Er nid yw eich cyfrif wedi'i gloi, oedd y staff {domain} yn meddwl efallai hoffech adolygu ceisiadau dilyn o'r cyfrifau rhain wrth law.",
"generic.saved": "Saved",
"generic.saved": "Wedi'i Gadw",
"getting_started.developers": "Datblygwyr",
"getting_started.directory": "Cyfeiriadur proffil",
"getting_started.documentation": "Dogfennaeth",
@ -242,7 +247,7 @@
"keyboard_shortcuts.reply": "i ateb",
"keyboard_shortcuts.requests": "i agor rhestr ceisiadau dilyn",
"keyboard_shortcuts.search": "i ffocysu chwilio",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.spoilers": "i ddangos/cuddio'r maes CW",
"keyboard_shortcuts.start": "i agor colofn \"dechrau arni\"",
"keyboard_shortcuts.toggle_hidden": "i ddangos/cuddio testun tu ôl i CW",
"keyboard_shortcuts.toggle_sensitivity": "i ddangos/gyddio cyfryngau",
@ -250,9 +255,10 @@
"keyboard_shortcuts.unfocus": "i ddad-ffocysu ardal cyfansoddi testun/chwilio",
"keyboard_shortcuts.up": "i symud yn uwch yn y rhestr",
"lightbox.close": "Cau",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Nesaf",
"lightbox.previous": "Blaenorol",
"lightbox.view_context": "Gweld cyd-destyn",
"lists.account.add": "Ychwanegwch at restr",
"lists.account.remove": "Dileu o'r rhestr",
"lists.delete": "Dileu rhestr",
@ -260,6 +266,10 @@
"lists.edit.submit": "Newid teitl",
"lists.new.create": "Ychwanegu rhestr",
"lists.new.title_placeholder": "Teitl rhestr newydd",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "Chwilio ymysg pobl yr ydych yn ei ddilyn",
"lists.subheading": "Eich rhestrau",
"load_pending": "{count, plural, one {# eitem newydd} other {# eitemau newydd}}",
@ -267,7 +277,9 @@
"media_gallery.toggle_visible": "Toglo gwelededd",
"missing_indicator.label": "Heb ei ganfod",
"missing_indicator.sublabel": "Ni ellid canfod yr adnodd hwn",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Cuddio hysbysiadau rhag y defnyddiwr hwn?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "Apiau symudol",
"navigation_bar.blocks": "Defnyddwyr wedi eu blocio",
"navigation_bar.bookmarks": "Tudalnodau",
@ -298,6 +310,7 @@
"notification.own_poll": "Mae eich pôl wedi diweddu",
"notification.poll": "Mae pleidlais rydych wedi pleidleisio ynddi wedi dod i ben",
"notification.reblog": "Hysbysebodd {name} eich tŵt",
"notification.status": "{name} just posted",
"notifications.clear": "Clirio hysbysiadau",
"notifications.clear_confirmation": "Ydych chi'n sicr eich bod am glirio'ch holl hysbysiadau am byth?",
"notifications.column_settings.alert": "Hysbysiadau bwrdd gwaith",
@ -313,13 +326,24 @@
"notifications.column_settings.reblog": "Hybiadau:",
"notifications.column_settings.show": "Dangos yn y golofn",
"notifications.column_settings.sound": "Chwarae sain",
"notifications.column_settings.status": "New toots:",
"notifications.filter.all": "Pob",
"notifications.filter.boosts": "Hybiadau",
"notifications.filter.favourites": "Ffefrynnau",
"notifications.filter.follows": "Yn dilyn",
"notifications.filter.mentions": "Crybwylliadau",
"notifications.filter.polls": "Canlyniadau pleidlais",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} o hysbysiadau",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Ar gau",
"poll.refresh": "Adnewyddu",
"poll.total_people": "{count, plural, one {# berson} other {# o bobl}}",
@ -420,16 +444,16 @@
"time_remaining.minutes": "{number, plural, one {# funud} other {# o funudau}} ar ôl",
"time_remaining.moments": "Munudau ar ôl",
"time_remaining.seconds": "{number, plural, one {# eiliad} other {# o eiliadau}} ar ôl",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} talking",
"timeline_hint.remote_resource_not_displayed": "ni chaiff {resource} o gweinyddion eraill ei ddangos.",
"timeline_hint.resources.followers": "Dilynwyr",
"timeline_hint.resources.follows": "Yn dilyn",
"timeline_hint.resources.statuses": "Tŵtiau henach",
"trends.counter_by_accounts": "{count, plural, one {{counter} berson} other {{counter} o bobl}}",
"trends.trending_now": "Yn tueddu nawr",
"ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"units.short.billion": "{count}biliwn",
"units.short.million": "{count}miliwn",
"units.short.thousand": "{count}mil",
"upload_area.title": "Llusgwch & gollwing i uwchlwytho",
"upload_button.label": "Ychwanegwch gyfryngau (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Wedi mynd heibio'r uchafswm terfyn uwchlwytho.",
@ -437,16 +461,17 @@
"upload_form.audio_description": "Disgrifio ar gyfer pobl sydd â cholled clyw",
"upload_form.description": "Disgrifio i'r rheini a nam ar ei golwg",
"upload_form.edit": "Golygu",
"upload_form.thumbnail": "Change thumbnail",
"upload_form.thumbnail": "Newid mân-lun",
"upload_form.undo": "Dileu",
"upload_form.video_description": "Disgrifio ar gyfer pobl sydd â cholled clyw neu amhariad golwg",
"upload_modal.analyzing_picture": "Dadansoddi llun…",
"upload_modal.apply": "Gweithredu",
"upload_modal.choose_image": "Choose image",
"upload_modal.choose_image": "Dewis delwedd",
"upload_modal.description_placeholder": "Mae ei phen bach llawn jocs, 'run peth a fy nghot golff, rhai dyddiau",
"upload_modal.detect_text": "Canfod testun o'r llun",
"upload_modal.edit_media": "Golygu cyfryngau",
"upload_modal.hint": "Cliciwch neu llusgwch y cylch ar y rhagolwg i ddewis y canolbwynt a fydd bob amser i'w weld ar bob mân-lunau.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "Rhagolwg ({ratio})",
"upload_progress.label": "Uwchlwytho...",
"video.close": "Cau fideo",

View File

@ -6,16 +6,18 @@
"account.block": "Bloker @{name}",
"account.block_domain": "Skjul alt fra {domain}",
"account.blocked": "Blokeret",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.browse_more_on_origin_server": "Gennemse mere på den oprindelige profil",
"account.cancel_follow_request": "Annullér følgeranmodning",
"account.direct": "Send en direkte besked til @{name}",
"account.disable_notifications": "Stop med at give mig besked når @{name} lægger noget op",
"account.domain_blocked": "Domænet er blevet skjult",
"account.edit_profile": "Rediger profil",
"account.enable_notifications": "Giv mig besked når @{name} lægger noget op",
"account.endorse": "Fremhæv på profil",
"account.follow": "Følg",
"account.followers": "Følgere",
"account.followers.empty": "Der er endnu ingen der følger denne bruger.",
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
"account.followers_counter": "{count, plural, one {{counter} Følger} other {{counter} Følgere}}",
"account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
"account.follows.empty": "Denne bruger følger endnu ikke nogen.",
"account.follows_you": "Følger dig",
@ -48,7 +50,7 @@
"alert.rate_limited.title": "Gradsbegrænset",
"alert.unexpected.message": "Der opstod en uventet fejl.",
"alert.unexpected.title": "Ups!",
"announcement.announcement": "Announcement",
"announcement.announcement": "Bekendtgørelse",
"autosuggest_hashtag.per_week": "{count} per uge",
"boost_modal.combo": "Du kan trykke {combo} for at springe dette over næste gang",
"bundle_column_error.body": "Noget gik galt under indlæsningen af dette komponent.",
@ -79,9 +81,9 @@
"column_header.show_settings": "Vis indstillinger",
"column_header.unpin": "Fastgør ikke længere",
"column_subheading.settings": "Indstillinger",
"community.column_settings.local_only": "Local only",
"community.column_settings.local_only": "Kun lokalt",
"community.column_settings.media_only": "Kun medie",
"community.column_settings.remote_only": "Remote only",
"community.column_settings.remote_only": "Kun fjernt",
"compose_form.direct_message_warning": "Dette trut vil kun blive sendt til de nævnte brugere.",
"compose_form.direct_message_warning_learn_more": "Lær mere",
"compose_form.hashtag_warning": "Dette trut vil ikke blive vist under noget hashtag da det ikke er listet. Kun offentlige trut kan blive vist under søgninger med hashtags.",
@ -92,8 +94,8 @@
"compose_form.poll.duration": "Afstemningens varighed",
"compose_form.poll.option_placeholder": "Valgmulighed {number}",
"compose_form.poll.remove_option": "Fjern denne valgmulighed",
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.poll.switch_to_multiple": "Ændre afstemning for at tillade flere valg",
"compose_form.poll.switch_to_single": "Ændre afstemning for at tillade et enkelt valg",
"compose_form.publish": "Trut",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Markér medie som følsomt",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Søgeresultater",
"emoji_button.symbols": "Symboler",
"emoji_button.travel": "Rejser & steder",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "Ingen bidrag her!",
"empty_column.account_unavailable": "Profil utilgængelig",
"empty_column.blocks": "Du har ikke blokeret nogen endnu.",
@ -166,13 +169,15 @@
"empty_column.notifications": "Du har endnu ingen notifikationer. Tag ud og bland dig med folkemængden for at starte samtalen.",
"empty_column.public": "Der er ikke noget at se her! Skriv noget offentligt eller start ud med manuelt at følge brugere fra andre server for at udfylde tomrummet",
"error.unexpected_crash.explanation": "På grund af en fejl i vores kode, eller en browser kompatibilitetsfejl, så kunne siden ikke vises korrekt.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Prøv at genindlæs siden. Hvis dette ikke hjælper, så forsøg venligst, at tilgå Mastodon via en anden browser eller app.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Kopiér stack trace til udklipsholderen",
"errors.unexpected_crash.report_issue": "Rapportér problem",
"follow_request.authorize": "Godkend",
"follow_request.reject": "Afvis",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"generic.saved": "Saved",
"follow_requests.unlocked_explanation": "Selvom din konto ikke er låst, troede {domain} -personalet, at du måske vil gennemgå dine anmodninger manuelt.",
"generic.saved": "Gemt",
"getting_started.developers": "Udviklere",
"getting_started.directory": "Profilliste",
"getting_started.documentation": "Dokumentation",
@ -193,8 +198,8 @@
"home.column_settings.basic": "Grundlæggende",
"home.column_settings.show_reblogs": "Vis fremhævelser",
"home.column_settings.show_replies": "Vis svar",
"home.hide_announcements": "Hide announcements",
"home.show_announcements": "Show announcements",
"home.hide_announcements": "Skjul bekendtgørelser",
"home.show_announcements": "Vis bekendtgørelser",
"intervals.full.days": "{number, plural, one {# dag} other {# dage}}",
"intervals.full.hours": "{number, plural, one {# time} other {# timer}}",
"intervals.full.minutes": "{number, plural, one {# minut} other {# minutter}}",
@ -236,13 +241,13 @@
"keyboard_shortcuts.muted": "for at åbne listen over dæmpede brugere",
"keyboard_shortcuts.my_profile": "for at åbne din profil",
"keyboard_shortcuts.notifications": "for at åbne notifikations kolonnen",
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.open_media": "for at åbne medier",
"keyboard_shortcuts.pinned": "for at åbne listen over fastgjorte trut",
"keyboard_shortcuts.profile": "til profil af åben forfatter",
"keyboard_shortcuts.reply": "for at svare",
"keyboard_shortcuts.requests": "for at åbne listen over følgeranmodninger",
"keyboard_shortcuts.search": "for at fokusere søgningen",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.spoilers": "for at vise/skjule CW-felt",
"keyboard_shortcuts.start": "for at åbne \"kom igen\" kolonnen",
"keyboard_shortcuts.toggle_hidden": "for at vise/skjule tekst bag CW",
"keyboard_shortcuts.toggle_sensitivity": "for at vise/skjule medier",
@ -250,9 +255,10 @@
"keyboard_shortcuts.unfocus": "for at fjerne fokus fra skriveområde/søgning",
"keyboard_shortcuts.up": "for at bevæge dig op ad listen",
"lightbox.close": "Luk",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Næste",
"lightbox.previous": "Forrige",
"lightbox.view_context": "Vis kontekst",
"lists.account.add": "Tilføj til liste",
"lists.account.remove": "Fjern fra liste",
"lists.delete": "Slet liste",
@ -260,6 +266,10 @@
"lists.edit.submit": "Skift titel",
"lists.new.create": "Tilføj liste",
"lists.new.title_placeholder": "Ny liste titel",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Vis svar til:",
"lists.search": "Søg iblandt folk du følger",
"lists.subheading": "Dine lister",
"load_pending": "{count, plural, one {# nyt punkt} other {# nye punkter}}",
@ -267,7 +277,9 @@
"media_gallery.toggle_visible": "Ændre synlighed",
"missing_indicator.label": "Ikke fundet",
"missing_indicator.sublabel": "Denne ressource kunne ikke blive fundet",
"mute_modal.duration": "Varighed",
"mute_modal.hide_notifications": "Skjul notifikationer fra denne bruger?",
"mute_modal.indefinite": "Uendeligt",
"navigation_bar.apps": "Mobil apps",
"navigation_bar.blocks": "Blokerede brugere",
"navigation_bar.bookmarks": "Bogmærker",
@ -293,11 +305,12 @@
"navigation_bar.security": "Sikkerhed",
"notification.favourite": "{name} favoriserede din status",
"notification.follow": "{name} fulgte dig",
"notification.follow_request": "{name} has requested to follow you",
"notification.follow_request": "{name} har anmodet om at følge dig",
"notification.mention": "{name} nævnte dig",
"notification.own_poll": "Din afstemning er afsluttet",
"notification.poll": "En afstemning, du stemte i, er slut",
"notification.reblog": "{name} boostede din status",
"notification.status": "{name} har lige lagt noget op",
"notifications.clear": "Ryd notifikationer",
"notifications.clear_confirmation": "Er du sikker på, du vil rydde alle dine notifikationer permanent?",
"notifications.column_settings.alert": "Skrivebordsnotifikationer",
@ -306,20 +319,31 @@
"notifications.column_settings.filter_bar.category": "Hurtigfilter",
"notifications.column_settings.filter_bar.show": "Vis",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.follow_request": "New follow requests:",
"notifications.column_settings.follow_request": "Nye følgeranmodninger:",
"notifications.column_settings.mention": "Statusser der nævner dig:",
"notifications.column_settings.poll": "Afstemningsresultat:",
"notifications.column_settings.push": "Pushnotifikationer",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Afspil lyd",
"notifications.column_settings.status": "Nye toots:",
"notifications.filter.all": "Alle",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favoritter",
"notifications.filter.follows": "Følger",
"notifications.filter.mentions": "Statusser der nævner dig",
"notifications.filter.polls": "Afstemningsresultat",
"notifications.filter.statuses": "Opdateringer fra personer, du følger",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} notifikationer",
"notifications.mark_as_read": "Markér alle notifikationer som læst",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Aktivér skrivebordsmeddelelser",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Gå aldrig glip af noget",
"picture_in_picture.restore": "Sæt den tilbage",
"poll.closed": "Lukket",
"poll.refresh": "Opdatér",
"poll.total_people": "{count, plural, one {# person} other {# personer}}",
@ -420,10 +444,10 @@
"time_remaining.minutes": "{number, plural, one {# minut} other {# minutter}} tilbage",
"time_remaining.moments": "Få øjeblikke tilbage",
"time_remaining.seconds": "{number, plural, one {# sekund} other {# sekunder}} tilbage",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"timeline_hint.remote_resource_not_displayed": "{resource} fra andre servere vises ikke.",
"timeline_hint.resources.followers": "Følgere",
"timeline_hint.resources.follows": "Følger",
"timeline_hint.resources.statuses": "Ældre toots",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} talking",
"trends.trending_now": "Hot lige nu",
"ui.beforeunload": "Din kladde vil gå tabt hvis du forlader Mastodon.",
@ -434,19 +458,20 @@
"upload_button.label": "Tilføj medie (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Uploadgrænse overskredet.",
"upload_error.poll": "Filupload ikke tilladt sammen med afstemninger.",
"upload_form.audio_description": "Describe for people with hearing loss",
"upload_form.audio_description": "Beskriv for personer med høretab",
"upload_form.description": "Beskriv for svagtseende",
"upload_form.edit": "Redigér",
"upload_form.thumbnail": "Change thumbnail",
"upload_form.undo": "Slet",
"upload_form.video_description": "Describe for people with hearing loss or visual impairment",
"upload_form.video_description": "Beskriv for personer med høretab eller nedsat syn",
"upload_modal.analyzing_picture": "Analyserer billede…",
"upload_modal.apply": "Anvend",
"upload_modal.choose_image": "Choose image",
"upload_modal.choose_image": "Vælg billede",
"upload_modal.description_placeholder": "En hurtig brun ræv hopper over den dovne hund",
"upload_modal.detect_text": "Find tekst i billede på automatisk vis",
"upload_modal.edit_media": "Redigér medie",
"upload_modal.hint": "Klik eller træk cirklen på billedet for at vælge et fokuspunkt.",
"upload_modal.preparing_ocr": "Forbereder OCR…",
"upload_modal.preview_label": "Forhåndsvisning ({ratio})",
"upload_progress.label": "Uploader...",
"video.close": "Luk video",

View File

@ -1,16 +1,18 @@
{
"account.account_note_header": "Deine Notiz für @{name}",
"account.account_note_header": "Notiz",
"account.add_or_remove_from_list": "Hinzufügen oder Entfernen von Listen",
"account.badges.bot": "Bot",
"account.badges.group": "Gruppe",
"account.block": "@{name} blockieren",
"account.block_domain": "Alles von {domain} blockieren",
"account.block_domain": "Alles von {domain} verstecken",
"account.blocked": "Blockiert",
"account.browse_more_on_origin_server": "Mehr auf dem Originalprofil durchsuchen",
"account.cancel_follow_request": "Folgeanfrage abbrechen",
"account.direct": "Direktnachricht an @{name}",
"account.disable_notifications": "Höre auf mich zu benachrichtigen wenn @{name} etwas postet",
"account.domain_blocked": "Domain versteckt",
"account.edit_profile": "Profil bearbeiten",
"account.enable_notifications": "Benachrichtige mich wenn @{name} etwas postet",
"account.endorse": "Auf Profil hervorheben",
"account.follow": "Folgen",
"account.followers": "Folgende",
@ -38,12 +40,12 @@
"account.show_reblogs": "Von @{name} geteilte Beiträge anzeigen",
"account.statuses_counter": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
"account.unblock": "@{name} entblocken",
"account.unblock_domain": "Blockieren von {domain} beenden",
"account.unblock_domain": "{domain} wieder anzeigen",
"account.unendorse": "Nicht auf Profil hervorheben",
"account.unfollow": "Entfolgen",
"account.unmute": "@{name} nicht mehr stummschalten",
"account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
"account_note.placeholder": "Kein Kommentar angegeben",
"account_note.placeholder": "Notiz durch Klicken hinzufügen",
"alert.rate_limited.message": "Bitte versuche es nach {retry_time, time, medium}.",
"alert.rate_limited.title": "Anfragelimit überschritten",
"alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
@ -62,7 +64,7 @@
"column.community": "Lokale Zeitleiste",
"column.direct": "Direktnachrichten",
"column.directory": "Profile durchsuchen",
"column.domain_blocks": "Versteckte Domains",
"column.domain_blocks": "Blockierte Domains",
"column.favourites": "Favoriten",
"column.follow_requests": "Folgeanfragen",
"column.home": "Startseite",
@ -94,11 +96,11 @@
"compose_form.poll.remove_option": "Wahl entfernen",
"compose_form.poll.switch_to_multiple": "Umfrage ändern, um mehrere Optionen zu erlauben",
"compose_form.poll.switch_to_single": "Umfrage ändern, um eine einzige Wahl zu erlauben",
"compose_form.publish": "Beitrag",
"compose_form.publish": "Tröt",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Medien als heikel markieren",
"compose_form.sensitive.marked": "Medien sind als heikel markiert",
"compose_form.sensitive.unmarked": "Medien sind nicht als heikel markiert",
"compose_form.sensitive.hide": "Medien als NSFW markieren",
"compose_form.sensitive.marked": "Medien sind als NSFW markiert",
"compose_form.sensitive.unmarked": "Medien sind nicht als NSFW markiert",
"compose_form.spoiler.marked": "Text ist hinter einer Warnung versteckt",
"compose_form.spoiler.unmarked": "Text ist nicht versteckt",
"compose_form.spoiler_placeholder": "Inhaltswarnung",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Suchergebnisse",
"emoji_button.symbols": "Symbole",
"emoji_button.travel": "Reisen und Orte",
"empty_column.account_suspended": "Konto gesperrt",
"empty_column.account_timeline": "Keine Beiträge!",
"empty_column.account_unavailable": "Konto nicht verfügbar",
"empty_column.blocks": "Du hast keine Profile blockiert.",
@ -165,8 +168,10 @@
"empty_column.mutes": "Du hast keine Profile stummgeschaltet.",
"empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
"empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Servern, um die Zeitleiste aufzufüllen",
"error.unexpected_crash.explanation": "Aufgrund eines Fehlers in unserem Code oder einer Browser-Inkompatibilität konnte diese Seite nicht korrekt angezeigt werden.",
"error.unexpected_crash.explanation": "Aufgrund eines Fehlers in unserem Code oder einer Browsereinkompatibilität konnte diese Seite nicht korrekt angezeigt werden.",
"error.unexpected_crash.explanation_addons": "Diese Seite konnte nicht korrekt angezeigt werden. Dieser Fehler wird wahrscheinlich durch ein Browser-Add-on oder automatische Übersetzungswerkzeuge verursacht.",
"error.unexpected_crash.next_steps": "Versuche die Seite zu aktualisieren. Wenn das nicht hilft, kannst du Mastodon über einen anderen Browser oder eine native App verwenden.",
"error.unexpected_crash.next_steps_addons": "Versuche sie zu deaktivieren und lade dann die Seite neu. Wenn das Problem weiterhin besteht, solltest du Mastodon über einen anderen Browser oder eine native App nutzen.",
"errors.unexpected_crash.copy_stacktrace": "Fehlerlog in die Zwischenablage kopieren",
"errors.unexpected_crash.report_issue": "Problem melden",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "Textfeld/die Suche nicht mehr fokussieren",
"keyboard_shortcuts.up": "sich in der Liste hinauf bewegen",
"lightbox.close": "Schließen",
"lightbox.compress": "Bildansicht komprimieren",
"lightbox.expand": "Bildansicht erweitern",
"lightbox.next": "Weiter",
"lightbox.previous": "Zurück",
"lightbox.view_context": "Beitrag sehen",
"lists.account.add": "Zur Liste hinzufügen",
"lists.account.remove": "Von der Liste entfernen",
"lists.delete": "Liste löschen",
@ -265,6 +271,10 @@
"lists.edit.submit": "Titel ändern",
"lists.new.create": "Liste hinzufügen",
"lists.new.title_placeholder": "Neuer Titel der Liste",
"lists.replies_policy.followed": "Jeder gefolgte Benutzer",
"lists.replies_policy.list": "Mitglieder der Liste",
"lists.replies_policy.none": "Niemand",
"lists.replies_policy.title": "Antworten anzeigen für:",
"lists.search": "Suche nach Leuten denen du folgst",
"lists.subheading": "Deine Listen",
"load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Sichtbarkeit umschalten",
"missing_indicator.label": "Nicht gefunden",
"missing_indicator.sublabel": "Die Ressource konnte nicht gefunden werden",
"mute_modal.duration": "Dauer",
"mute_modal.hide_notifications": "Benachrichtigungen von diesem Account verbergen?",
"mute_modal.indefinite": "Unbestimmt",
"navigation_bar.apps": "Mobile Apps",
"navigation_bar.blocks": "Blockierte Profile",
"navigation_bar.bookmarks": "Lesezeichen",
@ -303,6 +315,7 @@
"notification.own_poll": "Deine Umfrage ist beendet",
"notification.poll": "Eine Umfrage in der du abgestimmt hast ist vorbei",
"notification.reblog": "{name} hat deinen Beitrag geteilt",
"notification.status": "{name} hat gerade etwas gepostet",
"notifications.clear": "Mitteilungen löschen",
"notifications.clear_confirmation": "Bist du dir sicher, dass du alle Mitteilungen löschen möchtest?",
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Geteilte Beiträge:",
"notifications.column_settings.show": "In der Spalte anzeigen",
"notifications.column_settings.sound": "Ton abspielen",
"notifications.column_settings.status": "Neue Beiträge:",
"notifications.filter.all": "Alle",
"notifications.filter.boosts": "Geteilte Beiträge",
"notifications.filter.favourites": "Favorisierungen",
"notifications.filter.follows": "Folgt",
"notifications.filter.mentions": "Erwähnungen",
"notifications.filter.polls": "Ergebnisse der Umfrage",
"notifications.filter.statuses": "Updates von Personen, denen du folgst",
"notifications.grant_permission": "Zugriff gewährt.",
"notifications.group": "{count} Benachrichtigungen",
"notifications.mark_as_read": "Alle Benachrichtigungen als gelesen markieren",
"notifications.permission_denied": "Desktop-Benachrichtigungen können nicht aktiviert werden, da die Berechtigung verweigert wurde.",
"notifications.permission_denied_alert": "Desktop-Benachrichtigungen können nicht aktiviert werden, da die Browser-Berechtigung zuvor verweigert wurde",
"notifications.permission_required": "Desktop-Benachrichtigungen sind nicht verfügbar, da die erforderliche Berechtigung nicht erteilt wurde.",
"notifications_permission_banner.enable": "Aktiviere Desktop-Benachrichtigungen",
"notifications_permission_banner.how_to_control": "Um Benachrichtigungen zu erhalten, wenn Mastodon nicht geöffnet ist, aktiviere die Desktop-Benachrichtigungen. Du kannst genau bestimmen, welche Arten von Interaktionen Desktop-Benachrichtigungen über die {icon} -Taste erzeugen, sobald diese aktiviert sind.",
"notifications_permission_banner.title": "Verpasse nie etwas",
"picture_in_picture.restore": "Zurücksetzen",
"poll.closed": "Geschlossen",
"poll.refresh": "Aktualisieren",
"poll.total_people": "{count, plural, one {# Person} other {# Personen}}",
@ -362,7 +386,7 @@
"search_popout.search_format": "Fortgeschrittenes Suchformat",
"search_popout.tips.full_text": "Einfache Texteingabe gibt Beiträge, die du geschrieben, favorisiert und geteilt hast zurück. Außerdem auch Beiträge in denen du erwähnt wurdest, aber auch passende Nutzernamen, Anzeigenamen oder Hashtags.",
"search_popout.tips.hashtag": "Hashtag",
"search_popout.tips.status": "Beitrag",
"search_popout.tips.status": "Tröt",
"search_popout.tips.text": "Einfache Texteingabe gibt Anzeigenamen, Benutzernamen und Hashtags zurück",
"search_popout.tips.user": "Nutzer",
"search_results.accounts": "Personen",
@ -403,7 +427,7 @@
"status.reply": "Antworten",
"status.replyAll": "Allen antworten",
"status.report": "@{name} melden",
"status.sensitive_warning": "Heikle Inhalte",
"status.sensitive_warning": "NSFW",
"status.share": "Teilen",
"status.show_less": "Weniger anzeigen",
"status.show_less_all": "Alle Inhaltswarnungen zuklappen",
@ -436,7 +460,7 @@
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Zum Hochladen hereinziehen",
"upload_button.label": "Mediendatei hinzufügen ({formats})",
"upload_button.label": "Mediendatei hinzufügen",
"upload_error.limit": "Dateiupload-Limit erreicht.",
"upload_error.poll": "Dateiuploads sind in Kombination mit Umfragen nicht erlaubt.",
"upload_form.audio_description": "Beschreibe die Audiodatei für Menschen mit Hörschädigungen",
@ -452,6 +476,7 @@
"upload_modal.detect_text": "Text aus Bild erkennen",
"upload_modal.edit_media": "Medien bearbeiten",
"upload_modal.hint": "Klicke oder ziehe den Kreis auf die Vorschau, um den Brennpunkt auszuwählen, der immer auf allen Vorschaubilder angezeigt wird.",
"upload_modal.preparing_ocr": "Vorbereitung von OCR…",
"upload_modal.preview_label": "Vorschau ({ratio})",
"upload_progress.label": "Wird hochgeladen …",
"video.close": "Video schließen",

View File

@ -167,10 +167,18 @@
},
{
"descriptors": [
{
"defaultMessage": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"id": "error.unexpected_crash.explanation_addons"
},
{
"defaultMessage": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
"id": "error.unexpected_crash.explanation"
},
{
"defaultMessage": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"id": "error.unexpected_crash.next_steps_addons"
},
{
"defaultMessage": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"id": "error.unexpected_crash.next_steps"
@ -265,6 +273,15 @@
],
"path": "app/javascript/mastodon/components/missing_indicator.json"
},
{
"descriptors": [
{
"defaultMessage": "Put it back",
"id": "picture_in_picture.restore"
}
],
"path": "app/javascript/mastodon/components/picture_in_picture_placeholder.json"
},
{
"descriptors": [
{
@ -421,7 +438,7 @@
"id": "status.reblog"
},
{
"defaultMessage": "Boost to original audience",
"defaultMessage": "Boost with original visibility",
"id": "status.reblog_private"
},
{
@ -637,6 +654,19 @@
],
"path": "app/javascript/mastodon/containers/status_container.json"
},
{
"descriptors": [
{
"defaultMessage": "Account suspended",
"id": "empty_column.account_suspended"
},
{
"defaultMessage": "Profile unavailable",
"id": "empty_column.account_unavailable"
}
],
"path": "app/javascript/mastodon/features/account_gallery/index.json"
},
{
"descriptors": [
{
@ -690,6 +720,10 @@
"defaultMessage": "Older toots",
"id": "timeline_hint.resources.statuses"
},
{
"defaultMessage": "Account suspended",
"id": "empty_column.account_suspended"
},
{
"defaultMessage": "Profile unavailable",
"id": "empty_column.account_unavailable"
@ -800,6 +834,14 @@
"defaultMessage": "Show boosts from @{name}",
"id": "account.show_reblogs"
},
{
"defaultMessage": "Notify me when @{name} posts",
"id": "account.enable_notifications"
},
{
"defaultMessage": "Stop notifying me when @{name} posts",
"id": "account.disable_notifications"
},
{
"defaultMessage": "Pinned toots",
"id": "navigation_bar.pins"
@ -1334,15 +1376,15 @@
{
"descriptors": [
{
"defaultMessage": "Media is marked as sensitive",
"defaultMessage": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
"id": "compose_form.sensitive.marked"
},
{
"defaultMessage": "Media is not marked as sensitive",
"defaultMessage": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
"id": "compose_form.sensitive.unmarked"
},
{
"defaultMessage": "Mark media as sensitive",
"defaultMessage": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
"id": "compose_form.sensitive.hide"
}
],
@ -2154,6 +2196,18 @@
"defaultMessage": "Delete",
"id": "confirmations.delete_list.confirm"
},
{
"defaultMessage": "Any followed user",
"id": "lists.replies_policy.followed"
},
{
"defaultMessage": "No one",
"id": "lists.replies_policy.none"
},
{
"defaultMessage": "Members of the list",
"id": "lists.replies_policy.list"
},
{
"defaultMessage": "Edit list",
"id": "lists.edit"
@ -2162,6 +2216,10 @@
"defaultMessage": "Delete list",
"id": "lists.delete"
},
{
"defaultMessage": "Show replies to:",
"id": "lists.replies_policy.title"
},
{
"defaultMessage": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
"id": "empty_column.list"
@ -2247,6 +2305,14 @@
"defaultMessage": "Push notifications",
"id": "notifications.column_settings.push"
},
{
"defaultMessage": "Desktop notifications are unavailable due to previously denied browser permissions request",
"id": "notifications.permission_denied"
},
{
"defaultMessage": "Desktop notifications are unavailable because the required permission has not been granted.",
"id": "notifications.permission_required"
},
{
"defaultMessage": "Quick filter bar",
"id": "notifications.column_settings.filter_bar.category"
@ -2274,6 +2340,10 @@
{
"defaultMessage": "Poll results:",
"id": "notifications.column_settings.poll"
},
{
"defaultMessage": "New toots:",
"id": "notifications.column_settings.status"
}
],
"path": "app/javascript/mastodon/features/notifications/components/column_settings.json"
@ -2300,6 +2370,10 @@
"defaultMessage": "Follows",
"id": "notifications.filter.follows"
},
{
"defaultMessage": "Updates from people you follow",
"id": "notifications.filter.statuses"
},
{
"defaultMessage": "All",
"id": "notifications.filter.all"
@ -2320,6 +2394,15 @@
],
"path": "app/javascript/mastodon/features/notifications/components/follow_request.json"
},
{
"descriptors": [
{
"defaultMessage": "Grant permission.",
"id": "notifications.grant_permission"
}
],
"path": "app/javascript/mastodon/features/notifications/components/grant_permission_button.json"
},
{
"descriptors": [
{
@ -2342,6 +2425,10 @@
"defaultMessage": "{name} boosted your status",
"id": "notification.reblog"
},
{
"defaultMessage": "{name} just posted",
"id": "notification.status"
},
{
"defaultMessage": "{name} has requested to follow you",
"id": "notification.follow_request"
@ -2349,6 +2436,27 @@
],
"path": "app/javascript/mastodon/features/notifications/components/notification.json"
},
{
"descriptors": [
{
"defaultMessage": "Close",
"id": "lightbox.close"
},
{
"defaultMessage": "Never miss a thing",
"id": "notifications_permission_banner.title"
},
{
"defaultMessage": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"id": "notifications_permission_banner.how_to_control"
},
{
"defaultMessage": "Enable desktop notifications",
"id": "notifications_permission_banner.enable"
}
],
"path": "app/javascript/mastodon/features/notifications/components/notifications_permission_banner.json"
},
{
"descriptors": [
{
@ -2358,6 +2466,10 @@
{
"defaultMessage": "Clear notifications",
"id": "notifications.clear"
},
{
"defaultMessage": "Desktop notifications can't be enabled, as browser permission has been denied before",
"id": "notifications.permission_denied_alert"
}
],
"path": "app/javascript/mastodon/features/notifications/containers/column_settings_container.json"
@ -2368,6 +2480,10 @@
"defaultMessage": "Notifications",
"id": "column.notifications"
},
{
"defaultMessage": "Mark every notification as read",
"id": "notifications.mark_as_read"
},
{
"defaultMessage": "You don't have any notifications yet. Interact with others to start the conversation.",
"id": "empty_column.notifications"
@ -2375,6 +2491,60 @@
],
"path": "app/javascript/mastodon/features/notifications/index.json"
},
{
"descriptors": [
{
"defaultMessage": "Reply",
"id": "status.reply"
},
{
"defaultMessage": "Reply to thread",
"id": "status.replyAll"
},
{
"defaultMessage": "Boost",
"id": "status.reblog"
},
{
"defaultMessage": "Boost with original visibility",
"id": "status.reblog_private"
},
{
"defaultMessage": "Unboost",
"id": "status.cancel_reblog_private"
},
{
"defaultMessage": "This post cannot be boosted",
"id": "status.cannot_reblog"
},
{
"defaultMessage": "Favourite",
"id": "status.favourite"
},
{
"defaultMessage": "Reply",
"id": "confirmations.reply.confirm"
},
{
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.reply.message"
},
{
"defaultMessage": "Expand this status",
"id": "status.open"
}
],
"path": "app/javascript/mastodon/features/picture_in_picture/components/footer.json"
},
{
"descriptors": [
{
"defaultMessage": "Close",
"id": "lightbox.close"
}
],
"path": "app/javascript/mastodon/features/picture_in_picture/components/header.json"
},
{
"descriptors": [
{
@ -2450,7 +2620,7 @@
"id": "status.reblog"
},
{
"defaultMessage": "Boost to original audience",
"defaultMessage": "Boost with original visibility",
"id": "status.reblog_private"
},
{
@ -2652,15 +2822,6 @@
],
"path": "app/javascript/mastodon/features/status/index.json"
},
{
"descriptors": [
{
"defaultMessage": "View context",
"id": "lightbox.view_context"
}
],
"path": "app/javascript/mastodon/features/ui/components/audio_modal.json"
},
{
"descriptors": [
{
@ -2818,6 +2979,14 @@
"defaultMessage": "Describe for the visually impaired",
"id": "upload_form.description"
},
{
"defaultMessage": "Analyzing picture…",
"id": "upload_modal.analyzing_picture"
},
{
"defaultMessage": "Preparing OCR…",
"id": "upload_modal.preparing_ocr"
},
{
"defaultMessage": "Edit media",
"id": "upload_modal.edit_media"
@ -2830,10 +2999,6 @@
"defaultMessage": "Change thumbnail",
"id": "upload_form.thumbnail"
},
{
"defaultMessage": "Analyzing picture…",
"id": "upload_modal.analyzing_picture"
},
{
"defaultMessage": "Detect text from picture",
"id": "upload_modal.detect_text"
@ -2920,16 +3085,28 @@
{
"defaultMessage": "Next",
"id": "lightbox.next"
},
{
"defaultMessage": "View context",
"id": "lightbox.view_context"
}
],
"path": "app/javascript/mastodon/features/ui/components/media_modal.json"
},
{
"descriptors": [
{
"defaultMessage": "{number, plural, one {# minute} other {# minutes}}",
"id": "intervals.full.minutes"
},
{
"defaultMessage": "{number, plural, one {# hour} other {# hours}}",
"id": "intervals.full.hours"
},
{
"defaultMessage": "{number, plural, one {# day} other {# days}}",
"id": "intervals.full.days"
},
{
"defaultMessage": "Indefinite",
"id": "mute_modal.indefinite"
},
{
"defaultMessage": "Are you sure you want to mute {name}?",
"id": "confirmations.mute.message"
@ -2942,6 +3119,10 @@
"defaultMessage": "Hide notifications from this user?",
"id": "mute_modal.hide_notifications"
},
{
"defaultMessage": "Duration",
"id": "mute_modal.duration"
},
{
"defaultMessage": "Cancel",
"id": "confirmation_modal.cancel"
@ -3072,11 +3253,15 @@
{
"descriptors": [
{
"defaultMessage": "View context",
"id": "lightbox.view_context"
"defaultMessage": "Compress image view box",
"id": "lightbox.compress"
},
{
"defaultMessage": "Expand image view box",
"id": "lightbox.expand"
}
],
"path": "app/javascript/mastodon/features/ui/components/video_modal.json"
"path": "app/javascript/mastodon/features/ui/components/zoomable_image.json"
},
{
"descriptors": [

View File

@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Δες περισσότερα στο αρχικό προφίλ",
"account.cancel_follow_request": "Ακύρωση αιτήματος παρακολούθησης",
"account.direct": "Προσωπικό μήνυμα προς @{name}",
"account.disable_notifications": "Διακοπή ειδοποιήσεων για τις δημοσιεύσεις του/της @{name}",
"account.domain_blocked": "Κρυμμένος τομέας",
"account.edit_profile": "Επεξεργασία προφίλ",
"account.enable_notifications": "Έναρξη ειδοποιήσεων για τις δημοσιεύσεις του/της @{name}",
"account.endorse": "Προβολή στο προφίλ",
"account.follow": "Ακολούθησε",
"account.followers": "Ακόλουθοι",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Αποτελέσματα αναζήτησης",
"emoji_button.symbols": "Σύμβολα",
"emoji_button.travel": "Ταξίδια & Τοποθεσίες",
"empty_column.account_suspended": "Λογαριασμός σε αναστολή",
"empty_column.account_timeline": "Δεν έχει τουτ εδώ!",
"empty_column.account_unavailable": "Μη διαθέσιμο προφίλ",
"empty_column.blocks": "Δεν έχεις αποκλείσει κανέναν χρήστη ακόμα.",
@ -166,7 +169,9 @@
"empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμα. Αλληλεπίδρασε με άλλους χρήστες για να ξεκινήσεις την κουβέντα.",
"empty_column.public": "Δεν υπάρχει τίποτα εδώ! Γράψε κάτι δημόσιο, ή ακολούθησε χειροκίνητα χρήστες από άλλους κόμβους για να τη γεμίσεις",
"error.unexpected_crash.explanation": "Είτε λόγω λάθους στον κώδικά μας ή λόγω ασυμβατότητας με τον browser, η σελίδα δε μπόρεσε να εμφανιστεί σωστά.",
"error.unexpected_crash.explanation_addons": "Η σελίδα δεν μπόρεσε να εμφανιστεί σωστά. Το πρόβλημα οφείλεται πιθανόν σε κάποια επέκταση του φυλλομετρητή (browser extension) ή σε κάποιο αυτόματο εργαλείο μετάφρασης.",
"error.unexpected_crash.next_steps": "Δοκίμασε να ανανεώσεις τη σελίδα. Αν αυτό δε βοηθήσει, ίσως να μπορέσεις να χρησιμοποιήσεις το Mastodon μέσω διαφορετικού browser ή κάποιας εφαρμογής.",
"error.unexpected_crash.next_steps_addons": "Δοκίμασε να τα απενεργοποιήσεις και ανανέωσε τη σελίδα. Αν αυτό δεν βοηθήσει, ίσως να μπορέσεις να χρησιμοποιήσεις το Mastodon μέσω διαφορετικού φυλλομετρητή ή κάποιας εφαρμογής.",
"errors.unexpected_crash.copy_stacktrace": "Αντιγραφή μηνυμάτων κώδικα στο πρόχειρο",
"errors.unexpected_crash.report_issue": "Αναφορά προβλήματος",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "απο-εστίαση του πεδίου σύνθεσης/αναζήτησης",
"keyboard_shortcuts.up": "κίνηση προς την κορυφή της λίστας",
"lightbox.close": "Κλείσιμο",
"lightbox.compress": "Συμπίεση πλαισίου εμφάνισης εικόνας",
"lightbox.expand": "Ανάπτυξη πλαισίου εμφάνισης εικόνας",
"lightbox.next": "Επόμενο",
"lightbox.previous": "Προηγούμενο",
"lightbox.view_context": "Εμφάνιση πλαισίου",
"lists.account.add": "Πρόσθεσε στη λίστα",
"lists.account.remove": "Βγάλε από τη λίστα",
"lists.delete": "Διαγραφή λίστας",
@ -265,6 +271,10 @@
"lists.edit.submit": "Αλλαγή τίτλου",
"lists.new.create": "Προσθήκη λίστας",
"lists.new.title_placeholder": "Τίτλος νέας λίστα",
"lists.replies_policy.followed": "Οποιοσδήποτε χρήστης που ακολουθείς",
"lists.replies_policy.list": "Μέλη της λίστας",
"lists.replies_policy.none": "Κανένας",
"lists.replies_policy.title": "Εμφάνιση απαντήσεων σε:",
"lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς",
"lists.subheading": "Οι λίστες σου",
"load_pending": "{count, plural, one {# νέο} other {# νέα}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Εναλλαγή ορατότητας",
"missing_indicator.label": "Δε βρέθηκε",
"missing_indicator.sublabel": "Αδύνατη η εύρεση αυτού του πόρου",
"mute_modal.duration": "Διάρκεια",
"mute_modal.hide_notifications": "Απόκρυψη ειδοποιήσεων αυτού του χρήστη;",
"mute_modal.indefinite": "Αόριστη",
"navigation_bar.apps": "Εφαρμογές φορητών συσκευών",
"navigation_bar.blocks": "Αποκλεισμένοι χρήστες",
"navigation_bar.bookmarks": "Σελιδοδείκτες",
@ -303,6 +315,7 @@
"notification.own_poll": "Η ψηφοφορία σου έληξε",
"notification.poll": "Τελείωσε μια από τις ψηφοφορίες που συμμετείχες",
"notification.reblog": "Ο/Η {name} προώθησε την κατάστασή σου",
"notification.status": "Ο/Η {name} μόλις έγραψε κάτι",
"notifications.clear": "Καθαρισμός ειδοποιήσεων",
"notifications.clear_confirmation": "Σίγουρα θέλεις να καθαρίσεις όλες τις ειδοποιήσεις σου;",
"notifications.column_settings.alert": "Ειδοποιήσεις επιφάνειας εργασίας",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Προωθήσεις:",
"notifications.column_settings.show": "Εμφάνισε σε στήλη",
"notifications.column_settings.sound": "Ηχητική ειδοποίηση",
"notifications.column_settings.status": "Νέα τουτ:",
"notifications.filter.all": "Όλες",
"notifications.filter.boosts": "Προωθήσεις",
"notifications.filter.favourites": "Αγαπημένα",
"notifications.filter.follows": "Ακόλουθοι",
"notifications.filter.mentions": "Αναφορές",
"notifications.filter.polls": "Αποτελέσματα ψηφοφορίας",
"notifications.filter.statuses": "Ενημερώσεις από όσους ακολουθείς",
"notifications.grant_permission": "Χορήγηση άδειας.",
"notifications.group": "{count} ειδοποιήσεις",
"notifications.mark_as_read": "Σημείωσε όλες τις ειδοποιήσεις ως αναγνωσμένες",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Οι ειδοποιήσεις δεν είναι διαθέσιμες επειδή δεν έχει δοθεί η απαιτούμενη άδεια.",
"notifications_permission_banner.enable": "Ενεργοποίηση ειδοποιήσεων επιφάνειας εργασίας",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Μη χάσετε τίποτα",
"picture_in_picture.restore": "Επαναφορά",
"poll.closed": "Κλειστή",
"poll.refresh": "Ανανέωση",
"poll.total_people": "{count, plural, one {# άτομο} other {# άτομα}}",
@ -394,7 +418,7 @@
"status.pin": "Καρφίτσωσε στο προφίλ",
"status.pinned": "Καρφιτσωμένο τουτ",
"status.read_more": "Περισσότερα",
"status.reblog": "Προώθησε",
"status.reblog": "Προώθηση",
"status.reblog_private": "Προώθησε στους αρχικούς παραλήπτες",
"status.reblogged_by": "{name} προώθησε",
"status.reblogs.empty": "Κανείς δεν προώθησε αυτό το τουτ ακόμα. Μόλις το κάνει κάποια, θα εμφανιστούν εδώ.",
@ -436,7 +460,7 @@
"units.short.million": "{count}Ε",
"units.short.thousand": "{count}Χ",
"upload_area.title": "Drag & drop για να ανεβάσεις",
"upload_button.label": "Πρόσθεσε πολυμέσα ({formats})",
"upload_button.label": "Πρόσθεσε πολυμέσα",
"upload_error.limit": "Υπέρβαση ορίου μεγέθους ανεβασμένων αρχείων.",
"upload_error.poll": "Στις δημοσκοπήσεις δεν επιτρέπεται η μεταφόρτωση αρχείου.",
"upload_form.audio_description": "Περιγραφή για άτομα με προβλήματα ακοής",
@ -452,6 +476,7 @@
"upload_modal.detect_text": "Αναγνώριση κειμένου από την εικόνα",
"upload_modal.edit_media": "Επεξεργασία Πολυμέσων",
"upload_modal.hint": "Κάνε κλικ ή σείρε τον κύκλο στην προεπισκόπηση για να επιλέξεις το σημείο εστίασης που θα είναι πάντα εμφανές σε όλες τις μικρογραφίες.",
"upload_modal.preparing_ocr": "Προετοιμασία αναγνώρισης κειμένου…",
"upload_modal.preview_label": "Προεπισκόπηση ({ratio})",
"upload_progress.label": "Ανεβαίνει...",
"video.close": "Κλείσε το βίντεο",

View File

@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Cancel follow request",
"account.direct": "Direct message @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Domain blocked",
"account.edit_profile": "Edit profile",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile",
"account.follow": "Follow",
"account.followers": "Followers",
@ -96,9 +98,9 @@
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "No toots here!",
"empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "You haven't blocked any users yet.",
@ -166,7 +169,9 @@
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Close",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lightbox.view_context": "View context",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.delete": "Delete list",
@ -265,6 +271,10 @@
"lists.edit.submit": "Change title",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Hide {number, plural, one {image} other {images}}",
"missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Hide notifications from this user?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "Mobile apps",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.bookmarks": "Bookmarks",
@ -303,6 +315,7 @@
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your toot",
"notification.status": "{name} just posted",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.alert": "Desktop notifications",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
"notifications.column_settings.status": "New toots:",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.polls": "Poll results",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} notifications",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Closed",
"poll.refresh": "Refresh",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
@ -395,7 +419,7 @@
"status.pinned": "Pinned toot",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost to original audience",
"status.reblog_private": "Boost with original visibility",
"status.reblogged_by": "{name} boosted",
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft",
@ -452,6 +476,7 @@
"upload_modal.detect_text": "Detect text from picture",
"upload_modal.edit_media": "Edit media",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Uploading...",
"video.close": "Close video",

View File

@ -4,19 +4,21 @@
"account.badges.bot": "Roboto",
"account.badges.group": "Grupo",
"account.block": "Bloki @{name}",
"account.block_domain": "Kaŝi ĉion de {domain}",
"account.block_domain": "Bloki {domain}",
"account.blocked": "Blokita",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.browse_more_on_origin_server": "Rigardi pli al la originala profilo",
"account.cancel_follow_request": "Nuligi peton de sekvado",
"account.direct": "Rekte mesaĝi @{name}",
"account.domain_blocked": "Domajno kaŝita",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Domajno blokita",
"account.edit_profile": "Redakti profilon",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Montri en profilo",
"account.follow": "Sekvi",
"account.followers": "Sekvantoj",
"account.followers.empty": "Ankoraŭ neniu sekvas tiun uzanton.",
"account.followers_counter": "{count, plural, one{{counter} Sekvanto} other {{counter} Sekvantoj}}",
"account.following_counter": "{count, plural, other{{counter} Sekvi}}",
"account.following_counter": "{count, plural, one {{counter} Sekvato} other {{counter} Sekvatoj}}",
"account.follows.empty": "Tiu uzanto ankoraŭ ne sekvas iun.",
"account.follows_you": "Sekvas vin",
"account.hide_reblogs": "Kaŝi diskonigojn de @{name}",
@ -36,14 +38,14 @@
"account.requested": "Atendo de aprobo. Alklaku por nuligi peton de sekvado",
"account.share": "Diskonigi la profilon de @{name}",
"account.show_reblogs": "Montri diskonigojn de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Tooto} other {{counter} Tootoj}}",
"account.statuses_counter": "{count, plural, one {{counter} Mesaĝo} other {{counter} Mesaĝoj}}",
"account.unblock": "Malbloki @{name}",
"account.unblock_domain": "Malkaŝi {domain}",
"account.unblock_domain": "Malbloki {domain}",
"account.unendorse": "Ne montri en profilo",
"account.unfollow": "Ne plu sekvi",
"account.unmute": "Malsilentigi @{name}",
"account.unmute_notifications": "Malsilentigi sciigojn de @{name}",
"account_note.placeholder": "Click to add a note",
"account_note.placeholder": "Alklaku por aldoni noton",
"alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.",
"alert.rate_limited.title": "Mesaĝkvante limigita",
"alert.unexpected.message": "Neatendita eraro okazis.",
@ -62,7 +64,7 @@
"column.community": "Loka tempolinio",
"column.direct": "Rektaj mesaĝoj",
"column.directory": "Trarigardi profilojn",
"column.domain_blocks": "Kaŝitaj domajnoj",
"column.domain_blocks": "Blokitaj domajnoj",
"column.favourites": "Stelumoj",
"column.follow_requests": "Petoj de sekvado",
"column.home": "Hejmo",
@ -110,7 +112,7 @@
"confirmations.delete.message": "Ĉu vi certas, ke vi volas forigi ĉi tiun mesaĝon?",
"confirmations.delete_list.confirm": "Forigi",
"confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?",
"confirmations.domain_block.confirm": "Kaŝi la tutan domajnon",
"confirmations.domain_block.confirm": "Bloki la tutan domajnon",
"confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas tute bloki {domain}? Plej ofte, trafa blokado kaj silentigado sufiĉas kaj preferindas. Vi ne vidos enhavon de tiu domajno en publika tempolinio aŭ en viaj sciigoj. Viaj sekvantoj de tiu domajno estos forigitaj.",
"confirmations.logout.confirm": "Elsaluti",
"confirmations.logout.message": "Ĉu vi certas ke vi volas elsaluti?",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Serĉaj rezultoj",
"emoji_button.symbols": "Simboloj",
"emoji_button.travel": "Vojaĝoj kaj lokoj",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "Neniu mesaĝo ĉi tie!",
"empty_column.account_unavailable": "Profilo ne disponebla",
"empty_column.blocks": "Vi ankoraŭ ne blokis uzanton.",
@ -166,7 +169,9 @@
"empty_column.notifications": "Vi ankoraŭ ne havas sciigojn. Interagu kun aliaj por komenci konversacion.",
"empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj serviloj por plenigi la publikan tempolinion",
"error.unexpected_crash.explanation": "Pro eraro en nia kodo, aŭ problemo de kongruo en via retumilo, ĉi tiu paĝo ne povis esti montrata ĝuste.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Provu refreŝigi la paĝon. Se tio ne helpas, vi ankoraŭ povus uzi Mastodon per malsama retumilo aŭ operaciuma aplikajo.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo",
"errors.unexpected_crash.report_issue": "Raporti problemon",
"federation.change": "Adjust status federation",
@ -176,7 +181,7 @@
"federation.local_only.short": "Local-only",
"follow_request.authorize": "Rajtigi",
"follow_request.reject": "Rifuzi",
"follow_requests.unlocked_explanation": "137/5000\nKvankam via konto ne estas ŝlosita, la dungitaro de {domain} opiniis, ke vi eble volus revizii petojn de sekvadon el ĉi tiuj kontoj permane.",
"follow_requests.unlocked_explanation": "Kvankam via konto ne estas ŝlosita, la dungitaro de {domain} opiniis, ke vi eble volus revizii petojn de sekvadon el ĉi tiuj kontoj permane.",
"generic.saved": "Konservita",
"getting_started.developers": "Programistoj",
"getting_started.directory": "Profilujo",
@ -237,7 +242,7 @@
"keyboard_shortcuts.hotkey": "Rapidklavo",
"keyboard_shortcuts.legend": "montri ĉi tiun noton",
"keyboard_shortcuts.local": "malfermi la lokan tempolinion",
"keyboard_shortcuts.mention": "por mencii la aŭtoron",
"keyboard_shortcuts.mention": "mencii la aŭtoron",
"keyboard_shortcuts.muted": "malfermi la liston de silentigitaj uzantoj",
"keyboard_shortcuts.my_profile": "malfermi vian profilon",
"keyboard_shortcuts.notifications": "malfermi la kolumnon de sciigoj",
@ -247,7 +252,7 @@
"keyboard_shortcuts.reply": "respondi",
"keyboard_shortcuts.requests": "malfermi la liston de petoj de sekvado",
"keyboard_shortcuts.search": "enfokusigi la serĉilon",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.spoilers": "montri/kaŝi la kampon de enhava averto",
"keyboard_shortcuts.start": "malfermi la kolumnon «por komenci»",
"keyboard_shortcuts.toggle_hidden": "montri/kaŝi tekston malantaŭ enhava averto",
"keyboard_shortcuts.toggle_sensitivity": "montri/kaŝi aŭdovidaĵojn",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "malenfokusigi la tekstujon aŭ la serĉilon",
"keyboard_shortcuts.up": "iri supren en la listo",
"lightbox.close": "Fermi",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Sekva",
"lightbox.previous": "Antaŭa",
"lightbox.view_context": "Vidi kuntekston",
"lists.account.add": "Aldoni al la listo",
"lists.account.remove": "Forigi de la listo",
"lists.delete": "Forigi la liston",
@ -265,6 +271,10 @@
"lists.edit.submit": "Ŝanĝi titolon",
"lists.new.create": "Aldoni liston",
"lists.new.title_placeholder": "Titolo de la nova listo",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Montri respondon al:",
"lists.search": "Serĉi inter la homoj, kiujn vi sekvas",
"lists.subheading": "Viaj listoj",
"load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Baskuligi videblecon",
"missing_indicator.label": "Ne trovita",
"missing_indicator.sublabel": "Ĉi tiu elemento ne estis trovita",
"mute_modal.duration": "Daŭro",
"mute_modal.hide_notifications": "Ĉu vi volas kaŝi la sciigojn de ĉi tiu uzanto?",
"mute_modal.indefinite": "Nedifinita",
"navigation_bar.apps": "Telefonaj aplikaĵoj",
"navigation_bar.blocks": "Blokitaj uzantoj",
"navigation_bar.bookmarks": "Legosignoj",
@ -280,7 +292,7 @@
"navigation_bar.compose": "Skribi novan mesaĝon",
"navigation_bar.direct": "Rektaj mesaĝoj",
"navigation_bar.discover": "Esplori",
"navigation_bar.domain_blocks": "Kaŝitaj domajnoj",
"navigation_bar.domain_blocks": "Blokitaj domajnoj",
"navigation_bar.edit_profile": "Redakti profilon",
"navigation_bar.favourites": "Stelumoj",
"navigation_bar.filters": "Silentigitaj vortoj",
@ -303,6 +315,7 @@
"notification.own_poll": "Via balotenketo finiĝitis",
"notification.poll": "Partoprenita balotenketo finiĝis",
"notification.reblog": "{name} diskonigis vian mesaĝon",
"notification.status": "{name} ĵus afiŝita",
"notifications.clear": "Forviŝi sciigojn",
"notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?",
"notifications.column_settings.alert": "Retumilaj sciigoj",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Diskonigoj:",
"notifications.column_settings.show": "Montri en kolumno",
"notifications.column_settings.sound": "Eligi sonon",
"notifications.column_settings.status": "Novaj mesaĝoj:",
"notifications.filter.all": "Ĉiuj",
"notifications.filter.boosts": "Diskonigoj",
"notifications.filter.favourites": "Stelumoj",
"notifications.filter.follows": "Sekvoj",
"notifications.filter.mentions": "Mencioj",
"notifications.filter.polls": "Balotenketaj rezultoj",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} sciigoj",
"notifications.mark_as_read": "Marki ĉiujn sciigojn legita",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Ebligi retumilajn sciigojn",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Neniam preterlasas iun ajn",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Finita",
"poll.refresh": "Aktualigi",
"poll.total_people": "{count, plural, one {# homo} other {# homoj}}",
@ -334,13 +358,13 @@
"poll_button.add_poll": "Aldoni balotenketon",
"poll_button.remove_poll": "Forigi balotenketon",
"privacy.change": "Agordi mesaĝan privatecon",
"privacy.direct.long": "Afiŝi nur al menciitaj uzantoj",
"privacy.direct.long": "Videbla nur al menciitaj uzantoj",
"privacy.direct.short": "Rekta",
"privacy.private.long": "Afiŝi nur al sekvantoj",
"privacy.private.short": "Nur por sekvantoj",
"privacy.public.long": "Afiŝi en publikaj tempolinioj",
"privacy.private.long": "Videbla nur al viaj sekvantoj",
"privacy.private.short": "Nur al sekvantoj",
"privacy.public.long": "Videbla al ĉiuj, afiŝita en publikaj tempolinioj",
"privacy.public.short": "Publika",
"privacy.unlisted.long": "Ne afiŝi en publikaj tempolinioj",
"privacy.unlisted.long": "Videbla al ĉiuj, sed ne en publikaj tempolinioj",
"privacy.unlisted.short": "Nelistigita",
"refresh": "Refreŝigu",
"regeneration_indicator.label": "Ŝargado…",
@ -428,11 +452,11 @@
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Sekvantoj",
"timeline_hint.resources.follows": "Sekvatoj",
"timeline_hint.resources.statuses": "Pli malnovaj tootoj",
"timeline_hint.resources.statuses": "Pli malnovaj mesaĝoj",
"trends.counter_by_accounts": "{count, plural, one {{counter} persono} other {{counter} personoj}} parolante",
"trends.trending_now": "Nunaj furoraĵoj",
"ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.",
"units.short.billion": "{count}B",
"units.short.billion": "{count}Md",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Altreni kaj lasi por alŝuti",
@ -447,11 +471,12 @@
"upload_form.video_description": "Priskribi por homoj kiuj malfacile aŭdi aŭ vidi",
"upload_modal.analyzing_picture": "Bilda analizado…",
"upload_modal.apply": "Apliki",
"upload_modal.choose_image": "Choose image",
"upload_modal.choose_image": "Elekti bildon",
"upload_modal.description_placeholder": "Laŭ Ludoviko Zamenhof bongustas freŝa ĉeĥa manĝaĵo kun spicoj",
"upload_modal.detect_text": "Detekti tekston de la bildo",
"upload_modal.edit_media": "Redakti aŭdovidaĵon",
"upload_modal.hint": "Klaku aŭ trenu la cirklon en la antaŭvidilo por elekti la fokuspunkton kiu ĉiam videblos en ĉiuj etigitaj bildoj.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "Antaŭvido ({ratio})",
"upload_progress.label": "Alŝutado…",
"video.close": "Fermi la videon",

View File

@ -1,16 +1,18 @@
{
"account.account_note_header": "Tu nota para @{name}",
"account.account_note_header": "Nota",
"account.add_or_remove_from_list": "Agregar o quitar de las listas",
"account.badges.bot": "Bot",
"account.badges.group": "Grupo",
"account.block": "Bloquear a @{name}",
"account.block_domain": "Ocultar todo de {domain}",
"account.block_domain": "Bloquear dominio {domain}",
"account.blocked": "Bloqueado",
"account.browse_more_on_origin_server": "Explorar más en el perfil original",
"account.cancel_follow_request": "Cancelar la solicitud de seguimiento",
"account.direct": "Mensaje directo a @{name}",
"account.domain_blocked": "Dominio oculto",
"account.disable_notifications": "Dejar de notificarme cuando @{name} tootee",
"account.domain_blocked": "Dominio bloqueado",
"account.edit_profile": "Editar perfil",
"account.enable_notifications": "Notificarme cuando @{name} tootee",
"account.endorse": "Destacar en el perfil",
"account.follow": "Seguir",
"account.followers": "Seguidores",
@ -25,27 +27,27 @@
"account.locked_info": "El estado de privacidad de esta cuenta está establecido como bloqueado. El propietario manualmente revisa quién puede seguirle.",
"account.media": "Medios",
"account.mention": "Mencionar a @{name}",
"account.moved_to": "{name} se ha muó a:",
"account.moved_to": "{name} se ha mudó a:",
"account.mute": "Silenciar a @{name}",
"account.mute_notifications": "Silenciar notificaciones de @{name}",
"account.muted": "Silenciado",
"account.never_active": "Nunca",
"account.posts": "Toots",
"account.posts_with_replies": "Toots con respuestas",
"account.posts_with_replies": "Toots y respuestas",
"account.report": "Denunciar a @{name}",
"account.requested": "Esperando aprobación. Hacé clic para cancelar la solicitud de seguimiento.",
"account.requested": "Esperando aprobación. Hacé clic para cancelar la solicitud de seguimiento",
"account.share": "Compartir el perfil de @{name}",
"account.show_reblogs": "Mostrar retoots de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
"account.unblock": "Desbloquear a @{name}",
"account.unblock_domain": "Mostrar {domain}",
"account.unblock_domain": "Desbloquear dominio {domain}",
"account.unendorse": "No destacar en el perfil",
"account.unfollow": "Dejar de seguir",
"account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
"account_note.placeholder": "No se ofreció ningún comentario",
"account_note.placeholder": "Hacé clic par agregar una nota",
"alert.rate_limited.message": "Por favor, reintentá después de las {retry_time, time, medium}.",
"alert.rate_limited.title": "Tarifa limitada",
"alert.rate_limited.title": "Acción limitada",
"alert.unexpected.message": "Ocurrió un error.",
"alert.unexpected.title": "¡Epa!",
"announcement.announcement": "Anuncio",
@ -62,7 +64,7 @@
"column.community": "Línea temporal local",
"column.direct": "Mensajes directos",
"column.directory": "Explorar perfiles",
"column.domain_blocks": "Dominios ocultos",
"column.domain_blocks": "Dominios bloqueados",
"column.favourites": "Favoritos",
"column.follow_requests": "Solicitudes de seguimiento",
"column.home": "Principal",
@ -85,19 +87,19 @@
"compose_form.direct_message_warning": "Este toot sólo será enviado a los usuarios mencionados.",
"compose_form.direct_message_warning_learn_more": "Aprendé más",
"compose_form.hashtag_warning": "Este toot no se mostrará bajo hashtags porque no es público. Sólo los toots públicos se pueden buscar por hashtag.",
"compose_form.lock_disclaimer": "Tu cuenta no está {locked}. Todos pueden seguirte para ver tus toots marcados como \"sólo para seguidores\".",
"compose_form.lock_disclaimer": "Tu cuenta no está {locked}. Todos pueden seguirte para ver tus toots marcados como \"Sólo para seguidores\".",
"compose_form.lock_disclaimer.lock": "bloqueada",
"compose_form.placeholder": "¿Qué onda?",
"compose_form.poll.add_option": "Agregá una opción",
"compose_form.poll.duration": "Duración de la encuesta",
"compose_form.poll.option_placeholder": "Opción {number}",
"compose_form.poll.remove_option": "Quitá esta opción",
"compose_form.poll.remove_option": "Quitar esta opción",
"compose_form.poll.switch_to_multiple": "Cambiar encuesta para permitir opciones múltiples",
"compose_form.poll.switch_to_single": "Cambiar encuesta para permitir una sola opción",
"compose_form.publish": "Tootear",
"compose_form.publish_loud": "¡{publish}!",
"compose_form.sensitive.hide": "Marcar medio como sensible",
"compose_form.sensitive.marked": "El medio se marcó como sensible",
"compose_form.sensitive.marked": "{count, plural, one {El medio está marcado como sensible} other {Los medios están marcados como sensibles}}",
"compose_form.sensitive.unmarked": "El medio no está marcado como sensible",
"compose_form.spoiler.marked": "El texto está oculto detrás de la advertencia",
"compose_form.spoiler.unmarked": "El texto no está oculto",
@ -107,10 +109,10 @@
"confirmations.block.confirm": "Bloquear",
"confirmations.block.message": "¿Estás seguro que querés bloquear a {name}?",
"confirmations.delete.confirm": "Eliminar",
"confirmations.delete.message": "¿Estás seguro que querés eliminar este estado?",
"confirmations.delete.message": "¿Estás seguro que querés eliminar este toot?",
"confirmations.delete_list.confirm": "Eliminar",
"confirmations.delete_list.message": "¿Estás seguro que querés eliminar permanentemente esta lista?",
"confirmations.domain_block.confirm": "Ocultar dominio entero",
"confirmations.domain_block.confirm": "Bloquear dominio entero",
"confirmations.domain_block.message": "¿Estás completamente seguro que querés bloquear el {domain} entero? En la mayoría de los casos, unos cuantos bloqueos y silenciados puntuales son suficientes y preferibles. No vas a ver contenido de ese dominio en ninguna de tus líneas temporales o en tus notificaciones. Tus seguidores de ese dominio serán quitados.",
"confirmations.logout.confirm": "Cerrar sesión",
"confirmations.logout.message": "¿Estás seguro que querés cerrar la sesión?",
@ -118,19 +120,19 @@
"confirmations.mute.explanation": "Se ocultarán los mensajes de esta cuenta y los mensajes de otras cuentas que mencionen a ésta, pero todavía esta cuenta podrá ver tus mensajes o seguirte.",
"confirmations.mute.message": "¿Estás seguro que querés silenciar a {name}?",
"confirmations.redraft.confirm": "Eliminar toot original y editarlo",
"confirmations.redraft.message": "¿Estás seguro que querés eliminar este estado y volver a editarlo? Se perderán las veces marcadas como favoritos y los retoots, y las respuestas a la publicación original quedarán huérfanas.",
"confirmations.redraft.message": "¿Estás seguro que querés eliminar este toot y volver a editarlo? Se perderán las veces marcadas como favoritos y los retoots, y las respuestas a la publicación original quedarán huérfanas.",
"confirmations.reply.confirm": "Responder",
"confirmations.reply.message": "Responder ahora sobreescribirá el mensaje que estás redactando actualmente. ¿Estás seguro que querés seguir?",
"confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Estás seguro que querés dejar de seguir a {name}?",
"conversation.delete": "Eliminar conversación",
"conversation.mark_as_read": "Marcar como leído",
"conversation.mark_as_read": "Marcar como leída",
"conversation.open": "Ver conversación",
"conversation.with": "Con {names}",
"directory.federated": "Desde fediverso conocido",
"directory.local": "Sólo de {domain}",
"directory.new_arrivals": "Recién llegados",
"directory.recently_active": "Recientemente activo",
"directory.recently_active": "Recientemente activos",
"embed.instructions": "Insertá este toot a tu sitio web copiando el código de abajo.",
"embed.preview": "Así es cómo se verá:",
"emoji_button.activity": "Actividad",
@ -143,17 +145,18 @@
"emoji_button.objects": "Objetos",
"emoji_button.people": "Gente",
"emoji_button.recent": "Usados frecuentemente",
"emoji_button.search": "Buscar",
"emoji_button.search": "Buscar...",
"emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares",
"empty_column.account_timeline": "¡No hay toots aquí!",
"empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay toots acá!",
"empty_column.account_unavailable": "Perfil no disponible",
"empty_column.blocks": "Todavía no bloqueaste a ningún usuario.",
"empty_column.bookmarked_statuses": "Todavía no tenés toots guardados en marcadores. Cuando guardés uno en marcadores, se mostrará acá.",
"empty_column.bookmarked_statuses": "Todavía no tenés toots guardados en \"Marcadores\". Cuando guardés uno en \"Marcadores\", se mostrará acá.",
"empty_column.community": "La línea temporal local está vacía. ¡Escribí algo en modo público para que se empiece a correr la bola!",
"empty_column.direct": "Todavía no tenés ningún mensaje directo. Cuando enviés o recibás uno, se mostrará acá.",
"empty_column.domain_blocks": "Todavía no hay dominios ocultos.",
"empty_column.domain_blocks": "Todavía no hay dominios bloqueados.",
"empty_column.favourited_statuses": "Todavía no tenés toots favoritos. Cuando marqués uno como favorito, se mostrará acá.",
"empty_column.favourites": "Todavía nadie marcó este toot como favorito. Cuando alguien lo haga, se mostrará acá.",
"empty_column.follow_requests": "Todavía no tenés ninguna solicitud de seguimiento. Cuando recibás una, se mostrará acá.",
@ -161,12 +164,14 @@
"empty_column.home": "¡Tu línea temporal principal está vacía! Visitá {public} o usá la búsqueda para comenzar y encontrar a otros usuarios.",
"empty_column.home.public_timeline": "la línea temporal pública",
"empty_column.list": "Todavía no hay nada en esta lista. Cuando miembros de esta lista envíen nuevos toots, se mostrarán acá.",
"empty_column.lists": "Todavía no tienes ninguna lista. Cuando creés una, se mostrará acá.",
"empty_column.lists": "Todavía no tenés ninguna lista. Cuando creés una, se mostrará acá.",
"empty_column.mutes": "Todavía no silenciaste a ningún usuario.",
"empty_column.notifications": "Todavía no tenés ninguna notificación. Interactuá con otros para iniciar la conversación.",
"empty_column.public": "¡Naranja! Escribí algo públicamente, o seguí usuarios manualmente de otros servidores para ir llenando esta línea temporal.",
"empty_column.public": "¡Naranja! Escribí algo públicamente, o seguí usuarios manualmente de otros servidores para ir llenando esta línea temporal",
"error.unexpected_crash.explanation": "Debido a un error en nuestro código o a un problema de compatibilidad con el navegador web, esta página no se pudo mostrar correctamente.",
"error.unexpected_crash.explanation_addons": "No se pudo mostrar correctamente esta página. Este error probablemente es causado por un complemento del navegador web o por herramientas de traducción automática.",
"error.unexpected_crash.next_steps": "Intentá recargar la página. Si eso no ayuda, podés usar Mastodon a través de un navegador web diferente o aplicación nativa.",
"error.unexpected_crash.next_steps_addons": "Intentá deshabilitarlos y recargá la página. Si eso no ayuda, podés usar Mastodon a través de un navegador web diferente o aplicación nativa.",
"errors.unexpected_crash.copy_stacktrace": "Copiar stacktrace al portapapeles",
"errors.unexpected_crash.report_issue": "Informar problema",
"follow_request.authorize": "Autorizar",
@ -177,9 +182,9 @@
"getting_started.directory": "Directorio de perfiles",
"getting_started.documentation": "Documentación",
"getting_started.heading": "Introducción",
"getting_started.invite": "Invitar usuarios",
"getting_started.invite": "Invitar gente",
"getting_started.open_source_notice": "Mastodon es software libre. Podés contribuir o informar errores en {github}.",
"getting_started.security": "Seguridad",
"getting_started.security": "Configuración de la cuenta",
"getting_started.terms": "Términos del servicio",
"hashtag.column_header.tag_mode.all": "y {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
@ -199,31 +204,31 @@
"intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
"introduction.federation.action": "Siguiente",
"introduction.federation.federated.headline": "Federado",
"introduction.federation.federated.headline": "Federada",
"introduction.federation.federated.text": "Los toots públicos de otros servidores del fediverso aparecerán en la línea temporal federada.",
"introduction.federation.home.headline": "Principal",
"introduction.federation.home.text": "Los toots de las personas que seguís aparecerán en tu línea temporal principal. ¡Podés seguir a cualquiera en cualquier servidor!",
"introduction.federation.home.text": "Los toots de las cuentas que seguís aparecerán en tu línea temporal principal. ¡Podés seguir a cualquiera en cualquier servidor!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Los toots públicos de las personas en el mismo servidor aparecerán en la línea temporal local.",
"introduction.federation.local.text": "Los toots públicos de las cuentas en el mismo servidor aparecerán en la línea temporal local.",
"introduction.interactions.action": "¡Terminar tutorial!",
"introduction.interactions.favourite.headline": "Favorito",
"introduction.interactions.favourite.headline": "Favoritos",
"introduction.interactions.favourite.text": "Podés guardar un toot para más tarde, y hacerle saber al autor que te gustó, marcándolo como favorito.",
"introduction.interactions.reblog.headline": "Retootear",
"introduction.interactions.reblog.text": "Podés compartir los toots de otras personas con tus seguidores retooteando los mismos.",
"introduction.interactions.reblog.text": "Podés compartir los toots de otras cuentas con tus seguidores retooteando los mismos.",
"introduction.interactions.reply.headline": "Responder",
"introduction.interactions.reply.text": "Podés responder a tus propios toots y los de otras personas, que se encadenarán juntos en una conversación.",
"introduction.interactions.reply.text": "Podés responder a tus propios toots y los de otras cuentas, que se encadenarán juntos en una conversación.",
"introduction.welcome.action": "¡Dale!",
"introduction.welcome.headline": "Primeros pasos",
"introduction.welcome.text": "¡Bienvenido al fediverso! En unos pocos minutos, vas a poder transmitir mensajes y hablar con tus amigos a través de una amplia variedad de servidores. Pero este servidor, {domain}, es especial: aloja tu perfil, así que acordate de su nombre.",
"keyboard_shortcuts.back": "para volver",
"keyboard_shortcuts.blocked": "para abrir la lista de usuarios bloqueados",
"keyboard_shortcuts.boost": "para retootear",
"keyboard_shortcuts.column": "para enfocar un estado en una de las columnas",
"keyboard_shortcuts.column": "para enfocar un toot en una de las columnas",
"keyboard_shortcuts.compose": "para enfocar el área de texto de redacción",
"keyboard_shortcuts.description": "Descripción",
"keyboard_shortcuts.direct": "para abrir columna de mensajes directos",
"keyboard_shortcuts.down": "para bajar en la lista",
"keyboard_shortcuts.enter": "para abrir el estado",
"keyboard_shortcuts.enter": "para abrir el toot",
"keyboard_shortcuts.favourite": "para marcar como favorito",
"keyboard_shortcuts.favourites": "para abrir la lista de favoritos",
"keyboard_shortcuts.federated": "para abrir la línea temporal federada",
@ -233,11 +238,11 @@
"keyboard_shortcuts.legend": "para mostrar este texto",
"keyboard_shortcuts.local": "para abrir la línea temporal local",
"keyboard_shortcuts.mention": "para mencionar al autor",
"keyboard_shortcuts.muted": "abrir la lista de usuarios silenciados",
"keyboard_shortcuts.muted": "para abrir la lista de usuarios silenciados",
"keyboard_shortcuts.my_profile": "para abrir tu perfil",
"keyboard_shortcuts.notifications": "para abrir la columna de notificaciones",
"keyboard_shortcuts.open_media": "para abrir archivos de medios",
"keyboard_shortcuts.pinned": "para abrir lista de toots fijados",
"keyboard_shortcuts.open_media": "para abrir los archivos de medios",
"keyboard_shortcuts.pinned": "para abrir la lista de toots fijados",
"keyboard_shortcuts.profile": "para abrir el perfil del autor",
"keyboard_shortcuts.reply": "para responder",
"keyboard_shortcuts.requests": "para abrir la lista de solicitudes de seguimiento",
@ -250,9 +255,10 @@
"keyboard_shortcuts.unfocus": "para quitar el enfoque del área de texto de redacción o de búsqueda",
"keyboard_shortcuts.up": "para subir en la lista",
"lightbox.close": "Cerrar",
"lightbox.compress": "Comprimir cuadro de vista de imagen",
"lightbox.expand": "Expandir cuadro de vista de imagen",
"lightbox.next": "Siguiente",
"lightbox.previous": "Anterior",
"lightbox.view_context": "Ver contexto",
"lists.account.add": "Agregar a lista",
"lists.account.remove": "Quitar de lista",
"lists.delete": "Eliminar lista",
@ -260,14 +266,20 @@
"lists.edit.submit": "Cambiar título",
"lists.new.create": "Agregar lista",
"lists.new.title_placeholder": "Nuevo título de lista",
"lists.replies_policy.followed": "Cualquier cuenta seguida",
"lists.replies_policy.list": "Miembros de la lista",
"lists.replies_policy.none": "Nadie",
"lists.replies_policy.title": "Mostrar respuestas a:",
"lists.search": "Buscar entre la gente que seguís",
"lists.subheading": "Tus listas",
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
"loading_indicator.label": "Cargando",
"media_gallery.toggle_visible": "Cambiar visibilidad",
"loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Ocultar {number, plural, one {imagen} other {imágenes}}",
"missing_indicator.label": "No se encontró",
"missing_indicator.sublabel": "No se encontró este recurso",
"mute_modal.duration": "Duración",
"mute_modal.hide_notifications": "¿Querés ocultar las notificaciones de este usuario?",
"mute_modal.indefinite": "Indefinida",
"navigation_bar.apps": "Aplicaciones móviles",
"navigation_bar.blocks": "Usuarios bloqueados",
"navigation_bar.bookmarks": "Marcadores",
@ -275,12 +287,12 @@
"navigation_bar.compose": "Redactar un nuevo toot",
"navigation_bar.direct": "Mensajes directos",
"navigation_bar.discover": "Descubrir",
"navigation_bar.domain_blocks": "Dominios ocultos",
"navigation_bar.domain_blocks": "Dominios bloqueados",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.filters": "Palabras silenciadas",
"navigation_bar.follow_requests": "Solicitudes de seguimiento",
"navigation_bar.follows_and_followers": "Personas seguidas y seguidores",
"navigation_bar.follows_and_followers": "Cuentas seguidas y seguidores",
"navigation_bar.info": "Acerca de este servidor",
"navigation_bar.keyboard_shortcuts": "Atajos",
"navigation_bar.lists": "Listas",
@ -291,13 +303,14 @@
"navigation_bar.preferences": "Configuración",
"navigation_bar.public_timeline": "Línea temporal federada",
"navigation_bar.security": "Seguridad",
"notification.favourite": "{name} marcó tu estado como favorito",
"notification.favourite": "{name} marcó tu toot como favorito",
"notification.follow": "{name} te empezó a seguir",
"notification.follow_request": "{name} solicitó seguirte",
"notification.mention": "{name} te mencionó",
"notification.own_poll": "Tu encuesta finalizó",
"notification.poll": "Finalizó una encuesta en la que votaste",
"notification.reblog": "{name} retooteó tu estado",
"notification.status": "{name} acaba de tootear",
"notifications.clear": "Limpiar notificaciones",
"notifications.clear_confirmation": "¿Estás seguro que querés limpiar todas tus notificaciones permanentemente?",
"notifications.column_settings.alert": "Notificaciones de escritorio",
@ -313,13 +326,24 @@
"notifications.column_settings.reblog": "Retoots:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",
"notifications.column_settings.status": "Nuevos toots:",
"notifications.filter.all": "Todas",
"notifications.filter.boosts": "Retoots",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguidores",
"notifications.filter.mentions": "Menciones",
"notifications.filter.polls": "Resultados de la encuesta",
"notifications.filter.polls": "Resultados de encuesta",
"notifications.filter.statuses": "Actualizaciones de cuentas que seguís",
"notifications.grant_permission": "Conceder permiso.",
"notifications.group": "{count} notificaciones",
"notifications.mark_as_read": "Marcar cada notificación como leída",
"notifications.permission_denied": "Las notificaciones de escritorio no están disponibles, debido a una solicitud de permiso del navegador web previamente denegada",
"notifications.permission_denied_alert": "No se pueden habilitar las notificaciones de escritorio, ya que el permiso del navegador fue denegado antes",
"notifications.permission_required": "Las notificaciones de escritorio no están disponibles porque no se concedió el permiso requerido.",
"notifications_permission_banner.enable": "Habilitar notificaciones de escritorio",
"notifications_permission_banner.how_to_control": "Para recibir notificaciones cuando Mastodon no está abierto, habilitá las notificaciones de escritorio. Podés controlar con precisión qué tipos de interacciones generan notificaciones de escritorio a través del botón {icon} de arriba, una vez que estén habilitadas.",
"notifications_permission_banner.title": "No te pierdas nada",
"picture_in_picture.restore": "Restaurar",
"poll.closed": "Cerrada",
"poll.refresh": "Refrescar",
"poll.total_people": "{count, plural, one {# persona} other {# personas}}",
@ -328,14 +352,14 @@
"poll.voted": "Votaste esta opción",
"poll_button.add_poll": "Agregar una encuesta",
"poll_button.remove_poll": "Quitar encuesta",
"privacy.change": "Configurar privacidad de estado",
"privacy.direct.long": "Enviar toot sólo a los usuarios mencionados",
"privacy.change": "Configurar privacidad de toot",
"privacy.direct.long": "Visible sólo a los usuarios mencionados",
"privacy.direct.short": "Directo",
"privacy.private.long": "Enviar toot sólo a los seguidores",
"privacy.private.long": "Visible sólo a los seguidores",
"privacy.private.short": "Sólo a seguidores",
"privacy.public.long": "Enviar toot a las líneas temporales públicas",
"privacy.public.long": "Visible para todos, mostrado en las líneas temporales públicas",
"privacy.public.short": "Público",
"privacy.unlisted.long": "No enviar toot a las líneas temporales públicas",
"privacy.unlisted.long": "Visible para todos, pero no en las líneas temporales públicas",
"privacy.unlisted.short": "No listado",
"refresh": "Refrescar",
"regeneration_indicator.label": "Cargando…",
@ -355,28 +379,28 @@
"report.target": "Denunciando a {target}",
"search.placeholder": "Buscar",
"search_popout.search_format": "Formato de búsqueda avanzada",
"search_popout.tips.full_text": "Las búsquedas de texto simple devuelven los estados que escribiste, los marcados como favoritos, los retooteados o en los que te mencionaron, así como nombres usuarios, nombres mostrados y etiquetas.",
"search_popout.tips.full_text": "Las búsquedas de texto simple devuelven los toots que escribiste, los marcados como favoritos, los retooteados o en los que te mencionaron, así como nombres de usuarios, nombres mostrados y etiquetas.",
"search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "estado",
"search_popout.tips.status": "toot",
"search_popout.tips.text": "Las búsquedas de texto simple devuelven nombres de usuarios, nombres mostrados y etiquetas que coincidan",
"search_popout.tips.user": "usuario",
"search_results.accounts": "Gente",
"search_results.hashtags": "Etiquetas",
"search_results.statuses": "Toots",
"search_results.statuses_fts_disabled": "No se puede buscar toots por contenido en este servidor de Mastodon.",
"search_results.statuses_fts_disabled": "No se pueden buscar toots por contenido en este servidor de Mastodon.",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"status.admin_account": "Abrir interface de moderación para @{name}",
"status.admin_status": "Abrir este estado en la interface de moderación",
"status.admin_status": "Abrir este toot en la interface de moderación",
"status.block": "Bloquear a @{name}",
"status.bookmark": "Marcador",
"status.bookmark": "Marcar",
"status.cancel_reblog_private": "Quitar retoot",
"status.cannot_reblog": "No se puede retootear este toot",
"status.copy": "Copiar enlace al estado",
"status.copy": "Copiar enlace al toot",
"status.delete": "Eliminar",
"status.detailed_status": "Vista de conversación detallada",
"status.direct": "Mensaje directo a @{name}",
"status.embed": "Insertar",
"status.favourite": "Favorito",
"status.favourite": "Marcar como favorito",
"status.filtered": "Filtrado",
"status.load_more": "Cargar más",
"status.media_hidden": "Medios ocultos",
@ -384,10 +408,10 @@
"status.more": "Más",
"status.mute": "Silenciar a @{name}",
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir este estado",
"status.open": "Expandir este toot",
"status.pin": "Fijar en el perfil",
"status.pinned": "Toot fijado",
"status.read_more": "Leer más",
"status.read_more": "Leé más",
"status.reblog": "Retootear",
"status.reblog_private": "Retootear a la audiencia original",
"status.reblogged_by": "{name} retooteó",
@ -409,7 +433,7 @@
"status.unpin": "Dejar de fijar",
"suggestions.dismiss": "Descartar sugerencia",
"suggestions.header": "Es posible que te interese…",
"tabs_bar.federated_timeline": "Federado",
"tabs_bar.federated_timeline": "Federada",
"tabs_bar.home": "Principal",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificaciones",
@ -430,15 +454,15 @@
"units.short.million": "{count}M",
"units.short.thousand": "{count}mil",
"upload_area.title": "Para subir, arrastrá y soltá",
"upload_button.label": "Agregar medios ({formats})",
"upload_button.label": "Agregá imágenes o un archivo de audio o video",
"upload_error.limit": "Se excedió el límite de subida de archivos.",
"upload_error.poll": "No se permite la subida de archivos en encuestas.",
"upload_form.audio_description": "Describir para personas con problemas auditivos",
"upload_form.description": "Agregar descripción para los usuarios con dificultades visuales",
"upload_form.audio_description": "Agregá una descripción para personas con dificultades auditivas",
"upload_form.description": "Agregá una descripción para personas con dificultades visuales",
"upload_form.edit": "Editar",
"upload_form.thumbnail": "Cambiar miniatura",
"upload_form.undo": "Eliminar",
"upload_form.video_description": "Describir para personas con problemas auditivos o visuales",
"upload_form.video_description": "Agregá una descripción para personas con dificultades auditivas o visuales",
"upload_modal.analyzing_picture": "Analizando imagen…",
"upload_modal.apply": "Aplicar",
"upload_modal.choose_image": "Elegir imagen",
@ -446,12 +470,13 @@
"upload_modal.detect_text": "Detectar texto de la imagen",
"upload_modal.edit_media": "Editar medio",
"upload_modal.hint": "Hacé clic o arrastrá el círculo en la previsualización para elegir el punto focal que siempre estará a la vista en todas las miniaturas.",
"upload_modal.preparing_ocr": "Preparando OCR…",
"upload_modal.preview_label": "Previsualización ({ratio})",
"upload_progress.label": "Subiendo",
"upload_progress.label": "Subiendo...",
"video.close": "Cerrar video",
"video.download": "Descargar archivo",
"video.exit_fullscreen": "Salir de pantalla completa",
"video.expand": "Expandir vídeo",
"video.expand": "Expandir video",
"video.fullscreen": "Pantalla completa",
"video.hide": "Ocultar video",
"video.mute": "Silenciar sonido",

View File

@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Ver más en el perfil original",
"account.cancel_follow_request": "Cancelar la solicitud de seguimiento",
"account.direct": "Mensaje directo a @{name}",
"account.disable_notifications": "Dejar de notificarme cuando @{name} publique algo",
"account.domain_blocked": "Dominio oculto",
"account.edit_profile": "Editar perfil",
"account.enable_notifications": "Notificarme cuando @{name} publique algo",
"account.endorse": "Mostrar en perfil",
"account.follow": "Seguir",
"account.followers": "Seguidores",
@ -118,7 +120,7 @@
"confirmations.mute.explanation": "Esto esconderá las publicaciones de ellos y en las que los has mencionado, pero les permitirá ver tus mensajes y seguirte.",
"confirmations.mute.message": "¿Estás seguro de que quieres silenciar a {name}?",
"confirmations.redraft.confirm": "Borrar y volver a borrador",
"confirmations.redraft.message": "Estás seguro de que quieres borrar este estado y volverlo a borrador? Perderás todas las respuestas, impulsos y favoritos asociados a él, y las respuestas a la publicación original quedarán huérfanos.",
"confirmations.redraft.message": "¿Estás seguro de que quieres eliminar este toot y convertirlo en borrador? Perderás todas las respuestas, retoots y favoritos asociados a él, y las respuestas a la publicación original quedarán huérfanas.",
"confirmations.reply.confirm": "Responder",
"confirmations.reply.message": "Responder sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?",
"confirmations.unfollow.confirm": "Dejar de seguir",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares",
"empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay toots aquí!",
"empty_column.account_unavailable": "Perfil no disponible",
"empty_column.blocks": "Aún no has bloqueado a ningún usuario.",
@ -166,7 +169,9 @@
"empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
"empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo",
"error.unexpected_crash.explanation": "Debido a un error en nuestro código o a un problema de compatibilidad con el navegador, esta página no se ha podido mostrar correctamente.",
"error.unexpected_crash.explanation_addons": "No se pudo mostrar correctamente esta página. Este error probablemente fue causado por un complemento del navegador web o por herramientas de traducción automática.",
"error.unexpected_crash.next_steps": "Intenta actualizar la página. Si eso no ayuda, es posible que puedas usar Mastodon a través de otro navegador o aplicación nativa.",
"error.unexpected_crash.next_steps_addons": "Intenta deshabilitarlos y recarga la página. Si eso no ayuda, podrías usar Mastodon a través de un navegador web diferente o aplicación nativa.",
"errors.unexpected_crash.copy_stacktrace": "Copiar el seguimiento de pila en el portapapeles",
"errors.unexpected_crash.report_issue": "Informar de un problema/error",
"federation.change": "Adjust status federation",
@ -255,9 +260,10 @@
"keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
"keyboard_shortcuts.up": "para ir hacia arriba en la lista",
"lightbox.close": "Cerrar",
"lightbox.compress": "Comprimir cuadro de visualización de imagen",
"lightbox.expand": "Expandir cuadro de visualización de imagen",
"lightbox.next": "Siguiente",
"lightbox.previous": "Anterior",
"lightbox.view_context": "Ver contexto",
"lists.account.add": "Añadir a lista",
"lists.account.remove": "Quitar de lista",
"lists.delete": "Borrar lista",
@ -265,6 +271,10 @@
"lists.edit.submit": "Cambiar título",
"lists.new.create": "Añadir lista",
"lists.new.title_placeholder": "Título de la nueva lista",
"lists.replies_policy.followed": "Cualquier usuario seguido",
"lists.replies_policy.list": "Miembros de la lista",
"lists.replies_policy.none": "Nadie",
"lists.replies_policy.title": "Mostrar respuestas a:",
"lists.search": "Buscar entre la gente a la que sigues",
"lists.subheading": "Tus listas",
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
@ -272,7 +282,9 @@
"media_gallery.toggle_visible": "Cambiar visibilidad",
"missing_indicator.label": "No encontrado",
"missing_indicator.sublabel": "No se encontró este recurso",
"mute_modal.duration": "Duración",
"mute_modal.hide_notifications": "Ocultar notificaciones de este usuario?",
"mute_modal.indefinite": "Indefinida",
"navigation_bar.apps": "Aplicaciones móviles",
"navigation_bar.blocks": "Usuarios bloqueados",
"navigation_bar.bookmarks": "Marcadores",
@ -303,6 +315,7 @@
"notification.own_poll": "Tu encuesta ha terminado",
"notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.reblog": "{name} ha retooteado tu estado",
"notification.status": "{name} acaba de publicar",
"notifications.clear": "Limpiar notificaciones",
"notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?",
"notifications.column_settings.alert": "Notificaciones de escritorio",
@ -318,13 +331,24 @@
"notifications.column_settings.reblog": "Retoots:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",
"notifications.column_settings.status": "Nuevos toots:",
"notifications.filter.all": "Todos",
"notifications.filter.boosts": "Retoots",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguidores",
"notifications.filter.mentions": "Menciones",
"notifications.filter.polls": "Resultados de la votación",
"notifications.filter.statuses": "Actualizaciones de gente a la que sigues",
"notifications.grant_permission": "Conceder permiso.",
"notifications.group": "{count} notificaciones",
"notifications.mark_as_read": "Marcar todas las notificaciones como leídas",
"notifications.permission_denied": "No se pueden habilitar las notificaciones de escritorio ya que se denegó el permiso.",
"notifications.permission_denied_alert": "No se pueden habilitar las notificaciones de escritorio, ya que el permiso del navegador fue denegado anteriormente",
"notifications.permission_required": "Las notificaciones de escritorio no están disponibles porque no se ha concedido el permiso requerido.",
"notifications_permission_banner.enable": "Habilitar notificaciones de escritorio",
"notifications_permission_banner.how_to_control": "Para recibir notificaciones cuando Mastodon no esté abierto, habilite las notificaciones de escritorio. Puedes controlar con precisión qué tipos de interacciones generan notificaciones de escritorio a través del botón {icon} de arriba una vez que estén habilitadas.",
"notifications_permission_banner.title": "Nunca te pierdas nada",
"picture_in_picture.restore": "Restaurar",
"poll.closed": "Cerrada",
"poll.refresh": "Actualizar",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
@ -362,7 +386,7 @@
"search_popout.search_format": "Formato de búsqueda avanzada",
"search_popout.tips.full_text": "Búsquedas de texto recuperan posts que has escrito, marcado como favoritos, retooteado o en los que has sido mencionado, así como usuarios, nombres y hashtags.",
"search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "estado",
"search_popout.tips.status": "toot",
"search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag",
"search_popout.tips.user": "usuario",
"search_results.accounts": "Gente",
@ -373,8 +397,8 @@
"status.admin_account": "Abrir interfaz de moderación para @{name}",
"status.admin_status": "Abrir este estado en la interfaz de moderación",
"status.block": "Bloquear a @{name}",
"status.bookmark": "Marcador",
"status.cancel_reblog_private": "Des-impulsar",
"status.bookmark": "Añadir marcador",
"status.cancel_reblog_private": "Eliminar retoot",
"status.cannot_reblog": "Este toot no puede retootearse",
"status.copy": "Copiar enlace al estado",
"status.delete": "Borrar",
@ -397,11 +421,11 @@
"status.reblog": "Retootear",
"status.reblog_private": "Implusar a la audiencia original",
"status.reblogged_by": "Retooteado por {name}",
"status.reblogs.empty": "Nadie impulsó este toot todavía. Cuando alguien lo haga, aparecerá aqui.",
"status.reblogs.empty": "Nadie retooteó este toot todavía. Cuando alguien lo haga, aparecerá aquí.",
"status.redraft": "Borrar y volver a borrador",
"status.remove_bookmark": "Eliminar marcador",
"status.reply": "Responder",
"status.replyAll": "Responder al hilván",
"status.replyAll": "Responder al hilo",
"status.report": "Reportar",
"status.sensitive_warning": "Contenido sensible",
"status.share": "Compartir",
@ -409,7 +433,7 @@
"status.show_less_all": "Mostrar menos para todo",
"status.show_more": "Mostrar más",
"status.show_more_all": "Mostrar más para todo",
"status.show_thread": "Mostrar hilván",
"status.show_thread": "Mostrar hilo",
"status.uncached_media_warning": "No disponible",
"status.unmute_conversation": "Dejar de silenciar conversación",
"status.unpin": "Dejar de fijar",
@ -432,9 +456,9 @@
"trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} personas}} hablando",
"trends.trending_now": "Tendencia ahora",
"ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
"units.short.billion": "{count}MM",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}mil",
"units.short.thousand": "{count}K",
"upload_area.title": "Arrastra y suelta para subir",
"upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Límite de subida de archivos excedido.",
@ -452,6 +476,7 @@
"upload_modal.detect_text": "Detectar texto de la imagen",
"upload_modal.edit_media": "Editar multimedia",
"upload_modal.hint": "Haga clic o arrastre el círculo en la vista previa para elegir el punto focal que siempre estará a la vista en todas las miniaturas.",
"upload_modal.preparing_ocr": "Preparando OCR…",
"upload_modal.preview_label": "Vista previa ({ratio})",
"upload_progress.label": "Subiendo…",
"video.close": "Cerrar video",

View File

@ -9,8 +9,10 @@
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Tühista jälgimistaotlus",
"account.direct": "Otsesõnum @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Domeen peidetud",
"account.edit_profile": "Muuda profiili",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Too profiilil esile",
"account.follow": "Jälgi",
"account.followers": "Jälgijad",
@ -147,6 +149,7 @@
"emoji_button.search_results": "Otsitulemused",
"emoji_button.symbols": "Sümbolid",
"emoji_button.travel": "Reisimine & Kohad",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "Siin tuute ei ole!",
"empty_column.account_unavailable": "Profiil pole saadaval",
"empty_column.blocks": "Sa ei ole veel ühtegi kasutajat blokeerinud.",
@ -166,7 +169,9 @@
"empty_column.notifications": "Teil ei ole veel teateid. Suhelge teistega alustamaks vestlust.",
"empty_column.public": "Siin pole midagi! Kirjuta midagi avalikut või jälgi ise kasutajaid täitmaks seda ruumi",
"error.unexpected_crash.explanation": "Meie poolse probleemi või veebilehitseja ühilduvus probleemi tõttu ei suutnud me Teile seda lehekülge korrektselt näidata.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Proovige lehekülge uuesti avada. Kui see ei aita, võite proovida kasutada Mastodoni mõne muu veebilehitseja või äppi kaudu.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Kopeeri stacktrace lõikelauale",
"errors.unexpected_crash.report_issue": "Teavita veast",
"follow_request.authorize": "Autoriseeri",
@ -250,9 +255,10 @@
"keyboard_shortcuts.unfocus": "tekstiala/otsingu koostamise mittefokuseerimiseks",
"keyboard_shortcuts.up": "liikumaks nimistus üles",
"lightbox.close": "Sulge",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.next": "Järgmine",
"lightbox.previous": "Eelmine",
"lightbox.view_context": "Vaata konteksti",
"lists.account.add": "Lisa nimistusse",
"lists.account.remove": "Eemalda nimistust",
"lists.delete": "Kustuta nimistu",
@ -260,6 +266,10 @@
"lists.edit.submit": "Muuda pealkiri",
"lists.new.create": "Lisa nimistu",
"lists.new.title_placeholder": "Uus nimistu pealkiri",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
"lists.replies_policy.title": "Show replies to:",
"lists.search": "Otsi Teie poolt jälgitavate inimese hulgast",
"lists.subheading": "Teie nimistud",
"load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
@ -267,7 +277,9 @@
"media_gallery.toggle_visible": "Lülita nähtavus",
"missing_indicator.label": "Ei leitud",
"missing_indicator.sublabel": "Seda ressurssi ei leitud",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Kas peita teated sellelt kasutajalt?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.apps": "Mobiilrakendused",
"navigation_bar.blocks": "Blokeeritud kasutajad",
"navigation_bar.bookmarks": "Järjehoidjad",
@ -298,6 +310,7 @@
"notification.own_poll": "Teie küsitlus on lõppenud",
"notification.poll": "Küsitlus, milles osalesite, on lõppenud",
"notification.reblog": "{name} upitas Teie staatust",
"notification.status": "{name} just posted",
"notifications.clear": "Puhasta teated",
"notifications.clear_confirmation": "Olete kindel, et soovite püsivalt kõik oma teated eemaldada?",
"notifications.column_settings.alert": "Töölauateated",
@ -313,13 +326,24 @@
"notifications.column_settings.reblog": "Upitused:",
"notifications.column_settings.show": "Kuva tulbas",
"notifications.column_settings.sound": "Mängi heli",
"notifications.column_settings.status": "New toots:",
"notifications.filter.all": "Kõik",
"notifications.filter.boosts": "Upitused",
"notifications.filter.favourites": "Lemmikud",
"notifications.filter.follows": "Jälgib",
"notifications.filter.mentions": "Mainimised",
"notifications.filter.polls": "Küsitluse tulemused",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.group": "{count} teated",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Suletud",
"poll.refresh": "Värskenda",
"poll.total_people": "{count, plural,one {# inimene} other {# inimest}}",
@ -446,6 +470,7 @@
"upload_modal.detect_text": "Tuvasta teksti pildilt",
"upload_modal.edit_media": "Muuda meediat",
"upload_modal.hint": "Vajuta või tõmba ringi eelvaatel, et valida fookuspunkti, mis on alati nähtaval kõikidel eelvaadetel.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.preview_label": "Eelvaade ({ratio})",
"upload_progress.label": "Laeb üles....",
"video.close": "Sulge video",

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