Merge tag 'v3.3.0' into instance_only_statuses
This commit is contained in:
@ -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 => {
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
38
app/javascript/mastodon/actions/picture_in_picture.js
Normal file
38
app/javascript/mastodon/actions/picture_in_picture.js
Normal 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,
|
||||
});
|
@ -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));
|
||||
|
@ -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 });
|
||||
|
112
app/javascript/mastodon/blurhash.js
Normal file
112
app/javascript/mastodon/blurhash.js
Normal 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)));
|
||||
};
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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 () {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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'>
|
||||
|
@ -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'>
|
||||
|
@ -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')} />}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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'>
|
||||
|
@ -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}
|
||||
|
@ -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));
|
||||
},
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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']),
|
||||
|
@ -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) => {
|
||||
|
@ -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 && (
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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')}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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));
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
85
app/javascript/mastodon/features/picture_in_picture/index.js
Normal file
85
app/javascript/mastodon/features/picture_in_picture/index.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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' },
|
||||
|
@ -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')}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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'>
|
||||
|
@ -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>
|
||||
) : (
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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'>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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": "إغلاق الفيديو",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "ভিডিওটি বন্ধ করতে",
|
||||
|
@ -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",
|
||||
|
@ -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": "Notifica’m 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ç d’utilitzar 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ó d’imatge comprimida",
|
||||
"lightbox.expand": "Amplia el quadre de visualització de l’imatge",
|
||||
"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 s’ha pogut activar les notificacions d’escriptori perquè s’ha 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 d’escriptori",
|
||||
"notifications_permission_banner.how_to_control": "Per a rebre notificacions quan Mastodon no està obert cal activar les notificacions d’escriptori. Pots controlar amb precisió quins tipus d’interaccions generen notificacions d’escriptori després d’activar el botó {icon} de dalt.",
|
||||
"notifications_permission_banner.title": "Mai et perdis res",
|
||||
"picture_in_picture.restore": "Retorna’l",
|
||||
"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 d’hidrogen, 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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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} Dŵ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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": [
|
||||
|
@ -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": "Κλείσε το βίντεο",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
Reference in New Issue
Block a user