diff options
Diffstat (limited to 'app/javascript')
519 files changed, 22617 insertions, 6258 deletions
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index 3f6f187bc..ffdabe674 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -1,6 +1,7 @@ // This file will be loaded on admin pages, regardless of theme. import { delegate } from 'rails-ujs'; +import ready from '../mastodon/ready'; const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]'; @@ -31,7 +32,7 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => { }); }); -delegate(document, '#domain_block_severity', 'change', ({ target }) => { +const onDomainBlockSeverityChange = (target) => { const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media'); const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports'); @@ -42,4 +43,11 @@ delegate(document, '#domain_block_severity', 'change', ({ target }) => { if (rejectReportsDiv) { rejectReportsDiv.style.display = (target.value === 'suspend') ? 'none' : 'block'; } +}; + +delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target)); + +ready(() => { + const input = document.getElementById('domain_block_severity'); + if (input) onDomainBlockSeverityChange(input); }); diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js index 33b7a207d..0f4222139 100644 --- a/app/javascript/core/public.js +++ b/app/javascript/core/public.js @@ -47,7 +47,7 @@ const getProfileAvatarAnimationHandler = (swapTo) => { return ({ target }) => { const swapSrc = target.getAttribute(swapTo); //only change the img source if autoplay is off and the image src is actually different - if(target.getAttribute('data-autoplay') === 'false' && target.src !== swapSrc) { + if(target.getAttribute('data-autoplay') !== 'true' && target.src !== swapSrc) { target.src = swapSrc; } }; diff --git a/app/javascript/flavours/glitch/actions/alerts.js b/app/javascript/flavours/glitch/actions/alerts.js index ef2500e7b..cd36d8007 100644 --- a/app/javascript/flavours/glitch/actions/alerts.js +++ b/app/javascript/flavours/glitch/actions/alerts.js @@ -3,6 +3,8 @@ import { defineMessages } from 'react-intl'; const messages = defineMessages({ unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, + rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' }, + rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' }, }); export const ALERT_SHOW = 'ALERT_SHOW'; @@ -23,23 +25,29 @@ export function clearAlert() { }; }; -export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) { return { type: ALERT_SHOW, title, message, + message_values, }; }; export function showAlertForError(error) { if (error.response) { - const { data, status, statusText } = error.response; + const { data, status, statusText, headers } = error.response; if (status === 404 || status === 410) { // Skip these errors as they are reflected in the UI return { type: ALERT_NOOP }; } + if (status === 429 && headers['x-ratelimit-reset']) { + const reset_date = new Date(headers['x-ratelimit-reset']); + return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date }); + } + let message = statusText; let title = `${status}`; diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js index 498ce519f..adae9d83c 100644 --- a/app/javascript/flavours/glitch/actions/blocks.js +++ b/app/javascript/flavours/glitch/actions/blocks.js @@ -1,6 +1,7 @@ import api, { getLinks } from 'flavours/glitch/util/api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; +import { openModal } from './modal'; export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; @@ -10,6 +11,8 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; +export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL'; + export function fetchBlocks() { return (dispatch, getState) => { dispatch(fetchBlocksRequest()); @@ -83,3 +86,14 @@ export function expandBlocksFail(error) { error, }; }; + +export function initBlockModal(account) { + return dispatch => { + dispatch({ + type: BLOCKS_INIT_MODAL, + account, + }); + + dispatch(openModal('BLOCK')); + }; +} diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 69cc6827f..f80642bd8 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -12,7 +12,7 @@ import { showAlertForError } from './alerts'; import { showAlert } from './alerts'; import { defineMessages } from 'react-intl'; -let cancelFetchComposeSuggestionsAccounts; +let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CYCLE_ELEFRIEND = 'COMPOSE_CYCLE_ELEFRIEND'; @@ -138,7 +138,8 @@ export function submitCompose(routerHistory) { return function (dispatch, getState) { let status = getState().getIn(['compose', 'text'], ''); let media = getState().getIn(['compose', 'media_attachments']); - let spoilerText = getState().getIn(['compose', 'spoiler_text'], ''); + const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']); + let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : ''; if ((!status || !status.length) && media.size === 0) { return; @@ -231,10 +232,11 @@ export function uploadCompose(files) { return function (dispatch, getState) { const uploadLimit = 4; const media = getState().getIn(['compose', 'media_attachments']); + const pending = getState().getIn(['compose', 'pending_media_attachments']); const progress = new Array(files.length).fill(0); let total = Array.from(files).reduce((a, v) => a + v.size, 0); - if (files.length + media.size > uploadLimit) { + if (files.length + media.size + pending > uploadLimit) { dispatch(showAlert(undefined, messages.uploadErrorLimit)); return; } @@ -260,7 +262,7 @@ export function uploadCompose(files) { progress[i] = loaded; dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); }, - }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).then(({ data }) => dispatch(uploadComposeSuccess(data, f))); }).catch(error => dispatch(uploadComposeFail(error))); }; }; @@ -315,10 +317,11 @@ export function uploadComposeProgress(loaded, total) { }; }; -export function uploadComposeSuccess(media) { +export function uploadComposeSuccess(media, file) { return { type: COMPOSE_UPLOAD_SUCCESS, media: media, + file: file, skipLoading: true, }; }; @@ -351,10 +354,12 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => if (cancelFetchComposeSuggestionsAccounts) { cancelFetchComposeSuggestionsAccounts(); } + api(getState).get('/api/v1/accounts/search', { cancelToken: new CancelToken(cancel => { cancelFetchComposeSuggestionsAccounts = cancel; }), + params: { q: token.slice(1), resolve: false, @@ -375,9 +380,32 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { dispatch(readyComposeSuggestionsEmojis(token, results)); }; -const fetchComposeSuggestionsTags = (dispatch, getState, token) => { +const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { + if (cancelFetchComposeSuggestionsTags) { + cancelFetchComposeSuggestionsTags(); + } + dispatch(updateSuggestionTags(token)); -}; + + api(getState).get('/api/v2/search', { + cancelToken: new CancelToken(cancel => { + cancelFetchComposeSuggestionsTags = cancel; + }), + + params: { + type: 'hashtags', + q: token.slice(1), + resolve: false, + limit: 4, + }, + }).then(({ data }) => { + dispatch(readyComposeSuggestionsTags(token, data.hashtags)); + }).catch(error => { + if (!isCancel(error)) { + dispatch(showAlertForError(error)); + } + }); +}, 200, { leading: true, trailing: true }); export function fetchComposeSuggestions(token) { return (dispatch, getState) => { @@ -411,16 +439,22 @@ export function readyComposeSuggestionsAccounts(token, accounts) { }; }; +export const readyComposeSuggestionsTags = (token, tags) => ({ + type: COMPOSE_SUGGESTIONS_READY, + token, + tags, +}); + export function selectComposeSuggestion(position, token, suggestion, path) { return (dispatch, getState) => { let completion; - if (typeof suggestion === 'object' && suggestion.id) { + if (suggestion.type === 'emoji') { dispatch(useEmoji(suggestion)); completion = suggestion.native || suggestion.colons; - } else if (suggestion[0] === '#') { - completion = suggestion; - } else { - completion = '@' + getState().getIn(['accounts', suggestion, 'acct']); + } else if (suggestion.type === 'hashtag') { + completion = `#${suggestion.name}`; + } else if (suggestion.type === 'account') { + completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']); } dispatch({ diff --git a/app/javascript/flavours/glitch/actions/conversations.js b/app/javascript/flavours/glitch/actions/conversations.js index 856f8f10f..e5c85c65d 100644 --- a/app/javascript/flavours/glitch/actions/conversations.js +++ b/app/javascript/flavours/glitch/actions/conversations.js @@ -15,6 +15,10 @@ export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE'; export const CONVERSATIONS_READ = 'CONVERSATIONS_READ'; +export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST'; +export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS'; +export const CONVERSATIONS_DELETE_FAIL = 'CONVERSATIONS_DELETE_FAIL'; + export const mountConversations = () => ({ type: CONVERSATIONS_MOUNT, }); @@ -82,3 +86,27 @@ export const updateConversations = conversation => dispatch => { conversation, }); }; + +export const deleteConversation = conversationId => (dispatch, getState) => { + dispatch(deleteConversationRequest(conversationId)); + + api(getState).delete(`/api/v1/conversations/${conversationId}`) + .then(() => dispatch(deleteConversationSuccess(conversationId))) + .catch(error => dispatch(deleteConversationFail(conversationId, error))); +}; + +export const deleteConversationRequest = id => ({ + type: CONVERSATIONS_DELETE_REQUEST, + id, +}); + +export const deleteConversationSuccess = id => ({ + type: CONVERSATIONS_DELETE_SUCCESS, + id, +}); + +export const deleteConversationFail = (id, error) => ({ + type: CONVERSATIONS_DELETE_FAIL, + id, + error, +}); diff --git a/app/javascript/flavours/glitch/actions/directory.js b/app/javascript/flavours/glitch/actions/directory.js new file mode 100644 index 000000000..9fbfb7f5b --- /dev/null +++ b/app/javascript/flavours/glitch/actions/directory.js @@ -0,0 +1,61 @@ +import api from 'flavours/glitch/util/api'; +import { importFetchedAccounts } from './importer'; +import { fetchRelationships } from './accounts'; + +export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; +export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; +export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL'; + +export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST'; +export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS'; +export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL'; + +export const fetchDirectory = params => (dispatch, getState) => { + dispatch(fetchDirectoryRequest()); + + api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchDirectorySuccess(data)); + dispatch(fetchRelationships(data.map(x => x.id))); + }).catch(error => dispatch(fetchDirectoryFail(error))); +}; + +export const fetchDirectoryRequest = () => ({ + type: DIRECTORY_FETCH_REQUEST, +}); + +export const fetchDirectorySuccess = accounts => ({ + type: DIRECTORY_FETCH_SUCCESS, + accounts, +}); + +export const fetchDirectoryFail = error => ({ + type: DIRECTORY_FETCH_FAIL, + error, +}); + +export const expandDirectory = params => (dispatch, getState) => { + dispatch(expandDirectoryRequest()); + + const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size; + + api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(expandDirectorySuccess(data)); + dispatch(fetchRelationships(data.map(x => x.id))); + }).catch(error => dispatch(expandDirectoryFail(error))); +}; + +export const expandDirectoryRequest = () => ({ + type: DIRECTORY_EXPAND_REQUEST, +}); + +export const expandDirectorySuccess = accounts => ({ + type: DIRECTORY_EXPAND_SUCCESS, + accounts, +}); + +export const expandDirectoryFail = error => ({ + type: DIRECTORY_EXPAND_FAIL, + error, +}); diff --git a/app/javascript/flavours/glitch/actions/domain_blocks.js b/app/javascript/flavours/glitch/actions/domain_blocks.js index 7397f561b..6d3f471fa 100644 --- a/app/javascript/flavours/glitch/actions/domain_blocks.js +++ b/app/javascript/flavours/glitch/actions/domain_blocks.js @@ -23,6 +23,7 @@ export function blockDomain(domain) { api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { const at_domain = '@' + domain; const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); + dispatch(blockDomainSuccess(domain, accounts)); }).catch(err => { dispatch(blockDomainFail(domain, err)); diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index a8c3fe16a..2bc603930 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -10,6 +10,12 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { return obj; }, {}); +export function searchTextFromRawStatus (status) { + const spoilerText = status.spoiler_text || ''; + const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; +} + export function normalizeAccount(account) { account = { ...account }; @@ -22,7 +28,7 @@ export function normalizeAccount(account) { if (account.fields) { account.fields = account.fields.map(pair => ({ ...pair, - name_emojified: emojify(escapeTextContentForBrowser(pair.name)), + name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap), value_emojified: emojify(pair.value, emojiMap), value_plain: unescapeHTML(pair.value), })); @@ -55,7 +61,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); } else { const spoilerText = normalStatus.spoiler_text || ''; - const searchContent = [spoilerText, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const emojiMap = makeEmojiMap(normalStatus); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; @@ -71,8 +77,9 @@ export function normalizePoll(poll) { const emojiMap = makeEmojiMap(normalPoll); - normalPoll.options = poll.options.map(option => ({ + normalPoll.options = poll.options.map((option, index) => ({ ...option, + voted: poll.own_votes && poll.own_votes.includes(index), title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js new file mode 100644 index 000000000..c3a5fe86f --- /dev/null +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -0,0 +1,30 @@ +export const submitMarkers = () => (dispatch, getState) => { + const accessToken = getState().getIn(['meta', 'access_token'], ''); + const params = {}; + + const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]); + const lastNotificationId = getState().getIn(['notifications', 'items', 0, 'id']); + + if (lastHomeId) { + params.home = { + last_read_id: lastHomeId, + }; + } + + if (lastNotificationId) { + params.notifications = { + last_read_id: lastNotificationId, + }; + } + + if (Object.keys(params).length === 0) { + return; + } + + const client = new XMLHttpRequest(); + + client.open('POST', '/api/v1/markers', false); + client.setRequestHeader('Content-Type', 'application/json'); + client.setRequestHeader('Authorization', `Bearer ${accessToken}`); + client.send(JSON.stringify(params)); +}; diff --git a/app/javascript/flavours/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js index 80e15c28e..3d0299db5 100644 --- a/app/javascript/flavours/glitch/actions/modal.js +++ b/app/javascript/flavours/glitch/actions/modal.js @@ -9,8 +9,9 @@ export function openModal(type, props) { }; }; -export function closeModal() { +export function closeModal(type) { return { type: MODAL_CLOSE, + modalType: type, }; }; diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 57fecf63d..940f3c3d4 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -11,7 +11,10 @@ import { saveSettings } from './settings'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from 'flavours/glitch/util/html'; -import { getFilters, regexFromFilters } from 'flavours/glitch/selectors'; +import { getFiltersRegex } from 'flavours/glitch/selectors'; +import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state'; +import compareId from 'flavours/glitch/util/compare_id'; +import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; @@ -32,8 +35,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; @@ -52,18 +56,27 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; +export const loadPending = () => ({ + type: NOTIFICATIONS_LOAD_PENDING, +}); + export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); - const filters = getFilters(getState(), { contextType: 'notifications' }); + const filters = getFiltersRegex(getState(), { contextType: 'notifications' }); let filtered = false; if (notification.type === 'mention') { - const regex = regexFromFilters(filters); - const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); + const dropRegex = filters[0]; + const regex = filters[1]; + const searchIndex = searchTextFromRawStatus(notification.status); + + if (dropRegex && dropRegex.test(searchIndex)) { + return; + } filtered = regex && regex.test(searchIndex); } @@ -78,6 +91,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch({ type: NOTIFICATIONS_UPDATE, notification, + usePendingItems: preferPendingItems, meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, }); @@ -107,7 +121,7 @@ const excludeTypesFromSettings = state => state.getIn(['settings', 'notification const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']); + const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']); return allTypes.filterNot(item => item === filter).toJS(); }; @@ -131,10 +145,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) { : excludeTypesFromFilter(activeFilter), }; - if (!maxId && notifications.get('items').size > 0) { - params.since_id = notifications.getIn(['items', 0, 'id']); + if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { + const a = notifications.getIn(['pendingItems', 0, 'id']); + const b = notifications.getIn(['items', 0, 'id']); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } + const isLoadingRecent = !!params.since_id; + dispatch(expandNotificationsRequest(isLoadingMore)); api(getState).get('/api/v1/notifications', { params }).then(response => { @@ -143,7 +166,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { @@ -160,13 +183,13 @@ export function expandNotificationsRequest(isLoadingMore) { }; }; -export function expandNotificationsSuccess(notifications, next, isLoadingMore) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, - accounts: notifications.map(item => item.account), - statuses: notifications.map(item => item.status).filter(status => !!status), next, + isLoadingRecent: isLoadingRecent, + usePendingItems, skipLoading: !isLoadingMore, }; }; diff --git a/app/javascript/flavours/glitch/actions/polls.js b/app/javascript/flavours/glitch/actions/polls.js index 8e8b82df5..ca94a095f 100644 --- a/app/javascript/flavours/glitch/actions/polls.js +++ b/app/javascript/flavours/glitch/actions/polls.js @@ -1,4 +1,4 @@ -import api from '../api'; +import api from 'flavours/glitch/util/api'; import { importFetchedPoll } from './importer'; export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index b2d24e10b..a025f352a 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -10,6 +10,10 @@ export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; +export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST'; +export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS'; +export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL'; + export function changeSearch(value) { return { type: SEARCH_CHANGE, @@ -48,7 +52,7 @@ export function submitSearch() { dispatch(importFetchedStatuses(response.data.statuses)); } - dispatch(fetchSearchSuccess(response.data)); + dispatch(fetchSearchSuccess(response.data, value)); dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); }).catch(error => { dispatch(fetchSearchFail(error)); @@ -62,12 +66,11 @@ export function fetchSearchRequest() { }; }; -export function fetchSearchSuccess(results) { +export function fetchSearchSuccess(results, searchTerm) { return { type: SEARCH_FETCH_SUCCESS, results, - accounts: results.accounts, - statuses: results.statuses, + searchTerm, }; }; @@ -78,8 +81,50 @@ export function fetchSearchFail(error) { }; }; -export function showSearch() { - return { - type: SEARCH_SHOW, - }; +export const expandSearch = type => (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + const offset = getState().getIn(['search', 'results', type]).size; + + dispatch(expandSearchRequest()); + + api(getState).get('/api/v2/search', { + params: { + q: value, + type, + offset, + }, + }).then(({ data }) => { + if (data.accounts) { + dispatch(importFetchedAccounts(data.accounts)); + } + + if (data.statuses) { + dispatch(importFetchedStatuses(data.statuses)); + } + + dispatch(expandSearchSuccess(data, value, type)); + dispatch(fetchRelationships(data.accounts.map(item => item.id))); + }).catch(error => { + dispatch(expandSearchFail(error)); + }); }; + +export const expandSearchRequest = () => ({ + type: SEARCH_EXPAND_REQUEST, +}); + +export const expandSearchSuccess = (results, searchTerm, searchType) => ({ + type: SEARCH_EXPAND_SUCCESS, + results, + searchTerm, + searchType, +}); + +export const expandSearchFail = error => ({ + type: SEARCH_EXPAND_FAIL, + error, +}); + +export const showSearch = () => ({ + type: SEARCH_SHOW, +}); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index cca571583..097878c3b 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,6 +1,10 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; import api, { getLinks } from 'flavours/glitch/util/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import compareId from 'flavours/glitch/util/compare_id'; +import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state'; +import { getFiltersRegex } from 'flavours/glitch/selectors'; +import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -10,23 +14,40 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; -export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const loadPending = timeline => ({ + type: TIMELINE_LOAD_PENDING, + timeline, +}); export function updateTimeline(timeline, status, accept) { - return dispatch => { + return (dispatch, getState) => { if (typeof accept === 'function' && !accept(status)) { return; } + const filters = getFiltersRegex(getState(), { contextType: timeline }); + const dropRegex = filters[0]; + const regex = filters[1]; + const text = searchTextFromRawStatus(status); + let filtered = false; + + if (status.account.id !== me) { + filtered = (dropRegex && dropRegex.test(text)) || (regex && regex.test(text)); + } + dispatch(importFetchedStatus(status)); dispatch({ type: TIMELINE_UPDATE, timeline, status, + usePendingItems: preferPendingItems, + filtered }); }; }; @@ -71,8 +92,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { return; } - if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) { - params.since_id = timeline.getIn(['items', 0]); + if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) { + const a = timeline.getIn(['pendingItems', 0]); + const b = timeline.getIn(['items', 0]); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } const isLoadingRecent = !!params.since_id; @@ -81,8 +109,9 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); @@ -117,7 +146,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) { }; }; -export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) { +export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, @@ -125,6 +154,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi next, partial, isLoadingRecent, + usePendingItems, skipLoading: !isLoadingMore, }; }; @@ -153,9 +183,8 @@ export function connectTimeline(timeline) { }; }; -export function disconnectTimeline(timeline) { - return { - type: TIMELINE_DISCONNECT, - timeline, - }; -}; +export const disconnectTimeline = timeline => ({ + type: TIMELINE_DISCONNECT, + timeline, + usePendingItems: preferPendingItems, +}); diff --git a/app/javascript/flavours/glitch/actions/trends.js b/app/javascript/flavours/glitch/actions/trends.js new file mode 100644 index 000000000..1b0ce2b5b --- /dev/null +++ b/app/javascript/flavours/glitch/actions/trends.js @@ -0,0 +1,32 @@ +import api from 'flavours/glitch/util/api'; + +export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; +export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; +export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; + +export const fetchTrends = () => (dispatch, getState) => { + dispatch(fetchTrendsRequest()); + + api(getState) + .get('/api/v1/trends') + .then(({ data }) => dispatch(fetchTrendsSuccess(data))) + .catch(err => dispatch(fetchTrendsFail(err))); +}; + +export const fetchTrendsRequest = () => ({ + type: TRENDS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendsSuccess = trends => ({ + type: TRENDS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendsFail = error => ({ + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js index 3fc18cb72..f3e58dfe3 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.js @@ -19,8 +19,8 @@ const messages = defineMessages({ unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' }, }); -@injectIntl -export default class Account extends ImmutablePureComponent { +export default @injectIntl +class Account extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/components/attachment_list.js b/app/javascript/flavours/glitch/components/attachment_list.js index 8e5bb0e0b..68d8d29c7 100644 --- a/app/javascript/flavours/glitch/components/attachment_list.js +++ b/app/javascript/flavours/glitch/components/attachment_list.js @@ -2,6 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'flavours/glitch/components/icon'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; @@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent { return ( <li key={attachment.get('id')}> - <a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a> + <a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a> </li> ); })} @@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent { return ( <div className='attachment-list'> <div className='attachment-list__icon'> - <i className='fa fa-link' /> + <Icon id='link' /> </div> <ul className='attachment-list__list'> @@ -45,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent { return ( <li key={attachment.get('id')}> - <a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a> + <a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a> </li> ); })} diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js new file mode 100644 index 000000000..648987dfd --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { shortNumberFormat } from 'flavours/glitch/util/numbers'; +import { FormattedMessage } from 'react-intl'; + +export default class AutosuggestHashtag extends React.PureComponent { + + static propTypes = { + tag: PropTypes.shape({ + name: PropTypes.string.isRequired, + url: PropTypes.string, + history: PropTypes.array, + }).isRequired, + }; + + render () { + const { tag } = this.props; + const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0)); + + return ( + <div className='autosuggest-hashtag'> + <div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div> + {tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>} + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.js b/app/javascript/flavours/glitch/components/autosuggest_input.js index 5fc952d8e..1ef7ee216 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.js +++ b/app/javascript/flavours/glitch/components/autosuggest_input.js @@ -1,6 +1,7 @@ import React from 'react'; import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; +import AutosuggestHashtag from './autosuggest_hashtag'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { isRtl } from 'flavours/glitch/util/rtl'; @@ -167,15 +168,15 @@ export default class AutosuggestInput extends ImmutablePureComponent { const { selectedSuggestion } = this.state; let inner, key; - if (typeof suggestion === 'object') { + if (suggestion.type === 'emoji') { inner = <AutosuggestEmoji emoji={suggestion} />; key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; - } else { - inner = <AutosuggestAccountContainer id={suggestion} />; - key = suggestion; + } else if (suggestion.type ==='hashtag') { + inner = <AutosuggestHashtag tag={suggestion} />; + key = suggestion.name; + } else if (suggestion.type === 'account') { + inner = <AutosuggestAccountContainer id={suggestion.id} />; + key = suggestion.id; } return ( diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js index bbe0ffcbe..ec2fbbe4b 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js @@ -1,6 +1,7 @@ import React from 'react'; import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; +import AutosuggestHashtag from './autosuggest_hashtag'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { isRtl } from 'flavours/glitch/util/rtl'; @@ -173,15 +174,15 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { const { selectedSuggestion } = this.state; let inner, key; - if (typeof suggestion === 'object') { + if (suggestion.type === 'emoji') { inner = <AutosuggestEmoji emoji={suggestion} />; key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; - } else { - inner = <AutosuggestAccountContainer id={suggestion} />; - key = suggestion; + } else if (suggestion.type === 'hashtag') { + inner = <AutosuggestHashtag tag={suggestion} />; + key = suggestion.name; + } else if (suggestion.type === 'account') { + inner = <AutosuggestAccountContainer id={suggestion.id} />; + key = suggestion.id; } return ( diff --git a/app/javascript/flavours/glitch/components/avatar_composite.js b/app/javascript/flavours/glitch/components/avatar_composite.js index c52df043a..125b51c44 100644 --- a/app/javascript/flavours/glitch/components/avatar_composite.js +++ b/app/javascript/flavours/glitch/components/avatar_composite.js @@ -35,35 +35,35 @@ export default class AvatarComposite extends React.PureComponent { if (size === 2) { if (index === 0) { - right = '2px'; + right = '1px'; } else { - left = '2px'; + left = '1px'; } } else if (size === 3) { if (index === 0) { - right = '2px'; + right = '1px'; } else if (index > 0) { - left = '2px'; + left = '1px'; } if (index === 1) { - bottom = '2px'; + bottom = '1px'; } else if (index > 1) { - top = '2px'; + top = '1px'; } } else if (size === 4) { if (index === 0 || index === 2) { - right = '2px'; + right = '1px'; } if (index === 1 || index === 3) { - left = '2px'; + left = '1px'; } if (index < 2) { - bottom = '2px'; + bottom = '1px'; } else { - top = '2px'; + top = '1px'; } } @@ -96,7 +96,13 @@ export default class AvatarComposite extends React.PureComponent { return ( <div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}> - {accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))} + {accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))} + + {accounts.size > 4 && ( + <span className='account__avatar-composite__label'> + +{accounts.size - 4} + </span> + )} </div> ); } diff --git a/app/javascript/flavours/glitch/components/button.js b/app/javascript/flavours/glitch/components/button.js index 16868010c..cd6528f58 100644 --- a/app/javascript/flavours/glitch/components/button.js +++ b/app/javascript/flavours/glitch/components/button.js @@ -12,9 +12,9 @@ export default class Button extends React.PureComponent { secondary: PropTypes.bool, size: PropTypes.number, className: PropTypes.string, + title: PropTypes.string, style: PropTypes.object, children: PropTypes.node, - title: PropTypes.string, }; static defaultProps = { diff --git a/app/javascript/flavours/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js index dc87818a5..5819d5362 100644 --- a/app/javascript/flavours/glitch/components/column.js +++ b/app/javascript/flavours/glitch/components/column.js @@ -10,10 +10,11 @@ export default class Column extends React.PureComponent { extraClasses: PropTypes.string, name: PropTypes.string, label: PropTypes.string, + bindToDocument: PropTypes.bool, }; scrollTop () { - const scrollable = this.node.querySelector('.scrollable'); + const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable'); if (!scrollable) { return; @@ -35,11 +36,19 @@ export default class Column extends React.PureComponent { } componentDidMount () { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + if (this.props.bindToDocument) { + document.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + } else { + this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + } } componentWillUnmount () { - this.node.removeEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.removeEventListener('wheel', this.handleWheel); + } else { + this.node.removeEventListener('wheel', this.handleWheel); + } } render () { diff --git a/app/javascript/flavours/glitch/components/column_back_button.js b/app/javascript/flavours/glitch/components/column_back_button.js index 82556d22e..05688f867 100644 --- a/app/javascript/flavours/glitch/components/column_back_button.js +++ b/app/javascript/flavours/glitch/components/column_back_button.js @@ -1,6 +1,8 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; +import Icon from 'flavours/glitch/components/icon'; +import { createPortal } from 'react-dom'; export default class ColumnBackButton extends React.PureComponent { @@ -8,6 +10,10 @@ export default class ColumnBackButton extends React.PureComponent { router: PropTypes.object, }; + static propTypes = { + multiColumn: PropTypes.bool, + }; + handleClick = (event) => { // if history is exhausted, or we would leave mastodon, just go to root. if (window.history.state) { @@ -23,12 +29,32 @@ export default class ColumnBackButton extends React.PureComponent { } render () { - return ( + const { multiColumn } = this.props; + + const component = ( <button onClick={this.handleClick} className='column-back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> + <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </button> ); + + if (multiColumn) { + return component; + } else { + // The portal container and the component may be rendered to the DOM in + // the same React render pass, so the container might not be available at + // the time `render()` is called. + const container = document.getElementById('tabs-bar__portal'); + if (container === null) { + // The container wasn't available, force a re-render so that the + // component can eventually be inserted in the container and not scroll + // with the rest of the area. + this.forceUpdate(); + return component; + } else { + return createPortal(component, container); + } + } } } diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.js b/app/javascript/flavours/glitch/components/column_back_button_slim.js index 38afd3df3..faa0c23a8 100644 --- a/app/javascript/flavours/glitch/components/column_back_button_slim.js +++ b/app/javascript/flavours/glitch/components/column_back_button_slim.js @@ -1,6 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; +import Icon from 'flavours/glitch/components/icon'; export default class ColumnBackButtonSlim extends React.PureComponent { @@ -26,7 +27,7 @@ export default class ColumnBackButtonSlim extends React.PureComponent { return ( <div className='column-back-button--slim'> <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> + <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </div> </div> diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index a0ff09986..dd1162429 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import Icon from 'flavours/glitch/components/icon'; import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; @@ -14,8 +16,8 @@ const messages = defineMessages({ enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' }, }); -@injectIntl -export default class ColumnHeader extends React.PureComponent { +export default @injectIntl +class ColumnHeader extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -35,6 +37,7 @@ export default class ColumnHeader extends React.PureComponent { onEnterCleaningMode: PropTypes.func, children: PropTypes.node, pinned: PropTypes.bool, + placeholder: PropTypes.bool, onPin: PropTypes.func, onMove: PropTypes.func, onClick: PropTypes.func, @@ -103,7 +106,7 @@ export default class ColumnHeader extends React.PureComponent { } render () { - const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props; + const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive, placeholder } = this.props; const { collapsed, animating, animatingNCD } = this.state; let title = this.props.title; @@ -148,22 +151,22 @@ export default class ColumnHeader extends React.PureComponent { } if (multiColumn && pinned) { - pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; + pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; moveButtons = ( <div key='move-buttons' className='column-header__setting-arrows'> - <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button> - <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button> + <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button> + <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> </div> ); - } else if (multiColumn) { - pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; + } else if (multiColumn && this.props.onPin) { + pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; } if (!pinned && (multiColumn || showBackButton)) { backButton = ( <button onClick={this.handleBackClick} className='column-header__back-button'> - <i className='fa fa-fw fa-chevron-left column-back-button__icon' /> + <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </button> ); @@ -178,18 +181,18 @@ export default class ColumnHeader extends React.PureComponent { collapsedContent.push(pinButton); } - if (children || multiColumn) { - 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='fa fa-sliders' /></button>; + 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>; } const hasTitle = icon && title; - return ( + const component = ( <div className={wrapperClassName}> <h1 className={buttonClassName}> {hasTitle && ( <button onClick={this.handleTitleClick}> - <i className={`fa fa-fw fa-${icon} column-header__icon`} /> + <Icon id={icon} fixedWidth className='column-header__icon' /> {title} </button> )} @@ -206,7 +209,7 @@ export default class ColumnHeader extends React.PureComponent { onClick={this.onEnterCleaningMode} className={notifCleaningButtonClassName} > - <i className='fa fa-eraser' /> + <Icon id='eraser' /> </button> ) : null} {collapseButton} @@ -228,6 +231,24 @@ export default class ColumnHeader extends React.PureComponent { </div> </div> ); + + if (multiColumn || placeholder) { + return component; + } else { + // The portal container and the component may be rendered to the DOM in + // the same React render pass, so the container might not be available at + // the time `render()` is called. + const container = document.getElementById('tabs-bar__portal'); + if (container === null) { + // The container wasn't available, force a re-render so that the + // component can eventually be inserted in the container and not scroll + // with the rest of the area. + this.forceUpdate(); + return component; + } else { + return createPortal(component, container); + } + } } } diff --git a/app/javascript/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js index 7f6ef5a5d..44662a8b8 100644 --- a/app/javascript/flavours/glitch/components/display_name.js +++ b/app/javascript/flavours/glitch/components/display_name.js @@ -1,73 +1,112 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { autoPlayGif } from 'flavours/glitch/util/initial_state'; + +export default class DisplayName extends React.PureComponent { + + static propTypes = { + account: ImmutablePropTypes.map, + className: PropTypes.string, + inline: PropTypes.bool, + localDomain: PropTypes.string, + others: ImmutablePropTypes.list, + handleClick: PropTypes.func, + }; + + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } -// The component. -export default function DisplayName ({ - account, - className, - inline, - localDomain, - others, - onAccountClick, -}) { - const computedClass = classNames('display-name', { inline }, className); + componentDidMount () { + this._updateEmojis(); + } - if (!account) return null; + componentDidUpdate () { + this._updateEmojis(); + } - let displayName, suffix; + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } - let acct = account.get('acct'); + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } - if (acct.indexOf('@') === -1 && localDomain) { - acct = `${acct}@${localDomain}`; + setRef = (c) => { + this.node = c; } - if (others && others.size > 0) { - displayName = others.take(2).map(a => ( - <a - href={a.get('url')} - target='_blank' - onClick={(e) => onAccountClick(a.get('id'), e)} - title={`@${a.get('acct')}`} - > - <bdi key={a.get('id')}> - <strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /> - </bdi> - </a> - )).reduce((prev, cur) => [prev, ', ', cur]); - - if (others.size - 2 > 0) { - displayName.push(` +${others.size - 2}`); + render() { + const { account, className, inline, localDomain, others, onAccountClick } = this.props; + + const computedClass = classNames('display-name', { inline }, className); + + if (!account) return null; + + let displayName, suffix; + + let acct = account.get('acct'); + + if (acct.indexOf('@') === -1 && localDomain) { + acct = `${acct}@${localDomain}`; } - suffix = ( - <a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)}> - <span className='display-name__account'>@{acct}</span> - </a> + if (others && others.size > 0) { + displayName = others.take(2).map(a => ( + <a + href={a.get('url')} + target='_blank' + onClick={(e) => onAccountClick(a.get('id'), e)} + title={`@${a.get('acct')}`} + rel='noopener noreferrer' + > + <bdi key={a.get('id')}> + <strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /> + </bdi> + </a> + )).reduce((prev, cur) => [prev, ', ', cur]); + + if (others.size - 2 > 0) { + displayName.push(` +${others.size - 2}`); + } + + suffix = ( + <a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)} rel='noopener noreferrer'> + <span className='display-name__account'>@{acct}</span> + </a> + ); + } else { + displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>; + suffix = <span className='display-name__account'>@{acct}</span>; + } + + return ( + <span className={computedClass} ref={this.setRef}> + {displayName} + {inline ? ' ' : null} + {suffix} + </span> ); - } else { - displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>; - suffix = <span className='display-name__account'>@{acct}</span>; } - return ( - <span className={computedClass}> - {displayName} - {inline ? ' ' : null} - {suffix} - </span> - ); } - -// Props. -DisplayName.propTypes = { - account: ImmutablePropTypes.map, - className: PropTypes.string, - inline: PropTypes.bool, - localDomain: PropTypes.string, - others: ImmutablePropTypes.list, - handleClick: PropTypes.func, -}; diff --git a/app/javascript/flavours/glitch/components/domain.js b/app/javascript/flavours/glitch/components/domain.js index 74174f83d..85729ca94 100644 --- a/app/javascript/flavours/glitch/components/domain.js +++ b/app/javascript/flavours/glitch/components/domain.js @@ -8,8 +8,8 @@ const messages = defineMessages({ unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, }); -@injectIntl -export default class Account extends ImmutablePureComponent { +export default @injectIntl +class Account extends ImmutablePureComponent { static propTypes = { domain: PropTypes.string, diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 05611c135..ab5b7a572 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -45,7 +45,9 @@ class DropdownMenu extends React.PureComponent { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('keydown', this.handleKeyDown, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus(); + if (this.focusedItem && this.props.openedViaKeyboard) { + this.focusedItem.focus(); + } this.setState({ mounted: true }); } @@ -81,6 +83,18 @@ class DropdownMenu extends React.PureComponent { element.focus(); } break; + case 'Tab': + if (e.shiftKey) { + element = items[index-1] || items[items.length-1]; + } else { + element = items[index+1] || items[0]; + } + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + break; case 'Home': element = items[0]; if (element) { @@ -93,11 +107,14 @@ class DropdownMenu extends React.PureComponent { element.focus(); } break; + case 'Escape': + this.props.onClose(); + break; } } - handleItemKeyDown = e => { - if (e.key === 'Enter') { + handleItemKeyPress = e => { + if (e.key === 'Enter' || e.key === ' ') { this.handleClick(e); } } @@ -126,7 +143,7 @@ class DropdownMenu extends React.PureComponent { return ( <li className='dropdown-menu__item' key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}> + <a href={href} target='_blank' rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> {text} </a> </li> @@ -193,25 +210,41 @@ export default class Dropdown extends React.PureComponent { } else { const { top } = target.getBoundingClientRect(); const placement = top * 2 < innerHeight ? 'bottom' : 'top'; - this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click'); } } handleClose = () => { + if (this.activeElement) { + this.activeElement.focus(); + this.activeElement = null; + } this.props.onClose(this.state.id); } - handleKeyDown = e => { + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + } + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + } + + handleKeyPress = (e) => { switch(e.key) { case ' ': case 'Enter': this.handleClick(e); + e.stopPropagation(); e.preventDefault(); break; - case 'Escape': - this.handleClose(); - break; } } @@ -248,7 +281,7 @@ export default class Dropdown extends React.PureComponent { const open = this.state.id === openDropdownId; return ( - <div onKeyDown={this.handleKeyDown}> + <div> <IconButton icon={icon} title={ariaLabel} @@ -257,6 +290,9 @@ export default class Dropdown extends React.PureComponent { size={size} ref={this.setTargetRef} onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleButtonKeyDown} + onKeyPress={this.handleKeyPress} /> <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> diff --git a/app/javascript/flavours/glitch/components/error_boundary.js b/app/javascript/flavours/glitch/components/error_boundary.js index 142a0c21a..62950a7d3 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.js +++ b/app/javascript/flavours/glitch/components/error_boundary.js @@ -50,43 +50,43 @@ export default class ErrorBoundary extends React.PureComponent { <h1><FormattedMessage id='web_app_crash.title' defaultMessage="We're sorry, but something went wrong with the Mastodon app." /></h1> <p> <FormattedMessage id='web_app_crash.content' defaultMessage='You could try any of the following:' /> - <ul> - <li> - <FormattedMessage - id='web_app_crash.report_issue' - defaultMessage='Report a bug in the {issuetracker}' - values={{ issuetracker: <a href='https://github.com/glitch-soc/mastodon/issues' rel='noopener' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }} - /> - { debugInfo !== '' && ( - <details> - <summary><FormattedMessage id='web_app_crash.debug_info' defaultMessage='Debug information' /></summary> - <textarea - className='web_app_crash-stacktrace' - value={debugInfo} - rows='10' - readOnly - /> - </details> - )} - </li> + </p> + <ul> + <li> + <FormattedMessage + id='web_app_crash.report_issue' + defaultMessage='Report a bug in the {issuetracker}' + values={{ issuetracker: <a href='https://github.com/glitch-soc/mastodon/issues' rel='noopener noreferrer' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }} + /> + { debugInfo !== '' && ( + <details> + <summary><FormattedMessage id='web_app_crash.debug_info' defaultMessage='Debug information' /></summary> + <textarea + className='web_app_crash-stacktrace' + value={debugInfo} + rows='10' + readOnly + /> + </details> + )} + </li> + <li> + <FormattedMessage + id='web_app_crash.reload_page' + defaultMessage='{reload} the current page' + values={{ reload: <a href='#' onClick={this.handleReload}><FormattedMessage id='web_app_crash.reload' defaultMessage='Reload' /></a> }} + /> + </li> + { preferencesLink !== undefined && ( <li> <FormattedMessage - id='web_app_crash.reload_page' - defaultMessage='{reload} the current page' - values={{ reload: <a href='#' onClick={this.handleReload}><FormattedMessage id='web_app_crash.reload' defaultMessage='Reload' /></a> }} + id='web_app_crash.change_your_settings' + defaultMessage='Change your {settings}' + values={{ settings: <a href={preferencesLink}><FormattedMessage id='web_app_crash.settings' defaultMessage='settings' /></a> }} /> </li> - { preferencesLink !== undefined && ( - <li> - <FormattedMessage - id='web_app_crash.change_your_settings' - defaultMessage='Change your {settings}' - values={{ settings: <a href={preferencesLink}><FormattedMessage id='web_app_crash.settings' defaultMessage='settings' /></a> }} - /> - </li> - )} - </ul> - </p> + )} + </ul> </div> </div> ); diff --git a/app/javascript/flavours/glitch/components/extended_video_player.js b/app/javascript/flavours/glitch/components/extended_video_player.js deleted file mode 100644 index 009c0d559..000000000 --- a/app/javascript/flavours/glitch/components/extended_video_player.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class ExtendedVideoPlayer extends React.PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - time: PropTypes.number, - controls: PropTypes.bool.isRequired, - muted: PropTypes.bool.isRequired, - onClick: PropTypes.func, - }; - - handleLoadedData = () => { - if (this.props.time) { - this.video.currentTime = this.props.time; - } - } - - componentDidMount () { - this.video.addEventListener('loadeddata', this.handleLoadedData); - } - - componentWillUnmount () { - this.video.removeEventListener('loadeddata', this.handleLoadedData); - } - - setRef = (c) => { - this.video = c; - } - - handleClick = e => { - e.stopPropagation(); - const handler = this.props.onClick; - if (handler) handler(); - } - - render () { - const { src, muted, controls, alt } = this.props; - - return ( - <div className='extended-video-player'> - <video - ref={this.setRef} - src={src} - autoPlay - role='button' - tabIndex='0' - aria-label={alt} - title={alt} - muted={muted} - controls={controls} - loop={!controls} - onClick={this.handleClick} - /> - </div> - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/gifv.js b/app/javascript/flavours/glitch/components/gifv.js new file mode 100644 index 000000000..83cfae49c --- /dev/null +++ b/app/javascript/flavours/glitch/components/gifv.js @@ -0,0 +1,75 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class GIFV extends React.PureComponent { + + static propTypes = { + src: PropTypes.string.isRequired, + alt: PropTypes.string, + width: PropTypes.number, + height: PropTypes.number, + onClick: PropTypes.func, + }; + + state = { + loading: true, + }; + + handleLoadedData = () => { + this.setState({ loading: false }); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.src !== this.props.src) { + this.setState({ loading: true }); + } + } + + handleClick = e => { + const { onClick } = this.props; + + if (onClick) { + e.stopPropagation(); + onClick(); + } + } + + render () { + const { src, width, height, alt } = this.props; + const { loading } = this.state; + + return ( + <div className='gifv' style={{ position: 'relative' }}> + {loading && ( + <canvas + width={width} + height={height} + role='button' + tabIndex='0' + aria-label={alt} + title={alt} + onClick={this.handleClick} + /> + )} + + <video + src={src} + width={width} + height={height} + role='button' + tabIndex='0' + aria-label={alt} + title={alt} + muted + loop + autoPlay + playsInline + onClick={this.handleClick} + onLoadedData={this.handleLoadedData} + style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }} + /> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index d75edd994..d42bee0e9 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -12,11 +12,11 @@ const Hashtag = ({ hashtag }) => ( #<span>{hashtag.get('name')}</span> </Permalink> - <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} /> + <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} /> </div> <div className='trends__item__current'> - {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))} + {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)} </div> <div className='trends__item__sparkline'> diff --git a/app/javascript/flavours/glitch/components/icon.js b/app/javascript/flavours/glitch/components/icon.js index 8f55a0115..d8a17722f 100644 --- a/app/javascript/flavours/glitch/components/icon.js +++ b/app/javascript/flavours/glitch/components/icon.js @@ -1,26 +1,21 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; -// This just renders a FontAwesome icon. -export default function Icon ({ - className, - fullwidth, - icon, -}) { - const computedClass = classNames('icon', 'fa', { 'fa-fw': fullwidth }, `fa-${icon}`, className); - return icon ? ( - <span - aria-hidden='true' - className={computedClass} - /> - ) : null; -} +export default class Icon extends React.PureComponent { -// Props. -Icon.propTypes = { - className: PropTypes.string, - fullwidth: PropTypes.bool, - icon: PropTypes.string, -}; + static propTypes = { + id: PropTypes.string.isRequired, + className: PropTypes.string, + fixedWidth: PropTypes.bool, + }; + + render () { + const { id, className, fixedWidth, ...other } = this.props; + + return ( + <i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} /> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js index 6a25794d3..e134d0a39 100644 --- a/app/javascript/flavours/glitch/components/icon_button.js +++ b/app/javascript/flavours/glitch/components/icon_button.js @@ -3,6 +3,7 @@ import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; export default class IconButton extends React.PureComponent { @@ -11,6 +12,9 @@ export default class IconButton extends React.PureComponent { title: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, onClick: PropTypes.func, + onMouseDown: PropTypes.func, + onKeyDown: PropTypes.func, + onKeyPress: PropTypes.func, size: PropTypes.number, active: PropTypes.bool, pressed: PropTypes.bool, @@ -20,7 +24,6 @@ export default class IconButton extends React.PureComponent { disabled: PropTypes.bool, inverted: PropTypes.bool, animate: PropTypes.bool, - flip: PropTypes.bool, overlay: PropTypes.bool, tabIndex: PropTypes.string, label: PropTypes.string, @@ -35,6 +38,21 @@ export default class IconButton extends React.PureComponent { tabIndex: '0', }; + state = { + activate: false, + deactivate: false, + } + + componentWillReceiveProps (nextProps) { + if (!nextProps.animate) return; + + if (this.props.active && !nextProps.active) { + this.setState({ activate: false, deactivate: true }); + } else if (!this.props.active && nextProps.active) { + this.setState({ activate: true, deactivate: false }); + } + } + handleClick = (e) => { e.preventDefault(); @@ -43,6 +61,24 @@ export default class IconButton extends React.PureComponent { } } + handleKeyPress = (e) => { + if (this.props.onKeyPress && !this.props.disabled) { + this.props.onKeyPress(e); + } + } + + handleMouseDown = (e) => { + if (!this.props.disabled && this.props.onMouseDown) { + this.props.onMouseDown(e); + } + } + + handleKeyDown = (e) => { + if (!this.props.disabled && this.props.onKeyDown) { + this.props.onKeyDown(e); + } + } + render () { let style = { fontSize: `${this.props.size}px`, @@ -59,80 +95,49 @@ export default class IconButton extends React.PureComponent { const { active, - animate, className, disabled, expanded, icon, inverted, - flip, overlay, pressed, tabIndex, title, } = this.props; + const { + activate, + deactivate, + } = this.state; + const classes = classNames(className, 'icon-button', { active, disabled, inverted, + activate, + deactivate, overlayed: overlay, }); - const flipDeg = flip ? -180 : -360; - const rotateDeg = active ? flipDeg : 0; - - const motionDefaultStyle = { - rotate: rotateDeg, - }; - - const springOpts = { - stiffness: this.props.flip ? 60 : 120, - damping: 7, - }; - const motionStyle = { - rotate: animate ? spring(rotateDeg, springOpts) : 0, - }; - - if (!animate) { - // Perf optimization: avoid unnecessary <Motion> components unless - // we actually need to animate. - return ( - <button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - style={style} - tabIndex={tabIndex} - disabled={disabled} - > - <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> - </button> - ); - } - return ( - <Motion defaultStyle={motionDefaultStyle} style={motionStyle}> - {({ rotate }) => - (<button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - style={style} - tabIndex={tabIndex} - disabled={disabled} - > - <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' /> - {this.props.label} - </button>) - } - </Motion> + <button + aria-label={title} + aria-pressed={pressed} + aria-expanded={expanded} + title={title} + className={classes} + onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleKeyDown} + onKeyPress={this.handleKeyPress} + style={style} + tabIndex={tabIndex} + disabled={disabled} + > + <Icon id={icon} fixedWidth aria-hidden='true' /> + {this.props.label} + </button> ); } diff --git a/app/javascript/flavours/glitch/components/icon_with_badge.js b/app/javascript/flavours/glitch/components/icon_with_badge.js index 4a15ee5b4..219efc28c 100644 --- a/app/javascript/flavours/glitch/components/icon_with_badge.js +++ b/app/javascript/flavours/glitch/components/icon_with_badge.js @@ -6,7 +6,7 @@ const formatNumber = num => num > 40 ? '40+' : num; const IconWithBadge = ({ id, count, className }) => ( <i className='icon-with-badge'> - <Icon icon={id} fixedWidth className={className} /> + <Icon id={id} fixedWidth className={className} /> {count > 0 && <i className='icon-with-badge__badge'>{formatNumber(count)}</i>} </i> ); diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js index 900c98638..03b3700df 100644 --- a/app/javascript/flavours/glitch/components/intersection_observer_article.js +++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js @@ -1,10 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task'; import getRectFromEntry from 'flavours/glitch/util/get_rect_from_entry'; -export default class IntersectionObserverArticle extends ImmutablePureComponent { +// Diff these props in the "unrendered" state +const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; + +export default class IntersectionObserverArticle extends React.Component { static propTypes = { intersectionObserverWrapper: PropTypes.object.isRequired, @@ -22,20 +24,21 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent } shouldComponentUpdate (nextProps, nextState) { - if (!nextState.isIntersecting && nextState.isHidden) { - // It's only if we're not intersecting (i.e. offscreen) and isHidden is true - // that either "isIntersecting" or "isHidden" matter, and then they're - // the only things that matter (and updated ARIA attributes). - return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength; - } else if (nextState.isIntersecting && !this.state.isIntersecting) { - // If we're going from a non-intersecting state to an intersecting state, - // (i.e. offscreen to onscreen), then we definitely need to re-render + const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight); + const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight); + if (!!isUnrendered !== !!willBeUnrendered) { + // If we're going from rendered to unrendered (or vice versa) then update return true; } - // Otherwise, diff based on "updateOnProps" and "updateOnStates" - return super.shouldComponentUpdate(nextProps, nextState); + // 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 () { const { intersectionObserverWrapper, id } = this.props; @@ -119,7 +122,7 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent data-id={id} tabIndex='0' style={style}> - {children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || cachedHeight) })} + {children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })} </article> ); } diff --git a/app/javascript/flavours/glitch/components/load_gap.js b/app/javascript/flavours/glitch/components/load_gap.js index 012303ae1..fe3f60a58 100644 --- a/app/javascript/flavours/glitch/components/load_gap.js +++ b/app/javascript/flavours/glitch/components/load_gap.js @@ -1,13 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, }); -@injectIntl -export default class LoadGap extends React.PureComponent { +export default @injectIntl +class LoadGap extends React.PureComponent { static propTypes = { disabled: PropTypes.bool, @@ -25,7 +26,7 @@ export default class LoadGap extends React.PureComponent { return ( <button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}> - <i className='fa fa-ellipsis-h' /> + <Icon id='ellipsis-h' /> </button> ); } diff --git a/app/javascript/flavours/glitch/components/load_pending.js b/app/javascript/flavours/glitch/components/load_pending.js new file mode 100644 index 000000000..7e2702403 --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_pending.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +export default class LoadPending extends React.PureComponent { + + static propTypes = { + onClick: PropTypes.func, + count: PropTypes.number, + } + + render() { + const { count } = this.props; + + return ( + <button className='load-more load-gap' onClick={this.props.onClick}> + <FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} /> + </button> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 6ef101f11..85ee79e11 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -6,7 +6,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from 'flavours/glitch/util/is_mobile'; import classNames from 'classnames'; -import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state'; import { decode } from 'blurhash'; const messages = defineMessages({ @@ -101,6 +101,8 @@ class Item extends React.PureComponent { } _decode () { + if (!useBlurhash) return; + const hash = this.props.attachment.get('blurhash'); const pixels = decode(hash, 32, 32); @@ -177,7 +179,7 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}> + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> </a> </div> @@ -205,6 +207,7 @@ class Item extends React.PureComponent { href={attachment.get('remote_url') || originalUrl} onClick={this.handleClick} target='_blank' + rel='noopener noreferrer' > <img className={letterbox ? 'letterbox' : null} @@ -252,8 +255,8 @@ class Item extends React.PureComponent { } -@injectIntl -export default class MediaGallery extends React.PureComponent { +export default @injectIntl +class MediaGallery extends React.PureComponent { static propTypes = { sensitive: PropTypes.bool, @@ -327,7 +330,8 @@ export default class MediaGallery extends React.PureComponent { render () { const { media, intl, sensitive, letterbox, fullwidth, defaultWidth } = this.props; const { visible } = this.state; - const size = media.take(4).size; + const size = media.take(4).size; + const uncached = media.every(attachment => attachment.get('type') === 'unknown'); const width = this.state.width || defaultWidth; @@ -348,10 +352,16 @@ export default class MediaGallery extends React.PureComponent { if (this.isStandaloneEligible()) { children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible} />); + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />); } - if (visible) { + if (uncached) { + spoilerButton = ( + <button type='button' disabled className='spoiler-button__overlay'> + <span className='spoiler-button__overlay__label'><FormattedMessage id='status.uncached_media_warning' defaultMessage='Not available' /></span> + </button> + ); + } else if (visible) { spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />; } else { spoilerButton = ( @@ -363,7 +373,7 @@ export default class MediaGallery extends React.PureComponent { return ( <div className={computedClass} style={style} ref={this.handleRef}> - <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}> + <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}> {spoilerButton} {visible && sensitive && ( <span className='sensitive-marker'> diff --git a/app/javascript/flavours/glitch/components/missing_indicator.js b/app/javascript/flavours/glitch/components/missing_indicator.js index 70d8c3b98..ee5bf7c1e 100644 --- a/app/javascript/flavours/glitch/components/missing_indicator.js +++ b/app/javascript/flavours/glitch/components/missing_indicator.js @@ -1,17 +1,24 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import illustration from 'flavours/glitch/images/elephant_ui_disappointed.svg'; +import classNames from 'classnames'; -const MissingIndicator = () => ( - <div className='regeneration-indicator missing-indicator'> - <div> - <div className='regeneration-indicator__figure' /> +const MissingIndicator = ({ fullPage }) => ( + <div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}> + <div className='regeneration-indicator__figure'> + <img src={illustration} alt='' /> + </div> - <div className='regeneration-indicator__label'> - <FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' /> - <FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' /> - </div> + <div className='regeneration-indicator__label'> + <FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' /> + <FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' /> </div> </div> ); +MissingIndicator.propTypes = { + fullPage: PropTypes.bool, +}; + export default MissingIndicator; diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js index 4e8648b49..f9877d5ea 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import 'wicg-inert'; import createHistory from 'history/createBrowserHistory'; export default class ModalRoot extends React.PureComponent { @@ -26,8 +27,30 @@ export default class ModalRoot extends React.PureComponent { } } + handleKeyDown = (e) => { + if (e.key === 'Tab') { + const focusable = Array.from(this.node.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none'); + const index = focusable.indexOf(e.target); + + let element; + + if (e.shiftKey) { + element = focusable[index - 1] || focusable[focusable.length - 1]; + } else { + element = focusable[index + 1] || focusable[0]; + } + + if (element) { + element.focus(); + e.stopPropagation(); + e.preventDefault(); + } + } + } + componentDidMount () { window.addEventListener('keyup', this.handleKeyUp, false); + window.addEventListener('keydown', this.handleKeyDown, false); this.history = this.context.router ? this.context.router.history : createHistory(); } @@ -39,15 +62,22 @@ export default class ModalRoot extends React.PureComponent { } else if (!nextProps.children) { this.setState({ revealed: false }); } - if (!nextProps.children && !!this.props.children) { - this.activeElement.focus({ preventScroll: true }); - this.activeElement = null; - } } componentDidUpdate (prevProps) { if (!this.props.children && !!prevProps.children) { this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); + + // Because of the wicg-inert polyfill, the activeElement may not be + // immediately selectable, we have to wait for observers to run, as + // described in https://github.com/WICG/inert#performance-and-gotchas + Promise.resolve().then(() => { + this.activeElement.focus({ preventScroll: true }); + this.activeElement = null; + }).catch((error) => { + console.error(error); + }); + this.handleModalClose(); } if (this.props.children) { @@ -60,6 +90,7 @@ export default class ModalRoot extends React.PureComponent { componentWillUnmount () { window.removeEventListener('keyup', this.handleKeyUp); + window.removeEventListener('keydown', this.handleKeyDown); } handleModalClose () { diff --git a/app/javascript/flavours/glitch/components/notification_purge_buttons.js b/app/javascript/flavours/glitch/components/notification_purge_buttons.js index e0c1543b0..3c7d67109 100644 --- a/app/javascript/flavours/glitch/components/notification_purge_buttons.js +++ b/app/javascript/flavours/glitch/components/notification_purge_buttons.js @@ -10,6 +10,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' }, @@ -18,8 +19,8 @@ const messages = defineMessages({ btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' }, }); -@injectIntl -export default class NotificationPurgeButtons extends ImmutablePureComponent { +export default @injectIntl +class NotificationPurgeButtons extends ImmutablePureComponent { static propTypes = { onDeleteMarked : PropTypes.func.isRequired, @@ -49,7 +50,7 @@ export default class NotificationPurgeButtons extends ImmutablePureComponent { </button> <button onClick={this.props.onDeleteMarked}> - <i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)} + <Icon id='trash' /><br />{intl.formatMessage(messages.btnApply)} </button> </div> ); diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index 690f9ae5a..62965df94 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -4,15 +4,17 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { vote, fetchPoll } from 'mastodon/actions/polls'; -import Motion from 'mastodon/features/ui/util/optional_motion'; +import { vote, fetchPoll } from 'flavours/glitch/actions/polls'; +import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; import escapeTextContentForBrowser from 'escape-html'; -import emojify from 'mastodon/features/emoji/emoji'; +import emojify from 'flavours/glitch/util/emoji'; import RelativeTimestamp from './relative_timestamp'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ closed: { id: 'poll.closed', defaultMessage: 'Closed' }, + voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' }, }); const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { @@ -32,11 +34,40 @@ class Poll extends ImmutablePureComponent { state = { selected: {}, + expired: null, }; - handleOptionChange = e => { - const { target: { value } } = e; + static getDerivedStateFromProps (props, state) { + const { poll, intl } = props; + const expires_at = poll.get('expires_at'); + const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now(); + return (expired === state.expired) ? null : { expired }; + } + + componentDidMount () { + this._setupTimer(); + } + + componentDidUpdate () { + this._setupTimer(); + } + + componentWillUnmount () { + clearTimeout(this._timer); + } + + _setupTimer () { + const { poll, intl } = this.props; + clearTimeout(this._timer); + if (!this.state.expired) { + const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now(); + this._timer = setTimeout(() => { + this.setState({ expired: true }); + }, delay); + } + } + _toggleOption = value => { if (this.props.poll.get('multiple')) { const tmp = { ...this.state.selected }; if (tmp[value]) { @@ -50,8 +81,20 @@ class Poll extends ImmutablePureComponent { tmp[value] = true; this.setState({ selected: tmp }); } + } + + handleOptionChange = ({ target: { value } }) => { + this._toggleOption(value); }; + handleOptionKeyPress = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + this._toggleOption(e.target.getAttribute('data-index')); + e.stopPropagation(); + e.preventDefault(); + } + } + handleVote = () => { if (this.props.disabled) { return; @@ -68,12 +111,13 @@ class Poll extends ImmutablePureComponent { this.props.dispatch(fetchPoll(this.props.poll.get('id'))); }; - renderOption (option, optionIndex) { - const { poll, disabled } = this.props; - const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; - const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); - const active = !!this.state.selected[`${optionIndex}`]; - const showResults = poll.get('voted') || poll.get('expired'); + renderOption (option, optionIndex, showResults) { + const { poll, disabled, intl } = this.props; + const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); + const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100; + const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); + const active = !!this.state.selected[`${optionIndex}`]; + const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); let titleEmojified = option.get('title_emojified'); if (!titleEmojified) { @@ -101,8 +145,21 @@ class Poll extends ImmutablePureComponent { disabled={disabled} /> - {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />} - {showResults && <span className='poll__number'>{Math.round(percent)}%</span>} + {!showResults && ( + <span + className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} + tabIndex='0' + role={poll.get('multiple') ? 'checkbox' : 'radio'} + onKeyPress={this.handleOptionKeyPress} + aria-checked={active} + aria-label={option.get('title')} + data-index={optionIndex} + /> + )} + {showResults && <span className='poll__number'> + {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />} + {Math.round(percent)}% + </span>} <span dangerouslySetInnerHTML={{ __html: titleEmojified }} /> </label> @@ -112,25 +169,34 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; + const { expired } = this.state; if (!poll) { return null; } - const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />; - const showResults = poll.get('voted') || poll.get('expired'); + const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />; + const showResults = poll.get('voted') || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + let votesCount = null; + + if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) { + votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />; + } else { + votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />; + } + return ( <div className='poll'> <ul> - {poll.get('options').map((option, i) => this.renderOption(option, i))} + {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))} </ul> <div className='poll__footer'> {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>} {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>} - <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} /> + {votesCount} {poll.get('expires_at') && <span> · {timeRemaining}</span>} </div> </div> diff --git a/app/javascript/flavours/glitch/components/radio_button.js b/app/javascript/flavours/glitch/components/radio_button.js new file mode 100644 index 000000000..0496fa286 --- /dev/null +++ b/app/javascript/flavours/glitch/components/radio_button.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default class RadioButton extends React.PureComponent { + + static propTypes = { + value: PropTypes.string.isRequired, + checked: PropTypes.bool, + name: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + label: PropTypes.node.isRequired, + }; + + render () { + const { name, value, checked, onChange, label } = this.props; + + return ( + <label className='radio-button'> + <input + name={name} + type='radio' + value={value} + checked={checked} + onChange={onChange} + /> + + <span className={classNames('radio-button__input', { checked })} /> + + <span>{label}</span> + </label> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/regeneration_indicator.js b/app/javascript/flavours/glitch/components/regeneration_indicator.js new file mode 100644 index 000000000..f4e0a79ef --- /dev/null +++ b/app/javascript/flavours/glitch/components/regeneration_indicator.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import illustration from 'flavours/glitch/images/elephant_ui_working.svg'; + +const MissingIndicator = () => ( + <div className='regeneration-indicator'> + <div className='regeneration-indicator__figure'> + <img src={illustration} alt='' /> + </div> + + <div className='regeneration-indicator__label'> + <FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading…' /> + <FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' /> + </div> + </div> +); + +export default MissingIndicator; diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index 462185bbc..c022290a4 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4'; import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; import LoadMore from './load_more'; +import LoadPending from './load_pending'; import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; @@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent { static propTypes = { scrollKey: PropTypes.string.isRequired, onLoadMore: PropTypes.func, + onLoadPending: PropTypes.func, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -28,10 +30,12 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, showLoading: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, prepend: PropTypes.node, alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, + bindToDocument: PropTypes.bool, }; static defaultProps = { @@ -47,7 +51,9 @@ export default class ScrollableList extends PureComponent { handleScroll = throttle(() => { if (this.node) { - const { scrollTop, scrollHeight, clientHeight } = this.node; + const scrollTop = this.getScrollTop(); + const scrollHeight = this.getScrollHeight(); + const clientHeight = this.getClientHeight(); const offset = scrollHeight - scrollTop - clientHeight; if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) { @@ -77,9 +83,14 @@ export default class ScrollableList extends PureComponent { scrollToTopOnMouseIdle = false; setScrollTop = newScrollTop => { - if (this.node.scrollTop !== newScrollTop) { + if (this.getScrollTop() !== newScrollTop) { this.lastScrollWasSynthetic = true; - this.node.scrollTop = newScrollTop; + + if (this.props.bindToDocument) { + document.scrollingElement.scrollTop = newScrollTop; + } else { + this.node.scrollTop = newScrollTop; + } } }; @@ -97,7 +108,7 @@ export default class ScrollableList extends PureComponent { this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); - if (!this.mouseMovedRecently && this.node.scrollTop === 0) { + if (!this.mouseMovedRecently && this.getScrollTop() === 0) { // Only set if we just started moving and are scrolled to the top. this.scrollToTopOnMouseIdle = true; } @@ -129,15 +140,27 @@ export default class ScrollableList extends PureComponent { } getScrollPosition = () => { - if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return {height: this.node.scrollHeight, top: this.node.scrollTop}; + if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + return { height: this.getScrollHeight(), top: this.getScrollTop() }; } else { return null; } } + getScrollTop = () => { + return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop; + } + + getScrollHeight = () => { + return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight; + } + + getClientHeight = () => { + return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight; + } + updateScrollBottom = (snapshot) => { - const newScrollTop = this.node.scrollHeight - snapshot; + const newScrollTop = this.getScrollHeight() - snapshot; this.setScrollTop(newScrollTop); } @@ -150,8 +173,10 @@ export default class ScrollableList extends PureComponent { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); - if (someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return this.node.scrollHeight - this.node.scrollTop; + const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); + + if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + return this.getScrollHeight() - this.getScrollTop(); } else { return null; } @@ -160,7 +185,9 @@ export default class ScrollableList extends PureComponent { componentDidUpdate (prevProps, prevState, snapshot) { // Reset the scroll position when a new child comes in in order not to // jerk the scrollbar around if you're already scrolled down the page. - if (snapshot !== null) this.updateScrollBottom(snapshot); + if (snapshot !== null) { + this.updateScrollBottom(snapshot); + } } componentWillUnmount () { @@ -186,13 +213,23 @@ export default class ScrollableList extends PureComponent { } attachScrollListener () { - this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.addEventListener('scroll', this.handleScroll); + document.addEventListener('wheel', this.handleWheel); + } else { + this.node.addEventListener('scroll', this.handleScroll); + this.node.addEventListener('wheel', this.handleWheel); + } } detachScrollListener () { - this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.removeEventListener('scroll', this.handleScroll); + document.removeEventListener('wheel', this.handleWheel); + } else { + this.node.removeEventListener('scroll', this.handleScroll); + this.node.removeEventListener('wheel', this.handleWheel); + } } getFirstChildKey (props) { @@ -222,12 +259,25 @@ export default class ScrollableList extends PureComponent { return !(location.state && location.state.mastodonModalOpen); } + handleLoadPending = e => { + e.preventDefault(); + this.props.onLoadPending(); + // Prevent the weird scroll-jumping behavior, as we explicitly don't want to + // scroll to top, and we know the scroll height is going to change + this.scrollToTopOnMouseIdle = false; + this.lastScrollWasSynthetic = false; + this.clearMouseIdleTimer(); + this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); + this.mouseMovedRecently = true; + } + render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null; + const loadPending = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null; let scrollableArea = null; if (showLoading) { @@ -248,6 +298,8 @@ export default class ScrollableList extends PureComponent { <div role='feed' className='item-list'> {prepend} + {loadPending} + {React.Children.map(this.props.children, (child, index) => ( <IntersectionObserverArticleContainer key={child.key} diff --git a/app/javascript/flavours/glitch/components/spoilers.js b/app/javascript/flavours/glitch/components/spoilers.js new file mode 100644 index 000000000..8527403c1 --- /dev/null +++ b/app/javascript/flavours/glitch/components/spoilers.js @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +export default +class Spoilers extends React.PureComponent { + static propTypes = { + spoilerText: PropTypes.string, + children: PropTypes.node, + }; + + state = { + hidden: true, + } + + handleSpoilerClick = () => { + this.setState({ hidden: !this.state.hidden }); + } + + render () { + const { spoilerText, children } = this.props; + const { hidden } = this.state; + + const toggleText = hidden ? + <FormattedMessage + id='status.show_more' + defaultMessage='Show more' + key='0' + /> : + <FormattedMessage + id='status.show_less' + defaultMessage='Show less' + key='0' + />; + + return ([ + <p className='spoiler__text'> + {spoilerText} + {' '} + <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> + {toggleText} + </button> + </p>, + <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}> + {children} + </div> + ]); + } +} + diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index f6d73475a..23cdc0167 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -10,7 +10,7 @@ import AttachmentList from './attachment_list'; import Card from '../features/status/components/card'; import { injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video } from 'flavours/glitch/util/async-components'; +import { MediaGallery, Video, Audio } from 'flavours/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; import classNames from 'classnames'; @@ -55,8 +55,8 @@ export const defaultMediaVisibility = (status, settings) => { return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); } -@injectIntl -export default class Status extends ImmutablePureComponent { +export default @injectIntl +class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -105,6 +105,8 @@ export default class Status extends ImmutablePureComponent { showMedia: undefined, statusId: undefined, revealBehindCW: undefined, + showCard: false, + forceFilter: undefined, } // Avoid checking props that are functions (and whose equality will always @@ -125,6 +127,7 @@ export default class Status extends ImmutablePureComponent { 'isExpanded', 'isCollapsed', 'showMedia', + 'forceFilter', ] // If our settings have changed to disable collapsed statuses, then we @@ -255,28 +258,32 @@ export default class Status extends ImmutablePureComponent { this.setState({ autoCollapsed: true }); } - this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'); + // Hack to fix timeline jumps when a preview card is fetched + this.setState({ + showCard: !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'), + }); } + // Hack to fix timeline jumps on second rendering when auto-collapsing + // or on subsequent rendering when a preview card has been fetched getSnapshotBeforeUpdate (prevProps, prevState) { - if (this.props.getScrollPosition) { + if (!this.props.getScrollPosition) return null; + + const { muted, hidden, status, settings } = this.props; + + const doShowCard = !muted && !hidden && status && status.get('card') && settings.get('inline_preview_cards'); + if (this.state.autoCollapsed || (doShowCard && !this.state.showCard)) { + if (doShowCard) this.setState({ showCard: true }); + if (this.state.autoCollapsed) this.setState({ autoCollapsed: false }); return this.props.getScrollPosition(); } else { return null; } } - // Hack to fix timeline jumps on second rendering when auto-collapsing componentDidUpdate (prevProps, prevState, snapshot) { - const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'); - if (this.state.autoCollapsed || (doShowCard && !this.didShowCard)) { - if (doShowCard) this.didShowCard = true; - if (this.state.autoCollapsed) this.setState({ autoCollapsed: false }); - if (snapshot !== null && this.props.updateScrollBottom) { - if (this.node.offsetTop < snapshot.top) { - this.props.updateScrollBottom(snapshot.height - snapshot.top); - } - } + if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) { + this.props.updateScrollBottom(snapshot.height - snapshot.top); } } @@ -369,6 +376,22 @@ export default class Status extends ImmutablePureComponent { this.props.onOpenVideo(media, startTime); } + handleHotkeyOpenMedia = e => { + const { status, onOpenMedia, onOpenVideo } = 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') { + onOpenVideo(status.getIn(['media_attachments', 0]), 0); + } else { + onOpenMedia(status.get('media_attachments'), 0); + } + } + } + handleHotkeyReply = e => { e.preventDefault(); this.props.onReply(this.props.status, this.context.router.history); @@ -422,16 +445,29 @@ export default class Status extends ImmutablePureComponent { this.handleToggleMediaVisibility(); } + handleUnfilterClick = e => { + const { onUnfilter, status } = this.props; + onUnfilter(status.get('reblog') ? status.get('reblog') : status, () => this.setState({ forceFilter: false })); + } + + handleFilterClick = () => { + this.setState({ forceFilter: true }); + } + handleRef = c => { this.node = c; } renderLoadingMediaGallery () { - return <div className='media_gallery' style={{ height: '110px' }} />; + return <div className='media-gallery' style={{ height: '110px' }} />; } renderLoadingVideoPlayer () { - return <div className='media-spoiler-video' style={{ height: '110px' }} />; + return <div className='video-player' style={{ height: '110px' }} />; + } + + renderLoadingAudioPlayer () { + return <div className='audio-player' style={{ height: '110px' }} />; } render () { @@ -460,7 +496,7 @@ export default class Status extends ImmutablePureComponent { featured, ...other } = this.props; - const { isExpanded, isCollapsed } = this.state; + const { isExpanded, isCollapsed, forceFilter } = this.state; let background = null; let attachments = null; let media = null; @@ -470,17 +506,36 @@ export default class Status extends ImmutablePureComponent { return null; } + const handlers = { + reply: this.handleHotkeyReply, + favourite: this.handleHotkeyFavourite, + boost: this.handleHotkeyBoost, + mention: this.handleHotkeyMention, + open: this.handleHotkeyOpen, + openProfile: this.handleHotkeyOpenProfile, + moveUp: this.handleHotkeyMoveUp, + moveDown: this.handleHotkeyMoveDown, + toggleSpoiler: this.handleExpandedToggle, + bookmark: this.handleHotkeyBookmark, + toggleCollapse: this.handleHotkeyCollapse, + toggleSensitive: this.handleHotkeyToggleSensitive, + openMedia: this.handleHotkeyOpenMedia, + }; + if (hidden) { return ( - <div ref={this.handleRef}> - {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {' '} - {status.get('content')} - </div> + <HotKeys handlers={handlers}> + <div ref={this.handleRef} className='status focusable' tabIndex='0'> + {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} + {' '} + {status.get('content')} + </div> + </HotKeys> ); } - if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) { + const filtered = (status.get('filtered') || status.getIn(['reblog', 'filtered'])) && settings.get('filtering_behavior') !== 'content_warning'; + if (forceFilter === undefined ? filtered : forceFilter) { const minHandlers = this.props.muted ? {} : { moveUp: this.handleHotkeyMoveUp, moveDown: this.handleHotkeyMoveDown, @@ -490,6 +545,12 @@ export default class Status extends ImmutablePureComponent { <HotKeys handlers={minHandlers}> <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}> <FormattedMessage id='status.filtered' defaultMessage='Filtered' /> + {settings.get('filtering_behavior') !== 'upstream' && ' '} + {settings.get('filtering_behavior') !== 'upstream' && ( + <button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}> + <FormattedMessage id='status.show_filter_reason' defaultMessage='(show why)' /> + </button> + )} </div> </HotKeys> ); @@ -521,16 +582,33 @@ export default class Status extends ImmutablePureComponent { media={status.get('media_attachments')} /> ); - } else if (attachments.getIn([0, 'type']) === 'video') { // Media type is 'video' - const video = status.getIn(['media_attachments', 0]); + } else if (attachments.getIn([0, 'type']) === 'audio') { + const attachment = status.getIn(['media_attachments', 0]); + + media = ( + <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > + {Component => ( + <Component + src={attachment.get('url')} + alt={attachment.get('description')} + duration={attachment.getIn(['meta', 'original', 'duration'], 0)} + peaks={[0]} + height={70} + /> + )} + </Bundle> + ); + mediaIcon = 'music'; + } else if (attachments.getIn([0, 'type']) === 'video') { + const attachment = status.getIn(['media_attachments', 0]); media = ( <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > {Component => (<Component - preview={video.get('preview_url')} - blurhash={video.get('blurhash')} - src={video.get('url')} - alt={video.get('description')} + preview={attachment.get('preview_url')} + blurhash={attachment.get('blurhash')} + src={attachment.get('url')} + alt={attachment.get('description')} inline sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} @@ -605,21 +683,6 @@ export default class Status extends ImmutablePureComponent { rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') }); } - const handlers = { - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - open: this.handleHotkeyOpen, - openProfile: this.handleHotkeyOpenProfile, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleSpoiler: this.handleExpandedToggle, - bookmark: this.handleHotkeyBookmark, - toggleCollapse: this.handleHotkeyCollapse, - toggleSensitive: this.handleHotkeyToggleSensitive, - }; - const computedClass = classNames('status', `status-${status.get('visibility')}`, { collapsed: isCollapsed, 'has-background': isCollapsed && background, @@ -676,6 +739,8 @@ export default class Status extends ImmutablePureComponent { onExpandedToggle={this.handleExpandedToggle} parseClick={parseClick} disabled={!router} + tagLinks={settings.get('tag_misleading_links')} + rewriteMentions={settings.get('rewrite_mentions')} /> {!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar'])) ? ( <StatusActionBar @@ -684,6 +749,7 @@ export default class Status extends ImmutablePureComponent { account={status.get('account')} showReplyCount={settings.get('show_reply_count')} directMessage={!!otherAccounts} + onFilter={this.handleFilterClick} /> ) : null} {notification ? ( diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 85bc4a976..60b370622 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -35,6 +35,7 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, + hide: { id: 'status.hide', defaultMessage: 'Hide toot' }, }); const obfuscatedCount = count => { @@ -47,8 +48,8 @@ const obfuscatedCount = count => { } }; -@injectIntl -export default class StatusActionBar extends ImmutablePureComponent { +export default @injectIntl +class StatusActionBar extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -69,6 +70,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onMuteConversation: PropTypes.func, onPin: PropTypes.func, onBookmark: PropTypes.func, + onFilter: PropTypes.func, withDismiss: PropTypes.bool, showReplyCount: PropTypes.bool, directMessage: PropTypes.bool, @@ -191,6 +193,10 @@ export default class StatusActionBar extends ImmutablePureComponent { } } + handleFilterClick = () => { + this.props.onFilter(); + } + render () { const { status, intl, withDismiss, showReplyCount, directMessage } = this.props; @@ -263,6 +269,10 @@ export default class StatusActionBar extends ImmutablePureComponent { <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> ); + const filterButton = status.get('filtered') && ( + <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.hide)} icon='eye' onClick={this.handleFilterClick} /> + ); + let replyButton = ( <IconButton className='status__action-bar-button' @@ -284,11 +294,12 @@ export default class StatusActionBar extends ImmutablePureComponent { <div className='status__action-bar'> {replyButton} {!directMessage && [ - <IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} 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} />, + <IconButton key='reblog-button' className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} />, + <IconButton key='favourite-button' 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, - <IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />, - <div className='status__action-bar-dropdown'> + <IconButton key='bookmark-button' className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />, + filterButton, + <div key='dropdown-button' className='status__action-bar-dropdown'> <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> </div>, ]} diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 07a0d1d5d..2c79de4db 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -5,6 +5,55 @@ import { isRtl } from 'flavours/glitch/util/rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; import classnames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; +import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { decode as decodeIDNA } from 'flavours/glitch/util/idna'; + +const textMatchesTarget = (text, origin, host) => { + return (text === origin || text === host + || text.startsWith(origin + '/') || text.startsWith(host + '/') + || 'www.' + text === host || ('www.' + text).startsWith(host + '/')); +} + +const isLinkMisleading = (link) => { + let linkTextParts = []; + + // Reconstruct visible text, as we do not have much control over how links + // from remote software look, and we can't rely on `innerText` because the + // `invisible` class does not set `display` to `none`. + + const walk = (node) => { + switch (node.nodeType) { + case Node.TEXT_NODE: + linkTextParts.push(node.textContent); + break; + case Node.ELEMENT_NODE: + if (node.classList.contains('invisible')) return; + const children = node.childNodes; + for (let i = 0; i < children.length; i++) { + walk(children[i]); + } + break; + } + }; + + walk(link); + + const linkText = linkTextParts.join(''); + const targetURL = new URL(link.href); + + // The following may not work with international domain names + if (textMatchesTarget(linkText, targetURL.origin, targetURL.host) || textMatchesTarget(linkText.toLowerCase(), targetURL.origin, targetURL.host)) { + return false; + } + + // The link hasn't been recognized, maybe it features an international domain name + const hostname = decodeIDNA(targetURL.hostname).normalize('NFKC'); + const host = targetURL.host.replace(targetURL.hostname, hostname); + const origin = targetURL.origin.replace(targetURL.host, host); + const text = linkText.normalize('NFKC'); + return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host)); +}; export default class StatusContent extends React.PureComponent { @@ -18,6 +67,13 @@ export default class StatusContent extends React.PureComponent { parseClick: PropTypes.func, disabled: PropTypes.bool, onUpdate: PropTypes.func, + tagLinks: PropTypes.bool, + rewriteMentions: PropTypes.string, + }; + + static defaultProps = { + tagLinks: true, + rewriteMentions: 'no', }; state = { @@ -25,7 +81,8 @@ export default class StatusContent extends React.PureComponent { }; _updateStatusLinks () { - const node = this.node; + const node = this.contentsNode; + const { tagLinks, rewriteMentions } = this.props; if (!node) { return; @@ -45,24 +102,70 @@ export default class StatusContent extends React.PureComponent { if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); link.setAttribute('title', mention.get('acct')); + if (rewriteMentions !== 'no') { + while (link.firstChild) link.removeChild(link.firstChild); + link.appendChild(document.createTextNode('@')); + const acctSpan = document.createElement('span'); + acctSpan.textContent = rewriteMentions === 'acct' ? mention.get('acct') : mention.get('username'); + link.appendChild(acctSpan); + } } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); } else { link.addEventListener('click', this.onLinkClick.bind(this), false); link.setAttribute('title', link.href); + link.classList.add('unhandled-link'); + + try { + if (tagLinks && isLinkMisleading(link)) { + // Add a tag besides the link to display its origin + + const tag = document.createElement('span'); + tag.classList.add('link-origin-tag'); + tag.textContent = `[${new URL(link.href).host}]`; + link.insertAdjacentText('beforeend', ' '); + link.insertAdjacentElement('beforeend', tag); + } + } catch (e) { + // The URL is invalid, remove the href just to be safe + if (tagLinks && e instanceof TypeError) link.removeAttribute('href'); + } } link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); + link.setAttribute('rel', 'noopener noreferrer'); + } + } + + _updateStatusEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); } } componentDidMount () { this._updateStatusLinks(); + this._updateStatusEmojis(); } componentDidUpdate () { this._updateStatusLinks(); + this._updateStatusEmojis(); if (this.props.onUpdate) this.props.onUpdate(); } @@ -79,13 +182,21 @@ export default class StatusContent extends React.PureComponent { } onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, '').toLowerCase(); + hashtag = hashtag.replace(/^#/, ''); if (this.props.parseClick) { this.props.parseClick(e, `/timelines/tag/${hashtag}`); } } + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; } @@ -102,7 +213,7 @@ export default class StatusContent extends React.PureComponent { let element = e.target; while (element) { - if (element.localName === 'button' || element.localName === 'video' || element.localName === 'a' || element.localName === 'label') { + if (['button', 'video', 'a', 'label', 'wave'].includes(element.localName)) { return; } element = element.parentNode; @@ -129,6 +240,10 @@ export default class StatusContent extends React.PureComponent { this.node = c; } + setContentsRef = (c) => { + this.contentsNode = c; + } + render () { const { status, @@ -136,6 +251,8 @@ export default class StatusContent extends React.PureComponent { mediaIcon, parseClick, disabled, + tagLinks, + rewriteMentions, } = this.props; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; @@ -173,10 +290,10 @@ export default class StatusContent extends React.PureComponent { key='0' />, mediaIcon ? ( - <i - className={ - `fa fa-fw fa-${mediaIcon} status__content__spoiler-icon` - } + <Icon + fixedWidth + className='status__content__spoiler-icon' + id={mediaIcon} aria-hidden='true' key='1' /> @@ -194,11 +311,11 @@ export default class StatusContent extends React.PureComponent { } return ( - <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> + <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} ref={this.setRef}> <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }} > - <span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} /> + <span dangerouslySetInnerHTML={spoilerContent} /> {' '} <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> {toggleText} @@ -209,12 +326,12 @@ export default class StatusContent extends React.PureComponent { <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}> <div - ref={this.setRef} + ref={this.setContentsRef} + key={`contents-${tagLinks}`} style={directionStyle} tabIndex={!hidden ? 0 : null} dangerouslySetInnerHTML={content} className='status__content__text' - lang={status.get('language')} /> {media} </div> @@ -229,11 +346,12 @@ export default class StatusContent extends React.PureComponent { onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} tabIndex='0' + ref={this.setRef} > <div - ref={this.setRef} + ref={this.setContentsRef} + key={`contents-${tagLinks}-${rewriteMentions}`} dangerouslySetInnerHTML={content} - lang={status.get('language')} className='status__content__text' tabIndex='0' /> @@ -246,8 +364,9 @@ export default class StatusContent extends React.PureComponent { className='status__content' style={directionStyle} tabIndex='0' + ref={this.setRef} > - <div ref={this.setRef} className='status__content__text' dangerouslySetInnerHTML={content} lang={status.get('language')} tabIndex='0' /> + <div ref={this.setContentsRef} key={`contents-${tagLinks}`} className='status__content__text' dangerouslySetInnerHTML={content} tabIndex='0' /> {media} </div> ); diff --git a/app/javascript/flavours/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js index 23cff286a..06296e124 100644 --- a/app/javascript/flavours/glitch/components/status_header.js +++ b/app/javascript/flavours/glitch/components/status_header.js @@ -56,6 +56,7 @@ export default class StatusHeader extends React.PureComponent { target='_blank' className='status__avatar' onClick={this.handleAccountClick} + rel='noopener noreferrer' > {statusAvatar} </a> @@ -64,6 +65,7 @@ export default class StatusHeader extends React.PureComponent { target='_blank' className='status__display-name' onClick={this.handleAccountClick} + rel='noopener noreferrer' > <DisplayName account={account} others={otherAccounts} /> </a> diff --git a/app/javascript/flavours/glitch/components/status_icons.js b/app/javascript/flavours/glitch/components/status_icons.js index 4a2c62881..f4d0a7405 100644 --- a/app/javascript/flavours/glitch/components/status_icons.js +++ b/app/javascript/flavours/glitch/components/status_icons.js @@ -7,15 +7,23 @@ import { defineMessages, injectIntl } from 'react-intl'; // Mastodon imports. import IconButton from './icon_button'; import VisibilityIcon from './status_visibility_icon'; +import Icon from 'flavours/glitch/components/icon'; // Messages for use with internationalization stuff. const messages = defineMessages({ collapse: { id: 'status.collapse', defaultMessage: 'Collapse' }, uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' }, + inReplyTo: { id: 'status.in_reply_to', defaultMessage: 'This toot is a reply' }, + previewCard: { id: 'status.has_preview_card', defaultMessage: 'Features an attached preview card' }, + pictures: { id: 'status.has_pictures', defaultMessage: 'Features attached pictures' }, + poll: { id: 'status.is_poll', defaultMessage: 'This toot is a poll' }, + video: { id: 'status.has_video', defaultMessage: 'Features attached videos' }, + audio: { id: 'status.has_audio', defaultMessage: 'Features attached audio files' }, + localOnly: { id: 'status.local_only', defaultMessage: 'Only visible from your instance' }, }); -@injectIntl -export default class StatusIcons extends React.PureComponent { +export default @injectIntl +class StatusIcons extends React.PureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, @@ -36,6 +44,23 @@ export default class StatusIcons extends React.PureComponent { } } + mediaIconTitleText () { + const { intl, mediaIcon } = this.props; + + switch (mediaIcon) { + case 'link': + return intl.formatMessage(messages.previewCard); + case 'picture-o': + return intl.formatMessage(messages.pictures); + case 'tasks': + return intl.formatMessage(messages.poll); + case 'video-camera': + return intl.formatMessage(messages.video); + case 'music': + return intl.formatMessage(messages.audio); + } + } + // Rendering. render () { const { @@ -50,22 +75,35 @@ export default class StatusIcons extends React.PureComponent { return ( <div className='status__info__icons'> {status.get('in_reply_to_id', null) !== null ? ( - <i - className={`fa fa-fw fa-comment status__reply-icon`} + <Icon + className='status__reply-icon' + fixedWidth + id='comment' aria-hidden='true' + title={intl.formatMessage(messages.inReplyTo)} /> ) : null} + {status.get('local_only') && + <Icon + fixedWidth + id='home' + aria-hidden='true' + title={intl.formatMessage(messages.localOnly)} + />} {mediaIcon ? ( - <i - className={`fa fa-fw fa-${mediaIcon} status__media-icon`} + <Icon + fixedWidth + className='status__media-icon' + id={mediaIcon} aria-hidden='true' + title={this.mediaIconTitleText()} /> ) : null} {!directMessage && <VisibilityIcon visibility={status.get('visibility')} />} {collapsible ? ( <IconButton className='status__collapse-button' - animate flip + animate active={collapsed} title={ collapsed ? diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index c1f51b307..a399ff567 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -6,7 +6,7 @@ import StatusContainer from 'flavours/glitch/containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import LoadGap from './load_gap'; import ScrollableList from './scrollable_list'; -import { FormattedMessage } from 'react-intl'; +import RegenerationIndicator from 'flavours/glitch/components/regeneration_indicator'; export default class StatusList extends ImmutablePureComponent { @@ -81,18 +81,7 @@ export default class StatusList extends ImmutablePureComponent { const { isLoading, isPartial } = other; if (isPartial) { - return ( - <div className='regeneration-indicator'> - <div> - <div className='regeneration-indicator__figure' /> - - <div className='regeneration-indicator__label'> - <FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading…' /> - <FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' /> - </div> - </div> - </div> - ); + return <RegenerationIndicator />; } let scrollableContent = (isLoading || statusIds.size > 0) ? ( diff --git a/app/javascript/flavours/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js index 481e6644e..637c4f23a 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.js +++ b/app/javascript/flavours/glitch/components/status_prepend.js @@ -3,6 +3,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; +import { me } from 'flavours/glitch/util/initial_state'; export default class StatusPrepend extends React.PureComponent { @@ -63,12 +65,21 @@ export default class StatusPrepend extends React.PureComponent { /> ); case 'poll': - return ( - <FormattedMessage - id='notification.poll' - defaultMessage='A poll you have voted in has ended' - /> - ); + if (me === account.get('id')) { + return ( + <FormattedMessage + id='notification.own_poll' + defaultMessage='Your poll has ended' + /> + ); + } else { + return ( + <FormattedMessage + id='notification.poll' + defaultMessage='A poll you have voted in has ended' + /> + ); + } } return null; } @@ -80,10 +91,9 @@ export default class StatusPrepend extends React.PureComponent { return !type ? null : ( <aside className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend' : 'notification__message'}> <div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}> - <i - className={`fa fa-fw fa-${ - type === 'favourite' ? 'star star-icon' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'retweet')) - } status__prepend-icon`} + <Icon + className={`status__prepend-icon ${type === 'favourite' ? 'star-icon' : ''}`} + id={type === 'favourite' ? 'star' : (type === 'featured' ? 'thumb-tack' : (type === 'poll' ? 'tasks' : 'retweet'))} /> </div> <Message /> diff --git a/app/javascript/flavours/glitch/components/status_visibility_icon.js b/app/javascript/flavours/glitch/components/status_visibility_icon.js index 5e7b8ed00..e2e0f30b8 100644 --- a/app/javascript/flavours/glitch/components/status_visibility_icon.js +++ b/app/javascript/flavours/glitch/components/status_visibility_icon.js @@ -3,6 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ public: { id: 'privacy.public.short', defaultMessage: 'Public' }, @@ -11,8 +12,8 @@ const messages = defineMessages({ direct: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); -@injectIntl -export default class VisibilityIcon extends ImmutablePureComponent { +export default @injectIntl +class VisibilityIcon extends ImmutablePureComponent { static propTypes = { visibility: PropTypes.string, @@ -23,7 +24,7 @@ export default class VisibilityIcon extends ImmutablePureComponent { render() { const { withLabel, visibility, intl } = this.props; - const visibilityClass = { + const visibilityIcon = { public: 'globe', unlisted: 'unlock', private: 'lock', @@ -32,8 +33,10 @@ export default class VisibilityIcon extends ImmutablePureComponent { const label = intl.formatMessage(messages[visibility]); - const icon = (<i - className={`status__visibility-icon fa fa-fw fa-${visibilityClass}`} + const icon = (<Icon + className='status__visibility-icon' + fixedWidth + id={visibilityIcon} title={label} aria-hidden='true' />); diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index b2419a0fd..1378e75fe 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -25,7 +25,7 @@ const mapDispatchToProps = (dispatch, { status, items }) => ({ }) : openDropdownMenu(id, dropdownPlacement, keyboard)); }, onClose(id) { - dispatch(closeModal()); + dispatch(closeModal('ACTIONS')); dispatch(closeDropdownMenu(id)); }, }); diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index 59eef6636..8101be87e 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -49,6 +49,10 @@ export default class Mastodon extends React.PureComponent { } } + shouldUpdateScroll (_, { location }) { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { locale } = this.props; @@ -57,7 +61,7 @@ export default class Mastodon extends React.PureComponent { <Provider store={store}> <ErrorBoundary> <BrowserRouter basename='/web'> - <ScrollContext> + <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}> <Route path='/' component={UI} /> </ScrollContext> </BrowserRouter> diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js index 1b480658f..a483510b0 100644 --- a/app/javascript/flavours/glitch/containers/media_container.js +++ b/app/javascript/flavours/glitch/containers/media_container.js @@ -2,19 +2,22 @@ import React, { PureComponent, Fragment } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; +import { List as ImmutableList, fromJS } from 'immutable'; import { getLocale } from 'mastodon/locales'; +import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar'; import MediaGallery from 'flavours/glitch/components/media_gallery'; -import Video from 'flavours/glitch/features/video'; -import Card from 'flavours/glitch/features/status/components/card'; import Poll from 'flavours/glitch/components/poll'; +import Hashtag from 'flavours/glitch/components/hashtag'; import ModalRoot from 'flavours/glitch/components/modal_root'; import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; -import { List as ImmutableList, fromJS } from 'immutable'; +import Video from 'flavours/glitch/features/video'; +import Card from 'flavours/glitch/features/status/components/card'; +import Audio from 'flavours/glitch/features/audio'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll }; +const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; export default class MediaContainer extends PureComponent { @@ -31,6 +34,8 @@ export default class MediaContainer extends PureComponent { handleOpenMedia = (media, index) => { document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + this.setState({ media, index }); } @@ -38,11 +43,15 @@ export default class MediaContainer extends PureComponent { const media = ImmutableList([video]); document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + this.setState({ media, time }); } handleCloseMedia = () => { document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; + this.setState({ media: null, index: null, time: null }); } @@ -55,12 +64,13 @@ export default class MediaContainer extends PureComponent { {[].map.call(components, (component, i) => { const componentName = component.getAttribute('data-component'); const Component = MEDIA_COMPONENTS[componentName]; - const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props')); + const { media, card, poll, hashtag, ...props } = JSON.parse(component.getAttribute('data-props')); Object.assign(props, { - ...(media ? { media: fromJS(media) } : {}), - ...(card ? { card: fromJS(card) } : {}), - ...(poll ? { poll: fromJS(poll) } : {}), + ...(media ? { media: fromJS(media) } : {}), + ...(card ? { card: fromJS(card) } : {}), + ...(poll ? { poll: fromJS(poll) } : {}), + ...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(componentName === 'Video' ? { onOpenVideo: this.handleOpenVideo, @@ -74,6 +84,7 @@ export default class MediaContainer extends PureComponent { component, ); })} + <ModalRoot onClose={this.handleCloseMedia}> {this.state.media && ( <MediaModal diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index a6069cb90..4c3555dea 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -1,7 +1,7 @@ -import React from 'react'; import { connect } from 'react-redux'; import Status from 'flavours/glitch/components/status'; -import { makeGetStatus } from 'flavours/glitch/selectors'; +import { List as ImmutableList } from 'immutable'; +import { makeGetStatus, regexFromFilters, toServerSideType } from 'flavours/glitch/selectors'; import { replyCompose, mentionCompose, @@ -17,25 +17,31 @@ import { pin, unpin, } from 'flavours/glitch/actions/interactions'; -import { blockAccount } from 'flavours/glitch/actions/accounts'; import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { openModal } from 'flavours/glitch/actions/modal'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; +import { filterEditLink } from 'flavours/glitch/util/backend_links'; import { showAlertForError } from '../actions/alerts'; +import AccountContainer from 'flavours/glitch/containers/account_container'; +import Spoilers from '../components/spoilers'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, 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?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, + unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, + author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' }, + matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' }, + editFilter: { id: 'confirmations.unfilter.edit_filter', defaultMessage: 'Edit filter' }, }); const makeMapStateToProps = () => { @@ -69,11 +75,12 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -const mapDispatchToProps = (dispatch, { intl }) => ({ +const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ onReply (status, router) { dispatch((_, getState) => { let state = getState(); + if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) { dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.replyMessage), @@ -99,7 +106,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch((_, getState) => { let state = getState(); if (state.getIn(['local_settings', 'confirm_boost_missing_media_description']) && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) { - dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog, missingMediaDescription: true })); + dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog, missingMediaDescription: true })); } else if (e.shiftKey || !boostModal) { this.onModalReblog(status); } else { @@ -177,16 +184,49 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onBlock (status) { const account = status.get('account'); - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.get('id'))); - dispatch(initReport(account, status)); - }, - })); + dispatch(initBlockModal(account)); + }, + + onUnfilter (status, onConfirm) { + dispatch((_, getState) => { + let state = getState(); + const serverSideType = toServerSideType(contextType); + const enabledFilters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))).toArray(); + const searchIndex = status.get('search_index'); + const matchingFilters = enabledFilters.filter(filter => regexFromFilters([filter]).test(searchIndex)); + dispatch(openModal('CONFIRM', { + message: [ + <FormattedMessage id='confirmations.unfilter' defaultMessage='Information about this filtered toot' />, + <div className='filtered-status-info'> + <Spoilers spoilerText={intl.formatMessage(messages.author)}> + <AccountContainer id={status.getIn(['account', 'id'])} /> + </Spoilers> + <Spoilers spoilerText={intl.formatMessage(messages.matchingFilters, {count: matchingFilters.size})}> + <ul> + {matchingFilters.map(filter => ( + <li> + {filter.get('phrase')} + {!!filterEditLink && ' '} + {!!filterEditLink && ( + <a + target='_blank' + className='filtered-status-edit-link' + title={intl.formatMessage(messages.editFilter)} + href={filterEditLink(filter.get('id'))} + > + <Icon icon='pencil' /> + </a> + )} + </li> + ))} + </ul> + </Spoilers> + </div> + ], + confirm: intl.formatMessage(messages.unfilterConfirm), + onConfirm: onConfirm, + })); + }); }, onReport (status) { diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js index a2c00c1c2..6576bff8e 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -8,8 +8,8 @@ import { me, isStaff } from 'flavours/glitch/util/initial_state'; import { profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links'; import Icon from 'flavours/glitch/components/icon'; -@injectIntl -export default class ActionBar extends React.PureComponent { +export default @injectIntl +class ActionBar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, @@ -31,7 +31,7 @@ export default class ActionBar extends React.PureComponent { if (account.get('acct') !== account.get('username')) { extraInfo = ( <div className='account__disclaimer'> - <Icon icon='info-circle' fixedWidth /> <FormattedMessage + <Icon id='info-circle' fixedWidth /> <FormattedMessage id='account.disclaimer_full' defaultMessage="Information below may reflect the user's profile incompletely." /> diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 43c4f0d32..6b4aff616 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { autoPlayGif, me, isStaff } from 'flavours/glitch/util/initial_state'; +import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; import Avatar from 'flavours/glitch/components/avatar'; @@ -15,6 +16,7 @@ import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_cont const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -68,7 +70,48 @@ class Header extends ImmutablePureComponent { }; openEditProfile = () => { - window.open('/settings/profile', '_blank'); + window.open(profileLink, '_blank'); + } + + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + + setRef = (c) => { + this.node = c; } render () { @@ -100,13 +143,13 @@ class Header extends ImmutablePureComponent { 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.requested)} onClick={this.props.onFollow} />; + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />; } else if (!account.getIn(['relationship', 'blocking'])) { actionBtn = <Button 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} />; } else if (account.getIn(['relationship', 'blocking'])) { actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; } - } else { + } else if (profileLink) { actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />; } @@ -115,7 +158,7 @@ class Header extends ImmutablePureComponent { } if (account.get('locked')) { - lockedIcon = <Icon icon='lock' title={intl.formatMessage(messages.account_locked)} />; + lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />; } if (account.get('id') !== me) { @@ -130,8 +173,8 @@ class Header extends ImmutablePureComponent { } if (account.get('id') === me) { - menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); - menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); + if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); + if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink }); menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); menu.push(null); menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); @@ -181,19 +224,28 @@ class Header extends ImmutablePureComponent { } } - if (account.get('id') !== me && isStaff) { + if (account.get('id') !== me && isStaff && accountAdminLink) { menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) }); } const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); - const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null; const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + let badge; + + if (account.get('bot')) { + badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>); + } else if (account.get('group')) { + badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>); + } else { + badge = null; + } + return ( - <div className={classNames('account__header', { inactive: !!account.get('moved') })}> + <div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}> <div className='account__header__image'> <div className='account__header__info'> {info} @@ -204,7 +256,7 @@ class Header extends ImmutablePureComponent { <div className='account__header__bar'> <div className='account__header__tabs'> - <a className='avatar' href={account.get('url')} rel='noopener' target='_blank'> + <a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'> <Avatar account={account} size={90} /> </a> @@ -233,10 +285,10 @@ class Header extends ImmutablePureComponent { <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} /> <dd className='verified'> - <a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> + <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> <Icon id='check' className='verified__mark' /> </span></a> - <a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> + <a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> </dd> </dl> ))} diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js index b6d373a2c..17c08e375 100644 --- a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js +++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.js @@ -12,11 +12,12 @@ class ProfileColumnHeader extends React.PureComponent { static propTypes = { onClick: PropTypes.func, + multiColumn: PropTypes.bool, intl: PropTypes.object.isRequired, }; render() { - const { onClick, intl } = this.props; + const { onClick, intl, multiColumn } = this.props; return ( <ColumnHeader @@ -24,6 +25,7 @@ class ProfileColumnHeader extends React.PureComponent { title={intl.formatMessage(messages.profile)} onClick={onClick} showBackButton + multiColumn={multiColumn} /> ); } diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index 026136b2c..f1cb3f9e4 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -1,11 +1,12 @@ -import React from 'react'; +import { decode } from 'blurhash'; +import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; +import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state'; +import { isIOS } from 'flavours/glitch/util/is_mobile'; import PropTypes from 'prop-types'; +import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state'; -import classNames from 'classnames'; -import { decode } from 'blurhash'; -import { isIOS } from 'flavours/glitch/util/is_mobile'; export default class MediaItem extends ImmutablePureComponent { @@ -94,6 +95,12 @@ export default class MediaItem extends ImmutablePureComponent { if (attachment.get('type') === 'unknown') { // Skip + } else if (attachment.get('type') === 'audio') { + thumbnail = ( + <span className='account-gallery__item__icons'> + <Icon id='music' /> + </span> + ); } else if (attachment.get('type') === 'image') { const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0; const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0; @@ -111,6 +118,7 @@ export default class MediaItem extends ImmutablePureComponent { ); } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) { const autoPlay = !isIOS() && autoPlayGif; + const label = attachment.get('type') === 'video' ? <Icon id='play' /> : 'GIF'; thumbnail = ( <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> @@ -126,20 +134,21 @@ export default class MediaItem extends ImmutablePureComponent { loop muted /> - <span className='media-gallery__gifv__label'>GIF</span> + + <span className='media-gallery__gifv__label'>{label}</span> </div> ); } const icon = ( <span className='account-gallery__item__icons'> - <i className='fa fa-eye-slash' /> + <Icon id='eye-slash' /> </span> ); return ( <div className='account-gallery__item' style={{ width, height }}> - <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> {visible ? thumbnail : icon} </a> diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 3e4421306..f5fe6c930 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -45,8 +45,8 @@ class LoadMoreMedia extends ImmutablePureComponent { } -@connect(mapStateToProps) -export default class AccountGallery extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class AccountGallery extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, @@ -55,6 +55,7 @@ export default class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; state = { @@ -113,6 +114,8 @@ export default class AccountGallery extends ImmutablePureComponent { handleOpenMedia = attachment => { if (attachment.get('type') === 'video') { this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') })); + } else if (attachment.get('type') === 'audio') { + this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status') })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); @@ -128,7 +131,7 @@ export default class AccountGallery extends ImmutablePureComponent { } render () { - const { attachments, isLoading, hasMore, isAccount } = this.props; + const { attachments, isLoading, hasMore, isAccount, multiColumn } = this.props; const { width } = this.state; if (!isAccount) { @@ -155,7 +158,7 @@ export default class AccountGallery extends ImmutablePureComponent { return ( <Column ref={this.setColumnRef}> - <ProfileColumnHeader onClick={this.handleHeaderClick} /> + <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}> diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js index 1fab083db..fcaa7b494 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js @@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import AvatarOverlay from '../../../components/avatar_overlay'; import DisplayName from '../../../components/display_name'; +import Icon from 'flavours/glitch/components/icon'; export default class MovedNote extends ImmutablePureComponent { @@ -35,7 +36,7 @@ export default class MovedNote extends ImmutablePureComponent { return ( <div className='account__moved-note'> <div className='account__moved-note__message'> - <div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div> + <div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div> <FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} /> </div> diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index 787a36658..fff5e097f 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -5,7 +5,6 @@ import Header from '../components/header'; import { followAccount, unfollowAccount, - blockAccount, unblockAccount, unmuteAccount, pinAccount, @@ -16,6 +15,7 @@ import { directCompose } from 'flavours/glitch/actions/compose'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { openModal } from 'flavours/glitch/actions/modal'; import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks'; @@ -25,9 +25,7 @@ import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, }); const makeMapStateToProps = () => { @@ -64,16 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); } else { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.get('id'))); - dispatch(initReport(account)); - }, - })); + dispatch(initBlockModal(account)); } }, diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 93d8fc9ec..2ef4ff602 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -9,6 +9,7 @@ import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import HeaderContainer from './containers/header_container'; +import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; @@ -27,8 +28,8 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false }) }; }; -@connect(mapStateToProps) -export default class AccountTimeline extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class AccountTimeline extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, @@ -39,6 +40,7 @@ export default class AccountTimeline extends ImmutablePureComponent { hasMore: PropTypes.bool, withReplies: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -76,11 +78,12 @@ export default class AccountTimeline extends ImmutablePureComponent { } render () { - const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount } = this.props; + const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, multiColumn } = this.props; if (!isAccount) { return ( <Column> + <ColumnBackButton multiColumn={multiColumn} /> <MissingIndicator /> </Column> ); @@ -96,7 +99,7 @@ export default class AccountTimeline extends ImmutablePureComponent { return ( <Column ref={this.setRef} name='account'> - <ProfileColumnHeader onClick={this.handleHeaderClick} /> + <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <StatusList prepend={<HeaderContainer accountId={this.props.params.accountId} />} @@ -108,6 +111,7 @@ export default class AccountTimeline extends ImmutablePureComponent { hasMore={hasMore} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.js new file mode 100644 index 000000000..033d92adf --- /dev/null +++ b/app/javascript/flavours/glitch/features/audio/index.js @@ -0,0 +1,236 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import WaveSurfer from 'wavesurfer.js'; +import { defineMessages, injectIntl } from 'react-intl'; +import { formatTime } from 'flavours/glitch/features/video'; +import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; +import { throttle } from 'lodash'; + +const messages = defineMessages({ + play: { id: 'video.play', defaultMessage: 'Play' }, + pause: { id: 'video.pause', defaultMessage: 'Pause' }, + mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, + unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, + download: { id: 'video.download', defaultMessage: 'Download file' }, +}); + +export default @injectIntl +class Audio extends React.PureComponent { + + static propTypes = { + src: PropTypes.string.isRequired, + alt: PropTypes.string, + duration: PropTypes.number, + peaks: PropTypes.arrayOf(PropTypes.number), + height: PropTypes.number, + preload: PropTypes.bool, + editable: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + state = { + currentTime: 0, + duration: null, + paused: true, + muted: false, + volume: 0.5, + }; + + // hard coded in components.scss + // any way to get ::before values programatically? + + volWidth = 50; + + volOffset = 70; + + volHandleOffset = v => { + const offset = v * this.volWidth + this.volOffset; + return (offset > 110) ? 110 : offset; + } + + setVolumeRef = c => { + this.volume = c; + } + + setWaveformRef = c => { + this.waveform = c; + } + + componentDidMount () { + if (this.waveform) { + this._updateWaveform(); + } + } + + componentDidUpdate (prevProps) { + if (this.waveform && prevProps.src !== this.props.src) { + this._updateWaveform(); + } + } + + componentWillUnmount () { + if (this.wavesurfer) { + this.wavesurfer.destroy(); + this.wavesurfer = null; + } + } + + _updateWaveform () { + const { src, height, duration, peaks, preload } = this.props; + + const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color'); + const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color'); + + if (this.wavesurfer) { + this.wavesurfer.destroy(); + this.loaded = false; + } + + const wavesurfer = WaveSurfer.create({ + container: this.waveform, + height, + barWidth: 3, + cursorWidth: 0, + progressColor, + waveColor, + backend: 'MediaElement', + interact: preload, + }); + + wavesurfer.setVolume(this.state.volume); + + if (preload) { + wavesurfer.load(src); + this.loaded = true; + } else { + wavesurfer.load(src, peaks, 'none', duration); + this.loaded = false; + } + + wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) })); + wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) })); + wavesurfer.on('pause', () => this.setState({ paused: true })); + wavesurfer.on('play', () => this.setState({ paused: false })); + wavesurfer.on('volume', volume => this.setState({ volume })); + wavesurfer.on('mute', muted => this.setState({ muted })); + + this.wavesurfer = wavesurfer; + } + + togglePlay = () => { + if (this.state.paused) { + if (!this.props.preload && !this.loaded) { + this.wavesurfer.createBackend(); + this.wavesurfer.createPeakCache(); + this.wavesurfer.load(this.props.src); + this.wavesurfer.toggleInteraction(); + this.loaded = true; + } + + this.wavesurfer.play(); + this.setState({ paused: false }); + } else { + this.wavesurfer.pause(); + this.setState({ paused: true }); + } + } + + toggleMute = () => { + this.wavesurfer.setMute(!this.state.muted); + } + + handleVolumeMouseDown = e => { + document.addEventListener('mousemove', this.handleMouseVolSlide, true); + document.addEventListener('mouseup', this.handleVolumeMouseUp, true); + document.addEventListener('touchmove', this.handleMouseVolSlide, true); + document.addEventListener('touchend', this.handleVolumeMouseUp, true); + + this.handleMouseVolSlide(e); + + e.preventDefault(); + e.stopPropagation(); + } + + handleVolumeMouseUp = () => { + document.removeEventListener('mousemove', this.handleMouseVolSlide, true); + document.removeEventListener('mouseup', this.handleVolumeMouseUp, true); + document.removeEventListener('touchmove', this.handleMouseVolSlide, true); + document.removeEventListener('touchend', this.handleVolumeMouseUp, true); + } + + handleMouseVolSlide = throttle(e => { + const rect = this.volume.getBoundingClientRect(); + const x = (e.clientX - rect.left) / this.volWidth; // x position within the element. + + if(!isNaN(x)) { + let slideamt = x; + + if (x > 1) { + slideamt = 1; + } else if(x < 0) { + slideamt = 0; + } + + this.wavesurfer.setVolume(slideamt); + } + }, 60); + + render () { + const { height, intl, alt, editable } = this.props; + const { paused, muted, volume, currentTime } = this.state; + + const volumeWidth = muted ? 0 : volume * this.volWidth; + const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume); + + return ( + <div className={classNames('audio-player', { editable })}> + <div className='audio-player__progress-placeholder' style={{ display: 'none' }} /> + <div className='audio-player__wave-placeholder' style={{ display: 'none' }} /> + + <div + className='audio-player__waveform' + aria-label={alt} + title={alt} + style={{ height }} + ref={this.setWaveformRef} + /> + + <div className='video-player__controls active'> + <div className='video-player__buttons-bar'> + <div className='video-player__buttons left'> + <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> + + <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> + + <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> + + <span + className={classNames('video-player__volume__handle')} + tabIndex='0' + style={{ left: `${volumeHandleLoc}px` }} + /> + </div> + + <span> + <span className='video-player__time-current'>{formatTime(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> + </div> + + <div className='video-player__buttons right'> + <button type='button' aria-label={intl.formatMessage(messages.download)}> + <a className='video-player__download__icon' href={this.props.src} download> + <Icon id={'download'} fixedWidth /> + </a> + </button> + </div> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.js index 386a0ce63..9eb6fe02e 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.js +++ b/app/javascript/flavours/glitch/features/blocks/index.js @@ -1,14 +1,15 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; import PropTypes from 'prop-types'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; +import ScrollableList from '../../components/scrollable_list'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; import AccountContainer from 'flavours/glitch/containers/account_container'; import { fetchBlocks, expandBlocks } from 'flavours/glitch/actions/blocks'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; const messages = defineMessages({ @@ -17,38 +18,32 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'blocks', 'items']), + hasMore: !!state.getIn(['user_lists', 'blocks', 'next']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Blocks extends ImmutablePureComponent { +class Blocks extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { this.props.dispatch(fetchBlocks()); } - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandBlocks()); - } - } - - shouldUpdateScroll = (prevRouterProps, { location }) => { - if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false; - return !(location.state && location.state.mastodonModalOpen); - } + handleLoadMore = debounce(() => { + this.props.dispatch(expandBlocks()); + }, 300, { leading: true }); render () { - const { intl, accountIds } = this.props; + const { intl, accountIds, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -58,16 +53,22 @@ export default class Blocks extends ImmutablePureComponent { ); } + const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />; + return ( - <Column name='blocks' icon='ban' heading={intl.formatMessage(messages.heading)}> + <Column name='blocks' bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='blocks' shouldUpdateScroll={this.shouldUpdateScroll}> - <div className='scrollable' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> + <ScrollableList + scrollKey='blocks' + onLoadMore={this.handleLoadMore} + hasMore={hasMore} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > + {accountIds.map(id => + <AccountContainer key={id} id={id} /> + )} + </ScrollableList> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js index 9468ad81d..58b9e6396 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js @@ -2,14 +2,14 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import StatusList from 'flavours/glitch/components/status_list'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { debounce } from 'lodash'; const messages = defineMessages({ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, @@ -21,9 +21,9 @@ const mapStateToProps = state => ({ hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Bookmarks extends ImmutablePureComponent { +class Bookmarks extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, @@ -70,8 +70,10 @@ export default class Bookmarks extends ImmutablePureComponent { const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const pinned = !!columnId; + const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />; + return ( - <Column ref={this.setRef} name='bookmarks'> + <Column bindToDocument={!multiColumn} ref={this.setRef} name='bookmarks'> <ColumnHeader icon='bookmark' title={intl.formatMessage(messages.heading)} @@ -90,6 +92,8 @@ export default class Bookmarks extends ImmutablePureComponent { hasMore={hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js index 96db003ce..69a4699ac 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js @@ -10,8 +10,8 @@ const messages = defineMessages({ settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -@injectIntl -export default class ColumnSettings extends React.PureComponent { +export default @injectIntl +class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, @@ -26,7 +26,7 @@ export default class ColumnSettings extends React.PureComponent { return ( <div> <div className='column-settings__row'> - <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} /> + <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} /> </div> <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index 2c0fbff36..7341f9702 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -14,20 +14,22 @@ const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, }); -const mapStateToProps = (state, { onlyMedia, columnId }) => { +const mapStateToProps = (state, { columnId }) => { const uuid = columnId; const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === uuid); + const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']); + const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]); return { - hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0, - onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']), + hasUnread: !!timelineState && timelineState.get('unread') > 0, + onlyMedia, }; }; -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class CommunityTimeline extends React.PureComponent { +class CommunityTimeline extends React.PureComponent { static defaultProps = { onlyMedia: false, @@ -99,16 +101,12 @@ export default class CommunityTimeline extends React.PureComponent { dispatch(expandCommunityTimeline({ maxId, onlyMedia })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; const pinned = !!columnId; return ( - <Column ref={this.setRef} name='local' label={intl.formatMessage(messages.title)}> + <Column ref={this.setRef} name='local' bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='users' active={hasUnread} @@ -125,10 +123,10 @@ export default class CommunityTimeline extends React.PureComponent { <StatusListContainer trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} timelineId={`community${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/character_counter.js b/app/javascript/flavours/glitch/features/compose/components/character_counter.js new file mode 100644 index 000000000..0ecfc9141 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { length } from 'stringz'; + +export default class CharacterCounter extends React.PureComponent { + + static propTypes = { + text: PropTypes.string.isRequired, + max: PropTypes.number.isRequired, + }; + + checkRemainingText (diff) { + if (diff < 0) { + return <span className='character-counter character-counter--over'>{diff}</span>; + } + + return <span className='character-counter'>{diff}</span>; + } + + render () { + const diff = this.props.max - length(this.props.text); + return this.checkRemainingText(diff); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index cbce675d5..6e07998ec 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -15,6 +15,8 @@ import { countableText } from 'flavours/glitch/util/counter'; import OptionsContainer from '../containers/options_container'; import Publisher from './publisher'; import TextareaIcons from './textarea_icons'; +import { maxChars } from 'flavours/glitch/util/initial_state'; +import CharacterCounter from './character_counter'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, @@ -119,14 +121,8 @@ class ComposeForm extends ImmutablePureComponent { // Submit unless there are media with missing descriptions if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) { - const firstWithoutDescription = media.findIndex(item => !item.get('description')); - if (uploadForm) { - const inputs = uploadForm.querySelectorAll('.composer--upload_form--item input'); - if (inputs.length == media.size && firstWithoutDescription !== -1) { - inputs[firstWithoutDescription].focus(); - } - } - onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null); + const firstWithoutDescription = media.find(item => !item.get('description')); + onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id')); } else if (onSubmit) { onSubmit(this.context.router ? this.context.router.history : null); } @@ -197,7 +193,10 @@ class ComposeForm extends ImmutablePureComponent { handleFocus = () => { if (this.composeForm && !this.props.singleColumn) { - this.composeForm.scrollIntoView(); + const { left, right } = this.composeForm.getBoundingClientRect(); + if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { + this.composeForm.scrollIntoView(); + } } } @@ -295,13 +294,15 @@ class ComposeForm extends ImmutablePureComponent { let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia); + const countText = `${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`; + return ( - <div className='composer' ref={this.setRef}> + <div className='composer'> <WarningContainer /> <ReplyIndicatorContainer /> - <div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`}> + <div className={`composer--spoiler ${spoiler ? 'composer--spoiler--visible' : ''}`} ref={this.setRef}> <AutosuggestInput placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={spoilerText} @@ -344,19 +345,24 @@ class ComposeForm extends ImmutablePureComponent { </div> </AutosuggestTextarea> - <OptionsContainer - advancedOptions={advancedOptions} - disabled={isSubmitting} - onChangeVisibility={onChangeVisibility} - onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness} - onUpload={onPaste} - privacy={privacy} - sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)} - spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} - /> + <div className='composer--options-wrapper'> + <OptionsContainer + advancedOptions={advancedOptions} + disabled={isSubmitting} + onChangeVisibility={onChangeVisibility} + onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness} + onUpload={onPaste} + privacy={privacy} + sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)} + spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} + /> + <div className='compose--counter-wrapper'> + <CharacterCounter text={countText} max={maxChars} /> + </div> + </div> <Publisher - countText={`${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`} + countText={countText} disabled={disabledButton} onSecondarySubmit={handleSecondarySubmit} onSubmit={handleSubmit} diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index 8d982208f..60035b705 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -12,33 +12,101 @@ import DropdownMenu from './dropdown_menu'; import { isUserTouching } from 'flavours/glitch/util/is_mobile'; import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -// Handlers. -const handlers = { +// The component. +export default class ComposerOptionsDropdown extends React.PureComponent { - // Closes the dropdown. - handleClose () { - this.setState({ open: false }); - }, + static propTypes = { + active: PropTypes.bool, + disabled: PropTypes.bool, + icon: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.string, + meta: PropTypes.node, + name: PropTypes.string.isRequired, + on: PropTypes.bool, + text: PropTypes.node, + })).isRequired, + onModalOpen: PropTypes.func, + onModalClose: PropTypes.func, + title: PropTypes.string, + value: PropTypes.string, + onChange: PropTypes.func, + }; + + state = { + needsModalUpdate: false, + open: false, + openedViaKeyboard: undefined, + placement: 'bottom', + }; - // The enter key toggles the dropdown's open state, and the escape - // key closes it. - handleKeyDown ({ key }) { - const { - handleClose, - handleToggle, - } = this.handlers; - switch (key) { + // Toggles opening and closing the dropdown. + handleToggle = ({ target, type }) => { + const { onModalOpen } = this.props; + const { open } = this.state; + + if (isUserTouching()) { + if (this.state.open) { + this.props.onModalClose(); + } else { + const modal = this.handleMakeModal(); + if (modal && onModalOpen) { + onModalOpen(modal); + } + } + } else { + const { top } = target.getBoundingClientRect(); + if (this.state.open && this.activeElement) { + this.activeElement.focus(); + } + this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); + this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' }); + } + } + + handleKeyDown = (e) => { + switch (e.key) { + case 'Escape': + this.handleClose(); + break; + } + } + + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + } + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': case 'Enter': - handleToggle(key); + this.handleMouseDown(); break; - case 'Escape': - handleClose(); + } + } + + handleKeyPress = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleToggle(e); + e.stopPropagation(); + e.preventDefault(); break; } - }, + } + + handleClose = () => { + if (this.state.open && this.activeElement) { + this.activeElement.focus(); + } + this.setState({ open: false }); + } // Creates an action modal object. - handleMakeModal () { + handleMakeModal = () => { const component = this; const { items, @@ -76,74 +144,31 @@ const handlers = { }) ), }; - }, - - // Toggles opening and closing the dropdown. - handleToggle ({ target }) { - const { handleMakeModal } = this.handlers; - const { onModalOpen } = this.props; - const { open } = this.state; - - // If this is a touch device, we open a modal instead of the - // dropdown. - if (isUserTouching()) { - - // This gets the modal to open. - const modal = handleMakeModal(); - - // If we can, we then open the modal. - if (modal && onModalOpen) { - onModalOpen(modal); - return; - } - } - - const { top } = target.getBoundingClientRect(); - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); - // Otherwise, we just set our state to open. - this.setState({ open: !open }); - }, + } // If our modal is open and our props update, we need to also update // the modal. - handleUpdate () { - const { handleMakeModal } = this.handlers; + handleUpdate = () => { const { onModalOpen } = this.props; const { needsModalUpdate } = this.state; // Gets our modal object. - const modal = handleMakeModal(); + const modal = this.handleMakeModal(); // Reopens the modal with the new object. if (needsModalUpdate && modal && onModalOpen) { onModalOpen(modal); } - }, -}; - -// The component. -export default class ComposerOptionsDropdown extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - this.state = { - needsModalUpdate: false, - open: false, - placement: 'bottom', - }; } // Updates our modal as necessary. componentDidUpdate (prevProps) { - const { handleUpdate } = this.handlers; const { items } = this.props; const { needsModalUpdate } = this.state; if (needsModalUpdate && items.find( (item, i) => item.on !== prevProps.items[i].on )) { - handleUpdate(); + this.handleUpdate(); this.setState({ needsModalUpdate: false }); } } @@ -151,11 +176,6 @@ export default class ComposerOptionsDropdown extends React.PureComponent { // Rendering. render () { const { - handleClose, - handleKeyDown, - handleToggle, - } = this.handlers; - const { active, disabled, title, @@ -175,14 +195,18 @@ export default class ComposerOptionsDropdown extends React.PureComponent { return ( <div className={computedClass} - onKeyDown={handleKeyDown} + onKeyDown={this.handleKeyDown} > <IconButton active={open || active} className='value' disabled={disabled} icon={icon} - onClick={handleToggle} + inverted + onClick={this.handleToggle} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleButtonKeyDown} + onKeyPress={this.handleKeyPress} size={18} style={{ height: null, @@ -199,8 +223,9 @@ export default class ComposerOptionsDropdown extends React.PureComponent { <DropdownMenu items={items} onChange={onChange} - onClose={handleClose} + onClose={this.handleClose} value={value} + openedViaKeyboard={this.state.openedViaKeyboard} /> </Overlay> </div> @@ -208,22 +233,3 @@ export default class ComposerOptionsDropdown extends React.PureComponent { } } - -// Props. -ComposerOptionsDropdown.propTypes = { - active: PropTypes.bool, - disabled: PropTypes.bool, - icon: PropTypes.string, - items: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.string, - meta: PropTypes.node, - name: PropTypes.string.isRequired, - on: PropTypes.bool, - text: PropTypes.node, - })).isRequired, - onChange: PropTypes.func, - onModalClose: PropTypes.func, - onModalOpen: PropTypes.func, - title: PropTypes.string, - value: PropTypes.string, -}; diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index 19d35a8f4..404504e84 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -14,91 +14,6 @@ import { withPassive } from 'flavours/glitch/util/dom_helpers'; import Motion from 'flavours/glitch/util/optional_motion'; import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -class ComposerOptionsDropdownContentItem extends ImmutablePureComponent { - - static propTypes = { - active: PropTypes.bool, - name: PropTypes.string, - onChange: PropTypes.func, - onClose: PropTypes.func, - options: PropTypes.shape({ - icon: PropTypes.string, - meta: PropTypes.node, - on: PropTypes.bool, - text: PropTypes.node, - }), - }; - - handleActivate = (e) => { - const { - name, - onChange, - onClose, - options: { on }, - } = this.props; - - // If the escape key was pressed, we close the dropdown. - if (e.key === 'Escape' && onClose) { - onClose(); - - // Otherwise, we both close the dropdown and change the value. - } else if (onChange && (!e.key || e.key === 'Enter')) { - e.preventDefault(); // Prevents change in focus on click - if ((on === null || typeof on === 'undefined') && onClose) { - onClose(); - } - onChange(name); - } - } - - // Rendering. - render () { - const { - active, - options: { - icon, - meta, - on, - text, - }, - } = this.props; - const computedClass = classNames('composer--options--dropdown--content--item', { - active, - lengthy: meta, - 'toggled-off': !on && on !== null && typeof on !== 'undefined', - 'toggled-on': on, - 'with-icon': icon, - }); - - let prefix = null; - - if (on !== null && typeof on !== 'undefined') { - prefix = <Toggle checked={on} onChange={this.handleActivate} />; - } else if (icon) { - prefix = <Icon className='icon' fullwidth icon={icon} /> - } - - // The result. - return ( - <div - className={computedClass} - onClick={this.handleActivate} - onKeyDown={this.handleActivate} - role='button' - tabIndex='0' - > - {prefix} - - <div className='content'> - <strong>{text}</strong> - {meta} - </div> - </div> - ); - } - -}; - // The spring to use with our motion. const springMotion = spring(1, { damping: 35, @@ -116,10 +31,11 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent on: PropTypes.bool, text: PropTypes.node, })), - onChange: PropTypes.func, - onClose: PropTypes.func, + onChange: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, style: PropTypes.object, value: PropTypes.string, + openedViaKeyboard: PropTypes.bool, }; static defaultProps = { @@ -128,14 +44,13 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent state = { mounted: false, + value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined, }; // When the document is clicked elsewhere, we close the dropdown. - handleDocumentClick = ({ target }) => { - const { node } = this; - const { onClose } = this.props; - if (onClose && node && !node.contains(target)) { - onClose(); + handleDocumentClick = (e) => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); } } @@ -148,6 +63,11 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, withPassive); + if (this.focusedItem) { + this.focusedItem.focus(); + } else { + this.node.firstChild.focus(); + } this.setState({ mounted: true }); } @@ -157,6 +77,135 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent document.removeEventListener('touchend', this.handleDocumentClick, withPassive); } + handleClick = (name, e) => { + const { + onChange, + onClose, + items, + } = this.props; + + const { on } = this.props.items.find(item => item.name === name); + e.preventDefault(); // Prevents change in focus on click + if ((on === null || typeof on === 'undefined')) { + onClose(); + } + onChange(name); + } + + // Handle changes differently whether the dropdown is a list of options or actions + handleChange = (name) => { + if (this.props.value) { + this.props.onChange(name); + } else { + this.setState({ value: name }); + } + } + + handleKeyDown = (name, e) => { + const { items } = this.props; + const index = items.findIndex(item => { + return (item.name === name); + }); + let element; + + switch(e.key) { + case 'Escape': + this.props.onClose(); + break; + case 'Enter': + case ' ': + this.handleClick(e); + break; + case 'ArrowDown': + element = this.node.childNodes[index + 1]; + if (element) { + element.focus(); + this.handleChange(element.getAttribute('data-index')); + } + break; + case 'ArrowUp': + element = this.node.childNodes[index - 1]; + if (element) { + element.focus(); + this.handleChange(element.getAttribute('data-index')); + } + break; + case 'Tab': + if (e.shiftKey) { + element = this.node.childNodes[index - 1] || this.node.lastChild; + } else { + element = this.node.childNodes[index + 1] || this.node.firstChild; + } + if (element) { + element.focus(); + this.handleChange(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + break; + case 'Home': + element = this.node.firstChild; + if (element) { + element.focus(); + this.handleChange(element.getAttribute('data-index')); + } + break; + case 'End': + element = this.node.lastChild; + if (element) { + element.focus(); + this.handleChange(element.getAttribute('data-index')); + } + break; + } + } + + setFocusRef = c => { + this.focusedItem = c; + } + + renderItem = (item) => { + const { name, icon, meta, on, text } = item; + + const active = (name === (this.props.value || this.state.value)); + + const computedClass = classNames('composer--options--dropdown--content--item', { + active, + lengthy: meta, + 'toggled-off': !on && on !== null && typeof on !== 'undefined', + 'toggled-on': on, + 'with-icon': icon, + }); + + let prefix = null; + + if (on !== null && typeof on !== 'undefined') { + prefix = <Toggle checked={on} onChange={this.handleClick.bind(this, name)} />; + } else if (icon) { + prefix = <Icon className='icon' fixedWidth id={icon} /> + } + + return ( + <div + className={computedClass} + onClick={this.handleClick.bind(this, name)} + onKeyDown={this.handleKeyDown.bind(this, name)} + role='option' + tabIndex='0' + key={name} + data-index={name} + ref={active ? this.setFocusRef : null} + > + {prefix} + + <div className='content'> + <strong>{text}</strong> + {meta} + </div> + </div> + ); + } + // Rendering. render () { const { mounted } = this.state; @@ -165,7 +214,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent onChange, onClose, style, - value, } = this.props; // The result. @@ -189,27 +237,14 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent <div className='composer--options--dropdown--content' ref={this.handleRef} + role='listbox' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, }} > - {items ? items.map( - ({ - name, - ...rest - }) => ( - <ComposerOptionsDropdownContentItem - active={name === value} - key={name} - name={name} - onChange={onChange} - onClose={onClose} - options={rest} - /> - ) - ) : null} + {!!items && items.map(item => this.renderItem(item))} </div> )} </Motion> diff --git a/app/javascript/flavours/glitch/features/compose/components/header.js b/app/javascript/flavours/glitch/features/compose/components/header.js index 2e29084f2..5b456b717 100644 --- a/app/javascript/flavours/glitch/features/compose/components/header.js +++ b/app/javascript/flavours/glitch/features/compose/components/header.js @@ -53,8 +53,18 @@ class Header extends ImmutablePureComponent { showNotificationsBadge: PropTypes.bool, intl: PropTypes.object, onSettingsClick: PropTypes.func, + onLogout: PropTypes.func.isRequired, }; + handleLogoutClick = e => { + e.preventDefault(); + e.stopPropagation(); + + this.props.onLogout(); + + return false; + } + render () { const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props; @@ -72,13 +82,13 @@ class Header extends ImmutablePureComponent { aria-label={intl.formatMessage(messages.start)} title={intl.formatMessage(messages.start)} to='/getting-started' - ><Icon icon='asterisk' /></Link> + ><Icon id='asterisk' /></Link> {renderForColumn('HOME', ( <Link aria-label={intl.formatMessage(messages.home_timeline)} title={intl.formatMessage(messages.home_timeline)} to='/timelines/home' - ><Icon icon='home' /></Link> + ><Icon id='home' /></Link> ))} {renderForColumn('NOTIFICATIONS', ( <Link @@ -87,7 +97,7 @@ class Header extends ImmutablePureComponent { to='/notifications' > <span className='icon-badge-wrapper'> - <Icon icon='bell' /> + <Icon id='bell' /> { showNotificationsBadge && unreadNotifications > 0 && <div className='icon-badge' />} </span> </Link> @@ -97,27 +107,27 @@ class Header extends ImmutablePureComponent { aria-label={intl.formatMessage(messages.community)} title={intl.formatMessage(messages.community)} to='/timelines/public/local' - ><Icon icon='users' /></Link> + ><Icon id='users' /></Link> ))} {renderForColumn('PUBLIC', ( <Link aria-label={intl.formatMessage(messages.public)} title={intl.formatMessage(messages.public)} to='/timelines/public' - ><Icon icon='globe' /></Link> + ><Icon id='globe' /></Link> ))} <a aria-label={intl.formatMessage(messages.settings)} onClick={onSettingsClick} href='#' title={intl.formatMessage(messages.settings)} - ><Icon icon='cogs' /></a> + ><Icon id='cogs' /></a> <a aria-label={intl.formatMessage(messages.logout)} - data-method='delete' + onClick={this.handleLogoutClick} href={ signOutLink } title={intl.formatMessage(messages.logout)} - ><Icon icon='sign-out' /></a> + ><Icon id='sign-out' /></a> </nav> ); }; diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js index 3148434f1..f6bfbdd1e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js +++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js @@ -20,16 +20,18 @@ export default class NavigationBar extends ImmutablePureComponent { <Avatar account={this.props.account} size={48} /> </Permalink> - <Permalink className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> - <strong>@{this.props.account.get('acct')}</strong> - </Permalink> + <div className='navigation-bar__profile'> + <Permalink className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> + <strong>@{this.props.account.get('acct')}</strong> + </Permalink> - { profileLink !== undefined && ( - <a - className='edit' - href={ profileLink } - ><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> - )} + { profileLink !== undefined && ( + <a + className='edit' + href={ profileLink } + ><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> + )} + </div> </div> ); }; diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index 0c94f5514..92348b000 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -7,7 +7,7 @@ import spring from 'react-motion/lib/spring'; // Components. import IconButton from 'flavours/glitch/components/icon_button'; -import TextIconButton from 'flavours/glitch/components/text_icon_button'; +import TextIconButton from './text_icon_button'; import Dropdown from './dropdown'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -232,7 +232,7 @@ class ComposerOptions extends ImmutablePureComponent { const contentTypeItems = { plain: { - icon: 'align-left', + icon: 'file-text', name: 'text/plain', text: <FormattedMessage {...messages.plain} />, }, diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.js b/app/javascript/flavours/glitch/features/compose/components/poll_form.js index 21b5d3d73..3d818ea20 100644 --- a/app/javascript/flavours/glitch/features/compose/components/poll_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/poll_form.js @@ -132,7 +132,7 @@ class PollForm extends ImmutablePureComponent { {options.size < pollLimits.max_options && ( <label className='poll__text editable'> <span className={classNames('poll__input')} style={{ opacity: 0 }} /> - <button className='button button-secondary' onClick={this.handleAddOption}><Icon icon='plus' /> <FormattedMessage {...messages.add_option} /></button> + <button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button> </label> )} </ul> diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js index e283b32b9..b8d9d98bf 100644 --- a/app/javascript/flavours/glitch/features/compose/components/publisher.js +++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js @@ -49,7 +49,6 @@ class Publisher extends ImmutablePureComponent { return ( <div className={computedClass}> - <span className='count'>{diff}</span> {sideArm && sideArm !== 'none' ? ( <Button className='side_arm' @@ -59,7 +58,7 @@ class Publisher extends ImmutablePureComponent { text={ <span> <Icon - icon={{ + id={{ public: 'globe', unlisted: 'unlock', private: 'lock', @@ -81,7 +80,7 @@ class Publisher extends ImmutablePureComponent { return ( <span> <Icon - icon={{ + id={{ direct: 'envelope', private: 'lock', public: 'globe', diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index 1d96933ea..12d839637 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -36,7 +36,7 @@ class SearchPopout extends React.PureComponent { <div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}> <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> {({ opacity, scaleX, scaleY }) => ( - <div className='drawer--search--popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> + <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> <ul> @@ -73,12 +73,17 @@ class Search extends React.PureComponent { onShow: PropTypes.func.isRequired, openInRoute: PropTypes.bool, intl: PropTypes.object.isRequired, + singleColumn: PropTypes.bool, }; state = { expanded: false, }; + setRef = c => { + this.searchForm = c; + } + handleChange = (e) => { const { onChange } = this.props; if (onChange) { @@ -103,10 +108,14 @@ class Search extends React.PureComponent { } handleFocus = () => { - const { onShow } = this.props; this.setState({ expanded: true }); - if (onShow) { - onShow(); + this.props.onShow(); + + if (this.searchForm && !this.props.singleColumn) { + const { left, right } = this.searchForm.getBoundingClientRect(); + if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { + this.searchForm.scrollIntoView(); + } } } @@ -128,14 +137,15 @@ class Search extends React.PureComponent { render () { const { intl, value, submitted } = this.props; const { expanded } = this.state; - const active = value.length > 0 || submitted; - const computedClass = classNames('drawer--search', { active }); + const hasValue = value.length > 0 || submitted; return ( - <div className={computedClass}> + <div className='search'> <label> <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span> <input + ref={this.setRef} + className='search__input' type='text' placeholder={intl.formatMessage(messages.placeholder)} value={value || ''} @@ -145,17 +155,19 @@ class Search extends React.PureComponent { onBlur={this.handleBlur} /> </label> + <div aria-label={intl.formatMessage(messages.placeholder)} - className='icon' + className='search__icon' onClick={this.handleClear} role='button' tabIndex='0' > - <Icon icon='search' /> - <Icon icon='times-circle' /> + <Icon id='search' className={hasValue ? '' : 'active'} /> + <Icon id='times-circle' className={hasValue ? 'active' : ''} /> </div> - <Overlay show={expanded && !active} placement='bottom' target={this}> + + <Overlay show={expanded && !hasValue} placement='bottom' target={this}> <SearchPopout /> </Overlay> </div> diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js index 69df8cdc9..fa3487328 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search_results.js +++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js @@ -7,6 +7,8 @@ import StatusContainer from 'flavours/glitch/containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Hashtag from 'flavours/glitch/components/hashtag'; import Icon from 'flavours/glitch/components/icon'; +import { searchEnabled } from 'flavours/glitch/util/initial_state'; +import LoadMore from 'flavours/glitch/components/load_more'; const messages = defineMessages({ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, @@ -19,23 +21,33 @@ class SearchResults extends ImmutablePureComponent { results: ImmutablePropTypes.map.isRequired, suggestions: ImmutablePropTypes.list.isRequired, fetchSuggestions: PropTypes.func.isRequired, + expandSearch: PropTypes.func.isRequired, dismissSuggestion: PropTypes.func.isRequired, + searchTerm: PropTypes.string, intl: PropTypes.object.isRequired, }; componentDidMount () { - this.props.fetchSuggestions(); + if (this.props.searchTerm === '') { + this.props.fetchSuggestions(); + } } - render() { - const { intl, results, suggestions, dismissSuggestion } = this.props; + handleLoadMoreAccounts = () => this.props.expandSearch('accounts'); + + handleLoadMoreStatuses = () => this.props.expandSearch('statuses'); + + handleLoadMoreHashtags = () => this.props.expandSearch('hashtags'); + + render () { + const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; if (results.isEmpty() && !suggestions.isEmpty()) { return ( <div className='drawer--results'> <div className='trends'> <div className='trends__header'> - <i className='fa fa-user-plus fa-fw' /> + <Icon fixedWidth id='user-plus' /> <FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' /> </div> @@ -51,6 +63,16 @@ class SearchResults extends ImmutablePureComponent { </div> </div> ); + } else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) { + statuses = ( + <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> + </section> + ); } let accounts, statuses, hashtags; @@ -60,9 +82,11 @@ class SearchResults extends ImmutablePureComponent { count += results.get('accounts').size; accounts = ( <section> - <h5><Icon icon='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> + <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> {results.get('accounts').map(accountId => <AccountContainer id={accountId} key={accountId} />)} + + {results.get('accounts').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreAccounts} />} </section> ); } @@ -71,9 +95,11 @@ class SearchResults extends ImmutablePureComponent { count += results.get('statuses').size; statuses = ( <section> - <h5><Icon icon='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> + <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> {results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId}/>)} + + {results.get('statuses').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreStatuses} />} </section> ); } @@ -82,9 +108,11 @@ class SearchResults extends ImmutablePureComponent { count += results.get('hashtags').size; hashtags = ( <section> - <h5><Icon icon='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> + <h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> {results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} + + {results.get('hashtags').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreHashtags} />} </section> ); } @@ -93,7 +121,7 @@ class SearchResults extends ImmutablePureComponent { return ( <div className='drawer--results'> <header className='search-results__header'> - <Icon icon='search' fixedWidth /> + <Icon id='search' fixedWidth /> <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> </header> diff --git a/app/javascript/flavours/glitch/components/text_icon_button.js b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js index 9c8ffab1f..7f2005060 100644 --- a/app/javascript/flavours/glitch/components/text_icon_button.js +++ b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js @@ -1,6 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +const iconStyle = { + height: null, + lineHeight: '27px', + width: `${18 * 1.28571429}px`, +}; + export default class TextIconButton extends React.PureComponent { static propTypes = { @@ -20,7 +26,15 @@ export default class TextIconButton extends React.PureComponent { const { label, title, active, ariaControls } = this.props; return ( - <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}> + <button + title={title} + aria-label={title} + className={`text-icon-button ${active ? 'active' : ''}`} + aria-expanded={active} + onClick={this.handleClick} + aria-controls={ariaControls} + style={iconStyle} + > {label} </button> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js b/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js index ec696f9c3..b875fb15e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js +++ b/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js @@ -47,8 +47,8 @@ class TextareaIcons extends ImmutablePureComponent { title={intl.formatMessage(message)} > <Icon - fullwidth - icon={icon} + fixedWidth + id={icon} /> </span> ) : null diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js index 84edf664e..425b0fe5e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload.js @@ -4,18 +4,12 @@ import PropTypes from 'prop-types'; import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; import { isUserTouching } from 'flavours/glitch/util/is_mobile'; -const messages = defineMessages({ - description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, -}); - -// The component. -export default @injectIntl -class Upload extends ImmutablePureComponent { +export default class Upload extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -23,30 +17,10 @@ class Upload extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, onUndo: PropTypes.func.isRequired, - onDescriptionChange: PropTypes.func.isRequired, onOpenFocalPoint: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - }; - - state = { - hovered: false, - focused: false, - dirtyDescription: null, }; - handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } - } - - handleSubmit = () => { - this.handleInputBlur(); - this.props.onSubmit(this.context.router.history); - } - handleUndoClick = e => { e.stopPropagation(); this.props.onUndo(this.props.media.get('id')); @@ -57,69 +31,21 @@ class Upload extends ImmutablePureComponent { this.props.onOpenFocalPoint(this.props.media.get('id')); } - handleInputChange = e => { - this.setState({ dirtyDescription: e.target.value }); - } - - handleMouseEnter = () => { - this.setState({ hovered: true }); - } - - handleMouseLeave = () => { - this.setState({ hovered: false }); - } - - handleInputFocus = () => { - this.setState({ focused: true }); - } - - handleClick = () => { - this.setState({ focused: true }); - } - - handleInputBlur = () => { - const { dirtyDescription } = this.state; - - this.setState({ focused: false, dirtyDescription: null }); - - if (dirtyDescription !== null) { - this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription); - } - } - render () { const { intl, media } = this.props; - const active = this.state.hovered || this.state.focused || isUserTouching(); - const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''; - const computedClass = classNames('composer--upload_form--item', { active }); const focusX = media.getIn(['meta', 'focus', 'x']); const focusY = media.getIn(['meta', 'focus', 'y']); const x = ((focusX / 2) + .5) * 100; const y = ((focusY / -2) + .5) * 100; return ( - <div className={computedClass} tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'> + <div className='composer--upload_form--item' tabIndex='0' role='button'> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12, }) }}> {({ scale }) => ( <div style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> - <div className={classNames('composer--upload_form--actions', { active })}> - <button className='icon-button' onClick={this.handleUndoClick}><Icon icon='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> - {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>} - </div> - - <div className={classNames('composer--upload_form--description', { active })}> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span> - <textarea - placeholder={intl.formatMessage(messages.description)} - value={description} - maxLength={420} - onFocus={this.handleInputFocus} - onChange={this.handleInputChange} - onBlur={this.handleInputBlur} - onKeyDown={this.handleKeyDown} - /> - </label> + <div className={classNames('composer--upload_form--actions', { active: true })}> + <button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> + <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button> </div> </div> )} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js index 35880ddcc..43039c674 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js @@ -4,6 +4,7 @@ import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; +import { FormattedMessage } from 'react-intl'; export default class UploadForm extends ImmutablePureComponent { static propTypes = { @@ -15,7 +16,7 @@ export default class UploadForm extends ImmutablePureComponent { return ( <div className='composer--upload_form'> - <UploadProgressContainer /> + <UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} /> {mediaIds.size > 0 && ( <div className='content'> diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js index 264c563f2..493bb9ca5 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; -import { FormattedMessage } from 'react-intl'; import Icon from 'flavours/glitch/components/icon'; export default class UploadProgress extends React.PureComponent { @@ -10,10 +9,12 @@ export default class UploadProgress extends React.PureComponent { static propTypes = { active: PropTypes.bool, progress: PropTypes.number, + icon: PropTypes.string.isRequired, + message: PropTypes.node.isRequired, }; render () { - const { active, progress } = this.props; + const { active, progress, icon, message } = this.props; if (!active) { return null; @@ -21,10 +22,10 @@ export default class UploadProgress extends React.PureComponent { return ( <div className='composer--upload_form--progress'> - <Icon icon='upload' /> + <Icon id={icon} /> <div className='message'> - <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> + {message} <div className='backdrop'> <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js index 199d43913..18e2b2f39 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js @@ -25,6 +25,8 @@ const messages = defineMessages({ defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' }, missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm', defaultMessage: 'Send anyway' }, + missingDescriptionEdit: { id: 'confirmations.missing_media_description.edit', + defaultMessage: 'Edit media' }, }); // State mapping. @@ -112,11 +114,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(changeComposeVisibility(value)); }, - onMediaDescriptionConfirm(routerHistory) { + onMediaDescriptionConfirm(routerHistory, mediaId) { dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.missingDescriptionMessage), confirm: intl.formatMessage(messages.missingDescriptionConfirm), onConfirm: () => dispatch(submitCompose(routerHistory)), + secondary: intl.formatMessage(messages.missingDescriptionEdit), + onSecondary: () => dispatch(openModal('FOCAL_POINT', { id: mediaId })), onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)), })); }, diff --git a/app/javascript/flavours/glitch/features/compose/containers/header_container.js b/app/javascript/flavours/glitch/features/compose/containers/header_container.js index ce1dea319..b4dcb4d56 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/header_container.js @@ -1,6 +1,13 @@ import { openModal } from 'flavours/glitch/actions/modal'; import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; import Header from '../components/header'; +import { logOut } from 'flavours/glitch/util/log_out'; + +const messages = defineMessages({ + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, +}); const mapStateToProps = state => { return { @@ -16,6 +23,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ e.stopPropagation(); dispatch(openModal('SETTINGS', {})); }, + onLogout () { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + onConfirm: () => logOut(), + })); + }, }); -export default connect(mapStateToProps, mapDispatchToProps)(Header); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js index c8c7ecd43..c792aa582 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -12,11 +12,12 @@ function mapStateToProps (state) { const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']); const poll = state.getIn(['compose', 'poll']); const media = state.getIn(['compose', 'media_attachments']); + const pending_media = state.getIn(['compose', 'pending_media_attachments']); return { acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','), resetFileKey: state.getIn(['compose', 'resetFileKey']), hasPoll: !!poll, - allowMedia: !poll && (media ? media.size < 4 && !media.some(item => item.get('type') === 'video') : true), + allowMedia: !poll && (media ? media.size + pending_media < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : pending_media < 4), hasMedia: media && !!media.size, allowPoll: !(media && !!media.size), showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']), diff --git a/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js index f9637861a..5c2c1be23 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js @@ -1,14 +1,17 @@ import { connect } from 'react-redux'; import SearchResults from '../components/search_results'; -import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions'; +import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions'; +import { expandSearch } from 'flavours/glitch/actions/search'; const mapStateToProps = state => ({ results: state.getIn(['search', 'results']), suggestions: state.getIn(['suggestions', 'items']), + searchTerm: state.getIn(['search', 'searchTerm']), }); const mapDispatchToProps = dispatch => ({ fetchSuggestions: () => dispatch(fetchSuggestions()), + expandSearch: type => dispatch(expandSearch(type)), dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))), }); diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js index d6bff63ac..f687fae99 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import Upload from '../components/upload'; -import { undoUploadCompose, changeUploadCompose } from 'flavours/glitch/actions/compose'; +import { undoUploadCompose } from 'flavours/glitch/actions/compose'; import { openModal } from 'flavours/glitch/actions/modal'; import { submitCompose } from 'flavours/glitch/actions/compose'; @@ -14,10 +14,6 @@ const mapDispatchToProps = dispatch => ({ dispatch(undoUploadCompose(id)); }, - onDescriptionChange: (id, description) => { - dispatch(changeUploadCompose(id, { description })); - }, - onOpenFocalPoint: id => { dispatch(openModal('FOCAL_POINT', { id })); }, diff --git a/app/javascript/flavours/glitch/features/compose/containers/warning_container.js b/app/javascript/flavours/glitch/features/compose/containers/warning_container.js index fdd21f114..b9b0a2644 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/warning_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/warning_container.js @@ -4,6 +4,7 @@ import Warning from '../components/warning'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { me } from 'flavours/glitch/util/initial_state'; +import { profileLink, termsLink } from 'flavours/glitch/util/backend_links'; const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i; @@ -15,7 +16,7 @@ const mapStateToProps = state => ({ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => { if (needsLockWarning) { - return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />; + return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href={profileLink}><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />; } if (hashtagWarning) { @@ -25,7 +26,7 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning if (directMessageWarning) { const message = ( <span> - <FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This toot will only be sent to all the mentioned users.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a> + <FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This toot will only be sent to all the mentioned users.' /> {!!termsLink && <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>} </span> ); diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.js index 5adb44f2c..ce14e2a9d 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.js @@ -9,8 +9,8 @@ const messages = defineMessages({ settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -@injectIntl -export default class ColumnSettings extends React.PureComponent { +export default @injectIntl +class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js index 9ddeabe75..ba01f8d5c 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js @@ -2,9 +2,30 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import StatusContainer from 'flavours/glitch/containers/status_container'; +import StatusContent from 'flavours/glitch/components/status_content'; +import AttachmentList from 'flavours/glitch/components/attachment_list'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; +import AvatarComposite from 'flavours/glitch/components/avatar_composite'; +import Permalink from 'flavours/glitch/components/permalink'; +import IconButton from 'flavours/glitch/components/icon_button'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import { HotKeys } from 'react-hotkeys'; +import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import classNames from 'classnames'; -export default class Conversation extends ImmutablePureComponent { +const messages = defineMessages({ + more: { id: 'status.more', defaultMessage: 'More' }, + open: { id: 'conversation.open', defaultMessage: 'View conversation' }, + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' }, + delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' }, + muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, + unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, +}); + +export default @injectIntl +class Conversation extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -13,25 +34,99 @@ export default class Conversation extends ImmutablePureComponent { static propTypes = { conversationId: PropTypes.string.isRequired, accounts: ImmutablePropTypes.list.isRequired, - lastStatusId: PropTypes.string, + lastStatus: ImmutablePropTypes.map, unread:PropTypes.bool.isRequired, onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, markRead: PropTypes.func.isRequired, + delete: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + state = { + isExpanded: undefined, }; + parseClick = (e, destination) => { + const { router } = this.context; + const { lastStatus, unread, markRead } = this.props; + if (!router) return; + + if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) { + if (destination === undefined) { + if (unread) { + markRead(); + } + destination = `/statuses/${lastStatus.get('id')}`; + } + let state = {...router.history.location.state}; + state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; + router.history.push(destination, state); + e.preventDefault(); + } + } + + _updateEmojis () { + const node = this.namesNode; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + handleClick = () => { if (!this.context.router) { return; } - const { lastStatusId, unread, markRead } = this.props; + const { lastStatus, unread, markRead } = this.props; if (unread) { markRead(); } - this.context.router.history.push(`/statuses/${lastStatusId}`); + this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); + } + + handleMarkAsRead = () => { + this.props.markRead(); + } + + handleReply = () => { + this.props.reply(this.props.lastStatus, this.context.router.history); + } + + handleDelete = () => { + this.props.delete(); } handleHotkeyMoveUp = () => { @@ -42,22 +137,98 @@ export default class Conversation extends ImmutablePureComponent { this.props.onMoveDown(this.props.conversationId); } + handleConversationMute = () => { + this.props.onMute(this.props.lastStatus); + } + + handleShowMore = () => { + if (this.props.lastStatus.get('spoiler_text')) { + this.setExpansion(!this.state.isExpanded); + } + }; + + setExpansion = value => { + this.setState({ isExpanded: value }); + } + + setNamesRef = (c) => { + this.namesNode = c; + } + render () { - const { accounts, lastStatusId, unread } = this.props; + const { accounts, lastStatus, unread, intl } = this.props; + const { isExpanded } = this.state; - if (lastStatusId === null) { + if (lastStatus === null) { return null; } + const menu = [ + { text: intl.formatMessage(messages.open), action: this.handleClick }, + null, + ]; + + menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute }); + + if (unread) { + menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead }); + menu.push(null); + } + + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); + + const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]); + + const handlers = { + reply: this.handleReply, + open: this.handleClick, + moveUp: this.handleHotkeyMoveUp, + moveDown: this.handleHotkeyMoveDown, + toggleHidden: this.handleShowMore, + }; + + let media = null; + if (lastStatus.get('media_attachments').size > 0) { + media = <AttachmentList compact media={lastStatus.get('media_attachments')} />; + } + return ( - <StatusContainer - id={lastStatusId} - unread={unread} - otherAccounts={accounts} - onMoveUp={this.handleHotkeyMoveUp} - onMoveDown={this.handleHotkeyMoveDown} - onClick={this.handleClick} - /> + <HotKeys handlers={handlers}> + <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'> + <div className='conversation__avatar'> + <AvatarComposite accounts={accounts} size={48} /> + </div> + + <div className='conversation__content'> + <div className='conversation__content__info'> + <div className='conversation__content__relative-time'> + {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> + </div> + + <div className='conversation__content__names' ref={this.setNamesRef}> + <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} /> + </div> + </div> + + <StatusContent + status={lastStatus} + parseClick={this.parseClick} + expanded={isExpanded} + onExpandedToggle={this.handleShowMore} + collapsable + media={media} + /> + + <div className='status__action-bar'> + <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} /> + + <div className='status__action-bar-dropdown'> + <DropdownMenuContainer status={lastStatus} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} /> + </div> + </div> + </div> + </div> + </HotKeys> ); } diff --git a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js index bd6f6bfb0..b15ce9f0f 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/containers/conversation_container.js @@ -1,19 +1,74 @@ import { connect } from 'react-redux'; import Conversation from '../components/conversation'; -import { markConversationRead } from '../../../actions/conversations'; +import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations'; +import { makeGetStatus } from 'flavours/glitch/selectors'; +import { replyCompose } from 'flavours/glitch/actions/compose'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'flavours/glitch/actions/statuses'; +import { defineMessages, injectIntl } from 'react-intl'; -const mapStateToProps = (state, { conversationId }) => { - const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); +const messages = defineMessages({ + 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?' }, +}); + +const mapStateToProps = () => { + const getStatus = makeGetStatus(); + + return (state, { conversationId }) => { + const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); + const lastStatusId = conversation.get('last_status', null); - return { - accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), - unread: conversation.get('unread'), - lastStatusId: conversation.get('last_status', null), + return { + accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), + unread: conversation.get('unread'), + lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }), + }; }; }; -const mapDispatchToProps = (dispatch, { conversationId }) => ({ - markRead: () => dispatch(markConversationRead(conversationId)), +const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({ + + markRead () { + dispatch(markConversationRead(conversationId)); + }, + + reply (status, router) { + dispatch((_, getState) => { + let state = getState(); + + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(status, router)), + })); + } else { + dispatch(replyCompose(status, router)); + } + }); + }, + + delete () { + dispatch(deleteConversation(conversationId)); + }, + + onMute (status) { + if (status.get('muted')) { + dispatch(unmuteStatus(status.get('id'))); + } else { + dispatch(muteStatus(status.get('id'))); + } + }, + + onToggleHidden (status) { + if (status.get('hidden')) { + dispatch(revealStatus(status.get('id'))); + } else { + dispatch(hideStatus(status.get('id'))); + } + }, + }); -export default connect(mapStateToProps, mapDispatchToProps)(Conversation); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation)); diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js index 6fe8a1ce8..7741c6922 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/index.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js @@ -22,9 +22,9 @@ const mapStateToProps = state => ({ conversationsMode: state.getIn(['settings', 'direct', 'conversations']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class DirectTimeline extends React.PureComponent { +class DirectTimeline extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, @@ -135,7 +135,7 @@ export default class DirectTimeline extends React.PureComponent { } return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='envelope' active={hasUnread} diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js new file mode 100644 index 000000000..d1c406933 --- /dev/null +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -0,0 +1,190 @@ +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; +import Permalink from 'flavours/glitch/components/permalink'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import IconButton from 'flavours/glitch/components/icon_button'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state'; +import { shortNumberFormat } from 'flavours/glitch/util/numbers'; +import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'flavours/glitch/actions/accounts'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { initMuteModal } from 'flavours/glitch/actions/mutes'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { id }) => ({ + account: getAccount(state, id), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl }) => ({ + + onFollow (account) { + if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + if (unfollowModal) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + })); + } else { + dispatch(unfollowAccount(account.get('id'))); + } + } else { + dispatch(followAccount(account.get('id'))); + } + }, + + onBlock (account) { + if (account.getIn(['relationship', 'blocking'])) { + dispatch(unblockAccount(account.get('id'))); + } else { + dispatch(blockAccount(account.get('id'))); + } + }, + + onMute (account) { + if (account.getIn(['relationship', 'muting'])) { + dispatch(unmuteAccount(account.get('id'))); + } else { + dispatch(initMuteModal(account)); + } + }, + +}); + +export default @injectIntl +@connect(makeMapStateToProps, mapDispatchToProps) +class AccountCard extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, + }; + + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + + handleFollow = () => { + this.props.onFollow(this.props.account); + } + + handleBlock = () => { + this.props.onBlock(this.props.account); + } + + handleMute = () => { + this.props.onMute(this.props.account); + } + + setRef = (c) => { + this.node = c; + } + + render () { + const { account, intl } = this.props; + + let buttons; + + if (account.get('id') !== me && account.get('relationship', null) !== null) { + const following = account.getIn(['relationship', 'following']); + const requested = account.getIn(['relationship', 'requested']); + const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); + + if (requested) { + buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; + } else if (blocking) { + buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; + } else if (muting) { + buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />; + } else if (!account.get('moved') || following) { + buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; + } + } + + return ( + <div className='directory__card'> + <div className='directory__card__img'> + <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' /> + </div> + + <div className='directory__card__bar'> + <Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> + <Avatar account={account} size={48} /> + <DisplayName account={account} /> + </Permalink> + + <div className='directory__card__bar__relationship account__relationship'> + {buttons} + </div> + </div> + + <div className='directory__card__extra' ref={this.setRef}> + <div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} /> + </div> + + <div className='directory__card__extra'> + <div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div> + <div className='accounts-table__count'>{account.get('followers_count') < 0 ? '-' : shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div> + <div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/directory/index.js b/app/javascript/flavours/glitch/features/directory/index.js new file mode 100644 index 000000000..858a8fa55 --- /dev/null +++ b/app/javascript/flavours/glitch/features/directory/index.js @@ -0,0 +1,171 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from 'flavours/glitch/components/column'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns'; +import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory'; +import { List as ImmutableList } from 'immutable'; +import AccountCard from './components/account_card'; +import RadioButton from 'flavours/glitch/components/radio_button'; +import classNames from 'classnames'; +import LoadMore from 'flavours/glitch/components/load_more'; +import { ScrollContainer } from 'react-router-scroll-4'; + +const messages = defineMessages({ + title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, + recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' }, + newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' }, + local: { id: 'directory.local', defaultMessage: 'From {domain} only' }, + federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' }, +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()), + isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true), + domain: state.getIn(['meta', 'domain']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Directory extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + isLoading: PropTypes.bool, + accountIds: ImmutablePropTypes.list.isRequired, + dispatch: PropTypes.func.isRequired, + shouldUpdateScroll: PropTypes.func, + columnId: PropTypes.string, + intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + domain: PropTypes.string.isRequired, + params: PropTypes.shape({ + order: PropTypes.string, + local: PropTypes.bool, + }), + }; + + state = { + order: null, + local: null, + }; + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state))); + } + } + + getParams = (props, state) => ({ + order: state.order === null ? (props.params.order || 'active') : state.order, + local: state.local === null ? (props.params.local || false) : state.local, + }); + + handleMove = dir => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchDirectory(this.getParams(this.props, this.state))); + } + + componentDidUpdate (prevProps, prevState) { + const { dispatch } = this.props; + const paramsOld = this.getParams(prevProps, prevState); + const paramsNew = this.getParams(this.props, this.state); + + if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) { + dispatch(fetchDirectory(paramsNew)); + } + } + + setRef = c => { + this.column = c; + } + + handleChangeOrder = e => { + const { dispatch, columnId } = this.props; + + if (columnId) { + dispatch(changeColumnParams(columnId, ['order'], e.target.value)); + } else { + this.setState({ order: e.target.value }); + } + } + + handleChangeLocal = e => { + const { dispatch, columnId } = this.props; + + if (columnId) { + dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1')); + } else { + this.setState({ local: e.target.value === '1' }); + } + } + + handleLoadMore = () => { + const { dispatch } = this.props; + dispatch(expandDirectory(this.getParams(this.props, this.state))); + } + + render () { + const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props; + const { order, local } = this.getParams(this.props, this.state); + const pinned = !!columnId; + + const scrollableArea = ( + <div className='scrollable' style={{ background: 'transparent' }}> + <div className='filter-form'> + <div className='filter-form__column' role='group'> + <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} /> + <RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} /> + </div> + + <div className='filter-form__column' role='group'> + <RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} /> + <RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} /> + </div> + </div> + + <div className={classNames('directory__list', { loading: isLoading })}> + {accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)} + </div> + + <LoadMore onClick={this.handleLoadMore} visible={!isLoading} /> + </div> + ); + + return ( + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> + <ColumnHeader + icon='address-book-o' + title={intl.formatMessage(messages.title)} + onPin={this.handlePin} + onMove={this.handleMove} + onClick={this.handleHeaderClick} + pinned={pinned} + multiColumn={multiColumn} + /> + + {multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea} + </Column> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js index 3b29e2a26..cd105a49b 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.js +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js @@ -1,16 +1,16 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; import PropTypes from 'prop-types'; import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import DomainContainer from '../../containers/domain_container'; import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { debounce } from 'lodash'; -import ScrollableList from '../../components/scrollable_list'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' }, @@ -19,17 +19,20 @@ const messages = defineMessages({ const mapStateToProps = state => ({ domains: state.getIn(['domain_lists', 'blocks', 'items']), + hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Blocks extends ImmutablePureComponent { +class Blocks extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, + hasMore: PropTypes.bool, domains: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -41,7 +44,7 @@ export default class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, domains } = this.props; + const { intl, domains, hasMore, multiColumn } = this.props; if (!domains) { return ( @@ -51,10 +54,18 @@ export default class Blocks extends ImmutablePureComponent { ); } + const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />; + return ( - <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore}> + <ScrollableList + scrollKey='domain_blocks' + onLoadMore={this.handleLoadMore} + hasMore={hasMore} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > {domains.map(domain => <DomainContainer key={domain} domain={domain} /> )} diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js index a78117971..6e5518b0c 100644 --- a/app/javascript/flavours/glitch/features/emoji_picker/index.js +++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js @@ -11,7 +11,8 @@ import Overlay from 'react-overlays/lib/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import detectPassiveEvents from 'detect-passive-events'; -import { buildCustomEmojis } from 'flavours/glitch/util/emoji'; +import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji'; +import { useSystemEmojiFont } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -110,19 +111,6 @@ let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; -const categoriesSort = [ - 'recent', - 'custom', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -]; - class ModifierPickerMenu extends React.PureComponent { static propTypes = { @@ -172,12 +160,12 @@ class ModifierPickerMenu extends React.PureComponent { return ( <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> - <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button> - <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> + <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> + <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> + <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> + <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> + <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button> </div> ); } @@ -212,7 +200,7 @@ class ModifierPicker extends React.PureComponent { return ( <div className='emoji-picker-dropdown__modifiers'> - <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> + <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /> <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> </div> ); @@ -320,8 +308,23 @@ class EmojiPickerMenu extends React.PureComponent { } const title = intl.formatMessage(messages.emoji); + const { modifierOpen } = this.state; + const categoriesSort = [ + 'recent', + 'people', + 'nature', + 'foods', + 'activity', + 'places', + 'objects', + 'symbols', + 'flags', + ]; + + categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort()); + return ( <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> <EmojiPicker @@ -342,6 +345,7 @@ class EmojiPickerMenu extends React.PureComponent { backgroundImageFn={backgroundImageFn} autoFocus emojiTooltip + native={useSystemEmojiFont} /> <ModifierPicker @@ -357,9 +361,9 @@ class EmojiPickerMenu extends React.PureComponent { } -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class EmojiPickerDropdown extends React.PureComponent { +class EmojiPickerDropdown extends React.PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js index 32bf4e71a..c6470ba74 100644 --- a/app/javascript/flavours/glitch/features/favourited_statuses/index.js +++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js @@ -2,14 +2,14 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glitch/actions/favourites'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import StatusList from 'flavours/glitch/components/status_list'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { debounce } from 'lodash'; const messages = defineMessages({ heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, @@ -21,9 +21,9 @@ const mapStateToProps = state => ({ hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Favourites extends ImmutablePureComponent { +class Favourites extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, @@ -70,8 +70,10 @@ export default class Favourites extends ImmutablePureComponent { const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const pinned = !!columnId; + const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />; + return ( - <Column ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}> <ColumnHeader icon='star' title={intl.formatMessage(messages.heading)} @@ -90,6 +92,8 @@ export default class Favourites extends ImmutablePureComponent { hasMore={hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js index eb86636c3..953bf171f 100644 --- a/app/javascript/flavours/glitch/features/favourites/index.js +++ b/app/javascript/flavours/glitch/features/favourites/index.js @@ -4,34 +4,39 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { fetchFavourites } from 'flavours/glitch/actions/interactions'; -import { ScrollContainer } from 'react-router-scroll-4'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; +import Icon from 'flavours/glitch/components/icon'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import ScrollableList from '../../components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.favourited_by', defaultMessage: 'Favourited by' }, + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, }); const mapStateToProps = (state, props) => ({ accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Favourites extends ImmutablePureComponent { +class Favourites extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, intl: PropTypes.object.isRequired, }; componentWillMount () { - this.props.dispatch(fetchFavourites(this.props.params.statusId)); + if (!this.props.accountIds) { + this.props.dispatch(fetchFavourites(this.props.params.statusId)); + } } componentWillReceiveProps (nextProps) { @@ -40,11 +45,6 @@ export default class Favourites extends ImmutablePureComponent { } } - shouldUpdateScroll = (prevRouterProps, { location }) => { - if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false; - return !(location.state && location.state.mastodonModalOpen); - } - handleHeaderClick = () => { this.column.scrollTop(); } @@ -53,8 +53,12 @@ export default class Favourites extends ImmutablePureComponent { this.column = c; } + handleRefresh = () => { + this.props.dispatch(fetchFavourites(this.props.params.statusId)); + } + render () { - const { intl, accountIds } = this.props; + const { intl, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -64,6 +68,8 @@ export default class Favourites extends ImmutablePureComponent { ); } + const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favourited this toot yet. When someone does, they will show up here.' />; + return ( <Column ref={this.setRef}> <ColumnHeader @@ -71,13 +77,20 @@ export default class Favourites extends ImmutablePureComponent { title={intl.formatMessage(messages.heading)} onClick={this.handleHeaderClick} showBackButton + multiColumn={multiColumn} + extraButton={( + <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button> + )} /> - - <ScrollContainer scrollKey='favourites' shouldUpdateScroll={this.shouldUpdateScroll}> - <div className='scrollable'> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - </div> - </ScrollContainer> + <ScrollableList + scrollKey='favourites' + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > + {accountIds.map(id => + <AccountContainer key={id} id={id} withNote={false} /> + )} + </ScrollableList> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js index dead0753f..bf145cb67 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js +++ b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js @@ -13,8 +13,8 @@ const messages = defineMessages({ reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, }); -@injectIntl -export default class AccountAuthorize extends ImmutablePureComponent { +export default @injectIntl +class AccountAuthorize extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js index d0845769e..36770aace 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.js +++ b/app/javascript/flavours/glitch/features/follow_requests/index.js @@ -2,14 +2,15 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; import AccountAuthorizeContainer from './containers/account_authorize_container'; import { fetchFollowRequests, expandFollowRequests } from 'flavours/glitch/actions/accounts'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, @@ -17,38 +18,32 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), + hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class FollowRequests extends ImmutablePureComponent { +class FollowRequests extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, + hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { this.props.dispatch(fetchFollowRequests()); } - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandFollowRequests()); - } - } - - shouldUpdateScroll = (prevRouterProps, { location }) => { - if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false; - return !(location.state && location.state.mastodonModalOpen); - } + handleLoadMore = debounce(() => { + this.props.dispatch(expandFollowRequests()); + }, 300, { leading: true }); render () { - const { intl, accountIds } = this.props; + const { intl, accountIds, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -58,17 +53,23 @@ export default class FollowRequests extends ImmutablePureComponent { ); } + const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />; + return ( - <Column name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='follow_requests' shouldUpdateScroll={this.shouldUpdateScroll}> - <div className='scrollable' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountAuthorizeContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> + <ScrollableList + scrollKey='follow_requests' + onLoadMore={this.handleLoadMore} + hasMore={hasMore} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > + {accountIds.map(id => + <AccountAuthorizeContainer key={id} id={id} /> + )} + </ScrollableList> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js index 2e47ab9b9..c78dcc8e4 100644 --- a/app/javascript/flavours/glitch/features/followers/index.js +++ b/app/javascript/flavours/glitch/features/followers/index.js @@ -2,20 +2,21 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { fetchAccount, fetchFollowers, expandFollowers, } from 'flavours/glitch/actions/accounts'; -import { ScrollContainer } from 'react-router-scroll-4'; +import { FormattedMessage } from 'react-intl'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; -import LoadMore from 'flavours/glitch/components/load_more'; import ImmutablePureComponent from 'react-immutable-pure-component'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; const mapStateToProps = (state, props) => ({ isAccount: !!state.getIn(['accounts', props.params.accountId]), @@ -23,8 +24,8 @@ const mapStateToProps = (state, props) => ({ hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), }); -@connect(mapStateToProps) -export default class Followers extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class Followers extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, @@ -32,11 +33,14 @@ export default class Followers extends ImmutablePureComponent { accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowers(this.props.params.accountId)); + if (!this.props.accountIds) { + this.props.dispatch(fetchAccount(this.props.params.accountId)); + this.props.dispatch(fetchFollowers(this.props.params.accountId)); + } } componentWillReceiveProps (nextProps) { @@ -58,22 +62,16 @@ export default class Followers extends ImmutablePureComponent { } } - handleLoadMore = (e) => { - e.preventDefault(); + handleLoadMore = debounce(() => { this.props.dispatch(expandFollowers(this.props.params.accountId)); - } - - shouldUpdateScroll = (prevRouterProps, { location }) => { - if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false; - return !(location.state && location.state.mastodonModalOpen); - } + }, 300, { leading: true }); setRef = c => { this.column = c; } render () { - const { accountIds, hasMore, isAccount } = this.props; + const { accountIds, hasMore, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -83,8 +81,6 @@ export default class Followers extends ImmutablePureComponent { ); } - let loadMore = null; - if (!accountIds) { return ( <Column> @@ -93,23 +89,25 @@ export default class Followers extends ImmutablePureComponent { ); } - if (hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } + const emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />; return ( <Column ref={this.setRef}> - <ProfileColumnHeader onClick={this.handleHeaderClick} /> - - <ScrollContainer scrollKey='followers' shouldUpdateScroll={this.shouldUpdateScroll}> - <div className='scrollable' onScroll={this.handleScroll}> - <div className='followers'> - <HeaderContainer accountId={this.props.params.accountId} hideTabs /> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - {loadMore} - </div> - </div> - </ScrollContainer> + <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> + + <ScrollableList + scrollKey='followers' + hasMore={hasMore} + onLoadMore={this.handleLoadMore} + prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} + alwaysPrepend + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > + {accountIds.map(id => + <AccountContainer key={id} id={id} withNote={false} /> + )} + </ScrollableList> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js index ad1445f3a..df7c19c22 100644 --- a/app/javascript/flavours/glitch/features/following/index.js +++ b/app/javascript/flavours/glitch/features/following/index.js @@ -2,20 +2,21 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { fetchAccount, fetchFollowing, expandFollowing, } from 'flavours/glitch/actions/accounts'; -import { ScrollContainer } from 'react-router-scroll-4'; +import { FormattedMessage } from 'react-intl'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; -import LoadMore from 'flavours/glitch/components/load_more'; import ImmutablePureComponent from 'react-immutable-pure-component'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; const mapStateToProps = (state, props) => ({ isAccount: !!state.getIn(['accounts', props.params.accountId]), @@ -23,8 +24,8 @@ const mapStateToProps = (state, props) => ({ hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), }); -@connect(mapStateToProps) -export default class Following extends ImmutablePureComponent { +export default @connect(mapStateToProps) +class Following extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, @@ -32,11 +33,14 @@ export default class Following extends ImmutablePureComponent { accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowing(this.props.params.accountId)); + if (!this.props.accountIds) { + this.props.dispatch(fetchAccount(this.props.params.accountId)); + this.props.dispatch(fetchFollowing(this.props.params.accountId)); + } } componentWillReceiveProps (nextProps) { @@ -58,22 +62,16 @@ export default class Following extends ImmutablePureComponent { } } - handleLoadMore = (e) => { - e.preventDefault(); + handleLoadMore = debounce(() => { this.props.dispatch(expandFollowing(this.props.params.accountId)); - } - - shouldUpdateScroll = (prevRouterProps, { location }) => { - if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false; - return !(location.state && location.state.mastodonModalOpen); - } + }, 300, { leading: true }); setRef = c => { this.column = c; } render () { - const { accountIds, hasMore, isAccount } = this.props; + const { accountIds, hasMore, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -83,8 +81,6 @@ export default class Following extends ImmutablePureComponent { ); } - let loadMore = null; - if (!accountIds) { return ( <Column> @@ -93,23 +89,25 @@ export default class Following extends ImmutablePureComponent { ); } - if (hasMore) { - loadMore = <LoadMore onClick={this.handleLoadMore} />; - } + const emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />; return ( <Column ref={this.setRef}> - <ProfileColumnHeader onClick={this.handleHeaderClick} /> - - <ScrollContainer scrollKey='following' shouldUpdateScroll={this.shouldUpdateScroll}> - <div className='scrollable' onScroll={this.handleScroll}> - <div className='following'> - <HeaderContainer accountId={this.props.params.accountId} hideTabs /> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - {loadMore} - </div> - </div> - </ScrollContainer> + <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> + + <ScrollableList + scrollKey='following' + hasMore={hasMore} + onLoadMore={this.handleLoadMore} + prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} + alwaysPrepend + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > + {accountIds.map(id => + <AccountContainer key={id} id={id} withNote={false} /> + )} + </ScrollableList> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/generic_not_found/index.js b/app/javascript/flavours/glitch/features/generic_not_found/index.js index d01a1ba47..4412adaed 100644 --- a/app/javascript/flavours/glitch/features/generic_not_found/index.js +++ b/app/javascript/flavours/glitch/features/generic_not_found/index.js @@ -4,7 +4,7 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator'; const GenericNotFound = () => ( <Column> - <MissingIndicator /> + <MissingIndicator fullPage /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/getting_started/components/trends.js b/app/javascript/flavours/glitch/features/getting_started/components/trends.js new file mode 100644 index 000000000..0734ec72b --- /dev/null +++ b/app/javascript/flavours/glitch/features/getting_started/components/trends.js @@ -0,0 +1,46 @@ +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Hashtag from 'flavours/glitch/components/hashtag'; +import { FormattedMessage } from 'react-intl'; + +export default class Trends extends ImmutablePureComponent { + + static defaultProps = { + loading: false, + }; + + static propTypes = { + trends: ImmutablePropTypes.list, + fetchTrends: PropTypes.func.isRequired, + }; + + componentDidMount () { + this.props.fetchTrends(); + this.refreshInterval = setInterval(() => this.props.fetchTrends(), 900 * 1000); + } + + componentWillUnmount () { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + } + } + + render () { + const { trends } = this.props; + + if (!trends || trends.isEmpty()) { + return null; + } + + return ( + <div className='getting-started__trends'> + <h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4> + + {trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js b/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js new file mode 100644 index 000000000..1df3fb4fe --- /dev/null +++ b/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { fetchTrends } from '../../../actions/trends'; +import Trends from '../components/trends'; + +const mapStateToProps = state => ({ + trends: state.getIn(['trends', 'items']), +}); + +const mapDispatchToProps = dispatch => ({ + fetchTrends: () => dispatch(fetchTrends()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Trends); diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js index f669220e3..d8a51c689 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.js +++ b/app/javascript/flavours/glitch/features/getting_started/index.js @@ -8,13 +8,15 @@ import { openModal } from 'flavours/glitch/actions/modal'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me, profile_directory, showTrends } from 'flavours/glitch/util/initial_state'; import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; import { List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { fetchLists } from 'flavours/glitch/actions/lists'; -import { preferencesLink, signOutLink } from 'flavours/glitch/util/backend_links'; +import { preferencesLink } from 'flavours/glitch/util/backend_links'; +import NavigationBar from '../compose/components/navigation_bar'; import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; +import TrendsContainer from './containers/trends_container'; const messages = defineMessages({ heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -29,13 +31,13 @@ const messages = defineMessages({ preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' }, misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' }, menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, + profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' }, }); const makeMapStateToProps = () => { @@ -102,16 +104,14 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); } componentDidMount () { - const { myAccount, fetchFollowRequests, multiColumn } = this.props; + const { fetchFollowRequests, multiColumn } = this.props; if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) { this.context.router.history.replace('/timelines/home'); return; } - if (myAccount.get('locked')) { - fetchFollowRequests(); - } + fetchFollowRequests(); } render () { @@ -146,37 +146,43 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); navItems.push(<ColumnLink key='5' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />); } - if (myAccount.get('locked')) { + if (myAccount.get('locked') || unreadFollowRequests > 0) { navItems.push(<ColumnLink key='6' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />); } - navItems.push(<ColumnLink key='7' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />); + if (profile_directory) { + navItems.push(<ColumnLink key='7' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />); + } + + navItems.push(<ColumnLink key='8' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />); listItems = listItems.concat([ - <div key='8'> - <ColumnLink key='9' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' /> + <div key='9'> + <ColumnLink key='10' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' /> {lists.map(list => - <ColumnLink key={(9 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> + <ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> )} </div>, ]); return ( - <Column name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile> + <Column bindToDocument={!multiColumn} name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile> <div className='scrollable optionally-scrollable'> <div className='getting-started__wrapper'> - <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} /> + {!multiColumn && <NavigationBar account={myAccount} />} + {multiColumn && <ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />} {navItems} <ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} /> {listItems} <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> { preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> } <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} /> - <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href={signOutLink} method='delete' /> </div> <LinkFooter /> </div> + + {multiColumn && showTrends && <TrendsContainer />} </Column> ); } diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.js b/app/javascript/flavours/glitch/features/getting_started_misc/index.js index ee4452472..570fe78bf 100644 --- a/app/javascript/flavours/glitch/features/getting_started_misc/index.js +++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.js @@ -24,9 +24,9 @@ const messages = defineMessages({ featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' }, }); -@connect() +export default @connect() @injectIntl -export default class gettingStartedMisc extends ImmutablePureComponent { +class gettingStartedMisc extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js index dc0ffee85..9c39b158a 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js @@ -3,15 +3,15 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Toggle from 'react-toggle'; -import AsyncSelect from 'react-select/lib/Async'; +import AsyncSelect from 'react-select/async'; const messages = defineMessages({ placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' }, noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' }, }); -@injectIntl -export default class ColumnSettings extends React.PureComponent { +export default @injectIntl +class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js index 757cd48fb..de1db692d 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js @@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { columnId }) => ({ }, onLoad (value) { - return api().get('/api/v2/search', { params: { q: value } }).then(response => { + return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => { return (response.data.hashtags || []).map((tag) => { return { value: tag.name, label: `#${tag.name}` }; }); diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index 21efaceea..16dd80c4f 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -15,8 +15,8 @@ const mapStateToProps = (state, props) => ({ hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0, }); -@connect(mapStateToProps) -export default class HashtagTimeline extends React.PureComponent { +export default @connect(mapStateToProps) +class HashtagTimeline extends React.PureComponent { disconnects = []; @@ -145,6 +145,7 @@ export default class HashtagTimeline extends React.PureComponent { pinned={pinned} multiColumn={multiColumn} showBackButton + bindToDocument={!multiColumn} > {columnId && <ColumnSettingsContainer columnId={columnId} />} </ColumnHeader> @@ -155,6 +156,7 @@ export default class HashtagTimeline extends React.PureComponent { timelineId={`hashtag:${id}`} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js index d7692513e..df615db65 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js @@ -10,8 +10,8 @@ const messages = defineMessages({ settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -@injectIntl -export default class ColumnSettings extends React.PureComponent { +export default @injectIntl +class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 8eb79fa60..9b71a4404 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -19,9 +19,9 @@ const mapStateToProps = state => ({ isPartial: state.getIn(['timelines', 'home', 'isPartial']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class HomeTimeline extends React.PureComponent { +class HomeTimeline extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, @@ -97,7 +97,7 @@ export default class HomeTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}> <ColumnHeader icon='home' active={hasUnread} @@ -117,6 +117,7 @@ export default class HomeTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} timelineId='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js index f7b475f8d..bc7571200 100644 --- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js +++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js @@ -14,9 +14,9 @@ const mapStateToProps = state => ({ collapseEnabled: state.getIn(['local_settings', 'collapsed', 'enabled']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class KeyboardShortcuts extends ImmutablePureComponent { +class KeyboardShortcuts extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, @@ -25,10 +25,10 @@ export default class KeyboardShortcuts extends ImmutablePureComponent { }; render () { - const { intl, collapseEnabled } = this.props; + const { intl, collapseEnabled, multiColumn } = this.props; return ( - <Column icon='question' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='question' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <div className='keyboard-shortcuts scrollable optionally-scrollable'> <table> @@ -68,6 +68,10 @@ export default class KeyboardShortcuts extends ImmutablePureComponent { <td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td> </tr> <tr> + <td><kbd>e</kbd></td> + <td><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></td> + </tr> + <tr> <td><kbd>x</kbd></td> <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td> </tr> diff --git a/app/javascript/flavours/glitch/features/list_adder/components/list.js b/app/javascript/flavours/glitch/features/list_adder/components/list.js index cb8eb7d7a..4666ca47b 100644 --- a/app/javascript/flavours/glitch/features/list_adder/components/list.js +++ b/app/javascript/flavours/glitch/features/list_adder/components/list.js @@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from '../../../components/icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import { removeFromListAdder, addToListAdder } from '../../../actions/lists'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, @@ -53,7 +54,7 @@ class List extends ImmutablePureComponent { <div className='list'> <div className='list__wrapper'> <div className='list__display-name'> - <i className='fa fa-fw fa-list-ul column-link__icon' /> + <Icon id='list-ul' className='column-link__icon' fixedWidth /> {list.get('title')} </div> diff --git a/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js b/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js index 24aaf82ac..a8cab2762 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js +++ b/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js @@ -11,7 +11,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ value: state.getIn(['listEditor', 'title']), - disabled: !state.getIn(['listEditor', 'isChanged']), + disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']), }); const mapDispatchToProps = dispatch => ({ @@ -19,9 +19,9 @@ const mapDispatchToProps = dispatch => ({ onSubmit: () => dispatch(submitListEditor(false)), }); -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class ListForm extends React.PureComponent { +class ListForm extends React.PureComponent { static propTypes = { value: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/list_editor/components/search.js b/app/javascript/flavours/glitch/features/list_editor/components/search.js index 280632652..192643f77 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/search.js +++ b/app/javascript/flavours/glitch/features/list_editor/components/search.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ search: { id: 'lists.search', defaultMessage: 'Search among people you follow' }, @@ -51,8 +52,8 @@ export default class Search extends React.PureComponent { </label> <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> - <i className={classNames('fa fa-search', { active: !hasValue })} /> - <i aria-label={intl.formatMessage(messages.search)} className={classNames('fa fa-times-circle', { active: hasValue })} /> + <Icon id='search' className={classNames({ active: !hasValue })} /> + <Icon id='times-circle' aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} /> </div> </div> ); diff --git a/app/javascript/flavours/glitch/features/list_editor/index.js b/app/javascript/flavours/glitch/features/list_editor/index.js index 5f552b113..75b0de3d3 100644 --- a/app/javascript/flavours/glitch/features/list_editor/index.js +++ b/app/javascript/flavours/glitch/features/list_editor/index.js @@ -22,9 +22,9 @@ const mapDispatchToProps = dispatch => ({ onReset: () => dispatch(resetListEditor()), }); -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class ListEditor extends ImmutablePureComponent { +class ListEditor extends ImmutablePureComponent { static propTypes = { listId: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js index 0405073c5..908a65597 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.js +++ b/app/javascript/flavours/glitch/features/list_timeline/index.js @@ -13,6 +13,7 @@ import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists import { openModal } from 'flavours/glitch/actions/modal'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' }, @@ -27,9 +28,9 @@ const mapStateToProps = (state, props) => ({ hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0, }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class ListTimeline extends React.PureComponent { +class ListTimeline extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -173,14 +174,15 @@ export default class ListTimeline extends React.PureComponent { onClick={this.handleHeaderClick} pinned={pinned} multiColumn={multiColumn} + bindToDocument={!multiColumn} > <div className='column-header__links'> <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}> - <i className='fa fa-pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' /> + <Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' /> </button> <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}> - <i className='fa fa-trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' /> + <Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' /> </button> </div> @@ -211,6 +213,7 @@ export default class ListTimeline extends React.PureComponent { timelineId={`list:${id}`} onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/lists/components/new_list_form.js b/app/javascript/flavours/glitch/features/lists/components/new_list_form.js index 61fcbeaf9..cc78d30b7 100644 --- a/app/javascript/flavours/glitch/features/lists/components/new_list_form.js +++ b/app/javascript/flavours/glitch/features/lists/components/new_list_form.js @@ -20,9 +20,9 @@ const mapDispatchToProps = dispatch => ({ onSubmit: () => dispatch(submitListEditor(true)), }); -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class NewListForm extends React.PureComponent { +class NewListForm extends React.PureComponent { static propTypes = { value: PropTypes.string.isRequired, @@ -66,7 +66,7 @@ export default class NewListForm extends React.PureComponent { </label> <IconButton - disabled={disabled} + disabled={disabled || !value} icon='plus' title={title} onClick={this.handleClick} diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js index 8b0470c92..adde3dd5c 100644 --- a/app/javascript/flavours/glitch/features/lists/index.js +++ b/app/javascript/flavours/glitch/features/lists/index.js @@ -6,12 +6,13 @@ import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; import { fetchLists } from 'flavours/glitch/actions/lists'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading'; import NewListForm from './components/new_list_form'; import { createSelector } from 'reselect'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.lists', defaultMessage: 'Lists' }, @@ -30,15 +31,16 @@ const mapStateToProps = state => ({ lists: getOrderedLists(state), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Lists extends ImmutablePureComponent { +class Lists extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, lists: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -46,7 +48,7 @@ export default class Lists extends ImmutablePureComponent { } render () { - const { intl, lists } = this.props; + const { intl, lists, multiColumn } = this.props; if (!lists) { return ( @@ -56,19 +58,24 @@ export default class Lists extends ImmutablePureComponent { ); } + const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />; + return ( - <Column icon='bars' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='bars' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <NewListForm /> - <div className='scrollable'> - <ColumnSubheading text={intl.formatMessage(messages.subheading)} /> - + <ColumnSubheading text={intl.formatMessage(messages.subheading)} /> + <ScrollableList + scrollKey='lists' + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > {lists.map(list => <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> )} - </div> + </ScrollableList> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js index c583c4863..ab3a554bf 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js @@ -13,14 +13,15 @@ const messages = defineMessages({ general: { id: 'settings.general', defaultMessage: 'General' }, compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box' }, content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' }, + filters: { id: 'settings.filters', defaultMessage: 'Filters' }, collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' }, media: { id: 'settings.media', defaultMessage: 'Media' }, preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' }, close: { id: 'settings.close', defaultMessage: 'Close' }, }); -@injectIntl -export default class LocalSettingsNavigation extends React.PureComponent { +export default @injectIntl +class LocalSettingsNavigation extends React.PureComponent { static propTypes = { index : PropTypes.number, @@ -60,27 +61,34 @@ export default class LocalSettingsNavigation extends React.PureComponent { active={index === 3} index={3} onNavigate={onNavigate} - icon='angle-double-up' - title={intl.formatMessage(messages.collapsed)} + icon='filter' + title={intl.formatMessage(messages.filters)} /> <LocalSettingsNavigationItem active={index === 4} index={4} onNavigate={onNavigate} + icon='angle-double-up' + title={intl.formatMessage(messages.collapsed)} + /> + <LocalSettingsNavigationItem + active={index === 5} + index={5} + onNavigate={onNavigate} icon='image' title={intl.formatMessage(messages.media)} /> <LocalSettingsNavigationItem - active={index === 5} + active={index === 6} href={ preferencesLink } - index={5} - icon='sliders' + index={6} + icon='cog' title={intl.formatMessage(messages.preferences)} /> <LocalSettingsNavigationItem - active={index === 6} + active={index === 7} className='close' - index={6} + index={7} onNavigate={onClose} icon='times' title={intl.formatMessage(messages.close)} diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js index 68a998b6c..4dec7d154 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js @@ -3,6 +3,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * export default class LocalSettingsPage extends React.PureComponent { @@ -42,7 +44,7 @@ export default class LocalSettingsPage extends React.PureComponent { active, }, className); - const iconElem = icon ? <i className={`fa fa-fw fa-${icon}`} /> : (textIcon ? <span className='text-icon-button'>{textIcon}</span> : null); + const iconElem = icon ? <Icon fixedWidth id={icon} /> : (textIcon ? <span className='text-icon-button'>{textIcon}</span> : null); if (href) return ( <a diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index 23499455b..e08c12c76 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -21,10 +21,17 @@ const messages = defineMessages({ side_arm_copy: { id: 'settings.side_arm_reply_mode.copy', defaultMessage: 'Copy privacy setting of the toot being replied to' }, side_arm_restrict: { id: 'settings.side_arm_reply_mode.restrict', defaultMessage: 'Restrict privacy setting to that of the toot being replied to' }, regexp: { id: 'settings.content_warnings.regexp', defaultMessage: 'Regular expression' }, + filters_drop: { id: 'settings.filtering_behavior.drop', defaultMessage: 'Hide filtered toots completely' }, + filters_upstream: { id: 'settings.filtering_behavior.upstream', defaultMessage: 'Show "filtered" like vanilla Mastodon' }, + filters_hide: { id: 'settings.filtering_behavior.hide', defaultMessage: 'Show "filtered" and add a button to display why' }, + filters_cw: { id: 'settings.filtering_behavior.cw', defaultMessage: 'Still display the post, and add filtered words to content warning' }, + rewrite_mentions_no: { id: 'settings.rewrite_mentions_no', defaultMessage: 'Do not rewrite mentions' }, + rewrite_mentions_acct: { id: 'settings.rewrite_mentions_acct', defaultMessage: 'Rewrite with username and domain (when the account is remote)' }, + rewrite_mentions_username: { id: 'settings.rewrite_mentions_username', defaultMessage: 'Rewrite with username' }, }); -@injectIntl -export default class LocalSettingsPage extends React.PureComponent { +export default @injectIntl +class LocalSettingsPage extends React.PureComponent { static propTypes = { index : PropTypes.number, @@ -62,6 +69,28 @@ export default class LocalSettingsPage extends React.PureComponent { > <FormattedMessage id='settings.confirm_boost_missing_media_description' defaultMessage='Show confirmation dialog before boosting toots lacking media descriptions' /> </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['tag_misleading_links']} + id='mastodon-settings--tag_misleading_links' + onChange={onChange} + > + <FormattedMessage id='settings.tag_misleading_links' defaultMessage='Tag misleading links' /> + <span className='hint'><FormattedMessage id='settings.tag_misleading_links.hint' defaultMessage="Add a visual indication with the link target host to every link not mentioning it explicitly" /></span> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['rewrite_mentions']} + id='mastodon-settings--rewrite_mentions' + options={[ + { value: 'no', message: intl.formatMessage(messages.rewrite_mentions_no) }, + { value: 'acct', message: intl.formatMessage(messages.rewrite_mentions_acct) }, + { value: 'username', message: intl.formatMessage(messages.rewrite_mentions_username) }, + ]} + onChange={onChange} + > + <FormattedMessage id='settings.rewrite_mentions' defaultMessage='Rewrite mentions in displayed statuses' /> + </LocalSettingsPageItem> <section> <h2><FormattedMessage id='settings.notifications_opts' defaultMessage='Notifications options' /></h2> <LocalSettingsPageItem @@ -223,6 +252,25 @@ export default class LocalSettingsPage extends React.PureComponent { </LocalSettingsPageItem> </div> ), + ({ intl, onChange, settings }) => ( + <div className='glitch local-settings__page filters'> + <h1><FormattedMessage id='settings.filters' defaultMessage='Filters' /></h1> + <LocalSettingsPageItem + settings={settings} + item={['filtering_behavior']} + id='mastodon-settings--filters-behavior' + onChange={onChange} + options={[ + { value: 'drop', message: intl.formatMessage(messages.filters_drop) }, + { value: 'upstream', message: intl.formatMessage(messages.filters_upstream) }, + { value: 'hide', message: intl.formatMessage(messages.filters_hide) }, + { value: 'content_warning', message: intl.formatMessage(messages.filters_cw) } + ]} + > + <FormattedMessage id='settings.filtering_behavior' defaultMessage='Filtering behavior' /> + </LocalSettingsPageItem> + </div> + ), ({ onChange, settings }) => ( <div className='glitch local-settings__page collapsed'> <h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1> diff --git a/app/javascript/flavours/glitch/features/local_settings/page/item/index.js b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js index 66b937365..5a68523f6 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/item/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js @@ -8,7 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; export default class LocalSettingsPageItem extends React.PureComponent { static propTypes = { - children: PropTypes.element.isRequired, + children: PropTypes.node.isRequired, dependsOn: PropTypes.array, dependsOnNot: PropTypes.array, id: PropTypes.string.isRequired, @@ -63,12 +63,12 @@ export default class LocalSettingsPageItem extends React.PureComponent { disabled={!enabled} /> {opt.message} - {opt.hint && <span class='hint'>{opt.hint}</span>} + {opt.hint && <span className='hint'>{opt.hint}</span>} </label> ); }); return ( - <div class='glitch local-settings__page__item radio_buttons'> + <div className='glitch local-settings__page__item radio_buttons'> <fieldset> <legend>{children}</legend> {optionElems} diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js index bbcbea701..c27a530d5 100644 --- a/app/javascript/flavours/glitch/features/mutes/index.js +++ b/app/javascript/flavours/glitch/features/mutes/index.js @@ -1,15 +1,16 @@ import React from 'react'; import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import { ScrollContainer } from 'react-router-scroll-4'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; import AccountContainer from 'flavours/glitch/containers/account_container'; import { fetchMutes, expandMutes } from 'flavours/glitch/actions/mutes'; -import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.mutes', defaultMessage: 'Muted users' }, @@ -17,38 +18,32 @@ const messages = defineMessages({ const mapStateToProps = state => ({ accountIds: state.getIn(['user_lists', 'mutes', 'items']), + hasMore: !!state.getIn(['user_lists', 'mutes', 'next']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Mutes extends ImmutablePureComponent { +class Mutes extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, + hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { this.props.dispatch(fetchMutes()); } - handleScroll = (e) => { - const { scrollTop, scrollHeight, clientHeight } = e.target; - - if (scrollTop === scrollHeight - clientHeight) { - this.props.dispatch(expandMutes()); - } - } - - shouldUpdateScroll = (prevRouterProps, { location }) => { - if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false; - return !(location.state && location.state.mastodonModalOpen); - } + handleLoadMore = debounce(() => { + this.props.dispatch(expandMutes()); + }, 300, { leading: true }); render () { - const { intl, accountIds } = this.props; + const { intl, accountIds, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -58,16 +53,22 @@ export default class Mutes extends ImmutablePureComponent { ); } + const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />; + return ( - <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='mutes' shouldUpdateScroll={this.shouldUpdateScroll}> - <div className='scrollable mutes' onScroll={this.handleScroll}> - {accountIds.map(id => - <AccountContainer key={id} id={id} /> - )} - </div> - </ScrollContainer> + <ScrollableList + scrollKey='mutes' + onLoadMore={this.handleLoadMore} + hasMore={hasMore} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > + {accountIds.map(id => + <AccountContainer key={id} id={id} /> + )} + </ScrollableList> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js b/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js index 22a10753f..ee77cfb8e 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js +++ b/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; export default class ClearColumnButton extends React.Component { @@ -10,7 +11,7 @@ export default class ClearColumnButton extends React.Component { render () { return ( - <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.props.onClick}><i className='fa fa-eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button> + <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.props.onClick}><Icon id='eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button> ); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js index e29bd61f5..e4d5d0eda 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js @@ -58,6 +58,17 @@ export default class ColumnSettings extends React.PureComponent { </div> </div> + <div role='group' aria-labelledby='notifications-follow-request'> + <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} /> + {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} /> + </div> + </div> + <div role='group' aria-labelledby='notifications-favourite'> <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> diff --git a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js index 3457b7598..6118305d6 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js +++ b/app/javascript/flavours/glitch/features/notifications/components/filter_bar.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; const tooltips = defineMessages({ mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, @@ -63,35 +64,35 @@ class FilterBar extends React.PureComponent { onClick={this.onClick('mention')} title={intl.formatMessage(tooltips.mentions)} > - <i className='fa fa-fw fa-at' /> + <Icon id='reply-all' fixedWidth /> </button> <button className={selectedFilter === 'favourite' ? 'active' : ''} onClick={this.onClick('favourite')} title={intl.formatMessage(tooltips.favourites)} > - <i className='fa fa-fw fa-star' /> + <Icon id='star' fixedWidth /> </button> <button className={selectedFilter === 'reblog' ? 'active' : ''} onClick={this.onClick('reblog')} title={intl.formatMessage(tooltips.boosts)} > - <i className='fa fa-fw fa-retweet' /> + <Icon id='retweet' fixedWidth /> </button> <button className={selectedFilter === 'poll' ? 'active' : ''} onClick={this.onClick('poll')} title={intl.formatMessage(tooltips.polls)} > - <i className='fa fa-fw fa-tasks' /> + <Icon id='tasks' fixedWidth /> </button> <button className={selectedFilter === 'follow' ? 'active' : ''} onClick={this.onClick('follow')} title={intl.formatMessage(tooltips.follows)} > - <i className='fa fa-fw fa-user-plus' /> + <Icon id='user-plus' fixedWidth /> </button> </div> ); diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js index ea81d9ab4..2b71f3107 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow.js +++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js @@ -10,6 +10,7 @@ import { HotKeys } from 'react-hotkeys'; import Permalink from 'flavours/glitch/components/permalink'; import AccountContainer from 'flavours/glitch/containers/account_container'; import NotificationOverlayContainer from '../containers/overlay_container'; +import Icon from 'flavours/glitch/components/icon'; export default class NotificationFollow extends ImmutablePureComponent { @@ -78,7 +79,7 @@ export default class NotificationFollow extends ImmutablePureComponent { <div className='notification notification-follow focusable' tabIndex='0'> <div className='notification__message'> <div className='notification__favourite-icon-wrapper'> - <i className='fa fa-fw fa-user-plus' /> + <Icon fixedWidth id='user-plus' /> </div> <FormattedMessage diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow_request.js b/app/javascript/flavours/glitch/features/notifications/components/follow_request.js new file mode 100644 index 000000000..d73dac434 --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/components/follow_request.js @@ -0,0 +1,130 @@ +import React, { Fragment } from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; +import Permalink from 'flavours/glitch/components/permalink'; +import IconButton from 'flavours/glitch/components/icon_button'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import NotificationOverlayContainer from '../containers/overlay_container'; +import { HotKeys } from 'react-hotkeys'; +import Icon from 'flavours/glitch/components/icon'; + +const messages = defineMessages({ + authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, + reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, +}); + +export default @injectIntl +class FollowRequest extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onAuthorize: PropTypes.func.isRequired, + onReject: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + notification: ImmutablePropTypes.map.isRequired, + }; + + handleMoveUp = () => { + const { notification, onMoveUp } = this.props; + onMoveUp(notification.get('id')); + } + + handleMoveDown = () => { + const { notification, onMoveDown } = this.props; + onMoveDown(notification.get('id')); + } + + handleOpen = () => { + this.handleOpenProfile(); + } + + handleOpenProfile = () => { + const { notification } = this.props; + this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`); + } + + handleMention = e => { + e.preventDefault(); + + const { notification, onMention } = this.props; + onMention(notification.get('account'), this.context.router.history); + } + + getHandlers () { + return { + moveUp: this.handleMoveUp, + moveDown: this.handleMoveDown, + open: this.handleOpen, + openProfile: this.handleOpenProfile, + mention: this.handleMention, + reply: this.handleMention, + }; + } + + render () { + const { intl, hidden, account, onAuthorize, onReject, notification } = this.props; + + if (!account) { + return <div />; + } + + if (hidden) { + return ( + <Fragment> + {account.get('display_name')} + {account.get('username')} + </Fragment> + ); + } + + // Links to the display name. + const displayName = account.get('display_name_html') || account.get('username'); + const link = ( + <bdi><Permalink + className='notification__display-name' + href={account.get('url')} + title={account.get('acct')} + to={`/accounts/${account.get('id')}`} + dangerouslySetInnerHTML={{ __html: displayName }} + /></bdi> + ); + + return ( + <HotKeys handlers={this.getHandlers()}> + <div className='notification notification-follow-request focusable' tabIndex='0'> + <div className='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <Icon id='user' fixedWidth /> + </div> + + <FormattedMessage + id='notification.follow_request' + defaultMessage='{name} has requested to follow you' + values={{ name: link }} + /> + </div> + + <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> + <DisplayName account={account} /> + </Permalink> + + <div className='account__relationship'> + <IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /> + <IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /> + </div> + </div> + </div> + + <NotificationOverlayContainer notification={notification} /> + </div> + </HotKeys> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js index 5c5bbf604..62fc28386 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js @@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; // Our imports, import StatusContainer from 'flavours/glitch/containers/status_container'; import NotificationFollow from './follow'; +import NotificationFollowRequestContainer from '../containers/follow_request_container'; export default class Notification extends ImmutablePureComponent { @@ -47,6 +48,18 @@ export default class Notification extends ImmutablePureComponent { onMention={onMention} /> ); + case 'follow_request': + return ( + <NotificationFollowRequestContainer + hidden={hidden} + id={notification.get('id')} + account={notification.get('account')} + notification={notification} + onMoveDown={onMoveDown} + onMoveUp={onMoveUp} + onMention={onMention} + /> + ); case 'mention': return ( <StatusContainer diff --git a/app/javascript/flavours/glitch/features/notifications/components/overlay.js b/app/javascript/flavours/glitch/features/notifications/components/overlay.js index e56f9c628..f3ccafc06 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/overlay.js +++ b/app/javascript/flavours/glitch/features/notifications/components/overlay.js @@ -9,13 +9,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ markForDeletion: { id: 'notification.markForDeletion', defaultMessage: 'Mark for deletion' }, }); -@injectIntl -export default class NotificationOverlay extends ImmutablePureComponent { +export default @injectIntl +class NotificationOverlay extends ImmutablePureComponent { static propTypes = { notification : ImmutablePropTypes.map.isRequired, @@ -47,7 +48,7 @@ export default class NotificationOverlay extends ImmutablePureComponent { > <div className='wrappy'> <div className='ckbox' aria-hidden='true' title={label}> - {active ? (<i className='fa fa-check' />) : ''} + {active ? (<Icon id='check' />) : ''} </div> </div> </div> diff --git a/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js index ac2211e48..0264b6815 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js +++ b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js @@ -12,6 +12,7 @@ export default class SettingToggle extends React.PureComponent { label: PropTypes.node.isRequired, meta: PropTypes.node, onChange: PropTypes.func.isRequired, + defaultValue: PropTypes.bool, } onChange = ({ target }) => { @@ -19,12 +20,12 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingPath, label, meta } = this.props; + const { prefix, settings, settingPath, label, meta, defaultValue } = this.props; const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-'); return ( <div className='setting-toggle'> - <Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} /> + <Toggle id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} /> <label htmlFor={id} className='setting-toggle__label'>{label}</label> {meta && <span className='setting-meta__label'>{meta}</span>} </div> diff --git a/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js b/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js new file mode 100644 index 000000000..82357adfb --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import FollowRequest from '../components/follow_request'; +import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts'; + +const mapDispatchToProps = (dispatch, { account }) => ({ + onAuthorize () { + dispatch(authorizeFollowRequest(account.get('id'))); + }, + + onReject () { + dispatch(rejectFollowRequest(account.get('id'))); + }, +}); + +export default connect(null, mapDispatchToProps)(FollowRequest); diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index f2a1ccc3b..7f06d70c5 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -10,6 +10,7 @@ import { scrollTopNotifications, mountNotifications, unmountNotifications, + loadPending, } from 'flavours/glitch/actions/notifications'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import NotificationContainer from './containers/notification_container'; @@ -46,8 +47,9 @@ const mapStateToProps = state => ({ notifications: getNotifications(state), localSettings: state.get('local_settings'), isLoading: state.getIn(['notifications', 'isLoading'], true), - isUnread: state.getIn(['notifications', 'unread']) > 0, + isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, hasMore: state.getIn(['notifications', 'hasMore']), + numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), }); @@ -65,9 +67,9 @@ const mapDispatchToProps = dispatch => ({ dispatch, }); -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class Notifications extends React.PureComponent { +class Notifications extends React.PureComponent { static propTypes = { columnId: PropTypes.string, @@ -80,6 +82,7 @@ export default class Notifications extends React.PureComponent { isUnread: PropTypes.bool, multiColumn: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, localSettings: ImmutablePropTypes.map, notifCleaningActive: PropTypes.bool, onEnterCleaningMode: PropTypes.func, @@ -100,6 +103,10 @@ export default class Notifications extends React.PureComponent { this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); }, 300, { leading: true }); + handleLoadPending = () => { + this.props.dispatch(loadPending()); + }; + handleScrollToTop = debounce(() => { this.props.dispatch(scrollTopNotifications(true)); }, 100); @@ -170,7 +177,7 @@ export default class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = 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." />; @@ -212,11 +219,14 @@ export default class Notifications extends React.PureComponent { isLoading={isLoading} showLoading={isLoading && notifications.size === 0} hasMore={hasMore} + numPending={numPending} emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} + onLoadPending={this.handleLoadPending} onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} > {scrollableContent} </ScrollableList> @@ -224,6 +234,7 @@ export default class Notifications extends React.PureComponent { return ( <Column + bindToDocument={!multiColumn} ref={this.setColumnRef} name='notifications' extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null} diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js index 7484e458e..5f03c7e93 100644 --- a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js +++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js @@ -21,9 +21,9 @@ const mapDispatchToProps = dispatch => ({ onReset: () => dispatch(resetPinnedAccountsEditor()), }); -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class PinnedAccountsEditor extends ImmutablePureComponent { +class PinnedAccountsEditor extends ImmutablePureComponent { static propTypes = { onClose: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js index f56d70176..34d8e465f 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.js +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js @@ -18,15 +18,16 @@ const mapStateToProps = state => ({ hasMore: !!state.getIn(['status_lists', 'pins', 'next']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class PinnedStatuses extends ImmutablePureComponent { +class PinnedStatuses extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, statusIds: ImmutablePropTypes.list.isRequired, intl: PropTypes.object.isRequired, hasMore: PropTypes.bool.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -42,15 +43,16 @@ export default class PinnedStatuses extends ImmutablePureComponent { } render () { - const { intl, statusIds, hasMore } = this.props; + const { intl, statusIds, hasMore, multiColumn } = this.props; return ( - <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> + <Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> <ColumnBackButtonSlim /> <StatusList statusIds={statusIds} scrollKey='pinned_statuses' hasMore={hasMore} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js index 7fe472202..4d139a326 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/public_timeline/index.js @@ -14,20 +14,22 @@ const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Federated timeline' }, }); -const mapStateToProps = (state, { onlyMedia, columnId }) => { +const mapStateToProps = (state, { columnId }) => { const uuid = columnId; const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === uuid); + const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']); + const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]); return { - hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0, - onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']), + hasUnread: !!timelineState && timelineState.get('unread') > 0, + onlyMedia, }; }; -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class PublicTimeline extends React.PureComponent { +class PublicTimeline extends React.PureComponent { static defaultProps = { onlyMedia: false, @@ -99,16 +101,12 @@ export default class PublicTimeline extends React.PureComponent { dispatch(expandPublicTimeline({ maxId, onlyMedia })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props; const pinned = !!columnId; return ( - <Column ref={this.setRef} name='federated' label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} name='federated' label={intl.formatMessage(messages.title)}> <ColumnHeader icon='globe' active={hasUnread} @@ -128,6 +126,7 @@ export default class PublicTimeline extends React.PureComponent { trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js index e007506b7..258070358 100644 --- a/app/javascript/flavours/glitch/features/reblogs/index.js +++ b/app/javascript/flavours/glitch/features/reblogs/index.js @@ -4,34 +4,39 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { fetchReblogs } from 'flavours/glitch/actions/interactions'; -import { ScrollContainer } from 'react-router-scroll-4'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; +import Icon from 'flavours/glitch/components/icon'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' }, + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, }); const mapStateToProps = (state, props) => ({ accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class Reblogs extends ImmutablePureComponent { +class Reblogs extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, intl: PropTypes.object.isRequired, }; componentWillMount () { - this.props.dispatch(fetchReblogs(this.props.params.statusId)); + if (!this.props.accountIds) { + this.props.dispatch(fetchReblogs(this.props.params.statusId)); + } } componentWillReceiveProps(nextProps) { @@ -40,11 +45,6 @@ export default class Reblogs extends ImmutablePureComponent { } } - shouldUpdateScroll = (prevRouterProps, { location }) => { - if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false; - return !(location.state && location.state.mastodonModalOpen); - } - handleHeaderClick = () => { this.column.scrollTop(); } @@ -53,8 +53,12 @@ export default class Reblogs extends ImmutablePureComponent { this.column = c; } + handleRefresh = () => { + this.props.dispatch(fetchReblogs(this.props.params.statusId)); + } + render () { - const { intl, accountIds } = this.props; + const { intl, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -64,6 +68,8 @@ export default class Reblogs extends ImmutablePureComponent { ); } + const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.' />; + return ( <Column ref={this.setRef}> <ColumnHeader @@ -71,13 +77,21 @@ export default class Reblogs extends ImmutablePureComponent { title={intl.formatMessage(messages.heading)} onClick={this.handleHeaderClick} showBackButton + multiColumn={multiColumn} + extraButton={( + <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button> + )} /> - <ScrollContainer scrollKey='reblogs' shouldUpdateScroll={this.shouldUpdateScroll}> - <div className='scrollable reblogs'> - {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} - </div> - </ScrollContainer> + <ScrollableList + scrollKey='reblogs' + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + > + {accountIds.map(id => + <AccountContainer key={id} id={id} withNote={false} /> + )} + </ScrollableList> </Column> ); } diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 8291319c3..c48bfaccd 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -32,8 +32,8 @@ const messages = defineMessages({ copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, }); -@injectIntl -export default class ActionBar extends React.PureComponent { +export default @injectIntl +class ActionBar extends React.PureComponent { static contextTypes = { router: PropTypes.object, diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index f974a87a1..7352dc6b4 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -4,15 +4,8 @@ import Immutable from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import punycode from 'punycode'; import classnames from 'classnames'; - -const IDNA_PREFIX = 'xn--'; - -const decodeIDNA = domain => { - return domain - .split('.') - .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part) - .join('.'); -}; +import { decode as decodeIDNA } from 'flavours/glitch/util/idna'; +import Icon from 'flavours/glitch/components/icon'; const getHostname = url => { const parser = document.createElement('a'); @@ -147,7 +140,7 @@ export default class Card extends React.PureComponent { const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; const interactive = card.get('type') !== 'link'; const className = classnames('status-card', { horizontal, compact, interactive }); - const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; + const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; const ratio = card.get('width') / card.get('height'); const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); @@ -178,8 +171,8 @@ export default class Card extends React.PureComponent { <div className='status-card__actions'> <div> - <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button> - {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>} + <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> + {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>} </div> </div> </div> @@ -201,13 +194,13 @@ export default class Card extends React.PureComponent { } else { embed = ( <div className='status-card__image'> - <i className='fa fa-file-text' /> + <Icon id='file-text' /> </div> ); } return ( - <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}> + <a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}> {embed} {description} </a> diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index ddedac4d4..898011c88 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -11,10 +11,12 @@ import { FormattedDate, FormattedNumber } from 'react-intl'; import Card from './card'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from 'flavours/glitch/features/video'; +import Audio from 'flavours/glitch/features/audio'; import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon'; import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task'; import classNames from 'classnames'; import PollContainer from 'flavours/glitch/containers/poll_container'; +import Icon from 'flavours/glitch/components/icon'; export default class DetailedStatus extends ImmutablePureComponent { @@ -131,14 +133,27 @@ export default class DetailedStatus extends ImmutablePureComponent { } else if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { media = <AttachmentList media={status.get('media_attachments')} />; + } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { + const attachment = status.getIn(['media_attachments', 0]); + + media = ( + <Audio + src={attachment.get('url')} + alt={attachment.get('description')} + duration={attachment.getIn(['meta', 'original', 'duration'], 0)} + height={110} + preload + /> + ); + mediaIcon = 'music'; } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); + const attachment = status.getIn(['media_attachments', 0]); media = ( <Video - preview={video.get('preview_url')} - blurhash={video.get('blurhash')} - src={video.get('url')} - alt={video.get('description')} + preview={attachment.get('preview_url')} + blurhash={attachment.get('blurhash')} + src={attachment.get('url')} + alt={attachment.get('description')} inline sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} @@ -173,7 +188,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('application')) { - applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>; + applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>; } if (status.get('visibility') === 'direct') { @@ -183,11 +198,11 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('visibility') === 'private') { - reblogLink = <i className={`fa fa-${reblogIcon}`} />; + reblogLink = <Icon id={reblogIcon} />; } else if (this.context.router) { reblogLink = ( <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> - <i className={`fa fa-${reblogIcon}`} /> + <Icon id={reblogIcon} /> <span className='detailed-status__reblogs'> <FormattedNumber value={status.get('reblogs_count')} /> </span> @@ -196,7 +211,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } else { reblogLink = ( <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> - <i className={`fa fa-${reblogIcon}`} /> + <Icon id={reblogIcon} /> <span className='detailed-status__reblogs'> <FormattedNumber value={status.get('reblogs_count')} /> </span> @@ -207,7 +222,7 @@ export default class DetailedStatus extends ImmutablePureComponent { if (this.context.router) { favouriteLink = ( <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> - <i className='fa fa-star' /> + <Icon id='star' /> <span className='detailed-status__favorites'> <FormattedNumber value={status.get('favourites_count')} /> </span> @@ -216,7 +231,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } else { favouriteLink = ( <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}> - <i className='fa fa-star' /> + <Icon id='star' /> <span className='detailed-status__favorites'> <FormattedNumber value={status.get('favourites_count')} /> </span> @@ -241,11 +256,13 @@ export default class DetailedStatus extends ImmutablePureComponent { onExpandedToggle={onToggleHidden} parseClick={this.parseClick} onUpdate={this.handleChildUpdate} + tagLinks={settings.get('tag_misleading_links')} + rewriteMentions={settings.get('rewrite_mentions')} disabled /> <div className='detailed-status__meta'> - <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> + <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> </a>{applicationLink} · {reblogLink} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} /> </div> diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js index e6c390537..e71803328 100644 --- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js @@ -1,4 +1,3 @@ -import React from 'react'; import { connect } from 'react-redux'; import DetailedStatus from '../components/detailed_status'; import { makeGetStatus } from 'flavours/glitch/selectors'; @@ -15,7 +14,6 @@ import { pin, unpin, } from 'flavours/glitch/actions/interactions'; -import { blockAccount } from 'flavours/glitch/actions/accounts'; import { muteStatus, unmuteStatus, @@ -24,9 +22,10 @@ import { revealStatus, } from 'flavours/glitch/actions/statuses'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { openModal } from 'flavours/glitch/actions/modal'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state'; import { showAlertForError } from 'flavours/glitch/actions/alerts'; @@ -35,10 +34,8 @@ const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, 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?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, }); const makeMapStateToProps = () => { @@ -139,16 +136,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onBlock (status) { const account = status.get('account'); - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.get('id'))); - dispatch(initReport(account, status)); - }, - })); + dispatch(initBlockModal(account)); }, onReport (status) { diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 76bfaaffa..322f92477 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { createSelector } from 'reselect'; import { fetchStatus } from 'flavours/glitch/actions/statuses'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import DetailedStatus from './components/detailed_status'; @@ -25,9 +26,9 @@ import { directCompose, } from 'flavours/glitch/actions/compose'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; -import { blockAccount } from 'flavours/glitch/actions/accounts'; import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { makeGetStatus } from 'flavours/glitch/selectors'; import { ScrollContainer } from 'react-router-scroll-4'; @@ -35,67 +36,95 @@ import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import ColumnHeader from '../../components/column_header'; import StatusContainer from 'flavours/glitch/containers/status_container'; import { openModal } from 'flavours/glitch/actions/modal'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; import { autoUnfoldCW } from 'flavours/glitch/util/content_warning'; import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, 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?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' }, }); const makeMapStateToProps = () => { const getStatus = makeGetStatus(); - const mapStateToProps = (state, props) => { - const status = getStatus(state, { id: props.params.statusId }); + const getAncestorsIds = createSelector([ + (_, { id }) => id, + state => state.getIn(['contexts', 'inReplyTos']), + ], (statusId, inReplyTos) => { let ancestorsIds = Immutable.List(); - let descendantsIds = Immutable.List(); + ancestorsIds = ancestorsIds.withMutations(mutable => { + let id = statusId; - if (status) { - ancestorsIds = ancestorsIds.withMutations(mutable => { - let id = status.get('in_reply_to_id'); + while (id) { + mutable.unshift(id); + id = inReplyTos.get(id); + } + }); - while (id) { - mutable.unshift(id); - id = state.getIn(['contexts', 'inReplyTos', id]); - } - }); + return ancestorsIds; + }); + + const getDescendantsIds = createSelector([ + (_, { id }) => id, + state => state.getIn(['contexts', 'replies']), + state => state.get('statuses'), + ], (statusId, contextReplies, statuses) => { + let descendantsIds = []; + const ids = [statusId]; - descendantsIds = descendantsIds.withMutations(mutable => { - const ids = [status.get('id')]; + while (ids.length > 0) { + let id = ids.shift(); + const replies = contextReplies.get(id); - while (ids.length > 0) { - let id = ids.shift(); - const replies = state.getIn(['contexts', 'replies', id]); + if (statusId !== id) { + descendantsIds.push(id); + } - if (status.get('id') !== id) { - mutable.push(id); - } + if (replies) { + replies.reverse().forEach(reply => { + ids.unshift(reply); + }); + } + } - if (replies) { - replies.reverse().forEach(reply => { - ids.unshift(reply); - }); - } + let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account')); + if (insertAt !== -1) { + descendantsIds.forEach((id, idx) => { + if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) { + descendantsIds.splice(idx, 1); + descendantsIds.splice(insertAt, 0, id); + insertAt += 1; } }); } + return Immutable.List(descendantsIds); + }); + + const mapStateToProps = (state, props) => { + const status = getStatus(state, { id: props.params.statusId }); + let ancestorsIds = Immutable.List(); + let descendantsIds = Immutable.List(); + + if (status) { + ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); + descendantsIds = getDescendantsIds(state, { id: status.get('id') }); + } + return { status, ancestorsIds, @@ -109,9 +138,9 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -@injectIntl +export default @injectIntl @connect(makeMapStateToProps) -export default class Status extends ImmutablePureComponent { +class Status extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -126,6 +155,7 @@ export default class Status extends ImmutablePureComponent { descendantsIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, askReplyConfirmation: PropTypes.bool, + multiColumn: PropTypes.bool, domain: PropTypes.string.isRequired, }; @@ -290,6 +320,22 @@ export default class Status extends ImmutablePureComponent { this.props.dispatch(openModal('VIDEO', { media, time })); } + handleHotkeyOpenMedia = e => { + 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') { + this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0); + } else { + this.handleOpenMedia(status.get('media_attachments'), 0); + } + } + } + handleMuteClick = (account) => { this.props.dispatch(initMuteModal(account)); } @@ -308,19 +354,9 @@ export default class Status extends ImmutablePureComponent { } handleBlockClick = (status) => { - const { dispatch, intl } = this.props; + const { dispatch } = this.props; const account = status.get('account'); - - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.get('id'))); - dispatch(initReport(account, status)); - }, - })); + dispatch(initBlockModal(account)); } handleReport = (status) => { @@ -478,13 +514,13 @@ export default class Status extends ImmutablePureComponent { render () { let ancestors, descendants; const { setExpansion } = this; - const { status, settings, ancestorsIds, descendantsIds, intl, domain } = this.props; + const { status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props; const { fullscreen, isExpanded } = this.state; if (status === null) { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <MissingIndicator /> </Column> ); @@ -509,17 +545,19 @@ export default class Status extends ImmutablePureComponent { openProfile: this.handleHotkeyOpenProfile, toggleSpoiler: this.handleExpandedToggle, toggleSensitive: this.handleHotkeyToggleSensitive, + openMedia: this.handleHotkeyOpenMedia, }; return ( - <Column ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}> + <Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}> <ColumnHeader icon='comment' title={intl.formatMessage(messages.tootHeading)} onClick={this.handleHeaderClick} showBackButton + multiColumn={multiColumn} extraButton={( - <button className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={!isExpanded ? 'false' : 'true'}><i className={`fa fa-${!isExpanded ? 'eye-slash' : 'eye'}`} /></button> + <button className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={!isExpanded ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button> )} /> diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js index 724f1c764..24169036c 100644 --- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js @@ -68,8 +68,8 @@ export default class ActionsModal extends ImmutablePureComponent { return ( <Icon className='icon' - fullwidth - icon={icon} + fixedWidth + id={icon} /> ); default: @@ -92,12 +92,12 @@ export default class ActionsModal extends ImmutablePureComponent { <div className='status light'> <div className='boost-modal__status-header'> <div className='boost-modal__status-time'> - <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> + <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'> <RelativeTimestamp timestamp={this.props.status.get('created_at')} /> </a> </div> - <a href={this.props.status.getIn(['account', 'url'])} className='status__display-name'> + <a href={this.props.status.getIn(['account', 'url'])} className='status__display-name' rel='noopener noreferrer'> <div className='status__avatar'> <Avatar account={this.props.status.get('account')} size={48} /> </div> diff --git a/app/javascript/flavours/glitch/features/ui/components/audio_modal.js b/app/javascript/flavours/glitch/features/ui/components/audio_modal.js new file mode 100644 index 000000000..08fbddc91 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/audio_modal.js @@ -0,0 +1,53 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Audio from 'flavours/glitch/features/audio'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; + +export default class AudioModal extends ImmutablePureComponent { + + static propTypes = { + media: ImmutablePropTypes.map.isRequired, + status: ImmutablePropTypes.map, + onClose: PropTypes.func.isRequired, + }; + + static contextTypes = { + router: PropTypes.object, + }; + + 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 } = this.props; + + return ( + <div className='modal-root__modal audio-modal'> + <div className='audio-modal__container'> + <Audio + src={media.get('url')} + alt={media.get('description')} + duration={media.getIn(['meta', 'original', 'duration'], 0)} + height={135} + preload + /> + </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> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/components/block_modal.js b/app/javascript/flavours/glitch/features/ui/components/block_modal.js new file mode 100644 index 000000000..a07baeaa6 --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/block_modal.js @@ -0,0 +1,103 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import { makeGetAccount } from '../../../selectors'; +import Button from '../../../components/button'; +import { closeModal } from '../../../actions/modal'; +import { blockAccount } from '../../../actions/accounts'; +import { initReport } from '../../../actions/reports'; + + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = state => ({ + account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = dispatch => { + return { + onConfirm(account) { + dispatch(blockAccount(account.get('id'))); + }, + + onBlockAndReport(account) { + dispatch(blockAccount(account.get('id'))); + dispatch(initReport(account)); + }, + + onClose() { + dispatch(closeModal()); + }, + }; +}; + +export default @connect(makeMapStateToProps, mapDispatchToProps) +@injectIntl +class BlockModal extends React.PureComponent { + + static propTypes = { + account: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + onBlockAndReport: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + componentDidMount() { + this.button.focus(); + } + + handleClick = () => { + this.props.onClose(); + this.props.onConfirm(this.props.account); + } + + handleSecondary = () => { + this.props.onClose(); + this.props.onBlockAndReport(this.props.account); + } + + handleCancel = () => { + this.props.onClose(); + } + + setRef = (c) => { + this.button = c; + } + + render () { + const { account } = this.props; + + return ( + <div className='modal-root__modal block-modal'> + <div className='block-modal__container'> + <p> + <FormattedMessage + id='confirmations.block.message' + defaultMessage='Are you sure you want to block {name}?' + values={{ name: <strong>@{account.get('acct')}</strong> }} + /> + </p> + </div> + + <div className='block-modal__action-bar'> + <Button onClick={this.handleCancel} className='block-modal__cancel-button'> + <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /> + </Button> + <Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'> + <FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' /> + </Button> + <Button onClick={this.handleClick} ref={this.setRef}> + <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' /> + </Button> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js index 600e4422f..cd2929fdb 100644 --- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js @@ -8,6 +8,7 @@ import Avatar from 'flavours/glitch/components/avatar'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import DisplayName from 'flavours/glitch/components/display_name'; import AttachmentList from 'flavours/glitch/components/attachment_list'; +import Icon from 'flavours/glitch/components/icon'; import ImmutablePureComponent from 'react-immutable-pure-component'; const messages = defineMessages({ @@ -15,8 +16,8 @@ const messages = defineMessages({ reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, }); -@injectIntl -export default class BoostModal extends ImmutablePureComponent { +export default @injectIntl +class BoostModal extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -63,7 +64,7 @@ export default class BoostModal extends ImmutablePureComponent { <div className='status 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'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> </div> <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> @@ -91,7 +92,7 @@ export default class BoostModal extends ImmutablePureComponent { { missingMediaDescription ? <FormattedMessage id='boost_modal.missing_description' defaultMessage='This toot contains some media without description' /> : - <FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /> + <FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /> } </div> <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} /> diff --git a/app/javascript/flavours/glitch/features/ui/components/column_header.js b/app/javascript/flavours/glitch/features/ui/components/column_header.js index e8bdd8054..528ff73a6 100644 --- a/app/javascript/flavours/glitch/features/ui/components/column_header.js +++ b/app/javascript/flavours/glitch/features/ui/components/column_header.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; export default class ColumnHeader extends React.PureComponent { @@ -21,7 +22,7 @@ export default class ColumnHeader extends React.PureComponent { let iconElement = ''; if (icon) { - iconElement = <i className={`fa fa-fw fa-${icon} column-header__icon`} />; + iconElement = <Icon id={icon} fixedWidth className='column-header__icon' />; } return ( diff --git a/app/javascript/flavours/glitch/features/ui/components/column_link.js b/app/javascript/flavours/glitch/features/ui/components/column_link.js index 1b6d7d09e..d04b869b6 100644 --- a/app/javascript/flavours/glitch/features/ui/components/column_link.js +++ b/app/javascript/flavours/glitch/features/ui/components/column_link.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; +import Icon from 'flavours/glitch/components/icon'; const ColumnLink = ({ icon, text, to, onClick, href, method, badge }) => { const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null; @@ -8,7 +9,7 @@ const ColumnLink = ({ icon, text, to, onClick, href, method, badge }) => { if (href) { return ( <a href={href} className='column-link' data-method={method}> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> + <Icon id={icon} fixedWidth className='column-link__icon' /> {text} {badgeElement} </a> @@ -16,7 +17,7 @@ const ColumnLink = ({ icon, text, to, onClick, href, method, badge }) => { } else if (to) { return ( <Link to={to} className='column-link'> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> + <Icon id={icon} fixedWidth className='column-link__icon' /> {text} {badgeElement} </Link> @@ -29,7 +30,7 @@ const ColumnLink = ({ icon, text, to, onClick, href, method, badge }) => { } return ( <a href='#' onClick={onClick && handleOnClick} className='column-link' tabIndex='0'> - <i className={`fa fa-fw fa-${icon} column-link__icon`} /> + <Icon id={icon} fixedWidth className='column-link__icon' /> {text} {badgeElement} </a> diff --git a/app/javascript/flavours/glitch/features/ui/components/column_loading.js b/app/javascript/flavours/glitch/features/ui/components/column_loading.js index ba2d0824e..22c00c915 100644 --- a/app/javascript/flavours/glitch/features/ui/components/column_loading.js +++ b/app/javascript/flavours/glitch/features/ui/components/column_loading.js @@ -21,7 +21,7 @@ export default class ColumnLoading extends ImmutablePureComponent { let { title, icon } = this.props; return ( <Column> - <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} /> + <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder /> <div className='scrollable' /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index 3a188ca87..431909c72 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -12,7 +12,20 @@ import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; import BundleColumnError from './bundle_column_error'; -import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline } from 'flavours/glitch/util/async-components'; +import { + Compose, + Notifications, + HomeTimeline, + CommunityTimeline, + PublicTimeline, + HashtagTimeline, + DirectTimeline, + FavouritedStatuses, + BookmarkedStatuses, + ListTimeline, + Directory, +} from 'flavours/glitch/util/async-components'; +import Icon from 'flavours/glitch/components/icon'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; @@ -30,6 +43,7 @@ const componentMap = { 'FAVOURITES': FavouritedStatuses, 'BOOKMARKS': BookmarkedStatuses, 'LIST': ListTimeline, + 'DIRECTORY': Directory, }; const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/); @@ -38,8 +52,8 @@ const messages = defineMessages({ publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, }); -@component => injectIntl(component, { withRef: true }) -export default class ColumnsArea extends ImmutablePureComponent { +export default @(component => injectIntl(component, { withRef: true })) +class ColumnsArea extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object.isRequired, @@ -112,6 +126,11 @@ export default class ColumnsArea extends ImmutablePureComponent { // React-router does this for us, but too late, feeling laggy. document.querySelector(currentLinkSelector).classList.remove('active'); document.querySelector(nextLinkSelector).classList.add('active'); + + if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') { + this.context.router.history.push(getLink(this.pendingIndex)); + this.pendingIndex = null; + } } handleAnimationEnd = () => { @@ -162,13 +181,12 @@ export default class ColumnsArea extends ImmutablePureComponent { const { shouldAnimate } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); - this.pendingIndex = null; if (singleColumn) { - 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)}><i className='fa fa-pencil' /></Link>; + 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' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={!swipeToChangeColumns}> + <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={!swipeToChangeColumns}> {links.map(this.renderView)} </ReactSwipeableViews> ) : ( diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js index 970df30b6..47a49c0c7 100644 --- a/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage } from 'react-intl'; import Button from 'flavours/glitch/components/button'; -@injectIntl -export default class ConfirmationModal extends React.PureComponent { +export default @injectIntl +class ConfirmationModal extends React.PureComponent { static propTypes = { message: PropTypes.node.isRequired, diff --git a/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js b/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js index 72f7f30b9..0d10204fc 100644 --- a/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js @@ -145,8 +145,8 @@ const mapDispatchToProps = dispatch => ({ * - Ctrl + left mouse button: pick background * - Right mouse button: pick background */ -@connect(mapStateToProps, mapDispatchToProps) -export default class DoodleModal extends ImmutablePureComponent { +export default @connect(mapStateToProps, mapDispatchToProps) +class DoodleModal extends ImmutablePureComponent { static propTypes = { options: ImmutablePropTypes.map, diff --git a/app/javascript/flavours/glitch/features/ui/components/embed_modal.js b/app/javascript/flavours/glitch/features/ui/components/embed_modal.js index b1643df1c..b6f5e628d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/embed_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/embed_modal.js @@ -1,11 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import api from 'flavours/glitch/util/api'; +import IconButton from 'flavours/glitch/components/icon_button'; -@injectIntl -export default class EmbedModal extends ImmutablePureComponent { +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +export default @injectIntl +class EmbedModal extends ImmutablePureComponent { static propTypes = { url: PropTypes.string.isRequired, @@ -50,13 +55,17 @@ export default class EmbedModal extends ImmutablePureComponent { } render () { + const { intl, onClose } = this.props; const { oembed } = this.state; return ( - <div className='modal-root__modal embed-modal'> - <h4><FormattedMessage id='status.embed' defaultMessage='Embed' /></h4> + <div className='modal-root__modal report-modal embed-modal'> + <div className='report-modal__target'> + <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> + <FormattedMessage id='status.embed' defaultMessage='Embed' /> + </div> - <div className='embed-modal__container'> + <div className='report-modal__container embed-modal__container' style={{ display: 'block' }}> <p className='hint'> <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' /> </p> diff --git a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js index e0037a15f..176e7c487 100644 --- a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js @@ -7,14 +7,15 @@ import StatusContent from 'flavours/glitch/components/status_content'; import Avatar from 'flavours/glitch/components/avatar'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import DisplayName from 'flavours/glitch/components/display_name'; +import Icon from 'flavours/glitch/components/icon'; import ImmutablePureComponent from 'react-immutable-pure-component'; const messages = defineMessages({ favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, }); -@injectIntl -export default class FavouriteModal extends ImmutablePureComponent { +export default @injectIntl +class FavouriteModal extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -76,7 +77,7 @@ export default class FavouriteModal extends ImmutablePureComponent { </div> <div className='favourite-modal__action-bar'> - <div><FormattedMessage id='favourite_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-star' /></span> }} /></div> + <div><FormattedMessage id='favourite_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='star' /></span> }} /></div> <Button text={intl.formatMessage(messages.favourite)} onClick={this.handleFavourite} ref={this.setRef} /> </div> </div> diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js index 57c92cc66..77e4bbfa5 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js @@ -1,11 +1,28 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import ImageLoader from './image_loader'; import classNames from 'classnames'; import { changeUploadCompose } from 'flavours/glitch/actions/compose'; import { getPointerPosition } from 'flavours/glitch/features/video'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import IconButton from 'flavours/glitch/components/icon_button'; +import Button from 'flavours/glitch/components/button'; +import Video from 'flavours/glitch/features/video'; +import Audio from 'flavours/glitch/features/audio'; +import Textarea from 'react-textarea-autosize'; +import UploadProgress from 'flavours/glitch/features/compose/components/upload_progress'; +import CharacterCounter from 'flavours/glitch/features/compose/components/character_counter'; +import { length } from 'stringz'; +import { Tesseract as fetchTesseract } from 'flavours/glitch/util/async-components'; +import GIFV from 'flavours/glitch/components/gifv'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, + apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' }, + placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' }, +}); const mapStateToProps = (state, { id }) => ({ media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), @@ -13,17 +30,56 @@ const mapStateToProps = (state, { id }) => ({ const mapDispatchToProps = (dispatch, { id }) => ({ - onSave: (x, y) => { - dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` })); + onSave: (description, x, y) => { + dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` })); }, }); -@connect(mapStateToProps, mapDispatchToProps) -export default class FocalPointModal extends ImmutablePureComponent { +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 = { + src: PropTypes.string.isRequired, + width: PropTypes.number, + height: PropTypes.number, + }; + + state = { + loading: true, + }; + + componentDidMount() { + const image = new Image(); + image.addEventListener('load', () => this.setState({ loading: false })); + image.src = this.props.src; + } + + render () { + const { loading } = this.state; + + if (loading) { + return <canvas width={this.props.width} height={this.props.height} />; + } else { + return <img {...this.props} alt='' />; + } + } + +} + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class FocalPointModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, }; state = { @@ -32,6 +88,10 @@ export default class FocalPointModal extends ImmutablePureComponent { focusX: 0, focusY: 0, dragging: false, + description: '', + dirty: false, + progress: 0, + loading: true, }; componentWillMount () { @@ -57,6 +117,14 @@ export default class FocalPointModal extends ImmutablePureComponent { this.setState({ dragging: true }); } + handleTouchStart = e => { + document.addEventListener('touchmove', this.handleMouseMove); + document.addEventListener('touchend', this.handleTouchEnd); + + this.updatePosition(e); + this.setState({ dragging: true }); + } + handleMouseMove = e => { this.updatePosition(e); } @@ -66,7 +134,13 @@ export default class FocalPointModal extends ImmutablePureComponent { document.removeEventListener('mouseup', this.handleMouseUp); this.setState({ dragging: false }); - this.props.onSave(this.state.focusX, this.state.focusY); + } + + handleTouchEnd = () => { + document.removeEventListener('touchmove', this.handleMouseMove); + document.removeEventListener('touchend', this.handleTouchEnd); + + this.setState({ dragging: false }); } updatePosition = e => { @@ -74,46 +148,188 @@ export default class FocalPointModal extends ImmutablePureComponent { const focusX = (x - .5) * 2; const focusY = (y - .5) * -2; - this.setState({ x, y, focusX, focusY }); + this.setState({ x, y, focusX, focusY, dirty: true }); } updatePositionFromMedia = media => { - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); + const focusX = media.getIn(['meta', 'focus', 'x']); + const focusY = media.getIn(['meta', 'focus', 'y']); + const description = media.get('description') || ''; if (focusX && focusY) { const x = (focusX / 2) + .5; const y = (focusY / -2) + .5; - this.setState({ x, y, focusX, focusY }); + this.setState({ + x, + y, + focusX, + focusY, + description, + dirty: false, + }); } else { - this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 }); + this.setState({ + x: 0.5, + y: 0.5, + focusX: 0, + focusY: 0, + description, + dirty: false, + }); } } + handleChange = e => { + this.setState({ description: e.target.value, dirty: true }); + } + + handleKeyDown = (e) => { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + e.stopPropagation(); + this.setState({ description: e.target.value, dirty: true }); + this.handleSubmit(); + } + } + + handleSubmit = () => { + this.props.onSave(this.state.description, this.state.focusX, this.state.focusY); + this.props.onClose(); + } + setRef = c => { this.node = c; } - render () { + handleTextDetection = () => { const { media } = this.props; - const { x, y, dragging } = this.state; + + 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`, + }); + + let media_url = media.get('url'); + + if (window.URL && URL.createObjectURL) { + try { + media_url = URL.createObjectURL(media.get('file')); + } catch (error) { + console.error(error); + } + } + + 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 })); + } + + render () { + const { media, intl, onClose } = this.props; + const { x, y, dragging, description, dirty, detecting, progress } = this.state; const width = media.getIn(['meta', 'original', 'width']) || null; const height = media.getIn(['meta', 'original', 'height']) || null; + const focals = ['image', 'gifv'].includes(media.get('type')); + + const previewRatio = 16/9; + const previewWidth = 200; + const previewHeight = previewWidth / previewRatio; + + let descriptionLabel = null; + + if (media.get('type') === 'audio') { + descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people with hearing loss' />; + } else if (media.get('type') === 'video') { + descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people with hearing loss or visual impairment' />; + } else { + descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />; + } return ( - <div className='modal-root__modal video-modal focal-point-modal'> - <div className={classNames('focal-point', { dragging })} ref={this.setRef}> - <ImageLoader - previewSrc={media.get('preview_url')} - src={media.get('url')} - width={width} - height={height} - /> - - <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} /> - <div className='focal-point__overlay' onMouseDown={this.handleMouseDown} /> + <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}> + <div className='report-modal__target'> + <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> + <FormattedMessage id='upload_modal.edit_media' defaultMessage='Edit media' /> + </div> + + <div className='report-modal__container'> + <div className='report-modal__comment'> + {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>} + + <label className='setting-text-label' htmlFor='upload-modal__description'> + {descriptionLabel} + </label> + + <div className='setting-text__wrapper'> + <Textarea + id='upload-modal__description' + className='setting-text light' + value={detecting ? '…' : description} + onChange={this.handleChange} + onKeyDown={this.handleKeyDown} + disabled={detecting} + autoFocus + /> + + <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…' />} /> + </div> + </div> + + <div className='setting-text__toolbar'> + <button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button> + <CharacterCounter max={1500} text={detecting ? '' : description} /> + </div> + + <Button disabled={!dirty || detecting || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> + </div> + + <div className='focal-point-modal__content'> + {focals && ( + <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}> + {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />} + {media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />} + + <div className='focal-point__preview'> + <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong> + <div style={{ width: previewWidth, height: previewHeight, backgroundImage: `url(${media.get('preview_url')})`, backgroundSize: 'cover', backgroundPosition: `${x * 100}% ${y * 100}%` }} /> + </div> + + <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} /> + <div className='focal-point__overlay' /> + </div> + )} + + {media.get('type') === 'video' && ( + <Video + preview={media.get('preview_url')} + blurhash={media.get('blurhash')} + src={media.get('url')} + detailed + inline + editable + /> + )} + + {media.get('type') === 'audio' && ( + <Audio + src={media.get('url')} + duration={media.getIn(['meta', 'original', 'duration'], 0)} + height={150} + preload + editable + /> + )} + </div> </div> </div> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js b/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js index 189f403bd..c30427896 100644 --- a/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js +++ b/app/javascript/flavours/glitch/features/ui/components/follow_requests_nav_link.js @@ -4,12 +4,10 @@ import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; import { connect } from 'react-redux'; import { NavLink, withRouter } from 'react-router-dom'; import IconWithBadge from 'flavours/glitch/components/icon_with_badge'; -import { me } from 'flavours/glitch/util/initial_state'; import { List as ImmutableList } from 'immutable'; import { FormattedMessage } from 'react-intl'; const mapStateToProps = state => ({ - locked: state.getIn(['accounts', me, 'locked']), count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, }); @@ -19,22 +17,19 @@ class FollowRequestsNavLink extends React.Component { static propTypes = { dispatch: PropTypes.func.isRequired, - locked: PropTypes.bool, count: PropTypes.number.isRequired, }; componentDidMount () { - const { dispatch, locked } = this.props; + const { dispatch } = this.props; - if (locked) { - dispatch(fetchFollowRequests()); - } + dispatch(fetchFollowRequests()); } render () { - const { locked, count } = this.props; + const { count } = this.props; - if (!locked || count === 0) { + if (count === 0) { return null; } diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js index e63ed274e..f8d0d528d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js @@ -1,37 +1,72 @@ +import { connect } from 'react-redux'; import React from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import { invitesEnabled, version, repository, source_url } from 'flavours/glitch/util/initial_state'; -import { signOutLink } from 'flavours/glitch/util/backend_links'; - -const LinkFooter = () => ( - <div className='getting-started__footer'> - <ul> - {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} - <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> - <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> - <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> - <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> - <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> - <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li> - <li><a href={signOutLink} data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li> - </ul> - - <p> - <FormattedMessage - id='getting_started.open_source_notice' - defaultMessage='GlitchCafe is open source software, based on {Glitchsoc} which is a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.' - values={{ - github: <span><a href='https://github.com/pluralcafe/mastodon' rel='noopener' target='_blank'>pluralcafe/mastodon</a> (v{version})</span>, - Glitchsoc: <a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a>, - Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a> }} - /> - </p> - </div> -); - -LinkFooter.propTypes = { -}; +import { signOutLink, securityLink } from 'flavours/glitch/util/backend_links'; +import { logOut } from 'flavours/glitch/util/log_out'; +import { openModal } from 'flavours/glitch/actions/modal'; + +const messages = defineMessages({ + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, +}); + +const mapDispatchToProps = (dispatch, { intl }) => ({ + onLogout () { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + onConfirm: () => logOut(), + })); + }, +}); + +export default @injectIntl +@connect(null, mapDispatchToProps) +class LinkFooter extends React.PureComponent { + + static propTypes = { + onLogout: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; -export default LinkFooter; + handleLogoutClick = e => { + e.preventDefault(); + e.stopPropagation(); + + this.props.onLogout(); + + return false; + } + + render () { + return ( + <div className='getting-started__footer'> + <ul> + {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} + {!!securityLink && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>} + <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> + <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> + <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> + <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> + <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li> + <li><a href={signOutLink} onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li> + </ul> + + <p> + <FormattedMessage + id='getting_started.open_source_notice' + defaultMessage='GlitchCafe is open source software, based on {Glitchsoc} which is a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.' + values={{ + github: <span><a href='https://github.com/pluralcafe/mastodon' rel='noopener' target='_blank'>pluralcafe/mastodon</a> (v{version})</span>, + Glitchsoc: <a href='https://github.com/glitch-soc/mastodon' rel='noopener' target='_blank'>glitch-soc/mastodon</a>, + Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>Mastodon</a> }} + /> + </p> + </div> + ); + } + +}; diff --git a/app/javascript/flavours/glitch/features/ui/components/list_panel.js b/app/javascript/flavours/glitch/features/ui/components/list_panel.js index b2e6925b7..354e35027 100644 --- a/app/javascript/flavours/glitch/features/ui/components/list_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/list_panel.js @@ -46,7 +46,7 @@ class ListPanel extends ImmutablePureComponent { <hr /> {lists.map(list => ( - <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' icon='list-ul' fixedWidth />{list.get('title')}</NavLink> + <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink> ))} </div> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index ce6660480..c7d6c374c 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -3,12 +3,13 @@ import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; -import ExtendedVideoPlayer from 'flavours/glitch/components/extended_video_player'; import classNames from 'classnames'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from 'flavours/glitch/components/icon_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImageLoader from './image_loader'; +import Icon from 'flavours/glitch/components/icon'; +import GIFV from 'flavours/glitch/components/gifv'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -16,8 +17,8 @@ const messages = defineMessages({ next: { id: 'lightbox.next', defaultMessage: 'Next' }, }); -@injectIntl -export default class MediaModal extends ImmutablePureComponent { +export default @injectIntl +class MediaModal extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -100,8 +101,8 @@ export default class MediaModal extends ImmutablePureComponent { 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)}><i className='fa fa-fw fa-chevron-left' /></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)}><i className='fa fa-fw fa-chevron-right' /></button>; + 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) => { @@ -148,10 +149,8 @@ export default class MediaModal extends ImmutablePureComponent { ); } else if (image.get('type') === 'gifv') { return ( - <ExtendedVideoPlayer + <GIFV src={image.get('url')} - muted - controls={false} width={width} height={height} key={image.get('preview_url')} @@ -207,7 +206,7 @@ export default class MediaModal extends ImmutablePureComponent { {status && ( <div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}> - <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> + <a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> </div> )} diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index 303e05db6..488daf0cc 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Base from '../../../components/modal_root'; +import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar'; +import Base from 'flavours/glitch/components/modal_root'; import BundleContainer from '../containers/bundle_container'; import BundleModalError from './bundle_modal_error'; import ModalLoading from './modal_loading'; @@ -9,12 +10,14 @@ import MediaModal from './media_modal'; import VideoModal from './video_modal'; import BoostModal from './boost_modal'; import FavouriteModal from './favourite_modal'; +import AudioModal from './audio_modal'; import DoodleModal from './doodle_modal'; import ConfirmationModal from './confirmation_modal'; import FocalPointModal from './focal_point_modal'; import { OnboardingModal, MuteModal, + BlockModal, ReportModal, SettingsModal, EmbedModal, @@ -27,11 +30,13 @@ const MODAL_COMPONENTS = { 'MEDIA': () => Promise.resolve({ default: MediaModal }), 'ONBOARDING': OnboardingModal, 'VIDEO': () => Promise.resolve({ default: VideoModal }), + 'AUDIO': () => Promise.resolve({ default: AudioModal }), 'BOOST': () => Promise.resolve({ default: BoostModal }), 'FAVOURITE': () => Promise.resolve({ default: FavouriteModal }), 'DOODLE': () => Promise.resolve({ default: DoodleModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), 'MUTE': MuteModal, + 'BLOCK': BlockModal, 'REPORT': ReportModal, 'SETTINGS': SettingsModal, 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), @@ -57,8 +62,10 @@ export default class ModalRoot extends React.PureComponent { componentDidUpdate (prevProps, prevState, { visible }) { if (visible) { document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; } else { document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; } } diff --git a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js index 0202b1ab1..2aab82751 100644 --- a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js @@ -11,7 +11,6 @@ import { toggleHideNotifications } from 'flavours/glitch/actions/mutes'; const mapStateToProps = state => { return { - isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), account: state.getIn(['mutes', 'new', 'account']), notifications: state.getIn(['mutes', 'new', 'notifications']), }; @@ -33,12 +32,11 @@ const mapDispatchToProps = dispatch => { }; }; -@connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl -export default class MuteModal extends React.PureComponent { +class MuteModal extends React.PureComponent { static propTypes = { - isSubmitting: PropTypes.bool.isRequired, account: PropTypes.object.isRequired, notifications: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, @@ -81,11 +79,16 @@ export default class MuteModal extends React.PureComponent { values={{ name: <strong>@{account.get('acct')}</strong> }} /> </p> - <div> - <label htmlFor='mute-modal__hide-notifications-checkbox'> + <p className='mute-modal__explanation'> + <FormattedMessage + id='confirmations.mute.explanation' + defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.' + /> + </p> + <div className='setting-toggle'> + <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} /> + <label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'> <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' /> - {' '} - <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} /> </label> </div> </div> diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js index 4688c7766..50e7d5c48 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js @@ -2,30 +2,35 @@ import React from 'react'; import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; import Icon from 'flavours/glitch/components/icon'; -import { profile_directory } from 'flavours/glitch/util/initial_state'; +import { profile_directory, showTrends } from 'flavours/glitch/util/initial_state'; +import { preferencesLink, relationshipsLink } from 'flavours/glitch/util/backend_links'; import NotificationsCounterIcon from './notifications_counter_icon'; import FollowRequestsNavLink from './follow_requests_nav_link'; import ListPanel from './list_panel'; +import TrendsContainer from 'flavours/glitch/features/getting_started/containers/trends_container'; const NavigationPanel = ({ onOpenSettings }) => ( <div className='navigation-panel'> - <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' icon='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink> <FollowRequestsNavLink /> - <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' icon='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> - <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' icon='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> - <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' icon='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> - <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' icon='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> - <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' icon='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> + <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> + {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>} + <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> <ListPanel /> <hr /> - <a className='column-link column-link--transparent' href='/settings/preferences' target='_blank'><Icon className='column-link__icon' icon='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a> - <a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' icon='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a> - <a className='column-link column-link--transparent' href='/relationships' target='_blank'><Icon className='column-link__icon' icon='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a> - {!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>} + {!!preferencesLink && <a className='column-link column-link--transparent' href={preferencesLink} target='_blank'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>} + <a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' id='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a> + {!!relationshipsLink && <a className='column-link column-link--transparent' href={relationshipsLink} target='_blank'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>} + + {showTrends && <div className='flex-spacer' />} + {showTrends && <TrendsContainer />} </div> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js index 3db9ec77d..7419e2cd9 100644 --- a/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js @@ -159,9 +159,9 @@ const mapStateToProps = state => ({ domain: state.getIn(['meta', 'domain']), }); -@connect(mapStateToProps) +export default @connect(mapStateToProps) @injectIntl -export default class OnboardingModal extends React.PureComponent { +class OnboardingModal extends React.PureComponent { static propTypes = { onClose: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js index 8be1d5856..9016b08d7 100644 --- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js @@ -37,9 +37,9 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -@connect(makeMapStateToProps) +export default @connect(makeMapStateToProps) @injectIntl -export default class ReportModal extends ImmutablePureComponent { +class ReportModal extends ImmutablePureComponent { static propTypes = { isSubmitting: PropTypes.bool, diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js index dbd08aa2b..a67405215 100644 --- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js +++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js @@ -4,16 +4,16 @@ import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl } from 'react-intl'; import { debounce } from 'lodash'; import { isUserTouching } from 'flavours/glitch/util/is_mobile'; +import Icon from 'flavours/glitch/components/icon'; import NotificationsCounterIcon from './notifications_counter_icon'; export const links = [ - <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, + <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, - - <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, - <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, - <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, - <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>, + <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, + <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, + <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, + <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>, ]; export function getIndex (path) { @@ -24,9 +24,9 @@ export function getLink (index) { return links[index].props.to; } -@injectIntl +export default @injectIntl @withRouter -export default class TabsBar extends React.PureComponent { +class TabsBar extends React.PureComponent { static propTypes = { intl: PropTypes.object.isRequired, @@ -73,9 +73,13 @@ export default class TabsBar extends React.PureComponent { const { intl: { formatMessage } } = this.props; return ( - <nav className='tabs-bar' ref={this.setRef}> - {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} - </nav> + <div className='tabs-bar__wrapper'> + <nav className='tabs-bar' ref={this.setRef}> + {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} + </nav> + + <div id='tabs-bar__portal' /> + </div> ); } diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js index 3f742c260..e7309021e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; export default class VideoModal extends ImmutablePureComponent { @@ -28,22 +30,25 @@ export default class VideoModal extends ImmutablePureComponent { render () { const { media, status, time, onClose } = this.props; - const link = status && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>; - return ( <div className='modal-root__modal video-modal'> - <div> + <div className='video-modal__container'> <Video preview={media.get('preview_url')} blurhash={media.get('blurhash')} src={media.get('url')} startTime={time} onCloseVideo={onClose} - link={link} 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> ); } diff --git a/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js b/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js index 283aa2373..82278a3be 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js @@ -11,7 +11,7 @@ const mapStateToProps = (state, { intl }) => { const value = notification[key]; if (typeof value === 'object') { - notification[key] = intl.formatMessage(value); + notification[key] = intl.formatMessage(value, notification[`${key}_values`]); } })); diff --git a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js index e0c017f82..c01d0e5bc 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js @@ -1,24 +1,31 @@ import { connect } from 'react-redux'; import StatusList from 'flavours/glitch/components/status_list'; -import { scrollTopTimeline } from 'flavours/glitch/actions/timelines'; +import { scrollTopTimeline, loadPending } from 'flavours/glitch/actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; import { me } from 'flavours/glitch/util/initial_state'; -const makeGetStatusIds = () => createSelector([ - (state, { type }) => state.getIn(['settings', type], ImmutableMap()), - (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), - (state) => state.get('statuses'), -], (columnSettings, statusIds, statuses) => { - const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim(); - let regex = null; +const getRegex = createSelector([ + (state, { type }) => state.getIn(['settings', type, 'regex', 'body']), +], (rawRegex) => { + let regex = null; try { - regex = rawRegex && new RegExp(rawRegex, 'i'); + regex = rawRegex && new RegExp(rawRegex.trim(), 'i'); } catch (e) { // Bad regex, don't affect filters } + return regex; +}); + +const makeGetStatusIds = (pending = false) => createSelector([ + (state, { type }) => state.getIn(['settings', type], ImmutableMap()), + (state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()), + (state) => state.get('statuses'), + getRegex, +], (columnSettings, statusIds, statuses, regex) => { + const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim(); return statusIds.filter(id => { if (id === null) return true; @@ -49,12 +56,14 @@ const makeGetStatusIds = () => createSelector([ const makeMapStateToProps = () => { const getStatusIds = makeGetStatusIds(); + const getPendingStatusIds = makeGetStatusIds(true); const mapStateToProps = (state, { timelineId }) => ({ statusIds: getStatusIds(state, { type: timelineId }), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), + numPending: getPendingStatusIds(state, { type: timelineId }).size, }); return mapStateToProps; @@ -70,6 +79,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({ dispatch(scrollTopTimeline(timelineId, false)); }, 100), + onLoadPending: () => dispatch(loadPending(timelineId)), + }); export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 787488db4..5c861fdee 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -12,8 +12,10 @@ import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications'; import { fetchFilters } from 'flavours/glitch/actions/filters'; import { clearHeight } from 'flavours/glitch/actions/height_cache'; +import { submitMarkers } from 'flavours/glitch/actions/markers'; import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers'; import UploadArea from './components/upload_area'; +import PermaLink from 'flavours/glitch/components/permalink'; import ColumnsAreaContainer from './containers/columns_area_container'; import classNames from 'classnames'; import Favico from 'favico.js'; @@ -46,10 +48,11 @@ import { Lists, Search, GettingStartedMisc, + Directory, } from 'flavours/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; import { me } from 'flavours/glitch/util/initial_state'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; // Dummy import, to make sure that <Status /> ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. @@ -62,6 +65,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, + canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4, layout: state.getIn(['local_settings', 'layout']), isWide: state.getIn(['local_settings', 'stretch']), navbarUnder: state.getIn(['local_settings', 'navbar_under']), @@ -69,6 +73,7 @@ const mapStateToProps = state => ({ unreadNotifications: state.getIn(['notifications', 'unread']), showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']), hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']), + moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]), }); const keyMap = { @@ -102,12 +107,137 @@ const keyMap = { bookmark: 'd', toggleCollapse: 'shift+x', toggleSensitive: 'h', + openMedia: 'e', }; -@connect(mapStateToProps) +class SwitchingColumnsArea extends React.PureComponent { + + static propTypes = { + children: PropTypes.node, + layout: PropTypes.string, + location: PropTypes.object, + navbarUnder: PropTypes.bool, + onLayoutChange: PropTypes.func.isRequired, + }; + + state = { + mobile: isMobile(window.innerWidth, this.props.layout), + }; + + componentWillReceiveProps (nextProps) { + if (nextProps.layout !== this.props.layout) { + this.setState({ mobile: isMobile(window.innerWidth, nextProps.layout) }); + } + } + + componentWillMount () { + window.addEventListener('resize', this.handleResize, { passive: true }); + + if (this.state.mobile) { + document.body.classList.toggle('layout-single-column', true); + document.body.classList.toggle('layout-multiple-columns', false); + } else { + document.body.classList.toggle('layout-single-column', false); + document.body.classList.toggle('layout-multiple-columns', true); + } + } + + componentDidUpdate (prevProps, prevState) { + if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { + this.node.handleChildrenContentChange(); + } + + if (prevState.mobile !== this.state.mobile) { + document.body.classList.toggle('layout-single-column', this.state.mobile); + document.body.classList.toggle('layout-multiple-columns', !this.state.mobile); + } + } + + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + } + + handleLayoutChange = debounce(() => { + // The cached heights are no longer accurate, invalidate + this.props.onLayoutChange(); + }, 500, { + trailing: true, + }) + + handleResize = () => { + const mobile = isMobile(window.innerWidth, this.props.layout); + + 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(); + } + } + + render () { + const { children, navbarUnder } = this.props; + const singleColumn = this.state.mobile; + const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; + + return ( + <ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn} navbarUnder={navbarUnder}> + <WrappedSwitch> + {redirect} + <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> + <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> + <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> + <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> + <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} /> + <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} /> + <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> + <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} /> + + <WrappedRoute path='/notifications' component={Notifications} content={children} /> + <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> + <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> + <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> + + <WrappedRoute path='/search' component={Search} content={children} /> + <WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> + + <WrappedRoute path='/statuses/new' component={Compose} content={children} /> + <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> + <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> + <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> + + <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> + <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> + <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> + <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> + <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> + + <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> + <WrappedRoute path='/blocks' component={Blocks} content={children} /> + <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} /> + <WrappedRoute path='/mutes' component={Mutes} content={children} /> + <WrappedRoute path='/lists' component={Lists} content={children} /> + <WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} /> + + <WrappedRoute component={GenericNotFound} content={children} /> + </WrappedSwitch> + </ColumnsAreaContainer> + ); + }; + +} + +export default @connect(mapStateToProps) @injectIntl @withRouter -export default class UI extends React.Component { +class UI extends React.Component { static propTypes = { dispatch: PropTypes.func.isRequired, @@ -119,6 +249,7 @@ export default class UI extends React.Component { isComposing: PropTypes.bool, hasComposingText: PropTypes.bool, hasMediaAttachments: PropTypes.bool, + canUploadMore: PropTypes.bool, match: PropTypes.object.isRequired, location: PropTypes.object.isRequired, history: PropTypes.object.isRequired, @@ -126,15 +257,17 @@ export default class UI extends React.Component { dropdownMenuIsOpen: PropTypes.bool, unreadNotifications: PropTypes.number, showFaviconBadge: PropTypes.bool, + moved: PropTypes.map, }; state = { - width: window.innerWidth, draggingOver: false, }; handleBeforeUnload = (e) => { - const { intl, hasComposingText, hasMediaAttachments } = this.props; + const { intl, dispatch, hasComposingText, hasMediaAttachments } = this.props; + + dispatch(submitMarkers()); if (hasComposingText || hasMediaAttachments) { // Setting returnValue to any string causes confirmation dialog. @@ -144,14 +277,10 @@ export default class UI extends React.Component { } } - handleResize = debounce(() => { + handleLayoutChange = () => { // The cached heights are no longer accurate, invalidate this.props.dispatch(clearHeight()); - - this.setState({ width: window.innerWidth }); - }, 500, { - trailing: true, - }); + } handleDragEnter = (e) => { e.preventDefault(); @@ -164,7 +293,7 @@ export default class UI extends React.Component { this.dragTargets.push(e.target); } - if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer && e.dataTransfer.types.includes('Files') && this.props.canUploadMore) { this.setState({ draggingOver: true }); } } @@ -185,12 +314,13 @@ export default class UI extends React.Component { handleDrop = (e) => { if (this.dataTransferIsText(e.dataTransfer)) return; + e.preventDefault(); this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length >= 1) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } } @@ -209,7 +339,7 @@ export default class UI extends React.Component { } dataTransferIsText = (dataTransfer) => { - return (dataTransfer && Array.from(dataTransfer.types).includes('text/plain') && dataTransfer.items.length === 1); + return (dataTransfer && Array.from(dataTransfer.types).filter((type) => type === 'text/plain').length === 1); } closeUploadModal = () => { @@ -246,7 +376,6 @@ export default class UI extends React.Component { } 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); document.addEventListener('drop', this.handleDrop, false); @@ -271,13 +400,14 @@ export default class UI extends React.Component { } componentDidUpdate (prevProps) { - if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { - this.columnsAreaNode.handleChildrenContentChange(); - } if (this.props.unreadNotifications != prevProps.unreadNotifications || this.props.showFaviconBadge != prevProps.showFaviconBadge) { if (this.favicon) { - this.favicon.badge(this.props.showFaviconBadge ? this.props.unreadNotifications : 0); + try { + this.favicon.badge(this.props.showFaviconBadge ? this.props.unreadNotifications : 0); + } catch (err) { + console.error(err); + } } } } @@ -288,7 +418,6 @@ export default class UI extends React.Component { } window.removeEventListener('beforeunload', this.handleBeforeUnload); - window.removeEventListener('resize', this.handleResize); document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('drop', this.handleDrop); @@ -300,10 +429,6 @@ export default class UI extends React.Component { this.node = c; } - setColumnsAreaRef = c => { - this.columnsAreaNode = c.getWrappedInstance(); - } - handleHotkeyNew = e => { e.preventDefault(); @@ -317,7 +442,7 @@ export default class UI extends React.Component { handleHotkeySearch = e => { e.preventDefault(); - const element = this.node.querySelector('.drawer--search input'); + const element = this.node.querySelector('.search__input'); if (element) { element.focus(); @@ -417,10 +542,8 @@ export default class UI extends React.Component { } render () { - const { width, draggingOver } = this.state; - const { children, layout, isWide, navbarUnder, dropdownMenuIsOpen } = this.props; - const singleColumn = isMobile(width, layout); - const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; + const { draggingOver } = this.state; + const { children, layout, isWide, navbarUnder, location, dropdownMenuIsOpen, moved } = this.props; const columnsClass = layout => { switch (layout) { @@ -464,45 +587,20 @@ export default class UI extends React.Component { return ( <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused> <div className={className} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}> - <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={singleColumn} navbarUnder={navbarUnder}> - <WrappedSwitch> - {redirect} - <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> - <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> - <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> - <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> - <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} /> - <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} /> - <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> - <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} /> - <WrappedRoute path='/notifications' component={Notifications} content={children} /> - <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> - <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> - <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> - - <WrappedRoute path='/search' component={Search} content={children} /> - - <WrappedRoute path='/statuses/new' component={Compose} content={children} /> - <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> - <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> - <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> - - <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> - <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> - <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> - <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> - <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> - - <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> - <WrappedRoute path='/blocks' component={Blocks} content={children} /> - <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} /> - <WrappedRoute path='/mutes' component={Mutes} content={children} /> - <WrappedRoute path='/lists' component={Lists} content={children} /> - <WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} /> - - <WrappedRoute component={GenericNotFound} content={children} /> - </WrappedSwitch> - </ColumnsAreaContainer> + {moved && (<div className='flash-message alert'> + <FormattedMessage + id='moved_to_warning' + defaultMessage='This account is marked as moved to {moved_to_link}, and may thus not accept new follows.' + values={{ moved_to_link: ( + <PermaLink href={moved.get('url')} to={`/accounts/${moved.get('id')}`}> + @{moved.get('acct')} + </PermaLink> + )}} + /> + </div>)} + <SwitchingColumnsArea location={location} layout={layout} navbarUnder={navbarUnder} onLayoutChange={this.handleLayoutChange}> + {children} + </SwitchingColumnsArea> <NotificationsContainer /> <LoadingBarContainer className='loading-bar' /> diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index b73ea0b07..049baaee7 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -5,7 +5,8 @@ import { fromJS, is } from 'immutable'; import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen'; -import { displayMedia } from 'flavours/glitch/util/initial_state'; +import { displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state'; +import Icon from 'flavours/glitch/components/icon'; import { decode } from 'blurhash'; const messages = defineMessages({ @@ -18,9 +19,10 @@ const messages = defineMessages({ close: { id: 'video.close', defaultMessage: 'Close video' }, fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, + download: { id: 'video.download', defaultMessage: 'Download file' }, }); -const formatTime = secondsNum => { +export const formatTime = secondsNum => { let hours = Math.floor(secondsNum / 3600); let minutes = Math.floor((secondsNum - (hours * 3600)) / 60); let seconds = secondsNum - (hours * 3600) - (minutes * 60); @@ -84,8 +86,8 @@ export const getPointerPosition = (el, event) => { return position; }; -@injectIntl -export default class Video extends React.PureComponent { +export default @injectIntl +class Video extends React.PureComponent { static propTypes = { preview: PropTypes.string, @@ -101,6 +103,7 @@ export default class Video extends React.PureComponent { fullwidth: PropTypes.bool, detailed: PropTypes.bool, inline: PropTypes.bool, + editable: PropTypes.bool, cacheWidth: PropTypes.func, intl: PropTypes.object.isRequired, visible: PropTypes.bool, @@ -312,7 +315,7 @@ export default class Video extends React.PureComponent { } _decode () { - if (!this.canvas) return; + if (!this.canvas || !useBlurhash) return; const hash = this.props.blurhash; const pixels = decode(hash, 32, 32); @@ -393,7 +396,7 @@ export default class Video extends React.PureComponent { } render () { - const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link } = this.props; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link, editable } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; const playerStyle = {}; @@ -401,7 +404,7 @@ export default class Video extends React.PureComponent { const volumeWidth = (muted) ? 0 : volume * this.volWidth; const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume); - const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, letterbox, 'full-width': fullwidth }); + const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth }); let { width, height } = this.props; @@ -443,7 +446,7 @@ export default class Video extends React.PureComponent { > <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} /> - {revealed && <video + {(revealed || editable) && <video ref={this.setVideoRef} src={src} poster={preview} @@ -465,7 +468,7 @@ export default class Video extends React.PureComponent { onVolumeChange={this.handleVolumeChange} />} - <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}> + <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> <span className='spoiler-button__overlay__label'>{warning}</span> </button> @@ -485,9 +488,10 @@ export default class Video extends React.PureComponent { <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button> - <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button> + <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> + <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> <span className={classNames('video-player__volume__handle')} @@ -508,10 +512,16 @@ export default class Video extends React.PureComponent { </div> <div className='video-player__buttons right'> - {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye-slash' /></button>} - {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} - {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>} - <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> + {(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} + {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} + {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} + <button type='button' aria-label={intl.formatMessage(messages.download)}> + <a className='video-player__download__icon' href={this.props.src} download> + <Icon id={'download'} fixedWidth /> + </a> + </button> + <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> + </div> </div> </div> diff --git a/app/javascript/flavours/glitch/locales/pt.js b/app/javascript/flavours/glitch/locales/pt-PT.js index 0156f55ff..cf7afd17a 100644 --- a/app/javascript/flavours/glitch/locales/pt.js +++ b/app/javascript/flavours/glitch/locales/pt-PT.js @@ -1,4 +1,4 @@ -import inherited from 'mastodon/locales/pt.json'; +import inherited from 'mastodon/locales/pt-PT.json'; const messages = { // No translations available. diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index da0b4c8e0..973d6ee46 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -1,19 +1,21 @@ import loadPolyfills from 'flavours/glitch/util/load_polyfills'; import ready from 'flavours/glitch/util/ready'; +import loadKeyboardExtensions from 'flavours/glitch/util/load_keyboard_extensions'; function main() { const IntlMessageFormat = require('intl-messageformat').default; const { timeAgoString } = require('flavours/glitch/components/relative_timestamp'); + const { delegate } = require('rails-ujs'); const emojify = require('flavours/glitch/util/emoji').default; const { getLocale } = require('locales'); const { messages } = getLocale(); const React = require('react'); const ReactDOM = require('react-dom'); const Rellax = require('rellax'); - const createHistory = require('history').createBrowserHistory; + const { createBrowserHistory } = require('history'); const scrollToDetailedStatus = () => { - const history = createHistory(); + const history = createBrowserHistory(); const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status'); const location = history.location; @@ -23,6 +25,12 @@ function main() { } }; + const getEmojiAnimationHandler = (swapTo) => { + return ({ target }) => { + target.src = target.getAttribute(swapTo); + }; + }; + ready(() => { const locale = document.documentElement.lang; @@ -87,16 +95,33 @@ function main() { new Rellax('.parallax', { speed: -1 }); } - if (document.body.classList.contains('with-modals')) { - const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; - const scrollbarWidthStyle = document.createElement('style'); - scrollbarWidthStyle.id = 'scrollbar-width'; - document.head.appendChild(scrollbarWidthStyle); - scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0); + delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original')); + delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static')); + + delegate(document, '.blocks-table button.icon-button', 'click', function(e) { + e.preventDefault(); + + const classList = this.firstElementChild.classList; + classList.toggle('fa-chevron-down'); + classList.toggle('fa-chevron-up'); + this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden'); + }); + }); + + delegate(document, '.sidebar__toggle__icon', 'click', () => { + const target = document.querySelector('.sidebar ul'); + + if (target.style.display === 'block') { + target.style.display = 'none'; + } else { + target.style.display = 'block'; } }); } -loadPolyfills().then(main).catch(error => { - console.error(error); -}); +loadPolyfills() + .then(main) + .then(loadKeyboardExtensions) + .catch(error => { + console.error(error); + }); diff --git a/app/javascript/flavours/glitch/packs/settings.js b/app/javascript/flavours/glitch/packs/settings.js new file mode 100644 index 000000000..edf1b82e0 --- /dev/null +++ b/app/javascript/flavours/glitch/packs/settings.js @@ -0,0 +1,24 @@ +import loadPolyfills from 'flavours/glitch/util/load_polyfills'; +import ready from 'flavours/glitch/util/ready'; +import loadKeyboardExtensions from 'flavours/glitch/util/load_keyboard_extensions'; + +function main() { + const { delegate } = require('rails-ujs'); + + delegate(document, '.sidebar__toggle__icon', 'click', () => { + const target = document.querySelector('.sidebar ul'); + + if (target.style.display === 'block') { + target.style.display = 'none'; + } else { + target.style.display = 'block'; + } + }); +} + +loadPolyfills() + .then(main) + .then(loadKeyboardExtensions) + .catch(error => { + console.error(error); + }); diff --git a/app/javascript/flavours/glitch/reducers/alerts.js b/app/javascript/flavours/glitch/reducers/alerts.js index 50f8d30f7..ee3d54ab0 100644 --- a/app/javascript/flavours/glitch/reducers/alerts.js +++ b/app/javascript/flavours/glitch/reducers/alerts.js @@ -14,6 +14,7 @@ export default function alerts(state = initialState, action) { key: state.size > 0 ? state.last().get('key') + 1 : 0, title: action.title, message: action.message, + message_values: action.message_values, })); case ALERT_DISMISS: return state.filterNot(item => item.get('key') === action.alert.key); diff --git a/app/javascript/flavours/glitch/reducers/blocks.js b/app/javascript/flavours/glitch/reducers/blocks.js new file mode 100644 index 000000000..1b6507163 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/blocks.js @@ -0,0 +1,22 @@ +import Immutable from 'immutable'; + +import { + BLOCKS_INIT_MODAL, +} from '../actions/blocks'; + +const initialState = Immutable.Map({ + new: Immutable.Map({ + account_id: null, + }), +}); + +export default function mutes(state = initialState, action) { + switch (action.type) { + case BLOCKS_INIT_MODAL: + return state.withMutations((state) => { + state.setIn(['new', 'account_id'], action.account.get('id')); + }); + default: + return state; + } +} diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 36dfb8f15..0f807790b 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -78,6 +78,7 @@ const initialState = ImmutableMap({ is_changing_upload: false, progress: 0, media_attachments: ImmutableList(), + pending_media_attachments: 0, poll: null, suggestion_token: null, suggestions: ImmutableList(), @@ -182,6 +183,7 @@ function continueThread (state, status) { map.set('privacy', status.visibility); map.set('sensitive', false); map.update('media_attachments', list => list.clear()); + map.set('poll', null); map.set('idempotencyKey', uuid()); map.set('focusDate', new Date()); map.set('caretPosition', null); @@ -189,14 +191,18 @@ function continueThread (state, status) { }); } -function appendMedia(state, media) { +function appendMedia(state, media, file) { const prevSize = state.get('media_attachments').size; return state.withMutations(map => { + if (media.get('type') === 'image') { + media = media.set('file', file); + } map.update('media_attachments', list => list.push(media)); map.set('is_uploading', false); map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); map.set('idempotencyKey', uuid()); + map.update('pending_media_attachments', n => n - 1); if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { map.set('sensitive', true); @@ -230,15 +236,20 @@ const insertSuggestion = (state, position, token, completion, path) => { }); }; -const updateSuggestionTags = (state, token) => { - const prefix = token.slice(1); +const sortHashtagsByUse = (state, tags) => { + const personalHistory = state.get('tagHistory'); - return state.merge({ - suggestions: state.get('tagHistory') - .filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase())) - .slice(0, 4) - .map(tag => '#' + tag), - suggestion_token: token, + return tags.sort((a, b) => { + const usedA = personalHistory.includes(a.name); + const usedB = personalHistory.includes(b.name); + + if (usedA === usedB) { + return 0; + } else if (usedA && !usedB) { + return 1; + } else { + return -1; + } }); }; @@ -275,6 +286,42 @@ const expandMentions = status => { return fragment.innerHTML; }; +const expiresInFromExpiresAt = expires_at => { + if (!expires_at) return 24 * 3600; + const delta = (new Date(expires_at).getTime() - Date.now()) / 1000; + return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600; +}; + +const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => { + prefix = prefix.toLowerCase(); + if (suggestions.length < 4) { + const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase())); + return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag }))); + } else { + return suggestions; + } +}; + +const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => { + if (accounts) { + return accounts.map(item => ({ id: item.id, type: 'account' })); + } else if (emojis) { + return emojis.map(item => ({ ...item, type: 'emoji' })); + } else { + return mergeLocalHashtagResults(sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' }))), token.slice(1), state.get('tagHistory')); + } +}; + +const updateSuggestionTags = (state, token) => { + const prefix = token.slice(1); + + const suggestions = state.get('suggestions').toJS(); + return state.merge({ + suggestions: ImmutableList(mergeLocalHashtagResults(suggestions, prefix, state.get('tagHistory'))), + suggestion_token: token, + }); +}; + export default function compose(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: @@ -378,11 +425,11 @@ export default function compose(state = initialState, action) { case COMPOSE_UPLOAD_CHANGE_FAIL: return state.set('is_changing_upload', false); case COMPOSE_UPLOAD_REQUEST: - return state.set('is_uploading', true); + return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1); case COMPOSE_UPLOAD_SUCCESS: - return appendMedia(state, fromJS(action.media)); + return appendMedia(state, fromJS(action.media), action.file); case COMPOSE_UPLOAD_FAIL: - return state.set('is_uploading', false); + return state.set('is_uploading', false).update('pending_media_attachments', n => n - 1); case COMPOSE_UPLOAD_UNDO: return removeMedia(state, action.media_id); case COMPOSE_UPLOAD_PROGRESS: @@ -405,7 +452,7 @@ export default function compose(state = initialState, action) { case COMPOSE_SUGGESTIONS_CLEAR: return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); + return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion, action.path); case COMPOSE_SUGGESTION_TAGS_UPDATE: @@ -456,7 +503,7 @@ export default function compose(state = initialState, action) { map.set('poll', ImmutableMap({ options: action.status.getIn(['poll', 'options']).map(x => x.get('title')), multiple: action.status.getIn(['poll', 'multiple']), - expires_in: 24 * 3600, + expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])), })); } }); diff --git a/app/javascript/flavours/glitch/reducers/conversations.js b/app/javascript/flavours/glitch/reducers/conversations.js index c01659da5..fba0308bc 100644 --- a/app/javascript/flavours/glitch/reducers/conversations.js +++ b/app/javascript/flavours/glitch/reducers/conversations.js @@ -7,7 +7,10 @@ import { CONVERSATIONS_FETCH_FAIL, CONVERSATIONS_UPDATE, CONVERSATIONS_READ, + CONVERSATIONS_DELETE_SUCCESS, } from '../actions/conversations'; +import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'flavours/glitch/actions/accounts'; +import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks'; import compareId from 'flavours/glitch/util/compare_id'; const initialState = ImmutableMap({ @@ -74,6 +77,10 @@ const expandNormalizedConversations = (state, conversations, next, isLoadingRece }); }; +const filterConversations = (state, accountIds) => { + return state.update('items', list => list.filterNot(item => item.get('accounts').some(accountId => accountIds.includes(accountId)))); +}; + export default function conversations(state = initialState, action) { switch (action.type) { case CONVERSATIONS_FETCH_REQUEST: @@ -96,6 +103,13 @@ export default function conversations(state = initialState, action) { return item; })); + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return filterConversations(state, [action.relationship.id]); + case DOMAIN_BLOCK_SUCCESS: + return filterConversations(state, action.accounts); + case CONVERSATIONS_DELETE_SUCCESS: + return state.update('items', list => list.filterNot(item => item.get('id') === action.id)); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index 266d87dc1..7dbca3a29 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -16,6 +16,7 @@ import local_settings from './local_settings'; import push_notifications from './push_notifications'; import status_lists from './status_lists'; import mutes from './mutes'; +import blocks from './blocks'; import reports from './reports'; import contexts from './contexts'; import compose from './compose'; @@ -33,6 +34,7 @@ import suggestions from './suggestions'; import pinnedAccountsEditor from './pinned_accounts_editor'; import polls from './polls'; import identity_proofs from './identity_proofs'; +import trends from './trends'; const reducers = { dropdown_menu, @@ -52,6 +54,7 @@ const reducers = { local_settings, push_notifications, mutes, + blocks, reports, contexts, compose, @@ -69,6 +72,7 @@ const reducers = { suggestions, pinnedAccountsEditor, polls, + trends, }; export default combineReducers(reducers); diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index 68e1c8424..ad94ea243 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -21,6 +21,9 @@ const initialState = ImmutableMap({ inline_preview_cards: true, hicolor_privacy_icons: false, show_content_type_choice: false, + filtering_behavior: 'hide', + tag_misleading_links: true, + rewrite_mentions: 'no', content_warnings : ImmutableMap({ auto_unfold : false, filter : null, diff --git a/app/javascript/flavours/glitch/reducers/modal.js b/app/javascript/flavours/glitch/reducers/modal.js index 80bc11dda..7bd9d4b32 100644 --- a/app/javascript/flavours/glitch/reducers/modal.js +++ b/app/javascript/flavours/glitch/reducers/modal.js @@ -10,7 +10,7 @@ export default function modal(state = initialState, action) { case MODAL_OPEN: return { modalType: action.modalType, modalProps: action.modalProps }; case MODAL_CLOSE: - return initialState; + return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state; default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/mutes.js b/app/javascript/flavours/glitch/reducers/mutes.js index 8f52a7704..7111bb710 100644 --- a/app/javascript/flavours/glitch/reducers/mutes.js +++ b/app/javascript/flavours/glitch/reducers/mutes.js @@ -7,7 +7,6 @@ import { const initialState = Immutable.Map({ new: Immutable.Map({ - isSubmitting: false, account: null, notifications: true, }), @@ -17,7 +16,6 @@ export default function mutes(state = initialState, action) { switch (action.type) { case MUTES_INIT_MODAL: return state.withMutations((state) => { - state.setIn(['new', 'isSubmitting'], false); state.setIn(['new', 'account'], action.account); state.setIn(['new', 'notifications'], true); }); diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index 5bbf9c822..3623e90da 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -9,6 +9,7 @@ import { NOTIFICATIONS_FILTER_SET, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, + NOTIFICATIONS_LOAD_PENDING, NOTIFICATIONS_DELETE_MARKED_REQUEST, NOTIFICATIONS_DELETE_MARKED_SUCCESS, NOTIFICATION_MARK_FOR_DELETE, @@ -19,12 +20,16 @@ import { import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, + FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + FOLLOW_REQUEST_REJECT_SUCCESS, } from 'flavours/glitch/actions/accounts'; +import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'flavours/glitch/util/compare_id'; const initialState = ImmutableMap({ + pendingItems: ImmutableList(), items: ImmutableList(), hasMore: true, top: false, @@ -46,9 +51,13 @@ const notificationToMap = (state, notification) => ImmutableMap({ status: notification.status ? notification.status.id : null, }); -const normalizeNotification = (state, notification) => { +const normalizeNotification = (state, notification, usePendingItems) => { const top = !shouldCountUnreadNotifications(state); + if (usePendingItems || !state.get('pendingItems').isEmpty()) { + return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))).update('unread', unread => unread + 1); + } + if (top) { state = state.set('lastReadId', notification.id); } else { @@ -64,7 +73,7 @@ const normalizeNotification = (state, notification) => { }); }; -const expandNormalizedNotifications = (state, notifications, next) => { +const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => { const top = !(shouldCountUnreadNotifications(state)); const lastReadId = state.get('lastReadId'); let items = ImmutableList(); @@ -75,7 +84,9 @@ const expandNormalizedNotifications = (state, notifications, next) => { return state.withMutations(mutable => { if (!items.isEmpty()) { - mutable.update('items', list => { + usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty()); + + mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) ); @@ -104,12 +115,13 @@ const expandNormalizedNotifications = (state, notifications, next) => { }); }; -const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); +const filterNotifications = (state, accountIds, type) => { + const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type'))); + return state.update('items', helper).update('pendingItems', helper); }; const clearUnread = (state) => { - state = state.set('unread', 0); + state = state.set('unread', state.get('pendingItems').size); const lastNotification = state.get('items').find(item => item !== null); return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0'); } @@ -131,7 +143,10 @@ const deleteByStatus = (state, statusId) => { const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); state = state.update('unread', unread => unread - deletedUnread.size); } - return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); + const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); + state = state.update('unread', unread => unread - deletedUnread.size); + return state.update('items', helper).update('pendingItems', helper); }; const markForDelete = (state, notificationId, yes) => { @@ -192,6 +207,8 @@ export default function notifications(state = initialState, action) { return state.update('mounted', count => count - 1); case NOTIFICATIONS_SET_VISIBILITY: return updateVisibility(state, action.visibility); + case NOTIFICATIONS_LOAD_PENDING: + return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); case NOTIFICATIONS_EXPAND_REQUEST: case NOTIFICATIONS_DELETE_MARKED_REQUEST: return state.set('isLoading', true); @@ -203,20 +220,27 @@ export default function notifications(state = initialState, action) { case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification); + return normalizeNotification(state, action.notification, action.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next); + return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingRecent, action.usePendingItems); case ACCOUNT_BLOCK_SUCCESS: - return filterNotifications(state, action.relationship); + return filterNotifications(state, [action.relationship.id]); + case ACCOUNT_MUTE_SUCCESS: + return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state; + case DOMAIN_BLOCK_SUCCESS: + return filterNotifications(state, action.accounts); + case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: + case FOLLOW_REQUEST_REJECT_SUCCESS: + return filterNotifications(state, [action.id], 'follow_request'); case ACCOUNT_MUTE_SUCCESS: - return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state; + return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state; case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('hasMore', false); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); case TIMELINE_DELETE: return deleteByStatus(state, action.id); case TIMELINE_DISCONNECT: return action.timeline === 'home' ? - state.update('items', items => items.first() ? items.unshift(null) : items) : + state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; case NOTIFICATION_MARK_FOR_DELETE: diff --git a/app/javascript/flavours/glitch/reducers/polls.js b/app/javascript/flavours/glitch/reducers/polls.js index 9956cf83f..595f340bc 100644 --- a/app/javascript/flavours/glitch/reducers/polls.js +++ b/app/javascript/flavours/glitch/reducers/polls.js @@ -1,4 +1,4 @@ -import { POLLS_IMPORT } from 'mastodon/actions/importer'; +import { POLLS_IMPORT } from 'flavours/glitch/actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll)))); diff --git a/app/javascript/flavours/glitch/reducers/push_notifications.js b/app/javascript/flavours/glitch/reducers/push_notifications.js index e87e8fc1a..117fb5167 100644 --- a/app/javascript/flavours/glitch/reducers/push_notifications.js +++ b/app/javascript/flavours/glitch/reducers/push_notifications.js @@ -6,6 +6,7 @@ const initialState = Immutable.Map({ subscription: null, alerts: new Immutable.Map({ follow: false, + follow_request: false, favourite: false, reblog: false, mention: false, diff --git a/app/javascript/flavours/glitch/reducers/reports.js b/app/javascript/flavours/glitch/reducers/reports.js index fdcfb14a0..1f7f3f273 100644 --- a/app/javascript/flavours/glitch/reducers/reports.js +++ b/app/javascript/flavours/glitch/reducers/reports.js @@ -8,6 +8,9 @@ import { REPORT_COMMENT_CHANGE, REPORT_FORWARD_CHANGE, } from 'flavours/glitch/actions/reports'; +import { + TIMELINE_DELETE, +} from 'flavours/glitch/actions/timelines'; import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable'; const initialState = ImmutableMap({ @@ -20,6 +23,14 @@ const initialState = ImmutableMap({ }), }); +const deleteStatus = (state, id, references) => { + references.forEach(ref => { + state = deleteStatus(state, ref[0], []); + }); + + return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.remove(id)); +}; + export default function reports(state = initialState, action) { switch(action.type) { case REPORT_INIT: @@ -58,6 +69,8 @@ export default function reports(state = initialState, action) { map.setIn(['new', 'comment'], ''); map.setIn(['new', 'isSubmitting'], false); }); + case TIMELINE_DELETE: + return deleteStatus(state, action.id, action.references); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/search.js b/app/javascript/flavours/glitch/reducers/search.js index 9a525bf47..c346e958b 100644 --- a/app/javascript/flavours/glitch/reducers/search.js +++ b/app/javascript/flavours/glitch/reducers/search.js @@ -3,6 +3,7 @@ import { SEARCH_CLEAR, SEARCH_FETCH_SUCCESS, SEARCH_SHOW, + SEARCH_EXPAND_SUCCESS, } from 'flavours/glitch/actions/search'; import { COMPOSE_MENTION, @@ -16,6 +17,7 @@ const initialState = ImmutableMap({ submitted: false, hidden: false, results: ImmutableMap(), + searchTerm: '', }); export default function search(state = initialState, action) { @@ -40,7 +42,10 @@ export default function search(state = initialState, action) { accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), hashtags: fromJS(action.results.hashtags), - })).set('submitted', true); + })).set('submitted', true).set('searchTerm', action.searchTerm); + case SEARCH_EXPAND_SUCCESS: + const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id); + return state.updateIn(['results', action.searchType], list => list.concat(results)); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index a37863a69..ef99ad552 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -15,6 +15,10 @@ const initialState = ImmutableMap({ skinTone: 1, + trends: ImmutableMap({ + show: true, + }), + home: ImmutableMap({ shows: ImmutableMap({ reblog: true, @@ -30,6 +34,7 @@ const initialState = ImmutableMap({ notifications: ImmutableMap({ alerts: ImmutableMap({ follow: true, + follow_request: false, favourite: true, reblog: true, mention: true, @@ -44,6 +49,7 @@ const initialState = ImmutableMap({ shows: ImmutableMap({ follow: true, + follow_request: false, favourite: true, reblog: true, mention: true, @@ -52,6 +58,7 @@ const initialState = ImmutableMap({ sounds: ImmutableMap({ follow: true, + follow_request: false, favourite: true, reblog: true, mention: true, diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 96c9c6d04..ee8ac929d 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -3,6 +3,7 @@ import { REBLOG_FAIL, FAVOURITE_REQUEST, FAVOURITE_FAIL, + UNFAVOURITE_SUCCESS, BOOKMARK_REQUEST, BOOKMARK_FAIL, } from 'flavours/glitch/actions/interactions'; @@ -39,6 +40,9 @@ export default function statuses(state = initialState, action) { return importStatuses(state, action.statuses); case FAVOURITE_REQUEST: return state.setIn([action.status.get('id'), 'favourited'], true); + case UNFAVOURITE_SUCCESS: + const favouritesCount = action.status.get('favourites_count'); + return state.setIn([action.status.get('id'), 'favourites_count'], favouritesCount - 1); case FAVOURITE_FAIL: return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false); case BOOKMARK_REQUEST: diff --git a/app/javascript/flavours/glitch/reducers/suggestions.js b/app/javascript/flavours/glitch/reducers/suggestions.js index 9f4b89d58..a08fedc25 100644 --- a/app/javascript/flavours/glitch/reducers/suggestions.js +++ b/app/javascript/flavours/glitch/reducers/suggestions.js @@ -4,6 +4,8 @@ import { SUGGESTIONS_FETCH_FAIL, SUGGESTIONS_DISMISS, } from '../actions/suggestions'; +import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'flavours/glitch/actions/accounts'; +import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ @@ -24,6 +26,11 @@ export default function suggestionsReducer(state = initialState, action) { return state.set('isLoading', false); case SUGGESTIONS_DISMISS: return state.update('items', list => list.filterNot(id => id === action.id)); + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return state.update('items', list => list.filterNot(id => id === action.relationship.id)); + case DOMAIN_BLOCK_SUCCESS: + return state.update('items', list => list.filterNot(id => action.accounts.includes(id))); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index 440b370e6..d3318f8d3 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -8,6 +8,7 @@ import { TIMELINE_SCROLL_TOP, TIMELINE_CONNECT, TIMELINE_DISCONNECT, + TIMELINE_LOAD_PENDING, } from 'flavours/glitch/actions/timelines'; import { ACCOUNT_BLOCK_SUCCESS, @@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({ top: true, isLoading: false, hasMore: true, + pendingItems: ImmutableList(), items: ImmutableList(), }); -const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => { +const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); mMap.set('isPartial', isPartial); @@ -38,7 +40,9 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is if (timeline.endsWith(':pinned')) { mMap.set('items', statuses.map(status => status.get('id'))); } else if (!statuses.isEmpty()) { - mMap.update('items', ImmutableList(), oldIds => { + usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty()); + + mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0); @@ -56,8 +60,23 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is })); }; -const updateTimeline = (state, timeline, status) => { - const top = state.getIn([timeline, 'top']); +const updateTimeline = (state, timeline, status, usePendingItems, filtered) => { + const top = state.getIn([timeline, 'top']); + + if (usePendingItems || !state.getIn([timeline, 'pendingItems']).isEmpty()) { + if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { + return state; + } + + state = state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id')))); + + if (!filtered) { + state = state.update('unread', unread => unread + 1); + } + + return state; + } + const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); const unread = state.getIn([timeline, 'unread'], 0); @@ -69,7 +88,7 @@ const updateTimeline = (state, timeline, status) => { let newIds = ids; return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - if (!top) mMap.set('unread', unread + 1); + if (!top && !filtered) mMap.set('unread', unread + 1); if (top && ids.size > 40) newIds = newIds.take(20); mMap.set('items', newIds.unshift(status.get('id'))); })); @@ -77,8 +96,10 @@ const updateTimeline = (state, timeline, status) => { const deleteStatus = (state, id, accountId, references, exclude_account = null) => { state.keySeq().forEach(timeline => { - if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) { + const helper = list => list.filterNot(item => item === id); + state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper); + } }); // Remove reblogs of deleted status @@ -108,29 +129,31 @@ const filterTimelines = (state, relationship, statuses) => { return state; }; -const filterTimeline = (timeline, state, relationship, statuses) => - state.updateIn([timeline, 'items'], ImmutableList(), list => - list.filterNot(statusId => - statuses.getIn([statusId, 'account']) === relationship.id - )); +const filterTimeline = (timeline, state, relationship, statuses) => { + const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id); + return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper); +}; const updateTop = (state, timeline, top) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - if (top) mMap.set('unread', 0); + if (top) mMap.set('unread', mMap.get('pendingItems').size); mMap.set('top', top); })); }; export default function timelines(state = initialState, action) { switch(action.type) { + case TIMELINE_LOAD_PENDING: + return state.update(action.timeline, initialTimeline, map => + map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0)); case TIMELINE_EXPAND_REQUEST: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_EXPAND_SUCCESS: - return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent); + return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status)); + return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems, action.filtered); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case TIMELINE_CLEAR: @@ -148,7 +171,7 @@ export default function timelines(state = initialState, action) { return state.update( action.timeline, initialTimeline, - map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items) + map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) ); default: return state; diff --git a/app/javascript/flavours/glitch/reducers/trends.js b/app/javascript/flavours/glitch/reducers/trends.js new file mode 100644 index 000000000..5cecc8fca --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/trends.js @@ -0,0 +1,23 @@ +import { TRENDS_FETCH_REQUEST, TRENDS_FETCH_SUCCESS, TRENDS_FETCH_FAIL } from '../actions/trends'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; + +const initialState = ImmutableMap({ + items: ImmutableList(), + isLoading: false, +}); + +export default function trendsReducer(state = initialState, action) { + switch(action.type) { + case TRENDS_FETCH_REQUEST: + return state.set('isLoading', true); + case TRENDS_FETCH_SUCCESS: + return state.withMutations(map => { + map.set('items', fromJS(action.trends)); + map.set('isLoading', false); + }); + case TRENDS_FETCH_FAIL: + return state.set('isLoading', false); + default: + return state; + } +}; diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js index a4df9ec8d..c8a6f524e 100644 --- a/app/javascript/flavours/glitch/reducers/user_lists.js +++ b/app/javascript/flavours/glitch/reducers/user_lists.js @@ -1,4 +1,7 @@ import { + NOTIFICATIONS_UPDATE, +} from '../actions/notifications'; +import { FOLLOWERS_FETCH_SUCCESS, FOLLOWERS_EXPAND_SUCCESS, FOLLOWING_FETCH_SUCCESS, @@ -20,6 +23,14 @@ import { MUTES_FETCH_SUCCESS, MUTES_EXPAND_SUCCESS, } from 'flavours/glitch/actions/mutes'; +import { + DIRECTORY_FETCH_REQUEST, + DIRECTORY_FETCH_SUCCESS, + DIRECTORY_FETCH_FAIL, + DIRECTORY_EXPAND_REQUEST, + DIRECTORY_EXPAND_SUCCESS, + DIRECTORY_EXPAND_FAIL, +} from 'flavours/glitch/actions/directory'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; const initialState = ImmutableMap({ @@ -45,6 +56,12 @@ const appendToList = (state, type, id, accounts, next) => { }); }; +const normalizeFollowRequest = (state, notification) => { + return state.updateIn(['follow_requests', 'items'], list => { + return list.filterNot(item => item === notification.account.id).unshift(notification.account.id); + }); +}; + export default function userLists(state = initialState, action) { switch(action.type) { case FOLLOWERS_FETCH_SUCCESS: @@ -59,6 +76,8 @@ export default function userLists(state = initialState, action) { return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id))); case FAVOURITES_FETCH_SUCCESS: return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id))); + case NOTIFICATIONS_UPDATE: + return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state; case FOLLOW_REQUESTS_FETCH_SUCCESS: return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); case FOLLOW_REQUESTS_EXPAND_SUCCESS: @@ -74,6 +93,16 @@ export default function userLists(state = initialState, action) { return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); case MUTES_EXPAND_SUCCESS: return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + case DIRECTORY_FETCH_SUCCESS: + return state.setIn(['directory', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false); + case DIRECTORY_EXPAND_SUCCESS: + return state.updateIn(['directory', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false); + case DIRECTORY_FETCH_REQUEST: + case DIRECTORY_EXPAND_REQUEST: + return state.setIn(['directory', 'isLoading'], true); + case DIRECTORY_FETCH_FAIL: + case DIRECTORY_EXPAND_FAIL: + return state.setIn(['directory', 'isLoading'], false); default: return state; } diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index d1a88a2fc..8ceb71d03 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -1,5 +1,6 @@ +import escapeTextContentForBrowser from 'escape-html'; import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; +import { List as ImmutableList, is } from 'immutable'; import { me } from 'flavours/glitch/util/initial_state'; const getAccountBase = (state, id) => state.getIn(['accounts', id], null); @@ -20,7 +21,7 @@ export const makeGetAccount = () => { }); }; -const toServerSideType = columnType => { +export const toServerSideType = columnType => { switch (columnType) { case 'home': case 'notifications': @@ -36,8 +37,6 @@ const toServerSideType = columnType => { } }; -export const getFilters = (state, { contextType }) => state.get('filters', ImmutableList()).filter(filter => contextType && filter.get('context').includes(toServerSideType(contextType)) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); - const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string @@ -63,6 +62,27 @@ export const regexFromFilters = filters => { }).join('|'), 'i'); }; +// Memoize the filter regexps for each valid server contextType +const makeGetFiltersRegex = () => { + let memo = {}; + + return (state, { contextType }) => { + if (!contextType) return ImmutableList(); + + const serverSideType = toServerSideType(contextType); + const filters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); + + if (!memo[serverSideType] || !is(memo[serverSideType].filters, filters)) { + const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible'))); + const regex = regexFromFilters(filters); + memo[serverSideType] = { filters: filters, results: [dropRegex, regex] }; + } + return memo[serverSideType].results; + }; +}; + +export const getFiltersRegex = makeGetFiltersRegex(); + export const makeGetStatus = () => { return createSelector( [ @@ -70,15 +90,24 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), - getFilters, + (state, _) => state.getIn(['local_settings', 'filtering_behavior']), + (state, _) => state.get('filters', ImmutableList()), + (_, { contextType }) => contextType, + getFiltersRegex, ], - (statusBase, statusReblog, accountBase, accountReblog, filters) => { + (statusBase, statusReblog, accountBase, accountReblog, filteringBehavior, filters, contextType, filtersRegex) => { if (!statusBase) { return null; } - const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters); + const dropRegex = (accountReblog || accountBase).get('id') !== me && filtersRegex[0]; + + if (dropRegex && dropRegex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'))) { + return null; + } + + const regex = (accountReblog || accountBase).get('id') !== me && filtersRegex[1]; let filtered = false; if (statusReblog) { @@ -91,6 +120,26 @@ export const makeGetStatus = () => { filtered = filtered || regex && regex.test(statusBase.get('search_index')); + if (filtered && filteringBehavior === 'drop') { + return null; + } else if (filtered && filteringBehavior === 'content_warning') { + let spoilerText = (statusReblog || statusBase).get('spoiler_text', ''); + const searchIndex = (statusReblog || statusBase).get('search_index'); + const serverSideType = toServerSideType(contextType); + const enabledFilters = filters.filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))).toArray(); + const matchingFilters = enabledFilters.filter(filter => { + const regexp = regexFromFilters([filter]); + return regexp.test(searchIndex) && !regexp.test(spoilerText); + }); + if (statusReblog) { + statusReblog = statusReblog.set('spoiler_text', matchingFilters.map(filter => filter.get('phrase')).concat([spoilerText]).filter(cw => !!cw).join(', ')); + statusReblog = statusReblog.update('spoilerHtml', '', spoilerText => matchingFilters.map(filter => escapeTextContentForBrowser(filter.get('phrase'))).concat([spoilerText]).filter(cw => !!cw).join(', ')); + } else { + statusBase = statusBase.set('spoiler_text', matchingFilters.map(filter => filter.get('phrase')).concat([spoilerText]).filter(cw => !!cw).join(', ')); + statusBase = statusBase.update('spoilerHtml', '', spoilerText => matchingFilters.map(filter => escapeTextContentForBrowser(filter.get('phrase'))).concat([spoilerText]).filter(cw => !!cw).join(', ')); + } + } + return statusBase.withMutations(map => { map.set('reblog', statusReblog); map.set('account', accountBase); @@ -108,6 +157,7 @@ export const getAlerts = createSelector([getAlertsBase], (base) => { base.forEach(item => { arr.push({ message: item.get('message'), + message_values: item.get('message_values'), title: item.get('title'), key: item.get('key'), dismissAfter: 5000, diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss index d542b1083..088b41e76 100644 --- a/app/javascript/flavours/glitch/styles/_mixins.scss +++ b/app/javascript/flavours/glitch/styles/_mixins.scss @@ -62,24 +62,6 @@ color: $darker-text-color; font-size: 14px; margin: 0; - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } - - &:focus { - background: lighten($ui-base-color, 4%); - } - - @media screen and (max-width: 600px) { - font-size: 16px; - } } @mixin search-popout() { diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss index 0e910693d..a38ca99b4 100644 --- a/app/javascript/flavours/glitch/styles/about.scss +++ b/app/javascript/flavours/glitch/styles/about.scss @@ -17,109 +17,102 @@ $small-breakpoint: 960px; .rich-formatting { font-family: $font-sans-serif, sans-serif; - font-size: 16px; + font-size: 14px; font-weight: 400; - font-size: 16px; - line-height: 30px; + line-height: 1.7; + word-wrap: break-word; color: $darker-text-color; - padding-right: 10px; a { color: $highlight-text-color; text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } } p, li { - font-family: $font-sans-serif, sans-serif; - font-size: 16px; - font-weight: 400; - font-size: 16px; - line-height: 30px; - margin-bottom: 12px; color: $darker-text-color; + } - a { - color: $highlight-text-color; - text-decoration: underline; - } + p { + margin-top: 0; + margin-bottom: .85em; &:last-child { margin-bottom: 0; } } - strong, - em { + strong { font-weight: 700; - color: lighten($darker-text-color, 10%); + color: $secondary-text-color; } - h1 { - font-family: $font-display, sans-serif; - font-size: 26px; - line-height: 30px; - font-weight: 500; - margin-bottom: 20px; + em { + font-style: italic; color: $secondary-text-color; + } - small { - font-family: $font-sans-serif, sans-serif; - display: block; - font-size: 18px; - font-weight: 400; - color: lighten($darker-text-color, 10%); - } + code { + font-size: 0.85em; + background: darken($ui-base-color, 8%); + border-radius: 4px; + padding: 0.2em 0.3em; } - h2 { + h1, + h2, + h3, + h4, + h5, + h6 { font-family: $font-display, sans-serif; - font-size: 22px; - line-height: 26px; + margin-top: 1.275em; + margin-bottom: .85em; font-weight: 500; - margin-bottom: 20px; color: $secondary-text-color; } + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.75em; + } + h3 { - font-family: $font-display, sans-serif; - font-size: 18px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + font-size: 1.5em; } h4 { - font-family: $font-display, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + font-size: 1.25em; } - h5 { - font-family: $font-display, sans-serif; - font-size: 14px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + h5, + h6 { + font-size: 1em; } - h6 { - font-family: $font-display, sans-serif; - font-size: 12px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + ul { + list-style: disc; + } + + ol { + list-style: decimal; } ul, ol { - margin-left: 20px; + margin: 0; + padding: 0; + padding-left: 2em; + margin-bottom: 0.85em; &[type='a'] { list-style-type: lower-alpha; @@ -130,31 +123,74 @@ $small-breakpoint: 960px; } } - ul { - list-style: disc; - } - - ol { - list-style: decimal; - } - - li > ol, - li > ul { - margin-top: 6px; - } - hr { width: 100%; height: 0; border: 0; - border-bottom: 1px solid rgba($ui-base-lighter-color, .6); - margin: 20px 0; + border-bottom: 1px solid lighten($ui-base-color, 4%); + margin: 1.7em 0; &.spacer { height: 1px; border: 0; } } + + table { + width: 100%; + border-collapse: collapse; + break-inside: auto; + margin-top: 24px; + margin-bottom: 32px; + + thead tr, + tbody tr { + border-bottom: 1px solid lighten($ui-base-color, 4%); + font-size: 1em; + line-height: 1.625; + font-weight: 400; + text-align: left; + color: $darker-text-color; + } + + thead tr { + border-bottom-width: 2px; + line-height: 1.5; + font-weight: 500; + color: $dark-text-color; + } + + th, + td { + padding: 8px; + align-self: start; + align-items: start; + word-break: break-all; + + &.nowrap { + width: 25%; + position: relative; + + &::before { + content: ' '; + visibility: hidden; + } + + span { + position: absolute; + left: 8px; + right: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } + + & > :first-child { + margin-top: 0; + } } .information-board { @@ -418,7 +454,7 @@ $small-breakpoint: 960px; } &__call-to-action { - background: darken($ui-base-color, 4%); + background: $ui-base-color; border-radius: 4px; padding: 25px 40px; overflow: hidden; diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index 0fae137f0..a827d271a 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -226,6 +226,7 @@ } .account__header__fields { + max-width: 100vw; padding: 0; margin: 15px -15px -15px; border: 0 none; diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 74f91599a..1d25d0129 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -5,21 +5,66 @@ $content-width: 840px; .admin-wrapper { display: flex; justify-content: center; - height: 100%; + width: 100%; + min-height: 100vh; .sidebar-wrapper { - flex: 1 1 $sidebar-width; - height: 100%; - background: $ui-base-color; - display: flex; - justify-content: flex-end; + min-height: 100vh; + overflow: hidden; + pointer-events: none; + flex: 1 1 auto; + + &__inner { + display: flex; + justify-content: flex-end; + background: $ui-base-color; + height: 100%; + } } .sidebar { width: $sidebar-width; - height: 100%; padding: 0; - overflow-y: auto; + pointer-events: auto; + + &__toggle { + display: none; + background: lighten($ui-base-color, 8%); + height: 48px; + + &__logo { + flex: 1 1 auto; + + a { + display: inline-block; + padding: 15px; + } + + svg { + fill: $primary-text-color; + height: 20px; + position: relative; + bottom: -2px; + } + } + + &__icon { + display: block; + color: $darker-text-color; + text-decoration: none; + flex: 0 0 auto; + font-size: 20px; + padding: 15px; + } + + a { + &:hover, + &:focus, + &:active { + background: lighten($ui-base-color, 12%); + } + } + } .logo { display: block; @@ -52,6 +97,9 @@ $content-width: 840px; transition: all 200ms linear; transition-property: color, background-color; border-radius: 4px 0 0 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; i.fa { margin-right: 5px; @@ -99,12 +147,30 @@ $content-width: 840px; } .content-wrapper { - flex: 2 1 $content-width; - overflow: auto; + box-sizing: border-box; + width: 100%; + max-width: $content-width; + flex: 1 1 auto; + } + + @media screen and (max-width: $content-width + $sidebar-width) { + .sidebar-wrapper--empty { + display: none; + } + + .sidebar-wrapper { + width: $sidebar-width; + flex: 0 0 auto; + } + } + + @media screen and (max-width: $no-columns-breakpoint) { + .sidebar-wrapper { + width: 100%; + } } .content { - max-width: $content-width; padding: 20px 15px; padding-top: 60px; padding-left: 25px; @@ -123,6 +189,12 @@ $content-width: 840px; padding-bottom: 40px; border-bottom: 1px solid lighten($ui-base-color, 8%); margin-bottom: 40px; + + @media screen and (max-width: $no-columns-breakpoint) { + border-bottom: 0; + padding-bottom: 0; + font-weight: 700; + } } h3 { @@ -147,7 +219,7 @@ $content-width: 840px; font-size: 16px; color: $secondary-text-color; line-height: 28px; - font-weight: 400; + font-weight: 500; } .fields-group h6 { @@ -176,7 +248,7 @@ $content-width: 840px; & > p { font-size: 14px; - line-height: 18px; + line-height: 21px; color: $secondary-text-color; margin-bottom: 20px; @@ -204,61 +276,98 @@ $content-width: 840px; border: 0; } } - - .muted-hint { - color: $darker-text-color; - - a { - color: $highlight-text-color; - } - } - - .positive-hint { - color: $valid-value-color; - font-weight: 500; - } - - .negative-hint { - color: $error-value-color; - font-weight: 500; - } - - .neutral-hint { - color: $dark-text-color; - font-weight: 500; - } } @media screen and (max-width: $no-columns-breakpoint) { display: block; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - .sidebar-wrapper, - .content-wrapper { - flex: 0 0 auto; - height: auto; - overflow: initial; + .sidebar-wrapper { + min-height: 0; } .sidebar { width: 100%; padding: 0; height: auto; + + &__toggle { + display: flex; + } + + & > ul { + display: none; + } + + ul a, + ul ul a { + border-radius: 0; + border-bottom: 1px solid lighten($ui-base-color, 4%); + transition: none; + + &:hover { + transition: none; + } + } + + ul ul { + border-radius: 0; + } + + ul .simple-navigation-active-leaf a { + border-bottom-color: $ui-highlight-color; + } } } } +hr.spacer { + width: 100%; + border: 0; + margin: 20px 0; + height: 1px; +} + +body, +.admin-wrapper .content { + .muted-hint { + color: $darker-text-color; + + a { + color: $highlight-text-color; + } + } + + .positive-hint { + color: $valid-value-color; + font-weight: 500; + } + + .negative-hint { + color: $error-value-color; + font-weight: 500; + } + + .neutral-hint { + color: $dark-text-color; + font-weight: 500; + } + + .warning-hint { + color: $gold-star; + font-weight: 500; + } +} + .filters { display: flex; flex-wrap: wrap; .filter-subset { flex: 0 0 auto; - margin: 0 40px 10px 0; + margin: 0 40px 20px 0; &:last-child { - margin-bottom: 20px; + margin-bottom: 30px; } ul { @@ -720,3 +829,47 @@ a.name-tag, text-overflow: ellipsis; vertical-align: middle; } + +.admin-account-bio { + display: flex; + flex-wrap: wrap; + margin: 0 -5px; + margin-top: 20px; + + > div { + box-sizing: border-box; + padding: 0 5px; + margin-bottom: 10px; + flex: 1 0 50%; + } + + .account__header__fields, + .account__header__content { + background: lighten($ui-base-color, 8%); + border-radius: 4px; + height: 100%; + } + + .account__header__fields { + margin: 0; + border: 0; + + a { + color: lighten($ui-highlight-color, 8%); + } + + dl:first-child .verified { + border-radius: 0 4px 0 0; + } + + .verified a { + color: $valid-value-color; + } + } + + .account__header__content { + box-sizing: border-box; + padding: 20px; + color: $primary-text-color; + } +} diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss index 550b7fdfc..77631097a 100644 --- a/app/javascript/flavours/glitch/styles/basics.scss +++ b/app/javascript/flavours/glitch/styles/basics.scss @@ -7,7 +7,7 @@ body { font-family: $font-sans-serif, sans-serif; - background: darken($ui-base-color, 8%); + background: darken($ui-base-color, 7%); font-size: 13px; line-height: 18px; font-weight: 400; @@ -34,11 +34,19 @@ body { } &.app-body { - position: absolute; - width: 100%; - height: 100%; padding: 0; - background: $ui-base-color; + + &.layout-single-column { + height: auto; + min-height: 100vh; + overflow-y: scroll; + } + + &.layout-multiple-columns { + position: absolute; + width: 100%; + height: 100%; + } &.with-modals--active { overflow-y: hidden; @@ -55,7 +63,6 @@ body { &--active { overflow-y: hidden; - margin-right: 13px; } } @@ -74,9 +81,6 @@ body { &.admin { background: darken($ui-base-color, 4%); - position: fixed; - width: 100%; - height: 100%; padding: 0; } @@ -127,9 +131,22 @@ button { & > div { display: flex; width: 100%; - height: 100%; align-items: center; justify-content: center; outline: 0 !important; } } + +.layout-single-column .app-holder { + &, + & > div { + min-height: 100vh; + } +} + +.layout-multiple-columns .app-holder { + &, + & > div { + height: 100%; + } +} diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index d2233207d..5be4da48a 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -50,6 +50,8 @@ &-composite { @include avatar-radius; overflow: hidden; + position: relative; + cursor: default; & div { @include avatar-radius; @@ -57,6 +59,18 @@ position: relative; box-sizing: border-box; } + + &__label { + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: $primary-text-color; + text-shadow: 1px 1px 2px $base-shadow-color; + font-weight: 700; + font-size: 15px; + } } } @@ -245,6 +259,28 @@ .column-select { &__control { @include search-input(); + + &::placeholder { + color: lighten($darker-text-color, 4%); + } + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus, + &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($ui-base-color, 4%); + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } } &__placeholder { @@ -415,6 +451,24 @@ } } } + + &.directory__section-headline { + background: darken($ui-base-color, 2%); + border-bottom-color: transparent; + + a, + button { + &.active { + &::before { + display: none; + } + + &::after { + border-color: transparent transparent darken($ui-base-color, 7%); + } + } + } + } } .account__moved-note { diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index b354e7acf..6ba9698c5 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -17,6 +17,7 @@ justify-content: center; width: 100%; height: 100%; + min-height: 100vh; &__pane { height: 100%; @@ -24,12 +25,14 @@ pointer-events: none; display: flex; justify-content: flex-end; + min-width: 285px; &--start { justify-content: flex-start; } &__inner { + position: fixed; width: 285px; pointer-events: auto; height: 100%; @@ -40,16 +43,37 @@ box-sizing: border-box; width: 100%; max-width: 600px; + flex: 0 0 auto; display: flex; flex-direction: column; - @media screen and (min-width: 360px) { + @media screen and (min-width: $no-gap-breakpoint) { padding: 0 10px; } } } } +.tabs-bar__wrapper { + background: darken($ui-base-color, 8%); + position: sticky; + top: 0; + z-index: 2; + padding-top: 0; + + @media screen and (min-width: $no-gap-breakpoint) { + padding-top: 10px; + } + + .tabs-bar { + margin-bottom: 0; + + @media screen and (min-width: $no-gap-breakpoint) { + margin-bottom: 10px; + } + } +} + .react-swipeable-view-container { &, .columns-area, @@ -83,7 +107,6 @@ flex-direction: column; width: 100%; height: 100%; - background: darken($ui-base-color, 7%); } .column { @@ -91,6 +114,8 @@ } .column-back-button { + box-sizing: border-box; + width: 100%; background: lighten($ui-base-color, 4%); color: $highlight-text-color; cursor: pointer; @@ -466,14 +491,14 @@ } .auto-columns.navbar-under { - @media screen and (max-width: 360px) { + @media screen and (max-width: $no-gap-breakpoint) { @include fix-margins-for-navbar-under; } } .auto-columns.navbar-under .react-swipeable-view-container .columns-area, .single-column.navbar-under .react-swipeable-view-container .columns-area { - @media screen and (max-width: 360px) { + @media screen and (max-width: $no-gap-breakpoint) { height: 100% !important; } } diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index c06d79ffc..51287f62e 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -2,6 +2,18 @@ padding: 10px; } +.character-counter { + cursor: default; + font-family: $font-sans-serif, sans-serif; + font-size: 14px; + font-weight: 600; + color: $lighter-text-color; + + &.character-counter--over { + color: $warning-red; + } +} + .no-reduce-motion .composer--spoiler { transition: height 0.4s ease, opacity 0.4s ease; } @@ -32,6 +44,10 @@ font-family: inherit; resize: vertical; + &::placeholder { + color: $dark-text-color; + } + &:focus { outline: 0 } @include single-column('screen and (max-width: 630px)') { font-size: 16px } } @@ -127,6 +143,7 @@ overflow: visible; white-space: pre-wrap; padding-top: 5px; + overflow: hidden; p, pre, blockquote { margin-bottom: 20px; @@ -232,6 +249,7 @@ .compose-form__autosuggest-wrapper, .autosuggest-input { position: relative; + width: 100%; label { .autosuggest-textarea__textarea { @@ -251,6 +269,10 @@ resize: none; scrollbar-color: initial; + &::placeholder { + color: $dark-text-color; + } + &::-webkit-scrollbar { all: unset; } @@ -314,29 +336,46 @@ } .autosuggest-textarea__suggestions__item { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - border-radius: 4px; padding: 10px; - font-size: 14px; - line-height: 18px; - overflow: hidden; cursor: pointer; + border-radius: 4px; &:hover, &:focus, &:active, &.selected { background: darken($ui-secondary-color, 10%) } - & > .emoji { - img { - display: block; - float: left; - margin-right: 8px; - width: 18px; - height: 18px; + > .account, + > .emoji, + > .autosuggest-hashtag { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + line-height: 18px; + font-size: 14px; + } + + .autosuggest-hashtag { + justify-content: space-between; + + &__name { + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + strong { + font-weight: 500; + } + + &__uses { + flex: 0 0 auto; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } } @@ -370,6 +409,7 @@ border-radius: 4px; height: 140px; width: 100%; + background-color: $base-shadow-color; background-position: center; background-size: cover; background-repeat: no-repeat; @@ -488,12 +528,18 @@ background: $simple-background-color; } -.composer--options { +.composer--options-wrapper { padding: 10px; background: darken($simple-background-color, 8%); - box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05); border-radius: 0 0 4px 4px; height: 27px; + display: flex; + justify-content: space-between; + flex: 0 0 auto; +} + +.composer--options { + display: flex; flex: 0 0 auto; & > * { @@ -518,6 +564,11 @@ } } +.compose--counter-wrapper { + align-self: center; + margin-right: 4px; +} + .composer--options--dropdown { &.open { & > .value { @@ -588,13 +639,6 @@ justify-content: flex-end; flex: 0 0 auto; - & > .count { - display: inline-block; - margin: 0 16px 0 8px; - font-size: 16px; - line-height: 36px; - } - & > .primary { display: inline-block; margin: 0; @@ -604,7 +648,7 @@ & > .side_arm { display: inline-block; - margin: 0 2px 0 0; + margin: 0 2px; padding: 0; width: 36px; text-align: center; diff --git a/app/javascript/flavours/glitch/styles/components/directory.scss b/app/javascript/flavours/glitch/styles/components/directory.scss new file mode 100644 index 000000000..b0ad5a88a --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/directory.scss @@ -0,0 +1,180 @@ +.directory { + &__list { + width: 100%; + margin: 10px 0; + transition: opacity 100ms ease-in; + + &.loading { + opacity: 0.7; + } + + @media screen and (max-width: $no-gap-breakpoint) { + margin: 0; + } + } + + &__card { + box-sizing: border-box; + margin-bottom: 10px; + + &__img { + height: 125px; + position: relative; + background: darken($ui-base-color, 12%); + overflow: hidden; + + img { + display: block; + width: 100%; + height: 100%; + margin: 0; + object-fit: cover; + } + } + + &__bar { + display: flex; + align-items: center; + background: lighten($ui-base-color, 4%); + padding: 10px; + + &__name { + flex: 1 1 auto; + display: flex; + align-items: center; + text-decoration: none; + overflow: hidden; + } + + &__relationship { + width: 23px; + min-height: 1px; + flex: 0 0 auto; + } + + .avatar { + flex: 0 0 auto; + width: 48px; + height: 48px; + padding-top: 2px; + + img { + width: 100%; + height: 100%; + display: block; + margin: 0; + border-radius: 4px; + background: darken($ui-base-color, 8%); + object-fit: cover; + } + } + + .display-name { + margin-left: 15px; + text-align: left; + + strong { + font-size: 15px; + color: $primary-text-color; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + } + + span { + display: block; + font-size: 14px; + color: $darker-text-color; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + &__extra { + background: $ui-base-color; + display: flex; + align-items: center; + justify-content: center; + + .accounts-table__count { + width: 33.33%; + flex: 0 0 auto; + padding: 15px 0; + } + + .account__header__content { + box-sizing: border-box; + padding: 15px 10px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + width: 100%; + min-height: 18px + 30px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + p { + display: none; + + &:first-child { + display: inline; + } + } + + br { + display: none; + } + } + } + } +} + +.filter-form { + background: $ui-base-color; + + &__column { + padding: 10px 15px; + } + + .radio-button { + display: block; + } +} + +.radio-button { + font-size: 14px; + position: relative; + display: inline-block; + padding: 6px 0; + line-height: 18px; + cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + + input[type=radio], + input[type=checkbox] { + display: none; + } + + &__input { + display: inline-block; + position: relative; + border: 1px solid $ui-primary-color; + box-sizing: border-box; + width: 18px; + height: 18px; + flex: 0 0 auto; + margin-right: 10px; + top: -1px; + border-radius: 50%; + vertical-align: middle; + + &.checked { + border-color: lighten($ui-highlight-color, 8%); + background: lighten($ui-highlight-color, 8%); + } + } +} diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index f054ddbc0..93a3f62ed 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -73,108 +73,31 @@ } } -.drawer--search { +.search { position: relative; margin-bottom: 10px; flex: none; - @include limited-single-column('screen and (max-width: 360px)') { margin-bottom: 0 } + @include limited-single-column('screen and (max-width: #{$no-gap-breakpoint})') { margin-bottom: 0 } @include single-column('screen and (max-width: 630px)') { font-size: 16px } - - input { - display: block; - box-sizing: border-box; - margin: 0; - border: none; - padding: 15px 30px 15px 15px; - width: 100%; - outline: 0; - color: $darker-text-color; - background: $ui-base-color; - font-size: 14px; - font-family: inherit; - line-height: 16px; - - &:focus { - outline: 0; - background: lighten($ui-base-color, 4%); - } - } - - & > .icon { - display: block; - position: absolute; - top: 10px; - right: 10px; - width: 18px; - height: 18px; - color: $secondary-text-color; - font-size: 18px; - line-height: 18px; - z-index: 2; - - .fa { - display: inline-block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - opacity: 0; - cursor: default; - pointer-events: none; - transition: all 100ms linear; - transition-property: color, transform, opacity; - } - - .fa-search { - opacity: 0.3; - transform: rotate(0deg); - } - - .fa-times-circle { - transform: rotate(-90deg); - cursor: pointer; - - &:hover { color: $primary-text-color } - } - } - - &.active { - & > .icon { - .fa-search { - opacity: 0; - transform: rotate(90deg); - } - - .fa-times-circle { - opacity: 0.3; - pointer-events: auto; - transform: rotate(0deg); - } - } - } } -.drawer--search--popout { +.search-popout { @include search-popout(); } .drawer--account { padding: 10px; color: $darker-text-color; + display: flex; + align-items: center; - & > a { + a { color: inherit; text-decoration: none; } - & > .avatar { - float: left; - margin-right: 10px; - } - - & > .acct { + .acct { display: block; color: $secondary-text-color; font-weight: 500; @@ -184,6 +107,12 @@ } } +.navigation-bar__profile { + flex: 1 1 auto; + margin-left: 8px; + overflow: hidden; +} + .drawer--results { background: $ui-base-color; overflow-x: hidden; diff --git a/app/javascript/flavours/glitch/styles/components/error_boundary.scss b/app/javascript/flavours/glitch/styles/components/error_boundary.scss index f9bf425f8..3176690e2 100644 --- a/app/javascript/flavours/glitch/styles/components/error_boundary.scss +++ b/app/javascript/flavours/glitch/styles/components/error_boundary.scss @@ -1,4 +1,8 @@ .error-boundary { + color: $primary-text-color; + font-size: 15px; + line-height: 20px; + h1 { font-size: 26px; line-height: 36px; @@ -6,27 +10,21 @@ margin-bottom: 8px; } - p { + a { color: $primary-text-color; - font-size: 15px; - line-height: 20px; - - a { - color: $primary-text-color; - text-decoration: underline; - } + text-decoration: underline; + } - ul { - list-style: disc; - margin-left: 0; - padding-left: 1em; - } + ul { + list-style: disc; + margin-left: 0; + padding-left: 1em; + } - textarea.web_app_crash-stacktrace { - width: 100%; - resize: none; - white-space: pre; - font-family: $font-monospace, monospace; - } + textarea.web_app_crash-stacktrace { + width: 100%; + resize: none; + white-space: pre; + font-family: $font-monospace, monospace; } } diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 9f96a3154..febc95513 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -3,6 +3,27 @@ -ms-overflow-style: -ms-autohiding-scrollbar; } +.link-button { + display: block; + font-size: 15px; + line-height: 20px; + color: $ui-highlight-color; + border: 0; + background: transparent; + padding: 0; + cursor: pointer; + + &:hover, + &:active { + text-decoration: underline; + } + + &:disabled { + color: $ui-primary-color; + cursor: default; + } +} + .button { background-color: darken($ui-highlight-color, 3%); border: 10px none; @@ -118,20 +139,29 @@ display: inline-block; padding: 0; color: $action-button-color; - border: none; + border: 0; + border-radius: 4px; background: transparent; cursor: pointer; - transition: color 100ms ease-in; + transition: all 100ms ease-in; + transition-property: background-color, color; &:hover, &:active, &:focus { color: lighten($action-button-color, 7%); - transition: color 200ms ease-out; + background-color: rgba($action-button-color, 0.15); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + + &:focus { + background-color: rgba($action-button-color, 0.3); } &.disabled { color: darken($action-button-color, 13%); + background-color: transparent; cursor: default; } @@ -156,10 +186,16 @@ &:active, &:focus { color: darken($lighter-text-color, 7%); + background-color: rgba($lighter-text-color, 0.15); + } + + &:focus { + background-color: rgba($lighter-text-color, 0.3); } &.disabled { color: lighten($lighter-text-color, 7%); + background-color: transparent; } &.active { @@ -186,7 +222,8 @@ .text-icon-button { color: $lighter-text-color; - border: none; + border: 0; + border-radius: 4px; background: transparent; cursor: pointer; font-weight: 600; @@ -194,17 +231,25 @@ padding: 0 3px; line-height: 27px; outline: 0; - transition: color 100ms ease-in; + transition: all 100ms ease-in; + transition-property: background-color, color; &:hover, &:active, &:focus { color: darken($lighter-text-color, 7%); - transition: color 200ms ease-out; + background-color: rgba($lighter-text-color, 0.15); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + + &:focus { + background-color: rgba($lighter-text-color, 0.3); } &.disabled { color: lighten($lighter-text-color, 20%); + background-color: transparent; cursor: default; } @@ -269,6 +314,20 @@ color: $red-bookmark; } +.no-reduce-motion .icon-button.star-icon { + &.activate { + & > .fa-star { + animation: spring-rotate-in 1s linear; + } + } + + &.deactivate { + & > .fa-star { + animation: spring-rotate-out 1s linear; + } + } +} + .notification__display-name { color: inherit; font-weight: 500; @@ -802,7 +861,8 @@ } .getting-started__wrapper, -.getting_started { +.getting_started, +.flex-spacer { background: $ui-base-color; } @@ -811,6 +871,10 @@ overflow-y: auto; } +.flex-spacer { + flex: 1 1 auto; +} + .getting-started { background: $ui-base-color; flex: 1 0 auto; @@ -858,6 +922,47 @@ } } } + + &__trends { + flex: 0 1 auto; + opacity: 1; + animation: fade 150ms linear; + margin-top: 10px; + + h4 { + font-size: 12px; + text-transform: uppercase; + color: $darker-text-color; + padding: 10px; + font-weight: 500; + border-bottom: 1px solid lighten($ui-base-color, 8%); + } + + @media screen and (max-height: 810px) { + .trends__item:nth-child(3) { + display: none; + } + } + + @media screen and (max-height: 720px) { + .trends__item:nth-child(2) { + display: none; + } + } + + @media screen and (max-height: 670px) { + display: none; + } + + .trends__item { + border-bottom: 0; + padding: 10px; + + &__current { + color: $darker-text-color; + } + } + } } .column-link__badge { @@ -1097,6 +1202,50 @@ animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000); } +@keyframes spring-rotate-in { + 0% { + transform: rotate(0deg); + } + + 30% { + transform: rotate(-484.8deg); + } + + 60% { + transform: rotate(-316.7deg); + } + + 90% { + transform: rotate(-375deg); + } + + 100% { + transform: rotate(-360deg); + } +} + +@keyframes spring-rotate-out { + 0% { + transform: rotate(-360deg); + } + + 30% { + transform: rotate(124.8deg); + } + + 60% { + transform: rotate(-43.27deg); + } + + 90% { + transform: rotate(15deg); + } + + 100% { + transform: rotate(0deg); + } +} + @keyframes loader-figure { 0% { width: 0; @@ -1148,6 +1297,10 @@ align-items: center; } + &--click-thru { + pointer-events: none; + } + &--hidden { display: none; } @@ -1176,6 +1329,12 @@ background: rgba($base-overlay-background, 0.8); } } + + &:disabled { + .spoiler-button__overlay__label { + background: rgba($base-overlay-background, 0.5); + } + } } } @@ -1314,6 +1473,7 @@ position: absolute; top: 0; left: 0; + z-index: 9999; } .icon-badge-wrapper { @@ -1332,52 +1492,104 @@ height: 1em; } -.layout-toggle { +.conversation { display: flex; + border-bottom: 1px solid lighten($ui-base-color, 8%); padding: 5px; + padding-bottom: 0; - button { - box-sizing: border-box; - flex: 0 0 50%; - background: transparent; - padding: 5px; - border: 0; + &:focus { + background: lighten($ui-base-color, 2%); + outline: 0; + } + + &__avatar { + flex: 0 0 auto; + padding: 10px; + padding-top: 12px; position: relative; + } - &:hover, - &:focus, - &:active { - svg path:first-child { - fill: lighten($ui-base-color, 16%); + &__unread { + display: inline-block; + background: $highlight-text-color; + border-radius: 50%; + width: 0.625rem; + height: 0.625rem; + margin: -.1ex .15em .1ex; + } + + &__content { + flex: 1 1 auto; + padding: 10px 5px; + padding-right: 15px; + overflow: hidden; + + &__info { + overflow: hidden; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + } + + &__relative-time { + font-size: 15px; + color: $darker-text-color; + padding-left: 15px; + } + + &__names { + color: $darker-text-color; + font-size: 15px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; + flex-basis: 90px; + flex-grow: 1; + + a { + color: $primary-text-color; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } } } + + .status__content { + margin: 0; + } } - svg { - width: 100%; - height: auto; + &--unread { + background: lighten($ui-base-color, 2%); - path:first-child { - fill: lighten($ui-base-color, 12%); + &:focus { + background: lighten($ui-base-color, 4%); } - path:last-child { - fill: darken($ui-base-color, 14%); + .conversation__content__info { + font-weight: 700; } - } - &__active { - color: $ui-highlight-color; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: lighten($ui-base-color, 12%); - border-radius: 50%; - padding: 0.35rem; + .conversation__content__relative-time { + color: $primary-text-color; + } } } +.ui .flash-message { + margin-top: 10px; + margin-left: auto; + margin-right: auto; + margin-bottom: 0; + min-width: 75%; +} + ::-webkit-scrollbar-thumb { border-radius: 0; } @@ -1405,6 +1617,10 @@ noscript { text-decoration: none; } } + + a { + word-break: break-word; + } } } @@ -1422,6 +1638,7 @@ noscript { @import 'composer'; @import 'columns'; @import 'regeneration_indicator'; +@import 'directory'; @import 'search'; @import 'emoji'; @import 'doodle'; diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 8b5d0486d..39bfaae9a 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -179,10 +179,13 @@ position: absolute; } -.video-modal { +.video-modal__container { max-width: 100vw; max-height: 100vh; - position: relative; +} + +.audio-modal__container { + width: 50vw; } .media-modal { @@ -282,6 +285,7 @@ } a { + pointer-events: auto; text-decoration: none; font-weight: 500; color: $ui-secondary-color; @@ -333,11 +337,66 @@ } +.audio-player { + box-sizing: border-box; + position: relative; + background: darken($ui-base-color, 8%); + border-radius: 4px; + padding-bottom: 44px; + direction: ltr; + + &.editable { + border-radius: 0; + height: 100%; + } + + &__waveform { + padding: 15px 0; + position: relative; + overflow: hidden; + + &::before { + content: ""; + display: block; + position: absolute; + border-top: 1px solid lighten($ui-base-color, 4%); + width: 100%; + height: 0; + left: 0; + top: calc(50% + 1px); + } + } + + &__progress-placeholder { + background-color: rgba(lighten($ui-highlight-color, 8%), 0.5); + } + + &__wave-placeholder { + background-color: lighten($ui-base-color, 16%); + } + + .video-player__controls { + padding: 0 15px; + padding-top: 10px; + background: darken($ui-base-color, 8%); + border-top: 1px solid lighten($ui-base-color, 4%); + border-radius: 0 0 4px 4px; + } +} + .video-player { overflow: hidden; position: relative; background: $base-shadow-color; max-width: 100%; + border-radius: 4px; + box-sizing: border-box; + direction: ltr; + + &.editable { + border-radius: 0; + height: 100% !important; + } &:focus { outline: 0; @@ -367,6 +426,7 @@ max-height: 100% !important; width: 100% !important; height: 100% !important; + outline: 0; } } @@ -444,6 +504,17 @@ display: flex; justify-content: space-between; padding-bottom: 10px; + + .video-player__download__icon { + color: inherit; + + .fa, + &:active .fa, + &:hover .fa, + &:focus .fa { + color: inherit; + } + } } &__buttons { @@ -616,38 +687,13 @@ } } -&.detailed, -&.fullscreen { - .video-player__buttons { - button { - padding-top: 10px; - padding-bottom: 10px; + &.detailed, + &.fullscreen { + .video-player__buttons { + button { + padding-top: 10px; + padding-bottom: 10px; + } } } } -} - -.media-spoiler-video { - background-size: cover; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; - margin-top: 8px; - position: relative; - - @include fullwidth-gallery; - - border: 0; - display: block; -} - -.media-spoiler-video-play-icon { - border-radius: 100px; - color: rgba($primary-text-color, 0.8); - font-size: 36px; - left: 50%; - padding: 5px; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); -} diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index 65b2e75f0..75bddeefc 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -405,7 +405,8 @@ .confirmation-modal, .report-modal, .actions-modal, -.mute-modal { +.mute-modal, +.block-modal { background: lighten($ui-secondary-color, 8%); color: $inverted-text-color; border-radius: 8px; @@ -465,7 +466,8 @@ .boost-modal__action-bar, .favourite-modal__action-bar, .confirmation-modal__action-bar, -.mute-modal__action-bar { +.mute-modal__action-bar, +.block-modal__action-bar { display: flex; justify-content: space-between; background: $ui-secondary-color; @@ -495,11 +497,13 @@ font-size: 14px; } -.mute-modal { +.mute-modal, +.block-modal { line-height: 24px; } -.mute-modal .react-toggle { +.mute-modal .react-toggle, +.block-modal .react-toggle { vertical-align: middle; } @@ -528,7 +532,8 @@ } } -.report-modal__statuses { +.report-modal__statuses, +.focal-point-modal__content { flex: 1 1 auto; min-height: 20vh; max-height: 80vh; @@ -544,6 +549,12 @@ } } +.focal-point-modal__content { + @media screen and (max-width: 480px) { + max-height: 40vh; + } +} + .report-modal__comment { padding: 20px; border-right: 1px solid $ui-secondary-color; @@ -565,16 +576,56 @@ padding: 10px; font-family: inherit; font-size: 14px; - resize: vertical; + resize: none; border: 0; outline: 0; border-radius: 4px; border: 1px solid $ui-secondary-color; - margin-bottom: 20px; + min-height: 100px; + max-height: 50vh; + margin-bottom: 10px; &:focus { border: 1px solid darken($ui-secondary-color, 8%); } + + &__wrapper { + background: $white; + border: 1px solid $ui-secondary-color; + margin-bottom: 10px; + border-radius: 4px; + + .setting-text { + border: 0; + margin-bottom: 0; + border-radius: 0; + + &:focus { + border: 0; + } + } + + &__modifiers { + color: $inverted-text-color; + font-family: inherit; + font-size: 14px; + background: $white; + } + } + + &__toolbar { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + } + } + + .setting-text-label { + display: block; + color: $inverted-text-color; + font-size: 14px; + font-weight: 500; + margin-bottom: 10px; } .setting-toggle { @@ -598,15 +649,6 @@ } } -.report-modal__target { - padding: 20px; - - .media-modal__close { - top: 19px; - right: 15px; - } -} - .actions-modal { .status { overflow-y: auto; @@ -674,27 +716,30 @@ } .confirmation-modal__action-bar, -.mute-modal__action-bar { - .confirmation-modal__secondary-button, - .confirmation-modal__cancel-button, - .mute-modal__cancel-button { - background-color: transparent; - color: $lighter-text-color; - font-size: 14px; - font-weight: 500; - - &:hover, - &:focus, - &:active { - color: darken($lighter-text-color, 4%); - } - } - +.mute-modal__action-bar, +.block-modal__action-bar { .confirmation-modal__secondary-button { flex-shrink: 1; } } +.confirmation-modal__secondary-button, +.confirmation-modal__cancel-button, +.mute-modal__cancel-button, +.block-modal__cancel-button { + background-color: transparent; + color: $lighter-text-color; + font-size: 14px; + font-weight: 500; + + &:hover, + &:focus, + &:active { + color: darken($lighter-text-color, 4%); + background-color: transparent; + } +} + .confirmation-modal__do_not_ask_again { padding-left: 20px; padding-right: 20px; @@ -709,10 +754,10 @@ .confirmation-modal__container, .mute-modal__container, +.block-modal__container, .report-modal__target { padding: 30px; font-size: 16px; - text-align: center; strong { font-weight: 500; @@ -725,7 +770,42 @@ } } +.confirmation-modal__container, +.report-modal__target { + text-align: center; +} + +.block-modal, +.mute-modal { + &__explanation { + margin-top: 20px; + } + + .setting-toggle { + margin-top: 20px; + margin-bottom: 24px; + display: flex; + align-items: center; + + &__label { + color: $inverted-text-color; + margin: 0; + margin-left: 8px; + } + } +} + +.report-modal__target { + padding: 15px; + + .media-modal__close { + top: 14px; + right: 15px; + } +} + .embed-modal { + width: auto; max-width: 80vw; max-height: 80vh; @@ -756,6 +836,7 @@ font-size: 14px; margin: 0; margin-bottom: 15px; + border-radius: 4px; &::-moz-focus-inner { border: 0; @@ -781,25 +862,31 @@ max-width: 100%; overflow: hidden; border: 0; + border-radius: 4px; } } } .focal-point { position: relative; - cursor: pointer; + cursor: move; overflow: hidden; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background: $base-shadow-color; - &.dragging { - cursor: move; - } - - img { - max-width: 80vw; + img, + video, + canvas { + display: block; max-height: 80vh; - width: auto; + width: 100%; height: auto; - margin: auto; + margin: 0; + object-fit: contain; + background: $base-shadow-color; } &__reticle { @@ -819,4 +906,80 @@ top: 0; left: 0; } + + &__preview { + position: absolute; + bottom: 10px; + right: 10px; + z-index: 2; + cursor: move; + transition: opacity 0.1s ease; + + &:hover { + opacity: 0.5; + } + + strong { + color: $primary-text-color; + font-size: 14px; + font-weight: 500; + display: block; + margin-bottom: 5px; + } + + div { + border-radius: 4px; + box-shadow: 0 0 14px rgba($base-shadow-color, 0.2); + } + } + + @media screen and (max-width: 480px) { + img, + video { + max-height: 100%; + } + + &__preview { + display: none; + } + } +} + +.filtered-status-info { + text-align: start; + + .spoiler__text { + margin-top: 20px; + } + + .account { + border-bottom: 0; + } + + .account__display-name strong { + color: $inverted-text-color; + } + + .status__content__spoiler { + display: none; + + &--visible { + display: flex; + } + } + + ul { + padding: 10px; + margin-left: 12px; + list-style: disc inside; + } + + .filtered-status-edit-link { + color: $action-button-color; + text-decoration: none; + + &:hover { + text-decoration: underline + } + } } diff --git a/app/javascript/flavours/glitch/styles/components/regeneration_indicator.scss b/app/javascript/flavours/glitch/styles/components/regeneration_indicator.scss index 178df6652..c65e6a9af 100644 --- a/app/javascript/flavours/glitch/styles/components/regeneration_indicator.scss +++ b/app/javascript/flavours/glitch/styles/components/regeneration_indicator.scss @@ -7,37 +7,27 @@ cursor: default; display: flex; flex: 1 1 auto; + flex-direction: column; align-items: center; justify-content: center; padding: 20px; - & > div { - width: 100%; - background: transparent; - padding-top: 0; - } - &__figure { - background: url('~flavours/glitch/images/elephant_ui_working.svg') no-repeat center 0; - width: 100%; - height: 160px; - background-size: contain; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + &, + img { + display: block; + width: auto; + height: 160px; + margin: 0; + } } - &.missing-indicator { + &--without-header { padding-top: 20px + 48px; - - .regeneration-indicator__figure { - background-image: url('~flavours/glitch/images/elephant_ui_disappointed.svg'); - } } &__label { - margin-top: 200px; + margin-top: 30px; strong { display: block; diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index 3ef141133..30d69d05c 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -3,13 +3,47 @@ } .search__input { + @include search-input(); + display: block; - padding: 10px; + padding: 15px; padding-right: 30px; - @include search-input(); + line-height: 18px; + font-size: 16px; + + &::placeholder { + color: lighten($darker-text-color, 4%); + } + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus, + &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($ui-base-color, 4%); + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } } .search__icon { + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus { + outline: 0 !important; + } + .fa { position: absolute; top: 16px; @@ -18,7 +52,7 @@ display: inline-block; opacity: 0; transition: all 100ms linear; - transition-property: transform, opacity; + transition-property: color, transform, opacity; font-size: 18px; width: 18px; height: 18px; @@ -33,17 +67,18 @@ } .fa-search { - transform: rotate(90deg); + transform: rotate(0deg); &.active { - pointer-events: none; - transform: rotate(0deg); + pointer-events: auto; + opacity: 0.3; } } .fa-times-circle { top: 17px; transform: rotate(0deg); + color: $action-button-color; cursor: pointer; &.active { @@ -51,7 +86,7 @@ } &:hover { - color: $primary-text-color; + color: lighten($action-button-color, 7%); } } } @@ -65,6 +100,12 @@ font-weight: 500; } +.search-results__info { + padding: 20px; + color: $darker-text-color; + text-align: center; +} + .trends { &__header { color: $dark-text-color; @@ -124,11 +165,12 @@ &__current { flex: 0 0 auto; - width: 100px; font-size: 24px; line-height: 36px; font-weight: 500; - text-align: center; + text-align: right; + padding-right: 15px; + margin-left: 5px; color: $secondary-text-color; } @@ -136,7 +178,12 @@ flex: 0 0 auto; width: 50px; - path { + path:first-child { + fill: rgba($highlight-text-color, 0.25) !important; + fill-opacity: 1 !important; + } + + path:last-child { stroke: lighten($highlight-text-color, 6%) !important; } } diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index ca962abd2..edf705b5f 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -6,7 +6,7 @@ height: calc(100% - 10px); overflow-y: hidden; - .drawer--search input { + .search__input { line-height: 18px; font-size: 16px; padding: 15px; @@ -54,13 +54,24 @@ margin-bottom: 10px; height: calc(100% - 20px); overflow-y: auto; + display: flex; + flex-direction: column; + + & > a { + flex: 0 0 auto; + } hr { + flex: 0 0 auto; border: 0; background: transparent; border-top: 1px solid lighten($ui-base-color, 4%); margin: 10px 0; } + + .flex-spacer { + background: transparent; + } } @media screen and (min-width: 600px) { @@ -83,6 +94,24 @@ padding: 0; } + .directory__list { + display: grid; + grid-gap: 10px; + grid-template-columns: minmax(0, 50%) minmax(0, 50%); + + @media screen and (max-width: $no-gap-breakpoint) { + display: block; + } + } + + .directory__card { + margin-bottom: 0; + } + + .filter-form { + display: flex; + } + .autosuggest-textarea__textarea { font-size: 16px; } @@ -98,8 +127,17 @@ top: 15px; } - @media screen and (min-width: 360px) { + .scrollable { + overflow: visible; + + @supports(display: grid) { + contain: content; + } + } + + @media screen and (min-width: $no-gap-breakpoint) { padding: 10px 0; + padding-top: 0; } @media screen and (min-width: 630px) { @@ -107,7 +145,8 @@ padding: 15px; .media-gallery, - .video-player { + .video-player, + .audio-player { margin-top: 15px; } } @@ -131,7 +170,8 @@ .media-gallery, &__action-bar, - .video-player { + .video-player, + .audio-player { margin-top: 10px; } } @@ -184,19 +224,16 @@ } } -@media screen and (min-width: 360px) { +@media screen and (min-width: $no-gap-breakpoint) { .tabs-bar { - margin: 10px auto; - margin-bottom: 0; width: 100%; } .react-swipeable-view-container .columns-area--mobile { - height: calc(100% - 20px) !important; + height: calc(100% - 10px) !important; } .getting-started__wrapper, - .getting-started__trends, .search { margin-bottom: 10px; } diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index ee4440e89..00f947cdc 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -1,3 +1,47 @@ +@keyframes spring-flip-in { + 0% { + transform: rotate(0deg); + } + + 30% { + transform: rotate(-242.4deg); + } + + 60% { + transform: rotate(-158.35deg); + } + + 90% { + transform: rotate(-187.5deg); + } + + 100% { + transform: rotate(-180deg); + } +} + +@keyframes spring-flip-out { + 0% { + transform: rotate(-180deg); + } + + 30% { + transform: rotate(62.4deg); + } + + 60% { + transform: rotate(-21.635deg); + } + + 90% { + transform: rotate(7.5deg); + } + + 100% { + transform: rotate(0deg); + } +} + .status__content--with-action { cursor: pointer; } @@ -33,6 +77,13 @@ .status__content__text, .e-content { + overflow: hidden; + + & > ul, + & > ol { + margin-bottom: 20px; + } + h1, h2, h3, h4, h5 { margin-top: 20px; margin-bottom: 20px; @@ -75,6 +126,11 @@ text-align: sub; } + sup { + font-size: smaller; + vertical-align: super; + } + ul, ol { margin-left: 1em; @@ -127,6 +183,15 @@ } } + a.unhandled-link { + color: lighten($ui-highlight-color, 8%); + + .link-origin-tag { + color: $gold-star; + font-size: 0.8em; + } + } + .status__content__spoiler-link { background: lighten($ui-base-color, 30%); @@ -167,7 +232,9 @@ } .notif-cleaning { - .status, .notification-follow { + .status, + .notification-follow, + .notification-follow-request { padding-right: ($dismiss-overlay-width + 0.5rem); } } @@ -187,13 +254,12 @@ } .status__prepend-icon-wrapper { - float: left; - margin: 0 10px 0 -58px; - width: 48px; - text-align: right; + left: -26px; + position: absolute; } -.notification-follow { +.notification-follow, +.notification-follow-request { position: relative; // same like Status @@ -245,7 +311,8 @@ opacity: 1; animation: fade 150ms linear; - .video-player { + .video-player, + .audio-player { margin-top: 8px; } @@ -364,9 +431,7 @@ .status__relative-time { display: inline-block; - margin-left: auto; - padding-left: 18px; - width: 120px; + flex-grow: 1; color: $dark-text-color; font-size: 14px; text-align: right; @@ -376,7 +441,6 @@ } .status__display-name { - margin: 0 auto 0 0; color: $dark-text-color; overflow: hidden; } @@ -388,6 +452,7 @@ .status__info { display: flex; + justify-content: space-between; font-size: 15px; > span { @@ -401,25 +466,41 @@ } .status__info__icons { - margin-left: auto; display: flex; align-items: center; height: 1em; color: $action-button-color; - .status__media-icon { - padding-left: 6px; - padding-right: 1px; + .status__media-icon, + .status__visibility-icon, + .status__reply-icon { + padding-left: 2px; + padding-right: 2px; + } + + .status__collapse-button.active > .fa-angle-double-up { + transform: rotate(-180deg); + } +} + +.no-reduce-motion .status__collapse-button { + &.activate { + & > .fa-angle-double-up { + animation: spring-flip-in 1s linear; + } } - .status__visibility-icon { - padding-left: 4px; + &.deactivate { + & > .fa-angle-double-up { + animation: spring-flip-out 1s linear; + } } } .status__info__account { display: flex; align-items: center; + justify-content: flex-start; } .status-check-box { @@ -439,7 +520,8 @@ white-space: normal; } - .video-player { + .video-player, + .audio-player { margin-top: 8px; max-width: 250px; } @@ -459,9 +541,12 @@ } .status__prepend { - margin: -10px -10px 10px; + margin-top: -10px; + margin-bottom: 10px; + margin-left: 58px; color: $dark-text-color; - padding: 8px 10px 0 68px; + padding: 8px 0; + padding-bottom: 2px; font-size: 14px; position: relative; @@ -544,7 +629,8 @@ } } - .video-player { + .video-player, + .audio-player { margin-top: 8px; } } @@ -620,6 +706,10 @@ a.status__display-name, color: inherit; } +.detailed-status .button.logo-button { + margin-bottom: 15px; +} + .detailed-status__display-name { color: $secondary-text-color; display: block; @@ -653,6 +743,7 @@ a.status__display-name, } .muted { + .status__content, .status__content p, .status__content a, .status__content__text { @@ -866,67 +957,6 @@ a.status-card.compact:hover { background-position: center center; } -.status__video-player { - display: flex; - align-items: center; - background: $base-shadow-color; - box-sizing: border-box; - cursor: default; /* May not be needed */ - margin-top: 8px; - overflow: hidden; - position: relative; - - @include fullwidth-gallery; -} - -.status__video-player-video { - height: 100%; - object-fit: contain; - position: relative; - top: 50%; - transform: translateY(-50%); - width: 100%; - z-index: 1; - - &:not(.letterbox) { - height: 100%; - object-fit: cover; - } -} - -.status__video-player-expand, -.status__video-player-mute { - color: $primary-text-color; - opacity: 0.8; - position: absolute; - right: 4px; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; -} - -.status__video-player-spoiler { - display: none; - color: $primary-text-color; - left: 4px; - position: absolute; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; - top: 4px; - z-index: 100; - - &.status__video-player-spoiler--visible { - display: block; - } -} - -.status__video-player-expand { - bottom: 4px; - z-index: 100; -} - -.status__video-player-mute { - top: 4px; - z-index: 5; -} - .attachment-list { display: flex; font-size: 14px; @@ -990,3 +1020,18 @@ a.status-card.compact:hover { } } } + +.status__wrapper--filtered__button { + display: inline; + color: lighten($ui-highlight-color, 8%); + border: 0; + background: transparent; + padding: 0; + font-size: inherit; + line-height: inherit; + + &:hover, + &:active { + text-decoration: underline; + } +} diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index dc60dd14b..d1c6c33d7 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -143,10 +143,71 @@ grid-row: 3; } + @media screen and (max-width: $no-gap-breakpoint) { + grid-gap: 0; + grid-template-columns: minmax(0, 100%); + + .column-0 { + grid-column: 1; + } + + .column-1 { + grid-column: 1; + grid-row: 3; + } + + .column-2 { + grid-column: 1; + grid-row: 2; + } + + .column-3 { + grid-column: 1; + grid-row: 4; + } + } +} + +.grid-4 { + display: grid; + grid-gap: 10px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-auto-columns: 25%; + grid-auto-rows: max-content; + + .column-0 { + grid-column: 1 / 5; + grid-row: 1; + } + + .column-1 { + grid-column: 1 / 4; + grid-row: 2; + } + + .column-2 { + grid-column: 4; + grid-row: 2; + } + + .column-3 { + grid-column: 2 / 5; + grid-row: 3; + } + + .column-4 { + grid-column: 1; + grid-row: 3; + } + .landing-page__call-to-action { min-height: 100%; } + .flash-message { + margin-bottom: 10px; + } + @media screen and (max-width: 738px) { grid-template-columns: minmax(0, 50%) minmax(0, 50%); @@ -188,6 +249,11 @@ .column-3 { grid-column: 1; + grid-row: 5; + } + + .column-4 { + grid-column: 1; grid-row: 4; } } @@ -348,6 +414,20 @@ } } + .directory__card { + border-radius: 4px; + + @media screen and (max-width: $no-gap-breakpoint) { + border-radius: 0; + } + } + + .page-header { + @media screen and (max-width: $no-gap-breakpoint) { + border-bottom: 0; + } + } + .public-account-header { overflow: hidden; margin-bottom: 10px; @@ -572,7 +652,7 @@ } .counter { - width: 33.3%; + min-width: 33.3%; box-sizing: border-box; flex: 0 0 auto; color: $darker-text-color; @@ -755,14 +835,22 @@ } } - .static-icon-button { - color: $action-button-color; - font-size: 18px; + .directory__list { + display: grid; + grid-gap: 10px; + grid-template-columns: minmax(0, 50%) minmax(0, 50%); - & > span { - font-size: 14px; - font-weight: 500; + @media screen and (max-width: $no-gap-breakpoint) { + display: block; } + + .icon-button { + font-size: 18px; + } + } + + .directory__card { + margin-bottom: 0; } .card-grid { diff --git a/app/javascript/flavours/glitch/styles/dashboard.scss b/app/javascript/flavours/glitch/styles/dashboard.scss index e4564f062..c0944d417 100644 --- a/app/javascript/flavours/glitch/styles/dashboard.scss +++ b/app/javascript/flavours/glitch/styles/dashboard.scss @@ -15,6 +15,8 @@ padding: 20px; background: lighten($ui-base-color, 4%); border-radius: 4px; + box-sizing: border-box; + height: 100%; } & > a { diff --git a/app/javascript/flavours/glitch/styles/footer.scss b/app/javascript/flavours/glitch/styles/footer.scss index f74c004e9..00d290883 100644 --- a/app/javascript/flavours/glitch/styles/footer.scss +++ b/app/javascript/flavours/glitch/styles/footer.scss @@ -128,7 +128,7 @@ &:hover, &:focus, &:active { - svg path { + svg { fill: lighten($ui-base-color, 38%); } } diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index dae29a003..1920c33ea 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -245,6 +245,10 @@ code { &-6 { max-width: 50%; } + + .actions { + margin-top: 27px; + } } .fields-group:last-child, @@ -300,6 +304,13 @@ code { } } + .input.static .label_input__wrapper { + font-size: 16px; + padding: 10px; + border: 1px solid $dark-text-color; + border-radius: 4px; + } + input[type=text], input[type=number], input[type=email], @@ -318,6 +329,10 @@ code { border-radius: 4px; padding: 10px; + &::placeholder { + color: lighten($darker-text-color, 4%); + } + &:invalid { box-shadow: none; } @@ -489,6 +504,10 @@ code { &__overlay-area { position: relative; + &__blurred form { + filter: blur(2px); + } + &__overlay { position: absolute; top: 0; @@ -499,8 +518,10 @@ code { justify-content: center; align-items: center; background: rgba($ui-base-color, 0.65); - backdrop-filter: blur(2px); border-radius: 4px; + margin-left: -4px; + margin-top: -4px; + padding: 4px; &__content { text-align: center; diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index 323b2e7fe..af73feb89 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -14,7 +14,7 @@ @import 'widgets'; @import 'forms'; @import 'accounts'; -@import 'stream_entries'; +@import 'statuses'; @import 'components/index'; @import 'polls'; @import 'about'; diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 7da8edbde..3b4ffdf3c 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -135,13 +135,12 @@ } } - .composer--options { + .composer--options-wrapper { background: lighten($ui-base-color, 10%); - box-shadow: unset; + } - & > hr { - display: none; - } + .composer--options > hr { + display: none; } .composer--options--dropdown--content--item { @@ -227,16 +226,29 @@ .boost-modal, .confirmation-modal, .mute-modal, +.block-modal, .report-modal, .embed-modal, .error-modal, -.onboarding-modal { - background: $ui-base-color; +.onboarding-modal, +.report-modal__comment .setting-text__wrapper, +.report-modal__comment .setting-text { + background: $white; + border: 1px solid lighten($ui-base-color, 8%); +} + +.report-modal__comment { + border-right-color: lighten($ui-base-color, 8%); +} + +.report-modal__container { + border-top-color: lighten($ui-base-color, 8%); } .boost-modal__action-bar, .confirmation-modal__action-bar, .mute-modal__action-bar, +.block-modal__action-bar, .onboarding-modal__paginator, .error-modal__footer { background: darken($ui-base-color, 6%); @@ -373,3 +385,10 @@ .directory__tag > div { box-shadow: none; } + +.audio-player .video-player__controls button, +.audio-player .video-player__time-sep, +.audio-player .video-player__time-current, +.audio-player .video-player__time-total { + color: $primary-text-color; +} diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss index 1b060b58d..312f5e314 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss @@ -18,6 +18,8 @@ $darker-text-color: $classic-base-color !default; $dark-text-color: #444b5d; $action-button-color: #606984; +$success-green: lighten(#3c754d, 8%); + $base-overlay-background: $white !default; $inverted-text-color: $black !default; diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index 5261f17f4..49d0e7f71 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -11,7 +11,6 @@ li { margin-bottom: 10px; position: relative; - height: 18px + 12px; } &__chart { @@ -30,13 +29,11 @@ &__text { position: relative; - display: inline-block; + display: flex; padding: 6px 0; line-height: 18px; cursor: default; - white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; input[type=radio], input[type=checkbox] { @@ -89,6 +86,9 @@ top: -1px; border-radius: 50%; vertical-align: middle; + margin-top: auto; + margin-bottom: auto; + flex: 0 0 18px; &.checkbox { border-radius: 4px; @@ -98,14 +98,40 @@ border-color: $valid-value-color; background: $valid-value-color; } + + &:active, + &:focus, + &:hover { + border-width: 4px; + background: none; + } + + &::-moz-focus-inner { + outline: 0 !important; + border: 0; + } + + &:focus, + &:active { + outline: 0 !important; + } } &__number { display: inline-block; - width: 36px; + width: 52px; font-weight: 700; padding: 0 10px; + padding-left: 8px; text-align: right; + margin-top: auto; + margin-bottom: auto; + flex: 0 0 52px; + } + + &__vote__mark { + float: left; + line-height: 18px; } &__footer { @@ -159,6 +185,10 @@ select { width: 100%; flex: 1 1 50%; + + &:focus { + border-color: $highlight-text-color; + } } } diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss index 11fae3121..2375bac90 100644 --- a/app/javascript/flavours/glitch/styles/rtl.scss +++ b/app/javascript/flavours/glitch/styles/rtl.scss @@ -7,6 +7,34 @@ body.rtl { padding-right: 15px; } + .radio-button__input { + margin-right: 0; + margin-left: 10px; + } + + .directory__card__bar .display-name { + margin-left: 0; + margin-right: 15px; + } + + .display-name { + text-align: right; + } + + .notification__message { + margin-left: 0; + margin-right: 68px; + } + + .drawer__inner__mastodon > img { + transform: scaleX(-1); + } + + .notification__favourite-icon-wrapper { + left: auto; + right: -26px; + } + .landing-page__logo { margin-right: 0; margin-left: 20px; @@ -28,6 +56,15 @@ body.rtl { margin-left: 4px; } + .composer--publisher { + text-align: left; + } + + .boost-modal__status-time, + .favourite-modal__status-time { + float: left; + } + .navigation-bar__profile { margin-left: 0; margin-right: 8px; @@ -50,8 +87,8 @@ body.rtl { .column-header__buttons { left: 0; right: auto; - margin-left: -15px; - margin-right: 0; + margin-left: 0; + margin-right: -15px; } .column-inline-form .icon-button { @@ -87,11 +124,14 @@ body.rtl { } .status__avatar { + margin-left: 10px; + margin-right: 0; + + // Those are used for public pages left: auto; right: 10px; } - .status, .activity-stream .status.light { padding-left: 10px; padding-right: 68px; @@ -110,7 +150,7 @@ body.rtl { .status__prepend { margin-left: 0; - margin-right: 68px; + margin-right: 58px; } .status__prepend-icon-wrapper { @@ -136,21 +176,10 @@ body.rtl { .status__relative-time, .activity-stream .status.light .status__header .status__meta { float: left; - } - - .activity-stream .detailed-status.light .detailed-status__display-name > div { - float: right; - margin-right: 0; - margin-left: 10px; - } - - .activity-stream .detailed-status.light .detailed-status__meta span > span { - margin-left: 0; - margin-right: 6px; + text-align: left; } .status__action-bar { - &__counter { margin-right: 0; margin-left: 11px; @@ -182,6 +211,10 @@ body.rtl { margin-right: 0; } + .detailed-status__display-name .display-name { + text-align: right; + } + .detailed-status__display-avatar { margin-right: 0; margin-left: 10px; @@ -195,7 +228,6 @@ body.rtl { } .fa-ul { - margin-left: 0; margin-left: 2.14285714em; } @@ -340,6 +372,12 @@ body.rtl { } } + .columns-area--mobile .column, + .columns-area--mobile .drawer { + padding-left: 0; + padding-right: 0; + } + .public-layout { .header { .nav-button { diff --git a/app/javascript/flavours/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/statuses.scss index de9c2612c..611d5185b 100644 --- a/app/javascript/flavours/glitch/styles/stream_entries.scss +++ b/app/javascript/flavours/glitch/styles/statuses.scss @@ -205,9 +205,20 @@ } .rtl { - .embed, .public-layout { - .status .status__relative-time { - float: left; + .embed, + .public-layout { + .status { + padding-left: 10px; + padding-right: 68px; + + .status__info .status__display-name { + padding-left: 25px; + padding-right: 0; + } + + .status__relative-time { + float: left; + } } } } diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss index 154844665..ec2ee7c1c 100644 --- a/app/javascript/flavours/glitch/styles/tables.scss +++ b/app/javascript/flavours/glitch/styles/tables.scss @@ -149,10 +149,6 @@ a.table-action-link { margin-top: 0; } } - - @media screen and (max-width: $no-gap-breakpoint) { - display: none; - } } &__actions, @@ -174,9 +170,17 @@ a.table-action-link { text-align: right; padding-right: 16px - 5px; } + } - @media screen and (max-width: $no-gap-breakpoint) { - display: none; + &__form { + padding: 16px; + border: 1px solid darken($ui-base-color, 8%); + border-top: 0; + background: $ui-base-color; + + .fields-row { + padding-top: 0; + margin-bottom: 0; } } @@ -186,7 +190,7 @@ a.table-action-link { background: darken($ui-base-color, 4%); @media screen and (max-width: $no-gap-breakpoint) { - &:first-child { + .optional &:first-child { border-top: 1px solid darken($ui-base-color, 8%); } } @@ -210,6 +214,52 @@ a.table-action-link { &--unpadded { padding: 0; } + + &--with-image { + display: flex; + align-items: center; + } + + &__image { + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + + .emojione { + width: 32px; + height: 32px; + } + } + + &__text { + flex: 1 1 auto; + } + + &__extra { + flex: 0 0 auto; + text-align: right; + color: $darker-text-color; + font-weight: 500; + } + } + + .directory__tag { + margin: 0; + width: 100%; + + a { + background: transparent; + border-radius: 0; + } + } + } + + &.optional .batch-table__toolbar, + &.optional .batch-table__row__select { + @media screen and (max-width: $no-gap-breakpoint) { + display: none; } } diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index acaf5b024..a6f7fc0be 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -100,6 +100,16 @@ background-size: 44px 44px; } } + + .trends__item { + padding: 10px; + } +} + +.trends-widget { + h4 { + color: $darker-text-color; + } } .box-widget { @@ -109,41 +119,52 @@ box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); } -.contact-widget, -.landing-page__information.contact-widget { - box-sizing: border-box; - padding: 20px; - min-height: 100%; +.placeholder-widget { + padding: 16px; border-radius: 4px; - background: $ui-base-color; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + border: 2px dashed $dark-text-color; + text-align: center; + color: $darker-text-color; + margin-bottom: 10px; } .contact-widget { + min-height: 100%; font-size: 15px; color: $darker-text-color; line-height: 20px; word-wrap: break-word; font-weight: 400; + padding: 0; - strong { - font-weight: 500; + h4 { + padding: 10px; + text-transform: uppercase; + font-weight: 700; + font-size: 13px; + color: $darker-text-color; } - p { - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } + .account { + border-bottom: 0; + padding: 10px 0; + padding-top: 5px; } - &__mail { - margin-top: 10px; + & > a { + display: inline-block; + padding: 10px; + padding-top: 0; + color: $darker-text-color; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; - a { - color: $primary-text-color; - text-decoration: none; + &:hover, + &:focus, + &:active { + text-decoration: underline; } } } @@ -521,6 +542,12 @@ $fluid-breakpoint: $maximum-width + 20px; a { font-size: 14px; line-height: 20px; + } +} + +.notice-widget, +.placeholder-widget { + a { text-decoration: none; font-weight: 500; color: $ui-highlight-color; @@ -532,3 +559,38 @@ $fluid-breakpoint: $maximum-width + 20px; } } } + +.table-of-contents { + background: darken($ui-base-color, 4%); + min-height: 100%; + font-size: 14px; + border-radius: 4px; + + li a { + display: block; + font-weight: 500; + padding: 15px; + overflow: hidden; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-decoration: none; + color: $primary-text-color; + border-bottom: 1px solid lighten($ui-base-color, 4%); + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + li:last-child a { + border-bottom: 0; + } + + li ul { + padding-left: 20px; + border-bottom: 1px solid lighten($ui-base-color, 4%); + } +} diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml index 06e26ade2..0fd627f19 100644 --- a/app/javascript/flavours/glitch/theme.yml +++ b/app/javascript/flavours/glitch/theme.yml @@ -18,7 +18,7 @@ pack: mailer: modal: public: packs/public.js - settings: + settings: packs/settings.js share: packs/share.js # (OPTIONAL) The directory which contains localization files for diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js index f2aeda834..26255bbb7 100644 --- a/app/javascript/flavours/glitch/util/async-components.js +++ b/app/javascript/flavours/glitch/util/async-components.js @@ -122,6 +122,10 @@ export function MuteModal () { return import(/* webpackChunkName: "flavours/glitch/async/mute_modal" */'flavours/glitch/features/ui/components/mute_modal'); } +export function BlockModal () { + return import(/* webpackChunkName: "flavours/glitch/async/block_modal" */'flavours/glitch/features/ui/components/block_modal'); +} + export function ReportModal () { return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'flavours/glitch/features/ui/components/report_modal'); } @@ -138,6 +142,10 @@ export function Video () { return import(/* webpackChunkName: "flavours/glitch/async/video" */'flavours/glitch/features/video'); } +export function Audio () { + return import(/* webpackChunkName: "features/glitch/async/audio" */'flavours/glitch/features/audio'); +} + export function EmbedModal () { return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'flavours/glitch/features/ui/components/embed_modal'); } @@ -153,3 +161,11 @@ export function ListAdder () { export function Search () { return import(/*webpackChunkName: "features/glitch/async/search" */'flavours/glitch/features/search'); } + +export function Tesseract () { + return import(/*webpackChunkName: "tesseract" */'tesseract.js'); +} + +export function Directory () { + return import(/* webpackChunkName: "features/glitch/async/directory" */'flavours/glitch/features/directory'); +} diff --git a/app/javascript/flavours/glitch/util/backend_links.js b/app/javascript/flavours/glitch/util/backend_links.js index 4fc03f919..0fb378cc1 100644 --- a/app/javascript/flavours/glitch/util/backend_links.js +++ b/app/javascript/flavours/glitch/util/backend_links.js @@ -4,3 +4,6 @@ export const signOutLink = '/auth/sign_out'; export const termsLink = '/terms'; export const accountAdminLink = (id) => `/admin/accounts/${id}`; export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`; +export const filterEditLink = (id) => `/filters/${id}/edit`; +export const relationshipsLink = '/relationships'; +export const securityLink = '/auth/edit'; diff --git a/app/javascript/flavours/glitch/util/compare_id.js b/app/javascript/flavours/glitch/util/compare_id.js index aaff66481..66cf51c4b 100644 --- a/app/javascript/flavours/glitch/util/compare_id.js +++ b/app/javascript/flavours/glitch/util/compare_id.js @@ -1,10 +1,11 @@ -export default function compareId(id1, id2) { +export default function compareId (id1, id2) { if (id1 === id2) { return 0; } + if (id1.length === id2.length) { return id1 > id2 ? 1 : -1; } else { return id1.length > id2.length ? 1 : -1; } -} +}; diff --git a/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js index 164fdcc0b..e4519a13e 100644 --- a/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js +++ b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js @@ -74,7 +74,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo return [emojisList['-1']]; } - let values = value.toLowerCase().split(/[\s|,|\-|_]+/), + let values = value.toLowerCase().split(/[\s|,\-_]+/), allResults = []; if (values.length > 2) { diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index 82a1ef89c..e1a244127 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -1,4 +1,4 @@ -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, useSystemEmojiFont } from 'flavours/glitch/util/initial_state'; import unicodeMapping from './emoji_unicode_mapping_light'; import Trie from 'substring-trie'; @@ -12,7 +12,7 @@ const emojify = (str, customEmojis = {}) => { let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0; for (;;) { let match, i = 0, tag; - while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) { + while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || useSystemEmojiFont || !(match = trie.search(str.slice(i))))) { i += str.codePointAt(i) < 65536 ? 1 : 2; } let rend, replacement = ''; @@ -29,7 +29,7 @@ const emojify = (str, customEmojis = {}) => { // if you want additional emoji handler, add statements below which set replacement and return true. if (shortname in customEmojis) { const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; - replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`; + replacement = `<img draggable="false" class="emojione custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`; return true; } return false; @@ -57,7 +57,7 @@ const emojify = (str, customEmojis = {}) => { } } i = rend; - } else { // matched to unicode emoji + } else if (!useSystemEmojiFont) { // matched to unicode emoji const { filename, shortCode } = unicodeMapping[match]; const title = shortCode ? `:${shortCode}:` : ''; replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`; @@ -93,8 +93,11 @@ export const buildCustomEmojis = (customEmojis) => { keywords: [name], imageUrl: url, custom: true, + customCategory: emoji.get('category'), }); }); return emojis; }; + +export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom'])); diff --git a/app/javascript/flavours/glitch/util/idna.js b/app/javascript/flavours/glitch/util/idna.js new file mode 100644 index 000000000..efab5bacf --- /dev/null +++ b/app/javascript/flavours/glitch/util/idna.js @@ -0,0 +1,10 @@ +import punycode from 'punycode'; + +const IDNA_PREFIX = 'xn--'; + +export const decode = domain => { + return domain + .split('.') + .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part) + .join('.'); +}; diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index f42c06a3a..911468e6f 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -26,8 +26,13 @@ export const pollLimits = (initialState && initialState.poll_limits); export const invitesEnabled = getMeta('invites_enabled'); export const version = getMeta('version'); export const mascot = getMeta('mascot'); +export const profile_directory = getMeta('profile_directory'); export const isStaff = getMeta('is_staff'); export const defaultContentType = getMeta('default_content_type'); export const forceSingleColumn = getMeta('advanced_layout') === false; +export const useBlurhash = getMeta('use_blurhash'); +export const usePendingItems = getMeta('use_pending_items'); +export const useSystemEmojiFont = getMeta('system_emoji_font'); +export const showTrends = getMeta('trends'); export default initialState; diff --git a/app/javascript/flavours/glitch/util/load_keyboard_extensions.js b/app/javascript/flavours/glitch/util/load_keyboard_extensions.js new file mode 100644 index 000000000..2dd0e45fa --- /dev/null +++ b/app/javascript/flavours/glitch/util/load_keyboard_extensions.js @@ -0,0 +1,16 @@ +// On KaiOS, we may not be able to use a mouse cursor or navigate using Tab-based focus, so we install +// special left/right focus navigation keyboard listeners, at least on public pages (i.e. so folks +// can at least log in using KaiOS devices). + +function importArrowKeyNavigation() { + return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation'); +} + +export default function loadKeyboardExtensions() { + if (/KAIOS/.test(navigator.userAgent)) { + return importArrowKeyNavigation().then(arrowKeyNav => { + arrowKeyNav.register(); + }); + } + return Promise.resolve(); +} diff --git a/app/javascript/flavours/glitch/util/log_out.js b/app/javascript/flavours/glitch/util/log_out.js new file mode 100644 index 000000000..8e1659293 --- /dev/null +++ b/app/javascript/flavours/glitch/util/log_out.js @@ -0,0 +1,34 @@ +import Rails from 'rails-ujs'; +import { signOutLink } from 'flavours/glitch/util/backend_links'; + +export const logOut = () => { + const form = document.createElement('form'); + + const methodInput = document.createElement('input'); + methodInput.setAttribute('name', '_method'); + methodInput.setAttribute('value', 'delete'); + methodInput.setAttribute('type', 'hidden'); + form.appendChild(methodInput); + + const csrfToken = Rails.csrfToken(); + const csrfParam = Rails.csrfParam(); + + if (csrfParam && csrfToken) { + const csrfInput = document.createElement('input'); + csrfInput.setAttribute('name', csrfParam); + csrfInput.setAttribute('value', csrfToken); + csrfInput.setAttribute('type', 'hidden'); + form.appendChild(csrfInput); + } + + const submitButton = document.createElement('input'); + submitButton.setAttribute('type', 'submit'); + form.appendChild(submitButton); + + form.method = 'post'; + form.action = signOutLink; + form.style.display = 'none'; + + document.body.appendChild(form); + submitButton.click(); +}; diff --git a/app/javascript/flavours/glitch/util/numbers.js b/app/javascript/flavours/glitch/util/numbers.js index fdd8269ae..f7e4ceb93 100644 --- a/app/javascript/flavours/glitch/util/numbers.js +++ b/app/javascript/flavours/glitch/util/numbers.js @@ -4,7 +4,9 @@ import { FormattedNumber } from 'react-intl'; export const shortNumberFormat = number => { if (number < 1000) { return <FormattedNumber value={number} />; - } else { + } else if (number < 1000000) { return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>; + } else { + return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>; } }; diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js index bbdbc865e..d566edb03 100644 --- a/app/javascript/flavours/glitch/util/resize_image.js +++ b/app/javascript/flavours/glitch/util/resize_image.js @@ -67,6 +67,14 @@ const processImage = (img, { width, height, orientation, type = 'image/png' }) = context.drawImage(img, 0, 0, width, height); + // The Tor Browser and maybe other browsers may prevent reading from canvas + // and return an all-white image instead. Assume reading failed if the resized + // image is perfectly white. + const imageData = context.getImageData(0, 0, width, height); + if (imageData.data.every(value => value === 255)) { + throw 'Failed to read from canvas'; + } + canvas.toBlob(resolve, type); }); diff --git a/app/javascript/flavours/glitch/util/rtl.js b/app/javascript/flavours/glitch/util/rtl.js index 00870a15d..89bed6de8 100644 --- a/app/javascript/flavours/glitch/util/rtl.js +++ b/app/javascript/flavours/glitch/util/rtl.js @@ -20,6 +20,7 @@ export function isRtl(text) { text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, ''); text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, ''); text = text.replace(/\s+/g, ''); + text = text.replace(/(\w\S+\.\w{2,}\S*)/g, ''); const matches = text.match(rtlChars); diff --git a/app/javascript/flavours/glitch/util/scroll.js b/app/javascript/flavours/glitch/util/scroll.js index 2af07e0fb..84fe58269 100644 --- a/app/javascript/flavours/glitch/util/scroll.js +++ b/app/javascript/flavours/glitch/util/scroll.js @@ -26,5 +26,7 @@ const scroll = (node, key, target) => { }; }; -export const scrollRight = (node, position) => scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => scroll(node, 'scrollTop', 0); +const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; + +export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); +export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); diff --git a/app/javascript/flavours/glitch/util/scrollbar.js b/app/javascript/flavours/glitch/util/scrollbar.js new file mode 100644 index 000000000..929b036d6 --- /dev/null +++ b/app/javascript/flavours/glitch/util/scrollbar.js @@ -0,0 +1,34 @@ +/** @type {number | null} */ +let cachedScrollbarWidth = null; + +/** + * @return {number} + */ +const getActualScrollbarWidth = () => { + const outer = document.createElement('div'); + outer.style.visibility = 'hidden'; + outer.style.overflow = 'scroll'; + document.body.appendChild(outer); + + const inner = document.createElement('div'); + outer.appendChild(inner); + + const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; + outer.parentNode.removeChild(outer); + + return scrollbarWidth; +}; + +/** + * @return {number} + */ +export const getScrollbarWidth = () => { + if (cachedScrollbarWidth !== null) { + return cachedScrollbarWidth; + } + + const scrollbarWidth = getActualScrollbarWidth(); + cachedScrollbarWidth = scrollbarWidth; + + return scrollbarWidth; +}; diff --git a/app/javascript/flavours/glitch/util/stream.js b/app/javascript/flavours/glitch/util/stream.js index c4642344f..50f90d44c 100644 --- a/app/javascript/flavours/glitch/util/stream.js +++ b/app/javascript/flavours/glitch/util/stream.js @@ -1,4 +1,4 @@ -import WebSocketClient from 'websocket.js'; +import WebSocketClient from '@gamestdio/websocket'; const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js index ef2500e7b..cd36d8007 100644 --- a/app/javascript/mastodon/actions/alerts.js +++ b/app/javascript/mastodon/actions/alerts.js @@ -3,6 +3,8 @@ import { defineMessages } from 'react-intl'; const messages = defineMessages({ unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, + rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' }, + rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' }, }); export const ALERT_SHOW = 'ALERT_SHOW'; @@ -23,23 +25,29 @@ export function clearAlert() { }; }; -export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) { return { type: ALERT_SHOW, title, message, + message_values, }; }; export function showAlertForError(error) { if (error.response) { - const { data, status, statusText } = error.response; + const { data, status, statusText, headers } = error.response; if (status === 404 || status === 410) { // Skip these errors as they are reflected in the UI return { type: ALERT_NOOP }; } + if (status === 429 && headers['x-ratelimit-reset']) { + const reset_date = new Date(headers['x-ratelimit-reset']); + return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date }); + } + let message = statusText; let title = `${status}`; diff --git a/app/javascript/mastodon/actions/app.js b/app/javascript/mastodon/actions/app.js new file mode 100644 index 000000000..414968f7d --- /dev/null +++ b/app/javascript/mastodon/actions/app.js @@ -0,0 +1,10 @@ +export const APP_FOCUS = 'APP_FOCUS'; +export const APP_UNFOCUS = 'APP_UNFOCUS'; + +export const focusApp = () => ({ + type: APP_FOCUS, +}); + +export const unfocusApp = () => ({ + type: APP_UNFOCUS, +}); diff --git a/app/javascript/mastodon/actions/blocks.js b/app/javascript/mastodon/actions/blocks.js index 7000f5a71..fd9881302 100644 --- a/app/javascript/mastodon/actions/blocks.js +++ b/app/javascript/mastodon/actions/blocks.js @@ -1,6 +1,7 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; +import { openModal } from './modal'; export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; @@ -10,6 +11,8 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; +export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL'; + export function fetchBlocks() { return (dispatch, getState) => { dispatch(fetchBlocksRequest()); @@ -83,3 +86,14 @@ export function expandBlocksFail(error) { error, }; }; + +export function initBlockModal(account) { + return dispatch => { + dispatch({ + type: BLOCKS_INIT_MODAL, + account, + }); + + dispatch(openModal('BLOCK')); + }; +} diff --git a/app/javascript/mastodon/actions/bookmarks.js b/app/javascript/mastodon/actions/bookmarks.js new file mode 100644 index 000000000..544ed2ff2 --- /dev/null +++ b/app/javascript/mastodon/actions/bookmarks.js @@ -0,0 +1,90 @@ +import api, { getLinks } from '../api'; +import { importFetchedStatuses } from './importer'; + +export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; +export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS'; +export const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL'; + +export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST'; +export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS'; +export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL'; + +export function fetchBookmarkedStatuses() { + return (dispatch, getState) => { + if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) { + return; + } + + dispatch(fetchBookmarkedStatusesRequest()); + + api(getState).get('/api/v1/bookmarks').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchBookmarkedStatusesFail(error)); + }); + }; +}; + +export function fetchBookmarkedStatusesRequest() { + return { + type: BOOKMARKED_STATUSES_FETCH_REQUEST, + }; +}; + +export function fetchBookmarkedStatusesSuccess(statuses, next) { + return { + type: BOOKMARKED_STATUSES_FETCH_SUCCESS, + statuses, + next, + }; +}; + +export function fetchBookmarkedStatusesFail(error) { + return { + type: BOOKMARKED_STATUSES_FETCH_FAIL, + error, + }; +}; + +export function expandBookmarkedStatuses() { + return (dispatch, getState) => { + const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null); + + if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) { + return; + } + + dispatch(expandBookmarkedStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandBookmarkedStatusesFail(error)); + }); + }; +}; + +export function expandBookmarkedStatusesRequest() { + return { + type: BOOKMARKED_STATUSES_EXPAND_REQUEST, + }; +}; + +export function expandBookmarkedStatusesSuccess(statuses, next) { + return { + type: BOOKMARKED_STATUSES_EXPAND_SUCCESS, + statuses, + next, + }; +}; + +export function expandBookmarkedStatusesFail(error) { + return { + type: BOOKMARKED_STATUSES_EXPAND_FAIL, + error, + }; +}; diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 300fb48a9..c3c6ff1a1 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -11,7 +11,7 @@ import { showAlertForError } from './alerts'; import { showAlert } from './alerts'; import { defineMessages } from 'react-intl'; -let cancelFetchComposeSuggestionsAccounts; +let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; @@ -139,7 +139,7 @@ export function submitCompose(routerHistory) { in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: media.map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']), - spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), + spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', visibility: getState().getIn(['compose', 'privacy']), poll: getState().getIn(['compose', 'poll'], null), }, { @@ -205,10 +205,11 @@ export function uploadCompose(files) { return function (dispatch, getState) { const uploadLimit = 4; const media = getState().getIn(['compose', 'media_attachments']); + const pending = getState().getIn(['compose', 'pending_media_attachments']); const progress = new Array(files.length).fill(0); let total = Array.from(files).reduce((a, v) => a + v.size, 0); - if (files.length + media.size > uploadLimit) { + if (files.length + media.size + pending > uploadLimit) { dispatch(showAlert(undefined, messages.uploadErrorLimit)); return; } @@ -234,7 +235,7 @@ export function uploadCompose(files) { progress[i] = loaded; dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); }, - }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).then(({ data }) => dispatch(uploadComposeSuccess(data, f))); }).catch(error => dispatch(uploadComposeFail(error))); }; }; @@ -289,10 +290,11 @@ export function uploadComposeProgress(loaded, total) { }; }; -export function uploadComposeSuccess(media) { +export function uploadComposeSuccess(media, file) { return { type: COMPOSE_UPLOAD_SUCCESS, media: media, + file: file, skipLoading: true, }; }; @@ -325,10 +327,12 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => if (cancelFetchComposeSuggestionsAccounts) { cancelFetchComposeSuggestionsAccounts(); } + api(getState).get('/api/v1/accounts/search', { cancelToken: new CancelToken(cancel => { cancelFetchComposeSuggestionsAccounts = cancel; }), + params: { q: token.slice(1), resolve: false, @@ -349,9 +353,33 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { dispatch(readyComposeSuggestionsEmojis(token, results)); }; -const fetchComposeSuggestionsTags = (dispatch, getState, token) => { +const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { + if (cancelFetchComposeSuggestionsTags) { + cancelFetchComposeSuggestionsTags(); + } + dispatch(updateSuggestionTags(token)); -}; + + api(getState).get('/api/v2/search', { + cancelToken: new CancelToken(cancel => { + cancelFetchComposeSuggestionsTags = cancel; + }), + + params: { + type: 'hashtags', + q: token.slice(1), + resolve: false, + limit: 4, + exclude_unreviewed: true, + }, + }).then(({ data }) => { + dispatch(readyComposeSuggestionsTags(token, data.hashtags)); + }).catch(error => { + if (!isCancel(error)) { + dispatch(showAlertForError(error)); + } + }); +}, 200, { leading: true, trailing: true }); export function fetchComposeSuggestions(token) { return (dispatch, getState) => { @@ -385,20 +413,26 @@ export function readyComposeSuggestionsAccounts(token, accounts) { }; }; +export const readyComposeSuggestionsTags = (token, tags) => ({ + type: COMPOSE_SUGGESTIONS_READY, + token, + tags, +}); + export function selectComposeSuggestion(position, token, suggestion, path) { return (dispatch, getState) => { let completion, startPosition; - if (typeof suggestion === 'object' && suggestion.id) { + if (suggestion.type === 'emoji') { completion = suggestion.native || suggestion.colons; startPosition = position - 1; dispatch(useEmoji(suggestion)); - } else if (suggestion[0] === '#') { - completion = suggestion; + } else if (suggestion.type === 'hashtag') { + completion = `#${suggestion.name}`; startPosition = position - 1; - } else { - completion = getState().getIn(['accounts', suggestion, 'acct']); + } else if (suggestion.type === 'account') { + completion = getState().getIn(['accounts', suggestion.id, 'acct']); startPosition = position; } diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js index c6e062ef7..4ef654b1f 100644 --- a/app/javascript/mastodon/actions/conversations.js +++ b/app/javascript/mastodon/actions/conversations.js @@ -15,6 +15,10 @@ export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE'; export const CONVERSATIONS_READ = 'CONVERSATIONS_READ'; +export const CONVERSATIONS_DELETE_REQUEST = 'CONVERSATIONS_DELETE_REQUEST'; +export const CONVERSATIONS_DELETE_SUCCESS = 'CONVERSATIONS_DELETE_SUCCESS'; +export const CONVERSATIONS_DELETE_FAIL = 'CONVERSATIONS_DELETE_FAIL'; + export const mountConversations = () => ({ type: CONVERSATIONS_MOUNT, }); @@ -82,3 +86,27 @@ export const updateConversations = conversation => dispatch => { conversation, }); }; + +export const deleteConversation = conversationId => (dispatch, getState) => { + dispatch(deleteConversationRequest(conversationId)); + + api(getState).delete(`/api/v1/conversations/${conversationId}`) + .then(() => dispatch(deleteConversationSuccess(conversationId))) + .catch(error => dispatch(deleteConversationFail(conversationId, error))); +}; + +export const deleteConversationRequest = id => ({ + type: CONVERSATIONS_DELETE_REQUEST, + id, +}); + +export const deleteConversationSuccess = id => ({ + type: CONVERSATIONS_DELETE_SUCCESS, + id, +}); + +export const deleteConversationFail = (id, error) => ({ + type: CONVERSATIONS_DELETE_FAIL, + id, + error, +}); diff --git a/app/javascript/mastodon/actions/directory.js b/app/javascript/mastodon/actions/directory.js new file mode 100644 index 000000000..4b2b6dd56 --- /dev/null +++ b/app/javascript/mastodon/actions/directory.js @@ -0,0 +1,61 @@ +import api from '../api'; +import { importFetchedAccounts } from './importer'; +import { fetchRelationships } from './accounts'; + +export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; +export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; +export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL'; + +export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST'; +export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS'; +export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL'; + +export const fetchDirectory = params => (dispatch, getState) => { + dispatch(fetchDirectoryRequest()); + + api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchDirectorySuccess(data)); + dispatch(fetchRelationships(data.map(x => x.id))); + }).catch(error => dispatch(fetchDirectoryFail(error))); +}; + +export const fetchDirectoryRequest = () => ({ + type: DIRECTORY_FETCH_REQUEST, +}); + +export const fetchDirectorySuccess = accounts => ({ + type: DIRECTORY_FETCH_SUCCESS, + accounts, +}); + +export const fetchDirectoryFail = error => ({ + type: DIRECTORY_FETCH_FAIL, + error, +}); + +export const expandDirectory = params => (dispatch, getState) => { + dispatch(expandDirectoryRequest()); + + const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size; + + api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(expandDirectorySuccess(data)); + dispatch(fetchRelationships(data.map(x => x.id))); + }).catch(error => dispatch(expandDirectoryFail(error))); +}; + +export const expandDirectoryRequest = () => ({ + type: DIRECTORY_EXPAND_REQUEST, +}); + +export const expandDirectorySuccess = accounts => ({ + type: DIRECTORY_EXPAND_SUCCESS, + accounts, +}); + +export const expandDirectoryFail = error => ({ + type: DIRECTORY_EXPAND_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js index 0445a5e10..34a33a654 100644 --- a/app/javascript/mastodon/actions/domain_blocks.js +++ b/app/javascript/mastodon/actions/domain_blocks.js @@ -23,6 +23,7 @@ export function blockDomain(domain) { api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { const at_domain = '@' + domain; const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); + dispatch(blockDomainSuccess(domain, accounts)); }).catch(err => { dispatch(blockDomainFail(domain, err)); diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 5badb0c49..78f321da4 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -10,6 +10,12 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { return obj; }, {}); +export function searchTextFromRawStatus (status) { + const spoilerText = status.spoiler_text || ''; + const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; +} + export function normalizeAccount(account) { account = { ...account }; @@ -22,7 +28,7 @@ export function normalizeAccount(account) { if (account.fields) { account.fields = account.fields.map(pair => ({ ...pair, - name_emojified: emojify(escapeTextContentForBrowser(pair.name)), + name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap), value_emojified: emojify(pair.value, emojiMap), value_plain: unescapeHTML(pair.value), })); @@ -56,7 +62,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.hidden = normalOldStatus.get('hidden'); } else { const spoilerText = normalStatus.spoiler_text || ''; - const searchContent = [spoilerText, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const emojiMap = makeEmojiMap(normalStatus); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; @@ -73,8 +79,9 @@ export function normalizePoll(poll) { const emojiMap = makeEmojiMap(normalPoll); - normalPoll.options = poll.options.map(option => ({ + normalPoll.options = poll.options.map((option, index) => ({ ...option, + voted: poll.own_votes && poll.own_votes.includes(index), title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 2dc4c574c..28c6b1a62 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -33,6 +33,14 @@ export const UNPIN_REQUEST = 'UNPIN_REQUEST'; export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; export const UNPIN_FAIL = 'UNPIN_FAIL'; +export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST'; +export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS'; +export const BOOKMARK_FAIL = 'BOOKMARKED_FAIL'; + +export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; +export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; +export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; + export function reblog(status) { return function (dispatch, getState) { dispatch(reblogRequest(status)); @@ -187,6 +195,78 @@ export function unfavouriteFail(status, error) { }; }; +export function bookmark(status) { + return function (dispatch, getState) { + dispatch(bookmarkRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) { + dispatch(importFetchedStatus(response.data)); + dispatch(bookmarkSuccess(status, response.data)); + }).catch(function (error) { + dispatch(bookmarkFail(status, error)); + }); + }; +}; + +export function unbookmark(status) { + return (dispatch, getState) => { + dispatch(unbookmarkRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(unbookmarkSuccess(status, response.data)); + }).catch(error => { + dispatch(unbookmarkFail(status, error)); + }); + }; +}; + +export function bookmarkRequest(status) { + return { + type: BOOKMARK_REQUEST, + status: status, + }; +}; + +export function bookmarkSuccess(status, response) { + return { + type: BOOKMARK_SUCCESS, + status: status, + response: response, + }; +}; + +export function bookmarkFail(status, error) { + return { + type: BOOKMARK_FAIL, + status: status, + error: error, + }; +}; + +export function unbookmarkRequest(status) { + return { + type: UNBOOKMARK_REQUEST, + status: status, + }; +}; + +export function unbookmarkSuccess(status, response) { + return { + type: UNBOOKMARK_SUCCESS, + status: status, + response: response, + }; +}; + +export function unbookmarkFail(status, error) { + return { + type: UNBOOKMARK_FAIL, + status: status, + error: error, + }; +}; + export function fetchReblogs(id) { return (dispatch, getState) => { dispatch(fetchReblogsRequest(id)); diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js new file mode 100644 index 000000000..c3a5fe86f --- /dev/null +++ b/app/javascript/mastodon/actions/markers.js @@ -0,0 +1,30 @@ +export const submitMarkers = () => (dispatch, getState) => { + const accessToken = getState().getIn(['meta', 'access_token'], ''); + const params = {}; + + const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]); + const lastNotificationId = getState().getIn(['notifications', 'items', 0, 'id']); + + if (lastHomeId) { + params.home = { + last_read_id: lastHomeId, + }; + } + + if (lastNotificationId) { + params.notifications = { + last_read_id: lastNotificationId, + }; + } + + if (Object.keys(params).length === 0) { + return; + } + + const client = new XMLHttpRequest(); + + client.open('POST', '/api/v1/markers', false); + client.setRequestHeader('Content-Type', 'application/json'); + client.setRequestHeader('Authorization', `Bearer ${accessToken}`); + client.send(JSON.stringify(params)); +}; diff --git a/app/javascript/mastodon/actions/modal.js b/app/javascript/mastodon/actions/modal.js index 80e15c28e..3d0299db5 100644 --- a/app/javascript/mastodon/actions/modal.js +++ b/app/javascript/mastodon/actions/modal.js @@ -9,8 +9,9 @@ export function openModal(type, props) { }; }; -export function closeModal() { +export function closeModal(type) { return { type: MODAL_CLOSE, + modalType: type, }; }; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index b0861fc6b..798f9b37e 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -11,7 +11,10 @@ import { saveSettings } from './settings'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; -import { getFilters, regexFromFilters } from '../selectors'; +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'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -22,8 +25,12 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; + +export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; +export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, @@ -38,18 +45,27 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; +export const loadPending = () => ({ + type: NOTIFICATIONS_LOAD_PENDING, +}); + export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); - const filters = getFilters(getState(), { contextType: 'notifications' }); + const filters = getFiltersRegex(getState(), { contextType: 'notifications' }); let filtered = false; if (notification.type === 'mention') { - const regex = regexFromFilters(filters); - const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); + const dropRegex = filters[0]; + const regex = filters[1]; + const searchIndex = searchTextFromRawStatus(notification.status); + + if (dropRegex && dropRegex.test(searchIndex)) { + return; + } filtered = regex && regex.test(searchIndex); } @@ -64,6 +80,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch({ type: NOTIFICATIONS_UPDATE, notification, + usePendingItems: preferPendingItems, meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, }); @@ -93,7 +110,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']); + const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']); return allTypes.filterNot(item => item === filter).toJS(); }; @@ -117,10 +134,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) { : excludeTypesFromFilter(activeFilter), }; - if (!maxId && notifications.get('items').size > 0) { - params.since_id = notifications.getIn(['items', 0, 'id']); + if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { + const a = notifications.getIn(['pendingItems', 0, 'id']); + const b = notifications.getIn(['items', 0, 'id']); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } + const isLoadingRecent = !!params.since_id; + dispatch(expandNotificationsRequest(isLoadingMore)); api(getState).get('/api/v1/notifications', { params }).then(response => { @@ -129,7 +155,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { @@ -146,11 +172,13 @@ export function expandNotificationsRequest(isLoadingMore) { }; }; -export function expandNotificationsSuccess(notifications, next, isLoadingMore) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, + isLoadingRecent: isLoadingRecent, + usePendingItems, skipLoading: !isLoadingMore, }; }; @@ -191,3 +219,11 @@ export function setFilter (filterType) { dispatch(saveSettings()); }; }; + +export const mountNotifications = () => ({ + type: NOTIFICATIONS_MOUNT, +}); + +export const unmountNotifications = () => ({ + type: NOTIFICATIONS_UNMOUNT, +}); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 7c06670eb..a178faead 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -10,6 +10,10 @@ export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; +export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST'; +export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS'; +export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL'; + export function changeSearch(value) { return { type: SEARCH_CHANGE, @@ -48,7 +52,7 @@ export function submitSearch() { dispatch(importFetchedStatuses(response.data.statuses)); } - dispatch(fetchSearchSuccess(response.data)); + dispatch(fetchSearchSuccess(response.data, value)); dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); }).catch(error => { dispatch(fetchSearchFail(error)); @@ -62,10 +66,11 @@ export function fetchSearchRequest() { }; }; -export function fetchSearchSuccess(results) { +export function fetchSearchSuccess(results, searchTerm) { return { type: SEARCH_FETCH_SUCCESS, results, + searchTerm, }; }; @@ -76,8 +81,50 @@ export function fetchSearchFail(error) { }; }; -export function showSearch() { - return { - type: SEARCH_SHOW, - }; +export const expandSearch = type => (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + const offset = getState().getIn(['search', 'results', type]).size; + + dispatch(expandSearchRequest()); + + api(getState).get('/api/v2/search', { + params: { + q: value, + type, + offset, + }, + }).then(({ data }) => { + if (data.accounts) { + dispatch(importFetchedAccounts(data.accounts)); + } + + if (data.statuses) { + dispatch(importFetchedStatuses(data.statuses)); + } + + dispatch(expandSearchSuccess(data, value, type)); + dispatch(fetchRelationships(data.accounts.map(item => item.id))); + }).catch(error => { + dispatch(expandSearchFail(error)); + }); }; + +export const expandSearchRequest = () => ({ + type: SEARCH_EXPAND_REQUEST, +}); + +export const expandSearchSuccess = (results, searchTerm, searchType) => ({ + type: SEARCH_EXPAND_SUCCESS, + results, + searchTerm, + searchType, +}); + +export const expandSearchFail = error => ({ + type: SEARCH_EXPAND_FAIL, + error, +}); + +export const showSearch = () => ({ + type: SEARCH_SHOW, +}); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 06c21b96b..bc2ac5e82 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,6 +1,8 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; -import api, { getLinks } from '../api'; +import api, { getLinks } from 'mastodon/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import compareId from 'mastodon/compare_id'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; -export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const loadPending = timeline => ({ + type: TIMELINE_LOAD_PENDING, + timeline, +}); export function updateTimeline(timeline, status, accept) { return dispatch => { @@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) { type: TIMELINE_UPDATE, timeline, status, + usePendingItems: preferPendingItems, }); }; }; @@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { return; } - if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) { - params.since_id = timeline.getIn(['items', 0]); + if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) { + const a = timeline.getIn(['pendingItems', 0]); + const b = timeline.getIn(['items', 0]); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } const isLoadingRecent = !!params.since_id; @@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); @@ -115,7 +130,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) { }; }; -export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) { +export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, @@ -123,6 +138,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi next, partial, isLoadingRecent, + usePendingItems, skipLoading: !isLoadingMore, }; }; @@ -151,9 +167,8 @@ export function connectTimeline(timeline) { }; }; -export function disconnectTimeline(timeline) { - return { - type: TIMELINE_DISCONNECT, - timeline, - }; -}; +export const disconnectTimeline = timeline => ({ + type: TIMELINE_DISCONNECT, + timeline, + usePendingItems: preferPendingItems, +}); diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js new file mode 100644 index 000000000..853e4f60a --- /dev/null +++ b/app/javascript/mastodon/actions/trends.js @@ -0,0 +1,32 @@ +import api from '../api'; + +export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; +export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; +export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; + +export const fetchTrends = () => (dispatch, getState) => { + dispatch(fetchTrendsRequest()); + + api(getState) + .get('/api/v1/trends') + .then(({ data }) => dispatch(fetchTrendsSuccess(data))) + .catch(err => dispatch(fetchTrendsFail(err))); +}; + +export const fetchTrendsRequest = () => ({ + type: TRENDS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendsSuccess = trends => ({ + type: TRENDS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendsFail = error => ({ + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); diff --git a/app/javascript/mastodon/compare_id.js b/app/javascript/mastodon/compare_id.js index aaff66481..66cf51c4b 100644 --- a/app/javascript/mastodon/compare_id.js +++ b/app/javascript/mastodon/compare_id.js @@ -1,10 +1,11 @@ -export default function compareId(id1, id2) { +export default function compareId (id1, id2) { if (id1 === id2) { return 0; } + if (id1.length === id2.length) { return id1 > id2 ? 1 : -1; } else { return id1.length > id2.length ? 1 : -1; } -} +}; diff --git a/app/javascript/mastodon/components/attachment_list.js b/app/javascript/mastodon/components/attachment_list.js index 5dfa1464c..ebd696583 100644 --- a/app/javascript/mastodon/components/attachment_list.js +++ b/app/javascript/mastodon/components/attachment_list.js @@ -25,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent { return ( <li key={attachment.get('id')}> - <a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a> + <a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a> </li> ); })} @@ -46,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent { return ( <li key={attachment.get('id')}> - <a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a> + <a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a> </li> ); })} diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.js b/app/javascript/mastodon/components/autosuggest_hashtag.js new file mode 100644 index 000000000..e2f4e320d --- /dev/null +++ b/app/javascript/mastodon/components/autosuggest_hashtag.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { shortNumberFormat } from 'mastodon/utils/numbers'; +import { FormattedMessage } from 'react-intl'; + +export default class AutosuggestHashtag extends React.PureComponent { + + static propTypes = { + tag: PropTypes.shape({ + name: PropTypes.string.isRequired, + url: PropTypes.string, + history: PropTypes.array, + }).isRequired, + }; + + render () { + const { tag } = this.props; + const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0)); + + return ( + <div className='autosuggest-hashtag'> + <div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div> + {tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>} + </div> + ); + } + +} diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js index c7d965b53..6d2035add 100644 --- a/app/javascript/mastodon/components/autosuggest_input.js +++ b/app/javascript/mastodon/components/autosuggest_input.js @@ -1,6 +1,7 @@ import React from 'react'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; 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'; @@ -167,15 +168,15 @@ export default class AutosuggestInput extends ImmutablePureComponent { const { selectedSuggestion } = this.state; let inner, key; - if (typeof suggestion === 'object') { + if (suggestion.type === 'emoji') { inner = <AutosuggestEmoji emoji={suggestion} />; key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; - } else { - inner = <AutosuggestAccountContainer id={suggestion} />; - key = suggestion; + } else if (suggestion.type ==='hashtag') { + inner = <AutosuggestHashtag tag={suggestion} />; + key = suggestion.name; + } else if (suggestion.type === 'account') { + inner = <AutosuggestAccountContainer id={suggestion.id} />; + key = suggestion.id; } return ( diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index b070fe3e5..ac2a6366a 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -1,6 +1,7 @@ import React from 'react'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; 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'; @@ -173,15 +174,15 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { const { selectedSuggestion } = this.state; let inner, key; - if (typeof suggestion === 'object') { + if (suggestion.type === 'emoji') { inner = <AutosuggestEmoji emoji={suggestion} />; key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; - } else { - inner = <AutosuggestAccountContainer id={suggestion} />; - key = suggestion; + } else if (suggestion.type === 'hashtag') { + inner = <AutosuggestHashtag tag={suggestion} />; + key = suggestion.name; + } else if (suggestion.type === 'account') { + inner = <AutosuggestAccountContainer id={suggestion.id} />; + key = suggestion.id; } return ( diff --git a/app/javascript/mastodon/components/avatar_composite.js b/app/javascript/mastodon/components/avatar_composite.js index 4a9a73c51..5d5b89749 100644 --- a/app/javascript/mastodon/components/avatar_composite.js +++ b/app/javascript/mastodon/components/avatar_composite.js @@ -35,35 +35,35 @@ export default class AvatarComposite extends React.PureComponent { if (size === 2) { if (index === 0) { - right = '2px'; + right = '1px'; } else { - left = '2px'; + left = '1px'; } } else if (size === 3) { if (index === 0) { - right = '2px'; + right = '1px'; } else if (index > 0) { - left = '2px'; + left = '1px'; } if (index === 1) { - bottom = '2px'; + bottom = '1px'; } else if (index > 1) { - top = '2px'; + top = '1px'; } } else if (size === 4) { if (index === 0 || index === 2) { - right = '2px'; + right = '1px'; } if (index === 1 || index === 3) { - left = '2px'; + left = '1px'; } if (index < 2) { - bottom = '2px'; + bottom = '1px'; } else { - top = '2px'; + top = '1px'; } } @@ -88,7 +88,13 @@ export default class AvatarComposite extends React.PureComponent { return ( <div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}> - {accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))} + {accounts.take(4).map((account, i) => this.renderItem(account, Math.min(accounts.size, 4), i))} + + {accounts.size > 4 && ( + <span className='account__avatar-composite__label'> + +{accounts.size - 4} + </span> + )} </div> ); } diff --git a/app/javascript/mastodon/components/button.js b/app/javascript/mastodon/components/button.js index 51e2e6a7a..eb8dd7dc8 100644 --- a/app/javascript/mastodon/components/button.js +++ b/app/javascript/mastodon/components/button.js @@ -12,6 +12,7 @@ export default class Button extends React.PureComponent { secondary: PropTypes.bool, size: PropTypes.number, className: PropTypes.string, + title: PropTypes.string, style: PropTypes.object, children: PropTypes.node, }; @@ -54,6 +55,7 @@ export default class Button extends React.PureComponent { onClick={this.handleClick} ref={this.setRef} style={style} + title={this.props.title} > {this.props.text || this.props.children} </button> diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js index d45387463..55e3bfd5e 100644 --- a/app/javascript/mastodon/components/column.js +++ b/app/javascript/mastodon/components/column.js @@ -8,10 +8,11 @@ export default class Column extends React.PureComponent { static propTypes = { children: PropTypes.node, label: PropTypes.string, + bindToDocument: PropTypes.bool, }; scrollTop () { - const scrollable = this.node.querySelector('.scrollable'); + const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable'); if (!scrollable) { return; @@ -33,11 +34,19 @@ export default class Column extends React.PureComponent { } componentDidMount () { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + if (this.props.bindToDocument) { + document.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + } else { + this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + } } componentWillUnmount () { - this.node.removeEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.removeEventListener('wheel', this.handleWheel); + } else { + this.node.removeEventListener('wheel', this.handleWheel); + } } render () { diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/mastodon/components/column_back_button.js index f41045787..d97622705 100644 --- a/app/javascript/mastodon/components/column_back_button.js +++ b/app/javascript/mastodon/components/column_back_button.js @@ -2,6 +2,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import Icon from 'mastodon/components/icon'; +import { createPortal } from 'react-dom'; export default class ColumnBackButton extends React.PureComponent { @@ -9,6 +10,10 @@ export default class ColumnBackButton extends React.PureComponent { router: PropTypes.object, }; + static propTypes = { + multiColumn: PropTypes.bool, + }; + handleClick = () => { if (window.history && window.history.length === 1) { this.context.router.history.push('/'); @@ -18,12 +23,32 @@ export default class ColumnBackButton extends React.PureComponent { } render () { - return ( + const { multiColumn } = this.props; + + const component = ( <button onClick={this.handleClick} className='column-back-button'> <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </button> ); + + if (multiColumn) { + return component; + } else { + // The portal container and the component may be rendered to the DOM in + // the same React render pass, so the container might not be available at + // the time `render()` is called. + const container = document.getElementById('tabs-bar__portal'); + if (container === null) { + // The container wasn't available, force a re-render so that the + // component can eventually be inserted in the container and not scroll + // with the rest of the area. + this.forceUpdate(); + return component; + } else { + return createPortal(component, container); + } + } } } diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index f33c689e7..0038995c8 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import Icon from 'mastodon/components/icon'; @@ -28,6 +29,7 @@ class ColumnHeader extends React.PureComponent { showBackButton: PropTypes.bool, children: PropTypes.node, pinned: PropTypes.bool, + placeholder: PropTypes.bool, onPin: PropTypes.func, onMove: PropTypes.func, onClick: PropTypes.func, @@ -79,7 +81,7 @@ class ColumnHeader extends React.PureComponent { } render () { - const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props; + const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props; const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { @@ -118,7 +120,7 @@ class ColumnHeader extends React.PureComponent { <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> </div> ); - } else if (multiColumn) { + } else if (multiColumn && this.props.onPin) { pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; } @@ -140,13 +142,13 @@ class ColumnHeader extends React.PureComponent { collapsedContent.push(pinButton); } - if (children || multiColumn) { + 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>; } const hasTitle = icon && title; - return ( + const component = ( <div className={wrapperClassName}> <h1 className={buttonClassName}> {hasTitle && ( @@ -172,6 +174,24 @@ class ColumnHeader extends React.PureComponent { </div> </div> ); + + if (multiColumn || placeholder) { + return component; + } else { + // The portal container and the component may be rendered to the DOM in + // the same React render pass, so the container might not be available at + // the time `render()` is called. + const container = document.getElementById('tabs-bar__portal'); + if (container === null) { + // The container wasn't available, force a re-render so that the + // component can eventually be inserted in the container and not scroll + // with the rest of the area. + this.forceUpdate(); + return component; + } else { + return createPortal(component, container); + } + } } } diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index 6b9dd6f81..70ef82789 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -1,6 +1,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; +import { autoPlayGif } from 'mastodon/initial_state'; export default class DisplayName extends React.PureComponent { @@ -10,6 +11,47 @@ export default class DisplayName extends React.PureComponent { localDomain: PropTypes.string, }; + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + + setRef = (c) => { + this.node = c; + } + render () { const { others, localDomain } = this.props; @@ -39,7 +81,7 @@ export default class DisplayName extends React.PureComponent { } return ( - <span className='display-name'> + <span className='display-name' ref={this.setRef}> {displayName} {suffix} </span> ); diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 91b65a02f..a4f262285 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -45,7 +45,9 @@ class DropdownMenu extends React.PureComponent { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('keydown', this.handleKeyDown, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus(); + if (this.focusedItem && this.props.openedViaKeyboard) { + this.focusedItem.focus(); + } this.setState({ mounted: true }); } @@ -81,6 +83,18 @@ class DropdownMenu extends React.PureComponent { element.focus(); } break; + case 'Tab': + if (e.shiftKey) { + element = items[index-1] || items[items.length-1]; + } else { + element = items[index+1] || items[0]; + } + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + break; case 'Home': element = items[0]; if (element) { @@ -93,11 +107,14 @@ class DropdownMenu extends React.PureComponent { element.focus(); } break; + case 'Escape': + this.props.onClose(); + break; } } - handleItemKeyDown = e => { - if (e.key === 'Enter') { + handleItemKeyPress = e => { + if (e.key === 'Enter' || e.key === ' ') { this.handleClick(e); } } @@ -122,11 +139,11 @@ class DropdownMenu extends React.PureComponent { return <li key={`sep-${i}`} className='dropdown-menu__separator' />; } - const { text, href = '#' } = option; + const { text, href = '#', target = '_blank', method } = option; return ( <li className='dropdown-menu__item' key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}> + <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> {text} </a> </li> @@ -193,25 +210,41 @@ export default class Dropdown extends React.PureComponent { } else { const { top } = target.getBoundingClientRect(); const placement = top * 2 < innerHeight ? 'bottom' : 'top'; - this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click'); } } handleClose = () => { + if (this.activeElement) { + this.activeElement.focus(); + this.activeElement = null; + } this.props.onClose(this.state.id); } - handleKeyDown = e => { + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + } + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + } + + handleKeyPress = (e) => { switch(e.key) { case ' ': case 'Enter': this.handleClick(e); + e.stopPropagation(); e.preventDefault(); break; - case 'Escape': - this.handleClose(); - break; } } @@ -249,7 +282,7 @@ export default class Dropdown extends React.PureComponent { const open = this.state.id === openDropdownId; return ( - <div onKeyDown={this.handleKeyDown}> + <div> <IconButton icon={icon} title={title} @@ -258,6 +291,9 @@ export default class Dropdown extends React.PureComponent { size={size} ref={this.setTargetRef} onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleButtonKeyDown} + onKeyPress={this.handleKeyPress} /> <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js index d1ca5bf75..800b1c270 100644 --- a/app/javascript/mastodon/components/error_boundary.js +++ b/app/javascript/mastodon/components/error_boundary.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import illustration from '../../images/elephant_ui_disappointed.svg'; +import { FormattedMessage } from 'react-intl'; +import { version, source_url } from 'mastodon/initial_state'; export default class ErrorBoundary extends React.PureComponent { @@ -12,26 +13,53 @@ export default class ErrorBoundary extends React.PureComponent { hasError: false, stackTrace: undefined, componentStack: undefined, - } + }; - componentDidCatch(error, info) { + componentDidCatch (error, info) { this.setState({ hasError: true, stackTrace: error.stack, componentStack: info && info.componentStack, + copied: false, }); } + handleCopyStackTrace = () => { + const { stackTrace } = this.state; + const textarea = document.createElement('textarea'); + + textarea.textContent = stackTrace; + textarea.style.position = 'fixed'; + + document.body.appendChild(textarea); + + try { + textarea.select(); + document.execCommand('copy'); + } catch (e) { + + } finally { + document.body.removeChild(textarea); + } + + this.setState({ copied: true }); + setTimeout(() => this.setState({ copied: false }), 700); + } + render() { - const { hasError } = this.state; + const { hasError, copied } = this.state; if (!hasError) { return this.props.children; } return ( - <div> - <img src={illustration} alt='' /> + <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__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> ); } diff --git a/app/javascript/mastodon/components/extended_video_player.js b/app/javascript/mastodon/components/extended_video_player.js deleted file mode 100644 index 009c0d559..000000000 --- a/app/javascript/mastodon/components/extended_video_player.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class ExtendedVideoPlayer extends React.PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - time: PropTypes.number, - controls: PropTypes.bool.isRequired, - muted: PropTypes.bool.isRequired, - onClick: PropTypes.func, - }; - - handleLoadedData = () => { - if (this.props.time) { - this.video.currentTime = this.props.time; - } - } - - componentDidMount () { - this.video.addEventListener('loadeddata', this.handleLoadedData); - } - - componentWillUnmount () { - this.video.removeEventListener('loadeddata', this.handleLoadedData); - } - - setRef = (c) => { - this.video = c; - } - - handleClick = e => { - e.stopPropagation(); - const handler = this.props.onClick; - if (handler) handler(); - } - - render () { - const { src, muted, controls, alt } = this.props; - - return ( - <div className='extended-video-player'> - <video - ref={this.setRef} - src={src} - autoPlay - role='button' - tabIndex='0' - aria-label={alt} - title={alt} - muted={muted} - controls={controls} - loop={!controls} - onClick={this.handleClick} - /> - </div> - ); - } - -} diff --git a/app/javascript/mastodon/components/gifv.js b/app/javascript/mastodon/components/gifv.js new file mode 100644 index 000000000..83cfae49c --- /dev/null +++ b/app/javascript/mastodon/components/gifv.js @@ -0,0 +1,75 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class GIFV extends React.PureComponent { + + static propTypes = { + src: PropTypes.string.isRequired, + alt: PropTypes.string, + width: PropTypes.number, + height: PropTypes.number, + onClick: PropTypes.func, + }; + + state = { + loading: true, + }; + + handleLoadedData = () => { + this.setState({ loading: false }); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.src !== this.props.src) { + this.setState({ loading: true }); + } + } + + handleClick = e => { + const { onClick } = this.props; + + if (onClick) { + e.stopPropagation(); + onClick(); + } + } + + render () { + const { src, width, height, alt } = this.props; + const { loading } = this.state; + + return ( + <div className='gifv' style={{ position: 'relative' }}> + {loading && ( + <canvas + width={width} + height={height} + role='button' + tabIndex='0' + aria-label={alt} + title={alt} + onClick={this.handleClick} + /> + )} + + <video + src={src} + width={width} + height={height} + role='button' + tabIndex='0' + aria-label={alt} + title={alt} + muted + loop + autoPlay + playsInline + onClick={this.handleClick} + onLoadedData={this.handleLoadedData} + style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }} + /> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js index f091d7893..62d613262 100644 --- a/app/javascript/mastodon/components/hashtag.js +++ b/app/javascript/mastodon/components/hashtag.js @@ -12,11 +12,11 @@ const Hashtag = ({ hashtag }) => ( #<span>{hashtag.get('name')}</span> </Permalink> - <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} /> + <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} /> </div> <div className='trends__item__current'> - {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))} + {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)} </div> <div className='trends__item__sparkline'> diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index 9d8a8d06b..fd715bc3c 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -1,6 +1,4 @@ import React from 'react'; -import Motion from '../features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; @@ -12,6 +10,9 @@ export default class IconButton extends React.PureComponent { title: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, onClick: PropTypes.func, + onMouseDown: PropTypes.func, + onKeyDown: PropTypes.func, + onKeyPress: PropTypes.func, size: PropTypes.number, active: PropTypes.bool, pressed: PropTypes.bool, @@ -34,6 +35,21 @@ export default class IconButton extends React.PureComponent { tabIndex: '0', }; + state = { + activate: false, + deactivate: false, + } + + componentWillReceiveProps (nextProps) { + if (!nextProps.animate) return; + + if (this.props.active && !nextProps.active) { + this.setState({ activate: false, deactivate: true }); + } else if (!this.props.active && nextProps.active) { + this.setState({ activate: true, deactivate: false }); + } + } + handleClick = (e) => { e.preventDefault(); @@ -42,6 +58,24 @@ export default class IconButton extends React.PureComponent { } } + handleKeyPress = (e) => { + if (this.props.onKeyPress && !this.props.disabled) { + this.props.onKeyPress(e); + } + } + + handleMouseDown = (e) => { + if (!this.props.disabled && this.props.onMouseDown) { + this.props.onMouseDown(e); + } + } + + handleKeyDown = (e) => { + if (!this.props.disabled && this.props.onKeyDown) { + this.props.onKeyDown(e); + } + } + render () { const style = { fontSize: `${this.props.size}px`, @@ -54,7 +88,6 @@ export default class IconButton extends React.PureComponent { const { active, - animate, className, disabled, expanded, @@ -66,51 +99,37 @@ export default class IconButton extends React.PureComponent { title, } = this.props; + const { + activate, + deactivate, + } = this.state; + const classes = classNames(className, 'icon-button', { active, disabled, inverted, + activate, + deactivate, overlayed: overlay, }); - if (!animate) { - // Perf optimization: avoid unnecessary <Motion> components unless - // we actually need to animate. - return ( - <button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - style={style} - tabIndex={tabIndex} - disabled={disabled} - > - <Icon id={icon} fixedWidth aria-hidden='true' /> - </button> - ); - } - return ( - <Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}> - {({ rotate }) => ( - <button - aria-label={title} - aria-pressed={pressed} - aria-expanded={expanded} - title={title} - className={classes} - onClick={this.handleClick} - style={style} - tabIndex={tabIndex} - disabled={disabled} - > - <Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' /> - </button> - )} - </Motion> + <button + aria-label={title} + aria-pressed={pressed} + aria-expanded={expanded} + title={title} + className={classes} + onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleKeyDown} + onKeyPress={this.handleKeyPress} + style={style} + tabIndex={tabIndex} + disabled={disabled} + > + <Icon id={icon} fixedWidth aria-hidden='true' /> + </button> ); } diff --git a/app/javascript/mastodon/components/load_pending.js b/app/javascript/mastodon/components/load_pending.js new file mode 100644 index 000000000..7e2702403 --- /dev/null +++ b/app/javascript/mastodon/components/load_pending.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +export default class LoadPending extends React.PureComponent { + + static propTypes = { + onClick: PropTypes.func, + count: PropTypes.number, + } + + render() { + const { count } = this.props; + + return ( + <button className='load-more load-gap' onClick={this.props.onClick}> + <FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} /> + </button> + ); + } + +} diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 56618462b..12b7e5b66 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -6,7 +6,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from '../is_mobile'; import classNames from 'classnames'; -import { autoPlayGif, displayMedia } from '../initial_state'; +import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state'; import { decode } from 'blurhash'; const messages = defineMessages({ @@ -81,6 +81,8 @@ class Item extends React.PureComponent { } _decode () { + if (!useBlurhash) return; + const hash = this.props.attachment.get('blurhash'); const pixels = decode(hash, 32, 32); @@ -157,7 +159,7 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}> + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> </a> </div> @@ -185,6 +187,7 @@ class Item extends React.PureComponent { href={attachment.get('remote_url') || originalUrl} onClick={this.handleClick} target='_blank' + rel='noopener noreferrer' > <img src={previewUrl} @@ -278,7 +281,7 @@ class MediaGallery extends React.PureComponent { } handleRef = (node) => { - if (node /*&& this.isStandaloneEligible()*/) { + if (node) { // offsetWidth triggers a layout, so only calculate when we need to if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); @@ -288,13 +291,13 @@ class MediaGallery extends React.PureComponent { } } - isStandaloneEligible() { - const { media, standalone } = this.props; - return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']); + isFullSizeEligible() { + const { media } = this.props; + return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']); } render () { - const { media, intl, sensitive, height, defaultWidth } = this.props; + const { media, intl, sensitive, height, defaultWidth, standalone } = this.props; const { visible } = this.state; const width = this.state.width || defaultWidth; @@ -303,7 +306,7 @@ class MediaGallery extends React.PureComponent { const style = {}; - if (this.isStandaloneEligible()) { + if (this.isFullSizeEligible() && (standalone || !cropImages)) { if (width) { style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); } @@ -313,15 +316,22 @@ class MediaGallery extends React.PureComponent { style.height = height; } - const size = media.take(4).size; + const size = media.take(4).size; + const uncached = media.every(attachment => attachment.get('type') === 'unknown'); - if (this.isStandaloneEligible()) { + if (standalone && this.isFullSizeEligible()) { children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible} />); + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />); } - if (visible) { + if (uncached) { + spoilerButton = ( + <button type='button' disabled className='spoiler-button__overlay'> + <span className='spoiler-button__overlay__label'><FormattedMessage id='status.uncached_media_warning' defaultMessage='Not available' /></span> + </button> + ); + } else if (visible) { spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />; } else { spoilerButton = ( @@ -333,7 +343,7 @@ class MediaGallery extends React.PureComponent { return ( <div className='media-gallery' style={style} ref={this.handleRef}> - <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}> + <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}> {spoilerButton} </div> diff --git a/app/javascript/mastodon/components/missing_indicator.js b/app/javascript/mastodon/components/missing_indicator.js index 70d8c3b98..7b0101bab 100644 --- a/app/javascript/mastodon/components/missing_indicator.js +++ b/app/javascript/mastodon/components/missing_indicator.js @@ -1,17 +1,24 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import illustration from 'mastodon/../images/elephant_ui_disappointed.svg'; +import classNames from 'classnames'; -const MissingIndicator = () => ( - <div className='regeneration-indicator missing-indicator'> - <div> - <div className='regeneration-indicator__figure' /> +const MissingIndicator = ({ fullPage }) => ( + <div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}> + <div className='regeneration-indicator__figure'> + <img src={illustration} alt='' /> + </div> - <div className='regeneration-indicator__label'> - <FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' /> - <FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' /> - </div> + <div className='regeneration-indicator__label'> + <FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' /> + <FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' /> </div> </div> ); +MissingIndicator.propTypes = { + fullPage: PropTypes.bool, +}; + export default MissingIndicator; diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js index ef1156571..fa4e59192 100644 --- a/app/javascript/mastodon/components/modal_root.js +++ b/app/javascript/mastodon/components/modal_root.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import 'wicg-inert'; export default class ModalRoot extends React.PureComponent { @@ -21,8 +22,30 @@ export default class ModalRoot extends React.PureComponent { } } + handleKeyDown = (e) => { + if (e.key === 'Tab') { + const focusable = Array.from(this.node.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none'); + const index = focusable.indexOf(e.target); + + let element; + + if (e.shiftKey) { + element = focusable[index - 1] || focusable[focusable.length - 1]; + } else { + element = focusable[index + 1] || focusable[0]; + } + + if (element) { + element.focus(); + e.stopPropagation(); + e.preventDefault(); + } + } + } + componentDidMount () { window.addEventListener('keyup', this.handleKeyUp, false); + window.addEventListener('keydown', this.handleKeyDown, false); } componentWillReceiveProps (nextProps) { @@ -33,15 +56,21 @@ export default class ModalRoot extends React.PureComponent { } else if (!nextProps.children) { this.setState({ revealed: false }); } - if (!nextProps.children && !!this.props.children) { - this.activeElement.focus(); - this.activeElement = null; - } } componentDidUpdate (prevProps) { if (!this.props.children && !!prevProps.children) { this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); + + // Because of the wicg-inert polyfill, the activeElement may not be + // immediately selectable, we have to wait for observers to run, as + // described in https://github.com/WICG/inert#performance-and-gotchas + Promise.resolve().then(() => { + this.activeElement.focus(); + this.activeElement = null; + }).catch((error) => { + console.error(error); + }); } if (this.props.children) { requestAnimationFrame(() => { @@ -52,6 +81,7 @@ export default class ModalRoot extends React.PureComponent { componentWillUnmount () { window.removeEventListener('keyup', this.handleKeyUp); + window.removeEventListener('keydown', this.handleKeyDown); } getSiblings = () => { diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index 690f9ae5a..3a17e80e7 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -10,9 +10,11 @@ import spring from 'react-motion/lib/spring'; import escapeTextContentForBrowser from 'escape-html'; import emojify from 'mastodon/features/emoji/emoji'; import RelativeTimestamp from './relative_timestamp'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ closed: { id: 'poll.closed', defaultMessage: 'Closed' }, + voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' }, }); const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { @@ -32,11 +34,40 @@ class Poll extends ImmutablePureComponent { state = { selected: {}, + expired: null, }; - handleOptionChange = e => { - const { target: { value } } = e; + static getDerivedStateFromProps (props, state) { + const { poll, intl } = props; + const expires_at = poll.get('expires_at'); + const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now(); + return (expired === state.expired) ? null : { expired }; + } + + componentDidMount () { + this._setupTimer(); + } + + componentDidUpdate () { + this._setupTimer(); + } + + componentWillUnmount () { + clearTimeout(this._timer); + } + + _setupTimer () { + const { poll, intl } = this.props; + clearTimeout(this._timer); + if (!this.state.expired) { + const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now(); + this._timer = setTimeout(() => { + this.setState({ expired: true }); + }, delay); + } + } + _toggleOption = value => { if (this.props.poll.get('multiple')) { const tmp = { ...this.state.selected }; if (tmp[value]) { @@ -50,8 +81,20 @@ class Poll extends ImmutablePureComponent { tmp[value] = true; this.setState({ selected: tmp }); } + } + + handleOptionChange = ({ target: { value } }) => { + this._toggleOption(value); }; + handleOptionKeyPress = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + this._toggleOption(e.target.getAttribute('data-index')); + e.stopPropagation(); + e.preventDefault(); + } + } + handleVote = () => { if (this.props.disabled) { return; @@ -68,12 +111,13 @@ class Poll extends ImmutablePureComponent { this.props.dispatch(fetchPoll(this.props.poll.get('id'))); }; - renderOption (option, optionIndex) { - const { poll, disabled } = this.props; - const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; - const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); - const active = !!this.state.selected[`${optionIndex}`]; - const showResults = poll.get('voted') || poll.get('expired'); + renderOption (option, optionIndex, showResults) { + const { poll, disabled, intl } = this.props; + const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); + const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100; + const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); + const active = !!this.state.selected[`${optionIndex}`]; + const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); let titleEmojified = option.get('title_emojified'); if (!titleEmojified) { @@ -101,8 +145,21 @@ class Poll extends ImmutablePureComponent { disabled={disabled} /> - {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />} - {showResults && <span className='poll__number'>{Math.round(percent)}%</span>} + {!showResults && ( + <span + className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} + tabIndex='0' + role={poll.get('multiple') ? 'checkbox' : 'radio'} + onKeyPress={this.handleOptionKeyPress} + aria-checked={active} + aria-label={option.get('title')} + data-index={optionIndex} + /> + )} + {showResults && <span className='poll__number'> + {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />} + {Math.round(percent)}% + </span>} <span dangerouslySetInnerHTML={{ __html: titleEmojified }} /> </label> @@ -112,25 +169,34 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; + const { expired } = this.state; if (!poll) { return null; } - const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />; - const showResults = poll.get('voted') || poll.get('expired'); + const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />; + const showResults = poll.get('voted') || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + let votesCount = null; + + if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) { + votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />; + } else { + votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />; + } + return ( <div className='poll'> <ul> - {poll.get('options').map((option, i) => this.renderOption(option, i))} + {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))} </ul> <div className='poll__footer'> {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>} {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>} - <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} /> + {votesCount} {poll.get('expires_at') && <span> · {timeRemaining}</span>} </div> </div> diff --git a/app/javascript/mastodon/components/radio_button.js b/app/javascript/mastodon/components/radio_button.js new file mode 100644 index 000000000..0496fa286 --- /dev/null +++ b/app/javascript/mastodon/components/radio_button.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +export default class RadioButton extends React.PureComponent { + + static propTypes = { + value: PropTypes.string.isRequired, + checked: PropTypes.bool, + name: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + label: PropTypes.node.isRequired, + }; + + render () { + const { name, value, checked, onChange, label } = this.props; + + return ( + <label className='radio-button'> + <input + name={name} + type='radio' + value={value} + checked={checked} + onChange={onChange} + /> + + <span className={classNames('radio-button__input', { checked })} /> + + <span>{label}</span> + </label> + ); + } + +} diff --git a/app/javascript/mastodon/components/regeneration_indicator.js b/app/javascript/mastodon/components/regeneration_indicator.js new file mode 100644 index 000000000..faf88c6b5 --- /dev/null +++ b/app/javascript/mastodon/components/regeneration_indicator.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import illustration from 'mastodon/../images/elephant_ui_working.svg'; + +const MissingIndicator = () => ( + <div className='regeneration-indicator'> + <div className='regeneration-indicator__figure'> + <img src={illustration} alt='' /> + </div> + + <div className='regeneration-indicator__label'> + <FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading…' /> + <FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' /> + </div> + </div> +); + +export default MissingIndicator; diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 0376cf85a..421756803 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4'; import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import LoadMore from './load_more'; +import LoadPending from './load_pending'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; @@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent { static propTypes = { scrollKey: PropTypes.string.isRequired, onLoadMore: PropTypes.func, + onLoadPending: PropTypes.func, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -28,10 +30,12 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, showLoading: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, prepend: PropTypes.node, alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, + bindToDocument: PropTypes.bool, }; static defaultProps = { @@ -47,7 +51,9 @@ export default class ScrollableList extends PureComponent { handleScroll = throttle(() => { if (this.node) { - const { scrollTop, scrollHeight, clientHeight } = this.node; + const scrollTop = this.getScrollTop(); + const scrollHeight = this.getScrollHeight(); + const clientHeight = this.getClientHeight(); const offset = scrollHeight - scrollTop - clientHeight; if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) { @@ -77,9 +83,14 @@ export default class ScrollableList extends PureComponent { scrollToTopOnMouseIdle = false; setScrollTop = newScrollTop => { - if (this.node.scrollTop !== newScrollTop) { + if (this.getScrollTop() !== newScrollTop) { this.lastScrollWasSynthetic = true; - this.node.scrollTop = newScrollTop; + + if (this.props.bindToDocument) { + document.scrollingElement.scrollTop = newScrollTop; + } else { + this.node.scrollTop = newScrollTop; + } } }; @@ -97,7 +108,7 @@ export default class ScrollableList extends PureComponent { this.clearMouseIdleTimer(); this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); - if (!this.mouseMovedRecently && this.node.scrollTop === 0) { + if (!this.mouseMovedRecently && this.getScrollTop() === 0) { // Only set if we just started moving and are scrolled to the top. this.scrollToTopOnMouseIdle = true; } @@ -132,15 +143,27 @@ export default class ScrollableList extends PureComponent { } getScrollPosition = () => { - if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return { height: this.node.scrollHeight, top: this.node.scrollTop }; + if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + return { height: this.getScrollHeight(), top: this.getScrollTop() }; } else { return null; } } + getScrollTop = () => { + return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop; + } + + getScrollHeight = () => { + return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight; + } + + getClientHeight = () => { + return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight; + } + updateScrollBottom = (snapshot) => { - const newScrollTop = this.node.scrollHeight - snapshot; + const newScrollTop = this.getScrollHeight() - snapshot; this.setScrollTop(newScrollTop); } @@ -149,9 +172,10 @@ export default class ScrollableList extends PureComponent { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); + const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); - if (someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return this.node.scrollHeight - this.node.scrollTop; + if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + return this.getScrollHeight() - this.getScrollTop(); } else { return null; } @@ -161,7 +185,7 @@ export default class ScrollableList extends PureComponent { // Reset the scroll position when a new child comes in in order not to // jerk the scrollbar around if you're already scrolled down the page. if (snapshot !== null) { - this.setScrollTop(this.node.scrollHeight - snapshot); + this.setScrollTop(this.getScrollHeight() - snapshot); } } @@ -175,6 +199,7 @@ export default class ScrollableList extends PureComponent { this.clearMouseIdleTimer(); this.detachScrollListener(); this.detachIntersectionObserver(); + detachFullscreenListener(this.onFullScreenChange); } @@ -194,13 +219,23 @@ export default class ScrollableList extends PureComponent { } attachScrollListener () { - this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.addEventListener('scroll', this.handleScroll); + document.addEventListener('wheel', this.handleWheel); + } else { + this.node.addEventListener('scroll', this.handleScroll); + this.node.addEventListener('wheel', this.handleWheel); + } } detachScrollListener () { - this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.removeEventListener('scroll', this.handleScroll); + document.removeEventListener('wheel', this.handleWheel); + } else { + this.node.removeEventListener('scroll', this.handleScroll); + this.node.removeEventListener('wheel', this.handleWheel); + } } getFirstChildKey (props) { @@ -225,12 +260,25 @@ export default class ScrollableList extends PureComponent { this.props.onLoadMore(); } + handleLoadPending = e => { + e.preventDefault(); + this.props.onLoadPending(); + // Prevent the weird scroll-jumping behavior, as we explicitly don't want to + // scroll to top, and we know the scroll height is going to change + this.scrollToTopOnMouseIdle = false; + this.lastScrollWasSynthetic = false; + this.clearMouseIdleTimer(); + this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); + this.mouseMovedRecently = true; + } + render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null; + const loadPending = (numPending > 0) ? <LoadPending count={numPending} onClick={this.handleLoadPending} /> : null; let scrollableArea = null; if (showLoading) { @@ -251,6 +299,8 @@ export default class ScrollableList extends PureComponent { <div role='feed' className='item-list'> {prepend} + {loadPending} + {React.Children.map(this.props.children, (child, index) => ( <IntersectionObserverArticleContainer key={child.key} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index aa5e870dc..e120278a0 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -12,7 +12,7 @@ import AttachmentList from './attachment_list'; import Card from '../features/status/components/card'; import { injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video } from '../features/ui/util/async-components'; +import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { HotKeys } from 'react-hotkeys'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; @@ -199,17 +199,38 @@ class Status extends ImmutablePureComponent { }; renderLoadingMediaGallery () { - return <div className='media_gallery' style={{ height: '110px' }} />; + return <div className='media-gallery' style={{ height: '110px' }} />; } renderLoadingVideoPlayer () { - return <div className='media-spoiler-video' style={{ height: '110px' }} />; + return <div className='video-player' style={{ height: '110px' }} />; + } + + renderLoadingAudioPlayer () { + return <div className='audio-player' style={{ height: '110px' }} />; } handleOpenVideo = (media, startTime) => { this.props.onOpenVideo(media, startTime); } + handleHotkeyOpenMedia = e => { + const { onOpenMedia, onOpenVideo } = this.props; + const status = this._properStatus(); + + 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]), 0); + } else { + onOpenMedia(status.get('media_attachments'), 0); + } + } + } + handleHotkeyReply = e => { e.preventDefault(); this.props.onReply(this._properStatus(), this.context.router.history); @@ -278,12 +299,28 @@ class Status extends ImmutablePureComponent { return null; } + const handlers = this.props.muted ? {} : { + reply: this.handleHotkeyReply, + favourite: this.handleHotkeyFavourite, + boost: this.handleHotkeyBoost, + mention: this.handleHotkeyMention, + open: this.handleHotkeyOpen, + openProfile: this.handleHotkeyOpenProfile, + moveUp: this.handleHotkeyMoveUp, + moveDown: this.handleHotkeyMoveDown, + toggleHidden: this.handleHotkeyToggleHidden, + toggleSensitive: this.handleHotkeyToggleSensitive, + openMedia: this.handleHotkeyOpenMedia, + }; + if (hidden) { return ( - <div ref={this.handleRef}> - {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {status.get('content')} - </div> + <HotKeys handlers={handlers}> + <div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'> + {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} + {status.get('content')} + </div> + </HotKeys> ); } @@ -333,17 +370,33 @@ class Status extends ImmutablePureComponent { media={status.get('media_attachments')} /> ); + } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { + const attachment = status.getIn(['media_attachments', 0]); + + media = ( + <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > + {Component => ( + <Component + src={attachment.get('url')} + alt={attachment.get('description')} + duration={attachment.getIn(['meta', 'original', 'duration'], 0)} + peaks={[0]} + height={70} + /> + )} + </Bundle> + ); } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); + const attachment = status.getIn(['media_attachments', 0]); media = ( <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > {Component => ( <Component - preview={video.get('preview_url')} - blurhash={video.get('blurhash')} - src={video.get('url')} - alt={video.get('description')} + preview={attachment.get('preview_url')} + blurhash={attachment.get('blurhash')} + src={attachment.get('url')} + alt={attachment.get('description')} width={this.props.cachedMediaWidth} height={110} inline @@ -394,19 +447,6 @@ class Status extends ImmutablePureComponent { statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; } - const handlers = this.props.muted ? {} : { - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - open: this.handleHotkeyOpen, - openProfile: this.handleHotkeyOpenProfile, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleHidden: this.handleHotkeyToggleHidden, - toggleSensitive: this.handleHotkeyToggleSensitive, - }; - 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}> @@ -415,9 +455,9 @@ class Status extends ImmutablePureComponent { <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='status__expand' onClick={this.handleExpandClick} role='presentation' /> <div className='status__info'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> - <a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name'> + <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'> {statusAvatar} </div> diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 0bfbd8879..4b3c79d0d 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -1,5 +1,6 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import IconButton from './icon_button'; import DropdownMenuContainer from '../containers/dropdown_menu_container'; @@ -23,6 +24,8 @@ const messages = defineMessages({ 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' }, + bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, + removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' }, open: { id: 'status.open', defaultMessage: 'Expand this status' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, @@ -33,6 +36,10 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, + blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); const obfuscatedCount = count => { @@ -45,7 +52,12 @@ const obfuscatedCount = count => { } }; -export default @injectIntl +const mapStateToProps = (state, { status }) => ({ + relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), +}); + +export default @connect(mapStateToProps) +@injectIntl class StatusActionBar extends ImmutablePureComponent { static contextTypes = { @@ -54,6 +66,7 @@ class StatusActionBar extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + relationship: ImmutablePropTypes.map, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -61,11 +74,16 @@ class StatusActionBar extends ImmutablePureComponent { onDirect: PropTypes.func, onMention: PropTypes.func, onMute: PropTypes.func, + onUnmute: PropTypes.func, onBlock: PropTypes.func, + onUnblock: PropTypes.func, + onBlockDomain: PropTypes.func, + onUnblockDomain: PropTypes.func, onReport: PropTypes.func, onEmbed: PropTypes.func, onMuteConversation: PropTypes.func, onPin: PropTypes.func, + onBookmark: PropTypes.func, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -74,6 +92,7 @@ class StatusActionBar extends ImmutablePureComponent { // evaluate to false. See react-immutable-pure-component for usage. updateOnProps = [ 'status', + 'relationship', 'withDismiss', ] @@ -114,6 +133,10 @@ class StatusActionBar extends ImmutablePureComponent { window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); } + handleBookmarkClick = () => { + this.props.onBookmark(this.props.status); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -135,11 +158,39 @@ class StatusActionBar extends ImmutablePureComponent { } handleMuteClick = () => { - this.props.onMute(this.props.status.get('account')); + const { status, relationship, onMute, onUnmute } = this.props; + const account = status.get('account'); + + if (relationship && relationship.get('muting')) { + onUnmute(account); + } else { + onMute(account); + } } handleBlockClick = () => { - this.props.onBlock(this.props.status); + const { status, relationship, onBlock, onUnblock } = this.props; + const account = status.get('account'); + + if (relationship && relationship.get('blocking')) { + onUnblock(account); + } else { + onBlock(status); + } + } + + handleBlockDomain = () => { + const { status, onBlockDomain } = this.props; + const account = status.get('account'); + + onBlockDomain(account.get('acct').split('@')[1]); + } + + handleUnblockDomain = () => { + const { status, onUnblockDomain } = this.props; + const account = status.get('account'); + + onUnblockDomain(account.get('acct').split('@')[1]); } handleOpen = () => { @@ -178,11 +229,12 @@ class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, intl, withDismiss } = this.props; + const { status, relationship, intl, withDismiss } = this.props; const mutingConversation = status.get('muted'); const anonymousAccess = !me; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const account = status.get('account'); let menu = []; let reblogIcon = 'retweet'; @@ -196,6 +248,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } + menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); menu.push(null); if (status.getIn(['account', 'id']) === me || withDismiss) { @@ -215,16 +268,39 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); + menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + + if (relationship && relationship.get('muting')) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); + } else { + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick }); + } + + if (relationship && relationship.get('blocking')) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); + } else { + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick }); + } + + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport }); + + if (account.get('acct') !== account.get('username')) { + const domain = account.get('acct').split('@')[1]; + + menu.push(null); + + if (relationship && relationship.get('domain_blocking')) { + menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); + } else { + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain }); + } + } if (isStaff) { menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); } } diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 06f5b4aad..d13091325 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -7,6 +7,7 @@ import Permalink from './permalink'; import classnames from 'classnames'; import PollContainer from 'mastodon/containers/poll_container'; import Icon from 'mastodon/components/icon'; +import { autoPlayGif } from 'mastodon/initial_state'; const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top) @@ -54,10 +55,11 @@ export default class StatusContent extends React.PureComponent { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); } else { link.setAttribute('title', link.href); + link.classList.add('unhandled-link'); } link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); + link.setAttribute('rel', 'noopener noreferrer'); } if ( @@ -71,12 +73,35 @@ export default class StatusContent extends React.PureComponent { } } + _updateStatusEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + componentDidMount () { this._updateStatusLinks(); + this._updateStatusEmojis(); } componentDidUpdate () { this._updateStatusLinks(); + this._updateStatusEmojis(); } onMentionClick = (mention, e) => { @@ -87,7 +112,7 @@ export default class StatusContent extends React.PureComponent { } onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, '').toLowerCase(); + hashtag = hashtag.replace(/^#/, ''); if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); @@ -95,6 +120,14 @@ export default class StatusContent extends React.PureComponent { } } + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; } @@ -133,11 +166,6 @@ export default class StatusContent extends React.PureComponent { } } - handleCollapsedClick = (e) => { - e.preventDefault(); - this.setState({ collapsed: !this.state.collapsed }); - } - setRef = (c) => { this.node = c; } @@ -188,59 +216,40 @@ export default class StatusContent extends React.PureComponent { return ( <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}> - <span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} /> + <span dangerouslySetInnerHTML={spoilerContent} /> {' '} <button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button> </p> {mentionsPlaceholder} - <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} /> + <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} /> {!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />} </div> ); } else if (this.props.onClick) { const output = [ - <div - ref={this.setRef} - tabIndex='0' - key='content' - className={classNames} - style={directionStyle} - dangerouslySetInnerHTML={content} - lang={status.get('language')} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} - />, + <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} /> + + {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} + </div>, ]; if (this.state.collapsed) { output.push(readMoreButton); } - if (status.get('poll')) { - output.push(<PollContainer pollId={status.get('poll')} />); - } - return output; } else { - const output = [ - <div - tabIndex='0' - ref={this.setRef} - className='status__content' - style={directionStyle} - dangerouslySetInnerHTML={content} - lang={status.get('language')} - />, - ]; - - if (status.get('poll')) { - output.push(<PollContainer pollId={status.get('poll')} />); - } + return ( + <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}> + <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} /> - return output; + {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} + </div> + ); } } diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 745e6422d..e1b370c91 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -1,12 +1,12 @@ import { debounce } from 'lodash'; import React from 'react'; -import { FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import StatusContainer from '../containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import LoadGap from './load_gap'; import ScrollableList from './scrollable_list'; +import RegenerationIndicator from 'mastodon/components/regeneration_indicator'; export default class StatusList extends ImmutablePureComponent { @@ -81,18 +81,7 @@ export default class StatusList extends ImmutablePureComponent { const { isLoading, isPartial } = other; if (isPartial) { - return ( - <div className='regeneration-indicator'> - <div> - <div className='regeneration-indicator__figure' /> - - <div className='regeneration-indicator__label'> - <FormattedMessage id='regeneration_indicator.label' tagName='strong' defaultMessage='Loading…' /> - <FormattedMessage id='regeneration_indicator.sublabel' defaultMessage='Your home feed is being prepared!' /> - </div> - </div> - </div> - ); + return <RegenerationIndicator />; } let scrollableContent = (isLoading || statusIds.size > 0) ? ( diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js index 73c8a1e53..ab1823194 100644 --- a/app/javascript/mastodon/containers/dropdown_menu_container.js +++ b/app/javascript/mastodon/containers/dropdown_menu_container.js @@ -1,4 +1,5 @@ import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu'; +import { fetchRelationships } from 'mastodon/actions/accounts'; import { openModal, closeModal } from '../actions/modal'; import { connect } from 'react-redux'; import DropdownMenu from '../components/dropdown_menu'; @@ -13,14 +14,19 @@ const mapStateToProps = state => ({ const mapDispatchToProps = (dispatch, { status, items }) => ({ onOpen(id, onItemClick, dropdownPlacement, keyboard) { + if (status) { + dispatch(fetchRelationships([status.getIn(['account', 'id'])])); + } + dispatch(isUserTouching() ? openModal('ACTIONS', { status, actions: items, onClick: onItemClick, }) : openDropdownMenu(id, dropdownPlacement, keyboard)); }, + onClose(id) { - dispatch(closeModal()); + dispatch(closeModal('ACTIONS')); dispatch(closeDropdownMenu(id)); }, }); diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 542b68282..3ac58cf7c 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -12,6 +12,8 @@ import { hydrateStore } from '../actions/store'; import { connectUserStream } from '../actions/streaming'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; +import { previewState as previewMediaState } from 'mastodon/features/ui/components/media_modal'; +import { previewState as previewVideoState } from 'mastodon/features/ui/components/video_modal'; import initialState from '../initial_state'; import ErrorBoundary from '../components/error_boundary'; @@ -35,6 +37,10 @@ class MastodonMount extends React.PureComponent { showIntroduction: PropTypes.bool, }; + shouldUpdateScroll (_, { location }) { + return location.state !== previewMediaState && location.state !== previewVideoState; + } + render () { const { showIntroduction } = this.props; @@ -44,7 +50,7 @@ class MastodonMount extends React.PureComponent { return ( <BrowserRouter basename='/web'> - <ScrollContext> + <ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}> <Route path='/' component={UI} /> </ScrollContext> </BrowserRouter> diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js index 51d4f0fed..ba55ecbc7 100644 --- a/app/javascript/mastodon/containers/media_container.js +++ b/app/javascript/mastodon/containers/media_container.js @@ -2,19 +2,22 @@ import React, { PureComponent, Fragment } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from '../locales'; -import MediaGallery from '../components/media_gallery'; -import Video from '../features/video'; -import Card from '../features/status/components/card'; -import Poll from 'mastodon/components/poll'; -import ModalRoot from '../components/modal_root'; -import MediaModal from '../features/ui/components/media_modal'; import { List as ImmutableList, fromJS } from 'immutable'; +import { getLocale } from 'mastodon/locales'; +import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; +import MediaGallery from 'mastodon/components/media_gallery'; +import Poll from 'mastodon/components/poll'; +import Hashtag from 'mastodon/components/hashtag'; +import ModalRoot from 'mastodon/components/modal_root'; +import MediaModal from 'mastodon/features/ui/components/media_modal'; +import Video from 'mastodon/features/video'; +import Card from 'mastodon/features/status/components/card'; +import Audio from 'mastodon/features/audio'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll }; +const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; export default class MediaContainer extends PureComponent { @@ -31,6 +34,8 @@ export default class MediaContainer extends PureComponent { handleOpenMedia = (media, index) => { document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + this.setState({ media, index }); } @@ -38,11 +43,15 @@ export default class MediaContainer extends PureComponent { const media = ImmutableList([video]); document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + this.setState({ media, time }); } handleCloseMedia = () => { document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; + this.setState({ media: null, index: null, time: null }); } @@ -55,12 +64,13 @@ export default class MediaContainer extends PureComponent { {[].map.call(components, (component, i) => { const componentName = component.getAttribute('data-component'); const Component = MEDIA_COMPONENTS[componentName]; - const { media, card, poll, ...props } = JSON.parse(component.getAttribute('data-props')); + const { media, card, poll, hashtag, ...props } = JSON.parse(component.getAttribute('data-props')); Object.assign(props, { - ...(media ? { media: fromJS(media) } : {}), - ...(card ? { card: fromJS(card) } : {}), - ...(poll ? { poll: fromJS(poll) } : {}), + ...(media ? { media: fromJS(media) } : {}), + ...(card ? { card: fromJS(card) } : {}), + ...(poll ? { poll: fromJS(poll) } : {}), + ...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(componentName === 'Video' ? { onOpenVideo: this.handleOpenVideo, @@ -74,6 +84,7 @@ export default class MediaContainer extends PureComponent { component, ); })} + <ModalRoot onClose={this.handleCloseMedia}> {this.state.media && ( <MediaModal diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 86324b846..35c16a20c 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -10,12 +10,13 @@ import { import { reblog, favourite, + bookmark, unreblog, unfavourite, + unbookmark, pin, unpin, } from '../actions/interactions'; -import { blockAccount } from '../actions/accounts'; import { muteStatus, unmuteStatus, @@ -23,7 +24,16 @@ import { hideStatus, revealStatus, } from '../actions/statuses'; +import { + unmuteAccount, + unblockAccount, +} from '../actions/accounts'; +import { + blockDomain, + unblockDomain, +} from '../actions/domain_blocks'; import { initMuteModal } from '../actions/mutes'; +import { initBlockModal } from '../actions/blocks'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -35,10 +45,9 @@ const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, 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?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); const makeMapStateToProps = () => { @@ -56,6 +65,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onReply (status, router) { dispatch((_, getState) => { let state = getState(); + if (state.getIn(['compose', 'text']).trim().length !== 0) { dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.replyMessage), @@ -77,7 +87,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onReblog (status, e) { - if (e.shiftKey || !boostModal) { + if ((e && e.shiftKey) || !boostModal) { this.onModalReblog(status); } else { dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); @@ -92,6 +102,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onBookmark (status) { + if (status.get('bookmarked')) { + dispatch(unbookmark(status)); + } else { + dispatch(bookmark(status)); + } + }, + onPin (status) { if (status.get('pinned')) { dispatch(unpin(status)); @@ -137,16 +155,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onBlock (status) { const account = status.get('account'); - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.get('id'))); - dispatch(initReport(account, status)); - }, - })); + dispatch(initBlockModal(account)); + }, + + onUnblock (account) { + dispatch(unblockAccount(account.get('id'))); }, onReport (status) { @@ -157,6 +170,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(initMuteModal(account)); }, + onUnmute (account) { + dispatch(unmuteAccount(account.get('id'))); + }, + onMuteConversation (status) { if (status.get('muted')) { dispatch(unmuteStatus(status.get('id'))); @@ -173,6 +190,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onBlockDomain (domain) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + })); + }, + + onUnblockDomain (domain) { + dispatch(unblockDomain(domain)); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index e5b60e33e..8bd7f2db5 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -15,6 +15,7 @@ import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -79,6 +80,47 @@ class Header extends ImmutablePureComponent { return !location.pathname.match(/\/(followers|following)\/?$/); } + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + + setRef = (c) => { + this.node = c; + } + render () { const { account, intl, domain, identity_proofs } = this.props; @@ -107,7 +149,7 @@ class Header extends ImmutablePureComponent { 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.requested)} onClick={this.props.onFollow} />; + actionBtn = <Button className='logo-button' 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} />; } else if (account.getIn(['relationship', 'blocking'])) { @@ -196,11 +238,20 @@ class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); - const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null; const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + let badge; + + if (account.get('bot')) { + badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>); + } else if (account.get('group')) { + badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>); + } else { + badge = null; + } + return ( - <div className={classNames('account__header', { inactive: !!account.get('moved') })}> + <div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}> <div className='account__header__image'> <div className='account__header__info'> {info} @@ -211,7 +262,7 @@ class Header extends ImmutablePureComponent { <div className='account__header__bar'> <div className='account__header__tabs'> - <a className='avatar' href={account.get('url')} rel='noopener' target='_blank'> + <a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'> <Avatar account={account} size={90} /> </a> @@ -240,10 +291,10 @@ class Header extends ImmutablePureComponent { <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} /> <dd className='verified'> - <a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> + <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> <Icon id='check' className='verified__mark' /> </span></a> - <a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> + <a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> </dd> </dl> ))} diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index 2609b96ff..617a45d16 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -1,12 +1,12 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { decode } from 'blurhash'; +import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import { autoPlayGif, displayMedia } from 'mastodon/initial_state'; -import classNames from 'classnames'; -import { decode } from 'blurhash'; import { isIOS } from 'mastodon/is_mobile'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; export default class MediaItem extends ImmutablePureComponent { @@ -96,6 +96,12 @@ export default class MediaItem extends ImmutablePureComponent { if (attachment.get('type') === 'unknown') { // Skip + } else if (attachment.get('type') === 'audio') { + thumbnail = ( + <span className='account-gallery__item__icons'> + <Icon id='music' /> + </span> + ); } else if (attachment.get('type') === 'image') { const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0; const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0; @@ -113,6 +119,7 @@ export default class MediaItem extends ImmutablePureComponent { ); } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) { const autoPlay = !isIOS() && autoPlayGif; + const label = attachment.get('type') === 'video' ? <Icon id='play' /> : 'GIF'; thumbnail = ( <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> @@ -129,7 +136,7 @@ export default class MediaItem extends ImmutablePureComponent { muted /> - <span className='media-gallery__gifv__label'>GIF</span> + <span className='media-gallery__gifv__label'>{label}</span> </div> ); } @@ -144,7 +151,7 @@ export default class MediaItem extends ImmutablePureComponent { return ( <div className='account-gallery__item' style={{ width, height }}> - <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> {visible && thumbnail} {!visible && icon} diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index 5d6a53e18..de481075c 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -56,6 +56,7 @@ class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; state = { @@ -101,6 +102,8 @@ class AccountGallery extends ImmutablePureComponent { handleOpenMedia = attachment => { if (attachment.get('type') === 'video') { this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') })); + } else if (attachment.get('type') === 'audio') { + this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status') })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); @@ -116,7 +119,7 @@ class AccountGallery extends ImmutablePureComponent { } render () { - const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props; + const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props; const { width } = this.state; if (!isAccount) { @@ -143,7 +146,7 @@ class AccountGallery extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}> diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 4d4ae6e82..8728b4806 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -5,7 +5,6 @@ import Header from '../components/header'; import { followAccount, unfollowAccount, - blockAccount, unblockAccount, unmuteAccount, pinAccount, @@ -16,6 +15,7 @@ import { directCompose, } from '../../../actions/compose'; import { initMuteModal } from '../../../actions/mutes'; +import { initBlockModal } from '../../../actions/blocks'; import { initReport } from '../../../actions/reports'; import { openModal } from '../../../actions/modal'; import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; @@ -25,9 +25,7 @@ import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, }); const makeMapStateToProps = () => { @@ -64,16 +62,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); } else { - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.get('id'))); - dispatch(initReport(account)); - }, - })); + dispatch(initBlockModal(account)); } }, diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 27581bfdc..8d0cbe5a1 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -44,6 +44,7 @@ class AccountTimeline extends ImmutablePureComponent { withReplies: PropTypes.bool, blockedBy: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -77,11 +78,12 @@ class AccountTimeline extends ImmutablePureComponent { } render () { - const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount } = this.props; + const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn } = this.props; if (!isAccount) { return ( <Column> + <ColumnBackButton multiColumn={multiColumn} /> <MissingIndicator /> </Column> ); @@ -99,7 +101,7 @@ class AccountTimeline extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <StatusList prepend={<HeaderContainer accountId={this.props.params.accountId} />} @@ -112,6 +114,7 @@ class AccountTimeline extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js new file mode 100644 index 000000000..1b4cdbb4f --- /dev/null +++ b/app/javascript/mastodon/features/audio/index.js @@ -0,0 +1,236 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import WaveSurfer from 'wavesurfer.js'; +import { defineMessages, injectIntl } from 'react-intl'; +import { formatTime } from 'mastodon/features/video'; +import Icon from 'mastodon/components/icon'; +import classNames from 'classnames'; +import { throttle } from 'lodash'; + +const messages = defineMessages({ + play: { id: 'video.play', defaultMessage: 'Play' }, + pause: { id: 'video.pause', defaultMessage: 'Pause' }, + mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, + unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, + download: { id: 'video.download', defaultMessage: 'Download file' }, +}); + +export default @injectIntl +class Audio extends React.PureComponent { + + static propTypes = { + src: PropTypes.string.isRequired, + alt: PropTypes.string, + duration: PropTypes.number, + peaks: PropTypes.arrayOf(PropTypes.number), + height: PropTypes.number, + preload: PropTypes.bool, + editable: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + state = { + currentTime: 0, + duration: null, + paused: true, + muted: false, + volume: 0.5, + }; + + // hard coded in components.scss + // any way to get ::before values programatically? + + volWidth = 50; + + volOffset = 70; + + volHandleOffset = v => { + const offset = v * this.volWidth + this.volOffset; + return (offset > 110) ? 110 : offset; + } + + setVolumeRef = c => { + this.volume = c; + } + + setWaveformRef = c => { + this.waveform = c; + } + + componentDidMount () { + if (this.waveform) { + this._updateWaveform(); + } + } + + componentDidUpdate (prevProps) { + if (this.waveform && prevProps.src !== this.props.src) { + this._updateWaveform(); + } + } + + componentWillUnmount () { + if (this.wavesurfer) { + this.wavesurfer.destroy(); + this.wavesurfer = null; + } + } + + _updateWaveform () { + const { src, height, duration, peaks, preload } = this.props; + + const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color'); + const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color'); + + if (this.wavesurfer) { + this.wavesurfer.destroy(); + this.loaded = false; + } + + const wavesurfer = WaveSurfer.create({ + container: this.waveform, + height, + barWidth: 3, + cursorWidth: 0, + progressColor, + waveColor, + backend: 'MediaElement', + interact: preload, + }); + + wavesurfer.setVolume(this.state.volume); + + if (preload) { + wavesurfer.load(src); + this.loaded = true; + } else { + wavesurfer.load(src, peaks, 'none', duration); + this.loaded = false; + } + + wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) })); + wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) })); + wavesurfer.on('pause', () => this.setState({ paused: true })); + wavesurfer.on('play', () => this.setState({ paused: false })); + wavesurfer.on('volume', volume => this.setState({ volume })); + wavesurfer.on('mute', muted => this.setState({ muted })); + + this.wavesurfer = wavesurfer; + } + + togglePlay = () => { + if (this.state.paused) { + if (!this.props.preload && !this.loaded) { + this.wavesurfer.createBackend(); + this.wavesurfer.createPeakCache(); + this.wavesurfer.load(this.props.src); + this.wavesurfer.toggleInteraction(); + this.loaded = true; + } + + this.wavesurfer.play(); + this.setState({ paused: false }); + } else { + this.wavesurfer.pause(); + this.setState({ paused: true }); + } + } + + toggleMute = () => { + this.wavesurfer.setMute(!this.state.muted); + } + + handleVolumeMouseDown = e => { + document.addEventListener('mousemove', this.handleMouseVolSlide, true); + document.addEventListener('mouseup', this.handleVolumeMouseUp, true); + document.addEventListener('touchmove', this.handleMouseVolSlide, true); + document.addEventListener('touchend', this.handleVolumeMouseUp, true); + + this.handleMouseVolSlide(e); + + e.preventDefault(); + e.stopPropagation(); + } + + handleVolumeMouseUp = () => { + document.removeEventListener('mousemove', this.handleMouseVolSlide, true); + document.removeEventListener('mouseup', this.handleVolumeMouseUp, true); + document.removeEventListener('touchmove', this.handleMouseVolSlide, true); + document.removeEventListener('touchend', this.handleVolumeMouseUp, true); + } + + handleMouseVolSlide = throttle(e => { + const rect = this.volume.getBoundingClientRect(); + const x = (e.clientX - rect.left) / this.volWidth; // x position within the element. + + if(!isNaN(x)) { + let slideamt = x; + + if (x > 1) { + slideamt = 1; + } else if(x < 0) { + slideamt = 0; + } + + this.wavesurfer.setVolume(slideamt); + } + }, 60); + + render () { + const { height, intl, alt, editable } = this.props; + const { paused, muted, volume, currentTime } = this.state; + + const volumeWidth = muted ? 0 : volume * this.volWidth; + const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume); + + return ( + <div className={classNames('audio-player', { editable })}> + <div className='audio-player__progress-placeholder' style={{ display: 'none' }} /> + <div className='audio-player__wave-placeholder' style={{ display: 'none' }} /> + + <div + className='audio-player__waveform' + aria-label={alt} + title={alt} + style={{ height }} + ref={this.setWaveformRef} + /> + + <div className='video-player__controls active'> + <div className='video-player__buttons-bar'> + <div className='video-player__buttons left'> + <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> + + <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> + + <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> + + <span + className={classNames('video-player__volume__handle')} + tabIndex='0' + style={{ left: `${volumeHandleLoc}px` }} + /> + </div> + + <span> + <span className='video-player__time-current'>{formatTime(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> + </div> + + <div className='video-player__buttons right'> + <button type='button' aria-label={intl.formatMessage(messages.download)}> + <a className='video-player__download__icon' href={this.props.src} download> + <Icon id={'download'} fixedWidth /> + </a> + </button> + </div> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index 96a219c94..051431ed2 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -32,6 +32,7 @@ class Blocks extends ImmutablePureComponent { accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,7 +44,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props; + const { intl, accountIds, shouldUpdateScroll, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -56,7 +57,7 @@ class Blocks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />; return ( - <Column icon='ban' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='blocks' @@ -64,6 +65,7 @@ class Blocks extends ImmutablePureComponent { hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} /> diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.js b/app/javascript/mastodon/features/bookmarked_statuses/index.js new file mode 100644 index 000000000..c37cb9176 --- /dev/null +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.js @@ -0,0 +1,104 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions/bookmarks'; +import Column from '../ui/components/column'; +import ColumnHeader from '../../components/column_header'; +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import StatusList from '../../components/status_list'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { debounce } from 'lodash'; + +const messages = defineMessages({ + heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, +}); + +const mapStateToProps = state => ({ + statusIds: state.getIn(['status_lists', 'bookmarks', 'items']), + isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), + hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Bookmarks extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + shouldUpdateScroll: PropTypes.func, + statusIds: ImmutablePropTypes.list.isRequired, + intl: PropTypes.object.isRequired, + columnId: PropTypes.string, + multiColumn: PropTypes.bool, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, + }; + + componentWillMount () { + this.props.dispatch(fetchBookmarkedStatuses()); + } + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('BOOKMARKS', {})); + } + } + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + setRef = c => { + this.column = c; + } + + handleLoadMore = debounce(() => { + this.props.dispatch(expandBookmarkedStatuses()); + }, 300, { leading: true }) + + render () { + const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; + const pinned = !!columnId; + + const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />; + + return ( + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}> + <ColumnHeader + icon='bookmark' + title={intl.formatMessage(messages.heading)} + onPin={this.handlePin} + onMove={this.handleMove} + onClick={this.handleHeaderClick} + pinned={pinned} + multiColumn={multiColumn} + showBackButton + /> + + <StatusList + trackScroll={!pinned} + statusIds={statusIds} + scrollKey={`bookmarked_statuses-${columnId}`} + hasMore={hasMore} + isLoading={isLoading} + onLoadMore={this.handleLoadMore} + shouldUpdateScroll={shouldUpdateScroll} + emptyMessage={emptyMessage} + bindToDocument={!multiColumn} + /> + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/community_timeline/components/column_settings.js b/app/javascript/mastodon/features/community_timeline/components/column_settings.js index 8250190a7..0cb6db883 100644 --- a/app/javascript/mastodon/features/community_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/community_timeline/components/column_settings.js @@ -20,7 +20,7 @@ class ColumnSettings extends React.PureComponent { return ( <div> <div className='column-settings__row'> - <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} /> + <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} /> </div> </div> ); diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 7d26c98b0..b3cd39685 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -14,14 +14,16 @@ const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, }); -const mapStateToProps = (state, { onlyMedia, columnId }) => { +const mapStateToProps = (state, { columnId }) => { const uuid = columnId; const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === uuid); + const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']); + const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]); return { - hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0, - onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']), + hasUnread: !!timelineState && timelineState.get('unread') > 0, + onlyMedia, }; }; @@ -105,7 +107,7 @@ class CommunityTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='users' active={hasUnread} @@ -126,6 +128,7 @@ class CommunityTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/mastodon/features/compose/components/action_bar.js b/app/javascript/mastodon/features/compose/components/action_bar.js index 077226d70..dd2632796 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.js +++ b/app/javascript/mastodon/features/compose/components/action_bar.js @@ -15,6 +15,7 @@ const messages = defineMessages({ domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, }); export default @injectIntl @@ -22,9 +23,14 @@ class ActionBar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, + onLogout: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; + handleLogout = () => { + this.props.onLogout(); + } + render () { const { intl } = this.props; @@ -42,6 +48,8 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout }); return ( <div className='compose__action-bar'> diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index b3a04e421..46c52d9e4 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -118,7 +118,10 @@ class ComposeForm extends ImmutablePureComponent { handleFocus = () => { if (this.composeForm && !this.props.singleColumn) { - this.composeForm.scrollIntoView(); + const { left, right } = this.composeForm.getBoundingClientRect(); + if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { + this.composeForm.scrollIntoView(); + } } } @@ -189,12 +192,12 @@ class ComposeForm extends ImmutablePureComponent { } return ( - <div className='compose-form' ref={this.setRef}> + <div className='compose-form'> <WarningContainer /> <ReplyIndicatorContainer /> - <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}> + <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}> <AutosuggestInput placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index c1429c756..e57c3c20c 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -6,7 +6,7 @@ import Overlay from 'react-overlays/lib/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import detectPassiveEvents from 'detect-passive-events'; -import { buildCustomEmojis } from '../../emoji/emoji'; +import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -31,19 +31,6 @@ let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; -const categoriesSort = [ - 'recent', - 'custom', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -]; - class ModifierPickerMenu extends React.PureComponent { static propTypes = { @@ -241,8 +228,23 @@ class EmojiPickerMenu extends React.PureComponent { } const title = intl.formatMessage(messages.emoji); + const { modifierOpen } = this.state; + const categoriesSort = [ + 'recent', + 'people', + 'nature', + 'foods', + 'activity', + 'places', + 'objects', + 'symbols', + 'flags', + ]; + + categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort()); + return ( <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> <EmojiPicker diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js index d8d49cb95..840d0a3da 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.js +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js @@ -12,6 +12,7 @@ export default class NavigationBar extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, + onLogout: PropTypes.func.isRequired, onClose: PropTypes.func, }; @@ -33,7 +34,7 @@ export default class NavigationBar extends ImmutablePureComponent { <div className='navigation-bar__actions'> <IconButton className='close' title='' icon='close' onClick={this.props.onClose} /> - <ActionBar account={this.props.account} /> + <ActionBar account={this.props.account} onLogout={this.props.onLogout} /> </div> </div> ); diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js index 211601d52..791a4b1ad 100644 --- a/app/javascript/mastodon/features/compose/components/poll_form.js +++ b/app/javascript/mastodon/features/compose/components/poll_form.js @@ -13,6 +13,8 @@ const messages = defineMessages({ add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, + switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' }, + switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' }, 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}}' }, @@ -50,6 +52,12 @@ class Option extends React.PureComponent { e.stopPropagation(); }; + handleCheckboxKeypress = e => { + if (e.key === 'Enter' || e.key === ' ') { + this.handleToggleMultiple(e); + } + } + onSuggestionsClearRequested = () => { this.props.onClearSuggestions(); } @@ -71,13 +79,16 @@ class Option extends React.PureComponent { <span className={classNames('poll__input', { checkbox: isPollMultiple })} onClick={this.handleToggleMultiple} + onKeyPress={this.handleCheckboxKeypress} role='button' tabIndex='0' + title={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)} + aria-label={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)} /> <AutosuggestInput placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} - maxLength={25} + maxLength={100} value={title} onChange={this.handleOptionTitleChange} suggestions={this.props.suggestions} @@ -142,11 +153,9 @@ class PollForm extends ImmutablePureComponent { </ul> <div className='poll__footer'> - {options.size < 4 && ( - <button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button> - )} + <button disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button> - <select value={expiresIn} onChange={this.handleSelectDuration}> + <select value={expiresIn} onBlur={this.handleSelectDuration}> <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> diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index d02a55be0..7cbfe463a 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -73,6 +73,19 @@ class PrivacyDropdownMenu extends React.PureComponent { this.props.onChange(element.getAttribute('data-index')); } break; + case 'Tab': + if (e.shiftKey) { + element = this.node.childNodes[index - 1] || this.node.lastChild; + } else { + element = this.node.childNodes[index + 1] || this.node.firstChild; + } + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + break; case 'Home': element = this.node.firstChild; if (element) { @@ -180,6 +193,9 @@ class PrivacyDropdown extends React.PureComponent { } } else { const { top } = target.getBoundingClientRect(); + if (this.state.open && this.activeElement) { + this.activeElement.focus(); + } this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open }); } @@ -202,7 +218,25 @@ class PrivacyDropdown extends React.PureComponent { } } + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + } + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + } + handleClose = () => { + if (this.state.open && this.activeElement) { + this.activeElement.focus(); + } this.setState({ open: false }); } @@ -229,7 +263,7 @@ class PrivacyDropdown extends React.PureComponent { return ( <div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}> - <div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}> + <div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })}> <IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} @@ -239,6 +273,8 @@ class PrivacyDropdown extends React.PureComponent { active={open} inverted onClick={this.handleToggle} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleButtonKeyDown} style={{ height: null, lineHeight: '27px' }} /> </div> diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index 7f9edfeee..3e36a922b 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -60,12 +60,17 @@ class Search extends React.PureComponent { onShow: PropTypes.func.isRequired, openInRoute: PropTypes.bool, intl: PropTypes.object.isRequired, + singleColumn: PropTypes.bool, }; state = { expanded: false, }; + setRef = c => { + this.searchForm = c; + } + handleChange = (e) => { this.props.onChange(e.target.value); } @@ -95,6 +100,13 @@ class Search extends React.PureComponent { handleFocus = () => { this.setState({ expanded: true }); this.props.onShow(); + + if (this.searchForm && !this.props.singleColumn) { + const { left, right } = this.searchForm.getBoundingClientRect(); + if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { + this.searchForm.scrollIntoView(); + } + } } handleBlur = () => { @@ -111,6 +123,7 @@ class Search extends React.PureComponent { <label> <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span> <input + ref={this.setRef} className='search__input' type='text' placeholder={intl.formatMessage(messages.placeholder)} diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index e0966f52c..4b4cdff74 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -7,6 +7,8 @@ import StatusContainer from '../../../containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Hashtag from '../../../components/hashtag'; import Icon from 'mastodon/components/icon'; +import { searchEnabled } from '../../../initial_state'; +import LoadMore from 'mastodon/components/load_more'; const messages = defineMessages({ dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, @@ -19,16 +21,26 @@ class SearchResults extends ImmutablePureComponent { results: ImmutablePropTypes.map.isRequired, suggestions: ImmutablePropTypes.list.isRequired, fetchSuggestions: PropTypes.func.isRequired, + expandSearch: PropTypes.func.isRequired, dismissSuggestion: PropTypes.func.isRequired, + searchTerm: PropTypes.string, intl: PropTypes.object.isRequired, }; componentDidMount () { - this.props.fetchSuggestions(); + if (this.props.searchTerm === '') { + this.props.fetchSuggestions(); + } } + handleLoadMoreAccounts = () => this.props.expandSearch('accounts'); + + handleLoadMoreStatuses = () => this.props.expandSearch('statuses'); + + handleLoadMoreHashtags = () => this.props.expandSearch('hashtags'); + render () { - const { intl, results, suggestions, dismissSuggestion } = this.props; + const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; if (results.isEmpty() && !suggestions.isEmpty()) { return ( @@ -63,6 +75,8 @@ class SearchResults extends ImmutablePureComponent { <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} + + {results.get('accounts').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreAccounts} />} </div> ); } @@ -74,6 +88,18 @@ class SearchResults extends ImmutablePureComponent { <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} + + {results.get('statuses').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreStatuses} />} + </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> </div> ); } @@ -85,6 +111,8 @@ class SearchResults extends ImmutablePureComponent { <h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> {results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} + + {results.get('hashtags').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreHashtags} />} </div> ); } diff --git a/app/javascript/mastodon/features/compose/components/text_icon_button.js b/app/javascript/mastodon/features/compose/components/text_icon_button.js index 9c8ffab1f..f0b133538 100644 --- a/app/javascript/mastodon/features/compose/components/text_icon_button.js +++ b/app/javascript/mastodon/features/compose/components/text_icon_button.js @@ -1,6 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +const iconStyle = { + height: null, + lineHeight: '27px', + width: `${18 * 1.28571429}px`, +}; + export default class TextIconButton extends React.PureComponent { static propTypes = { @@ -20,7 +26,14 @@ export default class TextIconButton extends React.PureComponent { const { label, title, active, ariaControls } = this.props; return ( - <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}> + <button + title={title} + aria-label={title} + className={`text-icon-button ${active ? 'active' : ''}`} + aria-expanded={active} + onClick={this.handleClick} + aria-controls={ariaControls} style={iconStyle} + > {label} </button> ); diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js index 629cbc36a..b9f0fbe3a 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.js @@ -4,16 +4,11 @@ import PropTypes from 'prop-types'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; -const messages = defineMessages({ - description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, -}); - -export default @injectIntl -class Upload extends ImmutablePureComponent { +export default class Upload extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -21,30 +16,10 @@ class Upload extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, onUndo: PropTypes.func.isRequired, - onDescriptionChange: PropTypes.func.isRequired, onOpenFocalPoint: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - }; - - state = { - hovered: false, - focused: false, - dirtyDescription: null, }; - handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } - } - - handleSubmit = () => { - this.handleInputBlur(); - this.props.onSubmit(this.context.router.history); - } - handleUndoClick = e => { e.stopPropagation(); this.props.onUndo(this.props.media.get('id')); @@ -55,69 +30,21 @@ class Upload extends ImmutablePureComponent { this.props.onOpenFocalPoint(this.props.media.get('id')); } - handleInputChange = e => { - this.setState({ dirtyDescription: e.target.value }); - } - - handleMouseEnter = () => { - this.setState({ hovered: true }); - } - - handleMouseLeave = () => { - this.setState({ hovered: false }); - } - - handleInputFocus = () => { - this.setState({ focused: true }); - } - - handleClick = () => { - this.setState({ focused: true }); - } - - handleInputBlur = () => { - const { dirtyDescription } = this.state; - - this.setState({ focused: false, dirtyDescription: null }); - - if (dirtyDescription !== null) { - this.props.onDescriptionChange(this.props.media.get('id'), dirtyDescription); - } - } - render () { - const { intl, media } = this.props; - const active = this.state.hovered || this.state.focused; - const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''; + const { media } = this.props; const focusX = media.getIn(['meta', 'focus', 'x']); const focusY = media.getIn(['meta', 'focus', 'y']); const x = ((focusX / 2) + .5) * 100; const y = ((focusY / -2) + .5) * 100; return ( - <div className='compose-form__upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'> + <div className='compose-form__upload' tabIndex='0' role='button'> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> {({ scale }) => ( <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> - <div className={classNames('compose-form__upload__actions', { active })}> + <div className={classNames('compose-form__upload__actions', { active: true })}> <button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> - {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>} - </div> - - <div className={classNames('compose-form__upload-description', { active })}> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span> - - <textarea - placeholder={intl.formatMessage(messages.description)} - value={description} - maxLength={420} - onFocus={this.handleInputFocus} - onChange={this.handleInputChange} - onBlur={this.handleInputBlur} - onKeyDown={this.handleKeyDown} - /> - </label> + <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button> </div> </div> )} diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index 90e2769f3..d550019f4 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -7,9 +7,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; const messages = defineMessages({ - upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' }, + upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' }, }); +const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC'; + const makeMapStateToProps = () => { const mapStateToProps = state => ({ acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), @@ -60,9 +62,9 @@ class UploadButton extends ImmutablePureComponent { return ( <div className='compose-form__upload-button'> - <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> + <IconButton icon='paperclip' title={intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span> + <span style={{ display: 'none' }}>{intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })}</span> <input key={resetFileKey} ref={this.setRef} diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js index 9ff2aa0fa..c6eac554e 100644 --- a/app/javascript/mastodon/features/compose/components/upload_form.js +++ b/app/javascript/mastodon/features/compose/components/upload_form.js @@ -4,6 +4,7 @@ import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; +import { FormattedMessage } from 'react-intl'; export default class UploadForm extends ImmutablePureComponent { @@ -16,7 +17,7 @@ export default class UploadForm extends ImmutablePureComponent { return ( <div className='compose-form__upload-wrapper'> - <UploadProgressContainer /> + <UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} /> <div className='compose-form__uploads-wrapper'> {mediaIds.map(id => ( diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js index cbe58f573..b0bfe0c9a 100644 --- a/app/javascript/mastodon/features/compose/components/upload_progress.js +++ b/app/javascript/mastodon/features/compose/components/upload_progress.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; -import { FormattedMessage } from 'react-intl'; import Icon from 'mastodon/components/icon'; export default class UploadProgress extends React.PureComponent { @@ -10,10 +9,12 @@ export default class UploadProgress extends React.PureComponent { static propTypes = { active: PropTypes.bool, progress: PropTypes.number, + icon: PropTypes.string.isRequired, + message: PropTypes.node.isRequired, }; render () { - const { active, progress } = this.props; + const { active, progress, icon, message } = this.props; if (!active) { return null; @@ -22,11 +23,11 @@ export default class UploadProgress extends React.PureComponent { return ( <div className='upload-progress'> <div className='upload-progress__icon'> - <Icon id='upload' /> + <Icon id={icon} /> </div> <div className='upload-progress__message'> - <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> + {message} <div className='upload-progress__backdrop'> <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> diff --git a/app/javascript/mastodon/features/compose/containers/navigation_container.js b/app/javascript/mastodon/features/compose/containers/navigation_container.js index eb9f3ea45..8606a642e 100644 --- a/app/javascript/mastodon/features/compose/containers/navigation_container.js +++ b/app/javascript/mastodon/features/compose/containers/navigation_container.js @@ -1,11 +1,29 @@ import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; import NavigationBar from '../components/navigation_bar'; +import { logOut } from 'mastodon/utils/log_out'; +import { openModal } from 'mastodon/actions/modal'; import { me } from '../../../initial_state'; +const messages = defineMessages({ + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, +}); + const mapStateToProps = state => { return { account: state.getIn(['accounts', me]), }; }; -export default connect(mapStateToProps)(NavigationBar); +const mapDispatchToProps = (dispatch, { intl }) => ({ + onLogout () { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + onConfirm: () => logOut(), + })); + }, +}); + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar)); diff --git a/app/javascript/mastodon/features/compose/containers/search_results_container.js b/app/javascript/mastodon/features/compose/containers/search_results_container.js index f9637861a..1f714ff83 100644 --- a/app/javascript/mastodon/features/compose/containers/search_results_container.js +++ b/app/javascript/mastodon/features/compose/containers/search_results_container.js @@ -1,14 +1,17 @@ import { connect } from 'react-redux'; import SearchResults from '../components/search_results'; -import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions'; +import { fetchSuggestions, dismissSuggestion } from 'mastodon/actions/suggestions'; +import { expandSearch } from 'mastodon/actions/search'; const mapStateToProps = state => ({ results: state.getIn(['search', 'results']), suggestions: state.getIn(['suggestions', 'items']), + searchTerm: state.getIn(['search', 'searchTerm']), }); const mapDispatchToProps = dispatch => ({ fetchSuggestions: () => dispatch(fetchSuggestions()), + expandSearch: type => dispatch(expandSearch(type)), dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))), }); diff --git a/app/javascript/mastodon/features/compose/containers/upload_button_container.js b/app/javascript/mastodon/features/compose/containers/upload_button_container.js index d8b8c4b6e..221b98e31 100644 --- a/app/javascript/mastodon/features/compose/containers/upload_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/upload_button_container.js @@ -3,7 +3,7 @@ import UploadButton from '../components/upload_button'; import { uploadCompose } from '../../../actions/compose'; const mapStateToProps = state => ({ - disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), + disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))), unavailable: state.getIn(['compose', 'poll']) !== null, resetFileKey: state.getIn(['compose', 'resetFileKey']), }); diff --git a/app/javascript/mastodon/features/compose/containers/upload_container.js b/app/javascript/mastodon/features/compose/containers/upload_container.js index b6d81f03a..342b0c2a9 100644 --- a/app/javascript/mastodon/features/compose/containers/upload_container.js +++ b/app/javascript/mastodon/features/compose/containers/upload_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import Upload from '../components/upload'; -import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose'; +import { undoUploadCompose } from '../../../actions/compose'; import { openModal } from '../../../actions/modal'; import { submitCompose } from '../../../actions/compose'; @@ -14,10 +14,6 @@ const mapDispatchToProps = dispatch => ({ dispatch(undoUploadCompose(id)); }, - onDescriptionChange: (id, description) => { - dispatch(changeUploadCompose(id, { description })); - }, - onOpenFocalPoint: id => { dispatch(openModal('FOCAL_POINT', { id })); }, diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 0731abcf4..e2de8b0e6 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -12,9 +12,11 @@ import Motion from '../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import SearchResultsContainer from './containers/search_results_container'; import { changeComposing } from '../../actions/compose'; +import { openModal } from 'mastodon/actions/modal'; import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; import { mascot } from '../../initial_state'; import Icon from 'mastodon/components/icon'; +import { logOut } from 'mastodon/utils/log_out'; const messages = defineMessages({ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -25,6 +27,8 @@ const messages = defineMessages({ preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' }, + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, }); const mapStateToProps = (state, ownProps) => ({ @@ -61,6 +65,21 @@ class Compose extends React.PureComponent { } } + handleLogoutClick = e => { + const { dispatch, intl } = this.props; + + e.preventDefault(); + e.stopPropagation(); + + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + onConfirm: () => logOut(), + })); + + return false; + } + onFocus = () => { this.props.dispatch(changeComposing(true)); } @@ -92,7 +111,7 @@ class Compose extends React.PureComponent { <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link> )} <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a> - <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a> + <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a> </nav> ); } diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js index ffcd6d281..235cb7ad8 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -2,9 +2,30 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import StatusContainer from '../../../containers/status_container'; +import StatusContent from 'mastodon/components/status_content'; +import AttachmentList from 'mastodon/components/attachment_list'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; +import AvatarComposite from 'mastodon/components/avatar_composite'; +import Permalink from 'mastodon/components/permalink'; +import IconButton from 'mastodon/components/icon_button'; +import RelativeTimestamp from 'mastodon/components/relative_timestamp'; +import { HotKeys } from 'react-hotkeys'; +import { autoPlayGif } from 'mastodon/initial_state'; +import classNames from 'classnames'; -export default class Conversation extends ImmutablePureComponent { +const messages = defineMessages({ + more: { id: 'status.more', defaultMessage: 'More' }, + open: { id: 'conversation.open', defaultMessage: 'View conversation' }, + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' }, + delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' }, + muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, + unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, +}); + +export default @injectIntl +class Conversation extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, @@ -13,25 +34,76 @@ export default class Conversation extends ImmutablePureComponent { static propTypes = { conversationId: PropTypes.string.isRequired, accounts: ImmutablePropTypes.list.isRequired, - lastStatusId: PropTypes.string, + lastStatus: ImmutablePropTypes.map, unread:PropTypes.bool.isRequired, onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, markRead: PropTypes.func.isRequired, + delete: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, }; + _updateEmojis () { + const node = this.namesNode; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + handleClick = () => { if (!this.context.router) { return; } - const { lastStatusId, unread, markRead } = this.props; + const { lastStatus, unread, markRead } = this.props; if (unread) { markRead(); } - this.context.router.history.push(`/statuses/${lastStatusId}`); + this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); + } + + handleMarkAsRead = () => { + this.props.markRead(); + } + + handleReply = () => { + this.props.reply(this.props.lastStatus, this.context.router.history); + } + + handleDelete = () => { + this.props.delete(); } handleHotkeyMoveUp = () => { @@ -42,22 +114,92 @@ export default class Conversation extends ImmutablePureComponent { this.props.onMoveDown(this.props.conversationId); } + handleConversationMute = () => { + this.props.onMute(this.props.lastStatus); + } + + handleShowMore = () => { + this.props.onToggleHidden(this.props.lastStatus); + } + + setNamesRef = (c) => { + this.namesNode = c; + } + render () { - const { accounts, lastStatusId, unread } = this.props; + const { accounts, lastStatus, unread, intl } = this.props; - if (lastStatusId === null) { + if (lastStatus === null) { return null; } + const menu = [ + { text: intl.formatMessage(messages.open), action: this.handleClick }, + null, + ]; + + menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute }); + + if (unread) { + menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead }); + menu.push(null); + } + + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); + + const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]); + + const handlers = { + reply: this.handleReply, + open: this.handleClick, + moveUp: this.handleHotkeyMoveUp, + moveDown: this.handleHotkeyMoveDown, + toggleHidden: this.handleShowMore, + }; + return ( - <StatusContainer - id={lastStatusId} - unread={unread} - otherAccounts={accounts} - onMoveUp={this.handleHotkeyMoveUp} - onMoveDown={this.handleHotkeyMoveDown} - onClick={this.handleClick} - /> + <HotKeys handlers={handlers}> + <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'> + <div className='conversation__avatar'> + <AvatarComposite accounts={accounts} size={48} /> + </div> + + <div className='conversation__content'> + <div className='conversation__content__info'> + <div className='conversation__content__relative-time'> + {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> + </div> + + <div className='conversation__content__names' ref={this.setNamesRef}> + <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} /> + </div> + </div> + + <StatusContent + status={lastStatus} + onClick={this.handleClick} + expanded={!lastStatus.get('hidden')} + onExpandedToggle={this.handleShowMore} + collapsable + /> + + {lastStatus.get('media_attachments').size > 0 && ( + <AttachmentList + compact + media={lastStatus.get('media_attachments')} + /> + )} + + <div className='status__action-bar'> + <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} /> + + <div className='status__action-bar-dropdown'> + <DropdownMenuContainer status={lastStatus} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} /> + </div> + </div> + </div> + </div> + </HotKeys> ); } diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js index bd6f6bfb0..94cef81a7 100644 --- a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js +++ b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js @@ -1,19 +1,74 @@ import { connect } from 'react-redux'; import Conversation from '../components/conversation'; -import { markConversationRead } from '../../../actions/conversations'; +import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations'; +import { makeGetStatus } from 'mastodon/selectors'; +import { replyCompose } from 'mastodon/actions/compose'; +import { openModal } from 'mastodon/actions/modal'; +import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'mastodon/actions/statuses'; +import { defineMessages, injectIntl } from 'react-intl'; -const mapStateToProps = (state, { conversationId }) => { - const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); +const messages = defineMessages({ + 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?' }, +}); + +const mapStateToProps = () => { + const getStatus = makeGetStatus(); + + return (state, { conversationId }) => { + const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); + const lastStatusId = conversation.get('last_status', null); - return { - accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), - unread: conversation.get('unread'), - lastStatusId: conversation.get('last_status', null), + return { + accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), + unread: conversation.get('unread'), + lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }), + }; }; }; -const mapDispatchToProps = (dispatch, { conversationId }) => ({ - markRead: () => dispatch(markConversationRead(conversationId)), +const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({ + + markRead () { + dispatch(markConversationRead(conversationId)); + }, + + reply (status, router) { + dispatch((_, getState) => { + let state = getState(); + + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: () => dispatch(replyCompose(status, router)), + })); + } else { + dispatch(replyCompose(status, router)); + } + }); + }, + + delete () { + dispatch(deleteConversation(conversationId)); + }, + + onMute (status) { + if (status.get('muted')) { + dispatch(unmuteStatus(status.get('id'))); + } else { + dispatch(muteStatus(status.get('id'))); + } + }, + + onToggleHidden (status) { + if (status.get('hidden')) { + dispatch(revealStatus(status.get('id'))); + } else { + dispatch(hideStatus(status.get('id'))); + } + }, + }); -export default connect(mapStateToProps, mapDispatchToProps)(Conversation); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation)); diff --git a/app/javascript/mastodon/features/direct_timeline/index.js b/app/javascript/mastodon/features/direct_timeline/index.js index d202f3bfd..5ce795760 100644 --- a/app/javascript/mastodon/features/direct_timeline/index.js +++ b/app/javascript/mastodon/features/direct_timeline/index.js @@ -75,7 +75,7 @@ class DirectTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='envelope' active={hasUnread} diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js new file mode 100644 index 000000000..50ad74450 --- /dev/null +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -0,0 +1,190 @@ +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { makeGetAccount } from 'mastodon/selectors'; +import Avatar from 'mastodon/components/avatar'; +import DisplayName from 'mastodon/components/display_name'; +import Permalink from 'mastodon/components/permalink'; +import RelativeTimestamp from 'mastodon/components/relative_timestamp'; +import IconButton from 'mastodon/components/icon_button'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state'; +import { shortNumberFormat } from 'mastodon/utils/numbers'; +import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts'; +import { openModal } from 'mastodon/actions/modal'; +import { initMuteModal } from 'mastodon/actions/mutes'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { id }) => ({ + account: getAccount(state, id), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl }) => ({ + + onFollow (account) { + if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + if (unfollowModal) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + })); + } else { + dispatch(unfollowAccount(account.get('id'))); + } + } else { + dispatch(followAccount(account.get('id'))); + } + }, + + onBlock (account) { + if (account.getIn(['relationship', 'blocking'])) { + dispatch(unblockAccount(account.get('id'))); + } else { + dispatch(blockAccount(account.get('id'))); + } + }, + + onMute (account) { + if (account.getIn(['relationship', 'muting'])) { + dispatch(unmuteAccount(account.get('id'))); + } else { + dispatch(initMuteModal(account)); + } + }, + +}); + +export default @injectIntl +@connect(makeMapStateToProps, mapDispatchToProps) +class AccountCard extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, + }; + + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + + handleFollow = () => { + this.props.onFollow(this.props.account); + } + + handleBlock = () => { + this.props.onBlock(this.props.account); + } + + handleMute = () => { + this.props.onMute(this.props.account); + } + + setRef = (c) => { + this.node = c; + } + + render () { + const { account, intl } = this.props; + + let buttons; + + if (account.get('id') !== me && account.get('relationship', null) !== null) { + const following = account.getIn(['relationship', 'following']); + const requested = account.getIn(['relationship', 'requested']); + const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); + + if (requested) { + buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; + } else if (blocking) { + buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; + } else if (muting) { + buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />; + } else if (!account.get('moved') || following) { + buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; + } + } + + return ( + <div className='directory__card'> + <div className='directory__card__img'> + <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' /> + </div> + + <div className='directory__card__bar'> + <Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> + <Avatar account={account} size={48} /> + <DisplayName account={account} /> + </Permalink> + + <div className='directory__card__bar__relationship account__relationship'> + {buttons} + </div> + </div> + + <div className='directory__card__extra' ref={this.setRef}> + <div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} /> + </div> + + <div className='directory__card__extra'> + <div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div> + <div className='accounts-table__count'>{shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div> + <div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js new file mode 100644 index 000000000..2f91e759b --- /dev/null +++ b/app/javascript/mastodon/features/directory/index.js @@ -0,0 +1,171 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from 'mastodon/components/column'; +import ColumnHeader from 'mastodon/components/column_header'; +import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodon/actions/columns'; +import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory'; +import { List as ImmutableList } from 'immutable'; +import AccountCard from './components/account_card'; +import RadioButton from 'mastodon/components/radio_button'; +import classNames from 'classnames'; +import LoadMore from 'mastodon/components/load_more'; +import { ScrollContainer } from 'react-router-scroll-4'; + +const messages = defineMessages({ + title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, + recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' }, + newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' }, + local: { id: 'directory.local', defaultMessage: 'From {domain} only' }, + federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' }, +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()), + isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true), + domain: state.getIn(['meta', 'domain']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Directory extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + isLoading: PropTypes.bool, + accountIds: ImmutablePropTypes.list.isRequired, + dispatch: PropTypes.func.isRequired, + shouldUpdateScroll: PropTypes.func, + columnId: PropTypes.string, + intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + domain: PropTypes.string.isRequired, + params: PropTypes.shape({ + order: PropTypes.string, + local: PropTypes.bool, + }), + }; + + state = { + order: null, + local: null, + }; + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state))); + } + } + + getParams = (props, state) => ({ + order: state.order === null ? (props.params.order || 'active') : state.order, + local: state.local === null ? (props.params.local || false) : state.local, + }); + + handleMove = dir => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchDirectory(this.getParams(this.props, this.state))); + } + + componentDidUpdate (prevProps, prevState) { + const { dispatch } = this.props; + const paramsOld = this.getParams(prevProps, prevState); + const paramsNew = this.getParams(this.props, this.state); + + if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) { + dispatch(fetchDirectory(paramsNew)); + } + } + + setRef = c => { + this.column = c; + } + + handleChangeOrder = e => { + const { dispatch, columnId } = this.props; + + if (columnId) { + dispatch(changeColumnParams(columnId, ['order'], e.target.value)); + } else { + this.setState({ order: e.target.value }); + } + } + + handleChangeLocal = e => { + const { dispatch, columnId } = this.props; + + if (columnId) { + dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1')); + } else { + this.setState({ local: e.target.value === '1' }); + } + } + + handleLoadMore = () => { + const { dispatch } = this.props; + dispatch(expandDirectory(this.getParams(this.props, this.state))); + } + + render () { + const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props; + const { order, local } = this.getParams(this.props, this.state); + const pinned = !!columnId; + + const scrollableArea = ( + <div className='scrollable' style={{ background: 'transparent' }}> + <div className='filter-form'> + <div className='filter-form__column' role='group'> + <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} /> + <RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} /> + </div> + + <div className='filter-form__column' role='group'> + <RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} /> + <RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} /> + </div> + </div> + + <div className={classNames('directory__list', { loading: isLoading })}> + {accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)} + </div> + + <LoadMore onClick={this.handleLoadMore} visible={!isLoading} /> + </div> + ); + + return ( + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> + <ColumnHeader + icon='address-book-o' + title={intl.formatMessage(messages.title)} + onPin={this.handlePin} + onMove={this.handleMove} + onClick={this.handleHeaderClick} + pinned={pinned} + multiColumn={multiColumn} + /> + + {multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea} + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js index 7c075f5a5..482245c86 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.js +++ b/app/javascript/mastodon/features/domain_blocks/index.js @@ -33,6 +33,7 @@ class Blocks extends ImmutablePureComponent { hasMore: PropTypes.bool, domains: ImmutablePropTypes.orderedSet, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -44,7 +45,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, domains, shouldUpdateScroll, hasMore } = this.props; + const { intl, domains, shouldUpdateScroll, hasMore, multiColumn } = this.props; if (!domains) { return ( @@ -57,7 +58,7 @@ class Blocks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />; return ( - <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='domain_blocks' @@ -65,6 +66,7 @@ class Blocks extends ImmutablePureComponent { hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {domains.map(domain => <DomainContainer key={domain} domain={domain} /> diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 988cea253..cd10e20b7 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -29,7 +29,7 @@ const emojify = (str, customEmojis = {}) => { // if you want additional emoji handler, add statements below which set replacement and return true. if (shortname in customEmojis) { const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; - replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`; + replacement = `<img draggable="false" class="emojione custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`; return true; } return false; @@ -92,8 +92,11 @@ export const buildCustomEmojis = (customEmojis) => { keywords: [name], imageUrl: url, custom: true, + customCategory: emoji.get('category'), }); }); return emojis; }; + +export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom'])); diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js index 164fdcc0b..e4519a13e 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js +++ b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js @@ -74,7 +74,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo return [emojisList['-1']]; } - let values = value.toLowerCase().split(/[\s|,|\-|_]+/), + let values = value.toLowerCase().split(/[\s|,\-_]+/), allResults = []; if (values.length > 2) { diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index fa9401b90..db8a3f815 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -74,7 +74,7 @@ class Favourites extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}> <ColumnHeader icon='star' title={intl.formatMessage(messages.heading)} @@ -95,6 +95,7 @@ class Favourites extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js index d1ac229a2..249e6a044 100644 --- a/app/javascript/mastodon/features/favourites/index.js +++ b/app/javascript/mastodon/features/favourites/index.js @@ -5,17 +5,23 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from '../../components/loading_indicator'; import { fetchFavourites } from '../../actions/interactions'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import AccountContainer from '../../containers/account_container'; import Column from '../ui/components/column'; -import ColumnBackButton from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; +import Icon from 'mastodon/components/icon'; +import ColumnHeader from '../../components/column_header'; + +const messages = defineMessages({ + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, +}); const mapStateToProps = (state, props) => ({ accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]), }); export default @connect(mapStateToProps) +@injectIntl class Favourites extends ImmutablePureComponent { static propTypes = { @@ -23,10 +29,14 @@ class Favourites extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, + intl: PropTypes.object.isRequired, }; componentWillMount () { - this.props.dispatch(fetchFavourites(this.props.params.statusId)); + if (!this.props.accountIds) { + this.props.dispatch(fetchFavourites(this.props.params.statusId)); + } } componentWillReceiveProps (nextProps) { @@ -35,8 +45,12 @@ class Favourites extends ImmutablePureComponent { } } + handleRefresh = () => { + this.props.dispatch(fetchFavourites(this.props.params.statusId)); + } + render () { - const { shouldUpdateScroll, accountIds } = this.props; + const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -49,13 +63,20 @@ class Favourites extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favourited this toot yet. When someone does, they will show up here.' />; return ( - <Column> - <ColumnBackButton /> + <Column bindToDocument={!multiColumn}> + <ColumnHeader + showBackButton + multiColumn={multiColumn} + extraButton={( + <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button> + )} + /> <ScrollableList scrollKey='favourites' shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} /> diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 44624cb40..57ef44145 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -32,6 +32,7 @@ class FollowRequests extends ImmutablePureComponent { hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,7 +44,7 @@ class FollowRequests extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props; + const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -56,7 +57,7 @@ class FollowRequests extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />; return ( - <Column icon='user-plus' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='follow_requests' @@ -64,6 +65,7 @@ class FollowRequests extends ImmutablePureComponent { hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountAuthorizeContainer key={id} id={id} /> diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index e3387e1be..9e635d250 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -36,11 +36,14 @@ class Followers extends ImmutablePureComponent { hasMore: PropTypes.bool, blockedBy: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowers(this.props.params.accountId)); + if (!this.props.accountIds) { + this.props.dispatch(fetchAccount(this.props.params.accountId)); + this.props.dispatch(fetchFollowers(this.props.params.accountId)); + } } componentWillReceiveProps (nextProps) { @@ -55,7 +58,7 @@ class Followers extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount } = this.props; + const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -77,7 +80,7 @@ class Followers extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <ScrollableList scrollKey='followers' @@ -87,6 +90,7 @@ class Followers extends ImmutablePureComponent { prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} alwaysPrepend emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {blockedBy ? [] : accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} /> diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index 3bf89fb2b..284ae2c11 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -36,11 +36,14 @@ class Following extends ImmutablePureComponent { hasMore: PropTypes.bool, blockedBy: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowing(this.props.params.accountId)); + if (!this.props.accountIds) { + this.props.dispatch(fetchAccount(this.props.params.accountId)); + this.props.dispatch(fetchFollowing(this.props.params.accountId)); + } } componentWillReceiveProps (nextProps) { @@ -55,7 +58,7 @@ class Following extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount } = this.props; + const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -77,7 +80,7 @@ class Following extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <ScrollableList scrollKey='following' @@ -87,6 +90,7 @@ class Following extends ImmutablePureComponent { prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} alwaysPrepend emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {blockedBy ? [] : accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} /> diff --git a/app/javascript/mastodon/features/generic_not_found/index.js b/app/javascript/mastodon/features/generic_not_found/index.js index 0290be47f..41cd61a5f 100644 --- a/app/javascript/mastodon/features/generic_not_found/index.js +++ b/app/javascript/mastodon/features/generic_not_found/index.js @@ -4,7 +4,7 @@ import MissingIndicator from '../../components/missing_indicator'; const GenericNotFound = () => ( <Column> - <MissingIndicator /> + <MissingIndicator fullPage /> </Column> ); diff --git a/app/javascript/mastodon/features/getting_started/components/trends.js b/app/javascript/mastodon/features/getting_started/components/trends.js new file mode 100644 index 000000000..3b9a3075f --- /dev/null +++ b/app/javascript/mastodon/features/getting_started/components/trends.js @@ -0,0 +1,46 @@ +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Hashtag from 'mastodon/components/hashtag'; +import { FormattedMessage } from 'react-intl'; + +export default class Trends extends ImmutablePureComponent { + + static defaultProps = { + loading: false, + }; + + static propTypes = { + trends: ImmutablePropTypes.list, + fetchTrends: PropTypes.func.isRequired, + }; + + componentDidMount () { + this.props.fetchTrends(); + this.refreshInterval = setInterval(() => this.props.fetchTrends(), 900 * 1000); + } + + componentWillUnmount () { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + } + } + + render () { + const { trends } = this.props; + + if (!trends || trends.isEmpty()) { + return null; + } + + return ( + <div className='getting-started__trends'> + <h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4> + + {trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/getting_started/containers/trends_container.js b/app/javascript/mastodon/features/getting_started/containers/trends_container.js new file mode 100644 index 000000000..1df3fb4fe --- /dev/null +++ b/app/javascript/mastodon/features/getting_started/containers/trends_container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { fetchTrends } from '../../../actions/trends'; +import Trends from '../components/trends'; + +const mapStateToProps = state => ({ + trends: state.getIn(['trends', 'items']), +}); + +const mapDispatchToProps = dispatch => ({ + fetchTrends: () => dispatch(fetchTrends()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Trends); diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index fc7840ec1..adbc147d1 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -7,12 +7,13 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me, profile_directory } from '../../initial_state'; +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 Icon from 'mastodon/components/icon'; import LinkFooter from 'mastodon/features/ui/components/link_footer'; +import TrendsContainer from './containers/trends_container'; const messages = defineMessages({ home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, @@ -21,6 +22,7 @@ const messages = defineMessages({ settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, + bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, @@ -76,16 +78,14 @@ class GettingStarted extends ImmutablePureComponent { }; componentDidMount () { - const { myAccount, fetchFollowRequests, multiColumn } = this.props; + const { fetchFollowRequests, multiColumn } = this.props; if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) { this.context.router.history.replace('/timelines/home'); return; } - if (myAccount.get('locked')) { - fetchFollowRequests(); - } + fetchFollowRequests(); } render () { @@ -106,7 +106,7 @@ class GettingStarted extends ImmutablePureComponent { if (profile_directory) { navItems.push( - <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} href='/explore' /> + <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' /> ); height += 48; @@ -119,7 +119,7 @@ class GettingStarted extends ImmutablePureComponent { height += 34; } else if (profile_directory) { navItems.push( - <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} href='/explore' /> + <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' /> ); height += 48; @@ -127,13 +127,14 @@ class GettingStarted extends ImmutablePureComponent { 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' /> ); - height += 48*3; + height += 48*4; - if (myAccount.get('locked')) { + 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' />); height += 48; } @@ -148,7 +149,7 @@ class GettingStarted extends ImmutablePureComponent { } return ( - <Column label={intl.formatMessage(messages.menu)}> + <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.menu)}> {multiColumn && <div className='column-header__wrapper'> <h1 className='column-header'> <button> @@ -168,6 +169,8 @@ class GettingStarted extends ImmutablePureComponent { <LinkFooter withHotkeys={multiColumn} /> </div> + + {multiColumn && showTrends && <TrendsContainer />} </Column> ); } diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js index cdc138c8b..9c39b158a 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js +++ b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Toggle from 'react-toggle'; -import AsyncSelect from 'react-select/lib/Async'; +import AsyncSelect from 'react-select/async'; const messages = defineMessages({ placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' }, diff --git a/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js index c5098052c..5914bbeaf 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js @@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { columnId }) => ({ }, onLoad (value) { - return api().get('/api/v2/search', { params: { q: value } }).then(response => { + return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => { return (response.data.hashtags || []).map((tag) => { return { value: tag.name, label: `#${tag.name}` }; }); diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index 0d3c97a64..28200e6c2 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -135,7 +135,7 @@ class HashtagTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={`#${id}`}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}> <ColumnHeader icon='hashtag' active={hasUnread} @@ -157,6 +157,7 @@ class HashtagTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index 097f91c16..1cafb88ed 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -98,7 +98,7 @@ class HomeTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='home' active={hasUnread} @@ -119,6 +119,7 @@ class HomeTimeline extends React.PureComponent { timelineId='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js index 01b45652c..666baf621 100644 --- a/app/javascript/mastodon/features/keyboard_shortcuts/index.js +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js @@ -18,10 +18,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { }; render () { - const { intl } = this.props; + const { intl, multiColumn } = this.props; return ( - <Column icon='question' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='question' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <div className='keyboard-shortcuts scrollable optionally-scrollable'> <table> @@ -57,6 +57,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { <td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td> </tr> <tr> + <td><kbd>e</kbd></td> + <td><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></td> + </tr> + <tr> <td><kbd>x</kbd></td> <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td> </tr> diff --git a/app/javascript/mastodon/features/list_editor/components/edit_list_form.js b/app/javascript/mastodon/features/list_editor/components/edit_list_form.js index 3dc59c12e..3ccab12a8 100644 --- a/app/javascript/mastodon/features/list_editor/components/edit_list_form.js +++ b/app/javascript/mastodon/features/list_editor/components/edit_list_form.js @@ -11,7 +11,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ value: state.getIn(['listEditor', 'title']), - disabled: !state.getIn(['listEditor', 'isChanged']), + disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index 0db6d2228..f3205b2bf 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -148,14 +148,14 @@ class ListTimeline extends React.PureComponent { } else if (list === false) { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <MissingIndicator /> </Column> ); } return ( - <Column ref={this.setRef} label={title}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={title}> <ColumnHeader icon='list-ul' active={hasUnread} @@ -184,6 +184,7 @@ class ListTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/mastodon/features/lists/components/new_list_form.js b/app/javascript/mastodon/features/lists/components/new_list_form.js index 739246640..7faf50be8 100644 --- a/app/javascript/mastodon/features/lists/components/new_list_form.js +++ b/app/javascript/mastodon/features/lists/components/new_list_form.js @@ -66,7 +66,7 @@ class NewListForm extends React.PureComponent { </label> <IconButton - disabled={disabled} + disabled={disabled || !value} icon='plus' title={title} onClick={this.handleClick} diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js index 015e21b68..7f7f5009c 100644 --- a/app/javascript/mastodon/features/lists/index.js +++ b/app/javascript/mastodon/features/lists/index.js @@ -40,6 +40,7 @@ class Lists extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, lists: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -47,7 +48,7 @@ class Lists extends ImmutablePureComponent { } render () { - const { intl, shouldUpdateScroll, lists } = this.props; + const { intl, shouldUpdateScroll, lists, multiColumn } = this.props; if (!lists) { return ( @@ -60,7 +61,7 @@ class Lists extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />; return ( - <Column icon='list-ul' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='list-ul' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <NewListForm /> @@ -70,6 +71,7 @@ class Lists extends ImmutablePureComponent { shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />} + bindToDocument={!multiColumn} > {lists.map(list => <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index 4ed29a1ce..91dd268c1 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -32,6 +32,7 @@ class Mutes extends ImmutablePureComponent { hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,7 +44,7 @@ class Mutes extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props; + const { intl, shouldUpdateScroll, hasMore, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -56,7 +57,7 @@ class Mutes extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />; return ( - <Column icon='volume-off' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='volume-off' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='mutes' @@ -64,6 +65,7 @@ class Mutes extends ImmutablePureComponent { hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} /> diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index 60a86312a..8bd03fbda 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -57,6 +57,17 @@ export default class ColumnSettings extends React.PureComponent { </div> </div> + <div role='group' aria-labelledby='notifications-follow-request'> + <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} /> + {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} /> + </div> + </div> + <div role='group' aria-labelledby='notifications-favourite'> <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> diff --git a/app/javascript/mastodon/features/notifications/components/filter_bar.js b/app/javascript/mastodon/features/notifications/components/filter_bar.js index 3f3e6ab7d..2fd28d832 100644 --- a/app/javascript/mastodon/features/notifications/components/filter_bar.js +++ b/app/javascript/mastodon/features/notifications/components/filter_bar.js @@ -64,7 +64,7 @@ class FilterBar extends React.PureComponent { onClick={this.onClick('mention')} title={intl.formatMessage(tooltips.mentions)} > - <Icon id='at' fixedWidth /> + <Icon id='reply-all' fixedWidth /> </button> <button className={selectedFilter === 'favourite' ? 'active' : ''} diff --git a/app/javascript/mastodon/features/notifications/components/follow_request.js b/app/javascript/mastodon/features/notifications/components/follow_request.js new file mode 100644 index 000000000..a80cfb2fa --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/follow_request.js @@ -0,0 +1,59 @@ +import React, { Fragment } from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Avatar from 'mastodon/components/avatar'; +import DisplayName from 'mastodon/components/display_name'; +import Permalink from 'mastodon/components/permalink'; +import IconButton from 'mastodon/components/icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, + reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, +}); + +export default @injectIntl +class FollowRequest extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onAuthorize: PropTypes.func.isRequired, + onReject: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + render () { + const { intl, hidden, account, onAuthorize, onReject } = this.props; + + if (!account) { + return <div />; + } + + if (hidden) { + return ( + <Fragment> + {account.get('display_name')} + {account.get('username')} + </Fragment> + ); + } + + 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> + <DisplayName account={account} /> + </Permalink> + + <div className='account__relationship'> + <IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /> + <IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index 41e9324e6..74065e5e2 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -1,13 +1,23 @@ import React from 'react'; -import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import StatusContainer from '../../../containers/status_container'; -import AccountContainer from '../../../containers/account_container'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import Permalink from '../../../components/permalink'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { HotKeys } from 'react-hotkeys'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { me } from 'mastodon/initial_state'; +import StatusContainer from 'mastodon/containers/status_container'; +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'; + +const messages = defineMessages({ + favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, + follow: { id: 'notification.follow', defaultMessage: '{name} followed you' }, + 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' }, +}); const notificationForScreenReader = (intl, message, timestamp) => { const output = [message]; @@ -107,7 +117,7 @@ class Notification extends ImmutablePureComponent { return ( <HotKeys handlers={this.getHandlers()}> - <div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow', defaultMessage: '{name} followed you' }, { name: account.get('acct') }), notification.get('created_at'))}> + <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='notification__message'> <div className='notification__favourite-icon-wrapper'> <Icon id='user-plus' fixedWidth /> @@ -118,7 +128,29 @@ class Notification extends ImmutablePureComponent { </span> </div> - <AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} /> + <AccountContainer id={account.get('id')} hidden={this.props.hidden} /> + </div> + </HotKeys> + ); + } + + renderFollowRequest (notification, account, link) { + const { intl } = 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='notification__message'> + <div className='notification__favourite-icon-wrapper'> + <Icon id='user' fixedWidth /> + </div> + + <span title={notification.get('created_at')}> + <FormattedMessage id='notification.follow_request' defaultMessage='{name} has requested to follow you' values={{ name: link }} /> + </span> + </div> + + <FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} /> </div> </HotKeys> ); @@ -146,7 +178,7 @@ class Notification extends ImmutablePureComponent { return ( <HotKeys handlers={this.getHandlers()}> - <div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}> + <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='notification__message'> <div className='notification__favourite-icon-wrapper'> <Icon id='star' className='star-icon' fixedWidth /> @@ -178,7 +210,7 @@ class Notification extends ImmutablePureComponent { return ( <HotKeys handlers={this.getHandlers()}> - <div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.reblog', defaultMessage: '{name} boosted your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}> + <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='notification__message'> <div className='notification__favourite-icon-wrapper'> <Icon id='retweet' fixedWidth /> @@ -205,25 +237,31 @@ class Notification extends ImmutablePureComponent { ); } - renderPoll (notification) { + renderPoll (notification, account) { const { intl } = 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, intl.formatMessage({ id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' }), notification.get('created_at'))}> + <div className='notification notification-poll focusable' 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 /> </div> <span title={notification.get('created_at')}> - <FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' /> + {ownPoll ? ( + <FormattedMessage id='notification.own_poll' defaultMessage='Your poll has ended' /> + ) : ( + <FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' /> + )} </span> </div> <StatusContainer id={notification.get('status')} - account={notification.get('account')} + account={account} muted withDismiss hidden={this.props.hidden} @@ -246,6 +284,8 @@ class Notification extends ImmutablePureComponent { switch(notification.get('type')) { case 'follow': return this.renderFollow(notification, account, link); + case 'follow_request': + return this.renderFollowRequest(notification, account, link); case 'mention': return this.renderMention(notification); case 'favourite': @@ -253,7 +293,7 @@ class Notification extends ImmutablePureComponent { case 'reblog': return this.renderReblog(notification, link); case 'poll': - return this.renderPoll(notification); + return this.renderPoll(notification, account); } return null; diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js index 7aec16d2e..e6f593ef8 100644 --- a/app/javascript/mastodon/features/notifications/components/setting_toggle.js +++ b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -11,6 +11,7 @@ export default class SettingToggle extends React.PureComponent { settingPath: PropTypes.array.isRequired, label: PropTypes.node.isRequired, onChange: PropTypes.func.isRequired, + defaultValue: PropTypes.bool, } onChange = ({ target }) => { @@ -18,12 +19,12 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingPath, label } = this.props; + const { prefix, settings, settingPath, label, defaultValue } = this.props; const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-'); return ( <div className='setting-toggle'> - <Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} /> + <Toggle id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} /> <label htmlFor={id} className='setting-toggle__label'>{label}</label> </div> ); diff --git a/app/javascript/mastodon/features/notifications/containers/follow_request_container.js b/app/javascript/mastodon/features/notifications/containers/follow_request_container.js new file mode 100644 index 000000000..f9f6c577e --- /dev/null +++ b/app/javascript/mastodon/features/notifications/containers/follow_request_container.js @@ -0,0 +1,26 @@ +import { connect } from 'react-redux'; +import { makeGetAccount } from 'mastodon/selectors'; +import FollowRequest from '../components/follow_request'; +import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts'; + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, props) => ({ + account: getAccount(state, props.id), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { id }) => ({ + onAuthorize () { + dispatch(authorizeFollowRequest(id)); + }, + + onReject () { + dispatch(rejectFollowRequest(id)); + }, +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(FollowRequest); diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 006c45657..d16a0f33a 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -4,7 +4,7 @@ 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 } from '../../actions/notifications'; +import { expandNotifications, scrollTopNotifications, loadPending, mountNotifications, unmountNotifications } from '../../actions/notifications'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import NotificationContainer from './containers/notification_container'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -39,8 +39,9 @@ const mapStateToProps = state => ({ showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']), notifications: getNotifications(state), isLoading: state.getIn(['notifications', 'isLoading'], true), - isUnread: state.getIn(['notifications', 'unread']) > 0, + isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, hasMore: state.getIn(['notifications', 'hasMore']), + numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, }); export default @connect(mapStateToProps) @@ -58,17 +59,23 @@ class Notifications extends React.PureComponent { isUnread: PropTypes.bool, multiColumn: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, }; static defaultProps = { trackScroll: true, }; + componentWillMount() { + this.props.dispatch(mountNotifications()); + } + componentWillUnmount () { this.handleLoadOlder.cancel(); this.handleScrollToTop.cancel(); this.handleScroll.cancel(); this.props.dispatch(scrollTopNotifications(false)); + this.props.dispatch(unmountNotifications()); } handleLoadGap = (maxId) => { @@ -80,6 +87,10 @@ class Notifications extends React.PureComponent { this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); }, 300, { leading: true }); + handleLoadPending = () => { + this.props.dispatch(loadPending()); + }; + handleScrollToTop = debounce(() => { this.props.dispatch(scrollTopNotifications(true)); }, 100); @@ -136,7 +147,7 @@ class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = 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." />; @@ -178,18 +189,21 @@ class Notifications extends React.PureComponent { isLoading={isLoading} showLoading={isLoading && notifications.size === 0} hasMore={hasMore} + numPending={numPending} emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} + onLoadPending={this.handleLoadPending} onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} > {scrollableContent} </ScrollableList> ); return ( - <Column ref={this.setColumnRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='bell' active={isUnread} diff --git a/app/javascript/mastodon/features/pinned_statuses/index.js b/app/javascript/mastodon/features/pinned_statuses/index.js index 98cdbda3c..ad5c9cafc 100644 --- a/app/javascript/mastodon/features/pinned_statuses/index.js +++ b/app/javascript/mastodon/features/pinned_statuses/index.js @@ -28,6 +28,7 @@ class PinnedStatuses extends ImmutablePureComponent { statusIds: ImmutablePropTypes.list.isRequired, intl: PropTypes.object.isRequired, hasMore: PropTypes.bool.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,16 +44,17 @@ class PinnedStatuses extends ImmutablePureComponent { } render () { - const { intl, shouldUpdateScroll, statusIds, hasMore } = this.props; + const { intl, shouldUpdateScroll, statusIds, hasMore, multiColumn } = this.props; return ( - <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> + <Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> <ColumnBackButtonSlim /> <StatusList statusIds={statusIds} scrollKey='pinned_statuses' hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 2b7d9c56f..7aabd7f6e 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -14,14 +14,16 @@ const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Federated timeline' }, }); -const mapStateToProps = (state, { onlyMedia, columnId }) => { +const mapStateToProps = (state, { columnId }) => { const uuid = columnId; const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === uuid); + const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']); + const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]); return { - hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0, - onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']), + hasUnread: !!timelineState && timelineState.get('unread') > 0, + onlyMedia, }; }; @@ -105,7 +107,7 @@ class PublicTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='globe' active={hasUnread} @@ -126,6 +128,7 @@ class PublicTimeline extends React.PureComponent { scrollKey={`public_timeline-${columnId}`} emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> </Column> ); diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js index c05d21c74..9179e51db 100644 --- a/app/javascript/mastodon/features/reblogs/index.js +++ b/app/javascript/mastodon/features/reblogs/index.js @@ -5,17 +5,23 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import LoadingIndicator from '../../components/loading_indicator'; import { fetchReblogs } from '../../actions/interactions'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import AccountContainer from '../../containers/account_container'; import Column from '../ui/components/column'; -import ColumnBackButton from '../../components/column_back_button'; import ScrollableList from '../../components/scrollable_list'; +import Icon from 'mastodon/components/icon'; +import ColumnHeader from '../../components/column_header'; + +const messages = defineMessages({ + refresh: { id: 'refresh', defaultMessage: 'Refresh' }, +}); const mapStateToProps = (state, props) => ({ accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]), }); export default @connect(mapStateToProps) +@injectIntl class Reblogs extends ImmutablePureComponent { static propTypes = { @@ -23,10 +29,14 @@ class Reblogs extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, + intl: PropTypes.object.isRequired, }; componentWillMount () { - this.props.dispatch(fetchReblogs(this.props.params.statusId)); + if (!this.props.accountIds) { + this.props.dispatch(fetchReblogs(this.props.params.statusId)); + } } componentWillReceiveProps(nextProps) { @@ -35,8 +45,12 @@ class Reblogs extends ImmutablePureComponent { } } + handleRefresh = () => { + this.props.dispatch(fetchReblogs(this.props.params.statusId)); + } + render () { - const { shouldUpdateScroll, accountIds } = this.props; + const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -49,13 +63,20 @@ class Reblogs extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.' />; return ( - <Column> - <ColumnBackButton /> + <Column bindToDocument={!multiColumn}> + <ColumnHeader + showBackButton + multiColumn={multiColumn} + extraButton={( + <button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button> + )} + /> <ScrollableList scrollKey='reblogs' shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} /> diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 3e511b7a6..bf6469f2f 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import IconButton from '../../../components/icon_button'; import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; @@ -17,6 +18,7 @@ const messages = defineMessages({ 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' }, + bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, @@ -29,9 +31,18 @@ const messages = defineMessages({ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, + blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); -export default @injectIntl +const mapStateToProps = (state, { status }) => ({ + relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), +}); + +export default @connect(mapStateToProps) +@injectIntl class ActionBar extends React.PureComponent { static contextTypes = { @@ -40,15 +51,21 @@ class ActionBar extends React.PureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + relationship: ImmutablePropTypes.map, onReply: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, + onBookmark: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onMute: PropTypes.func, - onMuteConversation: PropTypes.func, + onUnmute: PropTypes.func, onBlock: PropTypes.func, + onUnblock: PropTypes.func, + onBlockDomain: PropTypes.func, + onUnblockDomain: PropTypes.func, + onMuteConversation: PropTypes.func, onReport: PropTypes.func, onPin: PropTypes.func, onEmbed: PropTypes.func, @@ -67,6 +84,10 @@ class ActionBar extends React.PureComponent { this.props.onFavourite(this.props.status); } + handleBookmarkClick = (e) => { + this.props.onBookmark(this.props.status, e); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -84,15 +105,43 @@ class ActionBar extends React.PureComponent { } handleMuteClick = () => { - this.props.onMute(this.props.status.get('account')); - } + const { status, relationship, onMute, onUnmute } = this.props; + const account = status.get('account'); - handleConversationMuteClick = () => { - this.props.onMuteConversation(this.props.status); + if (relationship && relationship.get('muting')) { + onUnmute(account); + } else { + onMute(account); + } } handleBlockClick = () => { - this.props.onBlock(this.props.status); + const { status, relationship, onBlock, onUnblock } = this.props; + const account = status.get('account'); + + if (relationship && relationship.get('blocking')) { + onUnblock(account); + } else { + onBlock(status); + } + } + + handleBlockDomain = () => { + const { status, onBlockDomain } = this.props; + const account = status.get('account'); + + onBlockDomain(account.get('acct').split('@')[1]); + } + + handleUnblockDomain = () => { + const { status, onUnblockDomain } = this.props; + const account = status.get('account'); + + onUnblockDomain(account.get('acct').split('@')[1]); + } + + handleConversationMuteClick = () => { + this.props.onMuteConversation(this.props.status); } handleReport = () => { @@ -134,10 +183,11 @@ class ActionBar extends React.PureComponent { } render () { - const { status, intl } = this.props; + const { status, relationship, intl } = this.props; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); + const account = status.get('account'); let menu = []; @@ -165,9 +215,33 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); + + if (relationship && relationship.get('muting')) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); + } else { + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick }); + } + + if (relationship && relationship.get('blocking')) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick }); + } else { + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick }); + } + menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + + if (account.get('acct') !== account.get('username')) { + const domain = account.get('acct').split('@')[1]; + + menu.push(null); + + if (relationship && relationship.get('domain_blocking')) { + menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); + } else { + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain }); + } + } + if (isStaff) { menu.push(null); menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); @@ -198,9 +272,10 @@ class ActionBar extends React.PureComponent { <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> {shareButton} + <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__action-bar-dropdown'> - <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title='More' /> + <DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title='More' /> </div> </div> ); diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 0eff54411..2993fe29a 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -148,7 +148,7 @@ export default class Card extends React.PureComponent { const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; const interactive = card.get('type') !== 'link'; const className = classnames('status-card', { horizontal, compact, interactive }); - const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; + const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; const ratio = card.get('width') / card.get('height'); const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); @@ -180,7 +180,7 @@ export default class Card extends React.PureComponent { <div className='status-card__actions'> <div> <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> - {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><Icon id='external-link' /></a>} + {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>} </div> </div> </div> @@ -208,7 +208,7 @@ export default class Card extends React.PureComponent { } return ( - <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}> + <a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}> {embed} {description} </a> diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index c7aa4d033..d5bc82735 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -10,6 +10,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl'; import Card from './card'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from '../../video'; +import Audio from '../../audio'; import scheduleIdleTask from '../../ui/util/schedule_idle_task'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; @@ -107,15 +108,27 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('media_attachments').size > 0) { - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); + if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { + const attachment = status.getIn(['media_attachments', 0]); + + media = ( + <Audio + src={attachment.get('url')} + alt={attachment.get('description')} + duration={attachment.getIn(['meta', 'original', 'duration'], 0)} + height={110} + preload + /> + ); + } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + const attachment = status.getIn(['media_attachments', 0]); media = ( <Video - preview={video.get('preview_url')} - blurhash={video.get('blurhash')} - src={video.get('url')} - alt={video.get('description')} + preview={attachment.get('preview_url')} + blurhash={attachment.get('blurhash')} + src={attachment.get('url')} + alt={attachment.get('description')} width={300} height={150} inline @@ -143,7 +156,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('application')) { - applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>; + applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>; } if (status.get('visibility') === 'direct') { @@ -207,7 +220,7 @@ export default class DetailedStatus extends ImmutablePureComponent { {media} <div className='detailed-status__meta'> - <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> + <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> </a>{applicationLink} · {reblogLink} · {favouriteLink} </div> diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js index 61e0c428a..333c295dc 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -1,4 +1,3 @@ -import React from 'react'; import { connect } from 'react-redux'; import DetailedStatus from '../components/detailed_status'; import { makeGetStatus } from '../../../selectors'; @@ -15,7 +14,6 @@ import { pin, unpin, } from '../../../actions/interactions'; -import { blockAccount } from '../../../actions/accounts'; import { muteStatus, unmuteStatus, @@ -24,9 +22,10 @@ import { revealStatus, } from '../../../actions/statuses'; import { initMuteModal } from '../../../actions/mutes'; +import { initBlockModal } from '../../../actions/blocks'; import { initReport } from '../../../actions/reports'; import { openModal } from '../../../actions/modal'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import { boostModal, deleteModal } from '../../../initial_state'; import { showAlertForError } from '../../../actions/alerts'; @@ -35,10 +34,8 @@ const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, 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?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, }); const makeMapStateToProps = () => { @@ -138,16 +135,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onBlock (status) { const account = status.get('account'); - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.get('id'))); - dispatch(initReport(account, status)); - }, - })); + dispatch(initBlockModal(account)); }, onReport (status) { diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 981eb9d58..6b18f34d1 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { createSelector } from 'reselect'; import { fetchStatus } from '../../actions/statuses'; import MissingIndicator from '../../components/missing_indicator'; import DetailedStatus from './components/detailed_status'; @@ -12,6 +13,8 @@ import Column from '../ui/components/column'; import { favourite, unfavourite, + bookmark, + unbookmark, reblog, unreblog, pin, @@ -22,7 +25,6 @@ import { mentionCompose, directCompose, } from '../../actions/compose'; -import { blockAccount } from '../../actions/accounts'; import { muteStatus, unmuteStatus, @@ -30,7 +32,16 @@ import { hideStatus, revealStatus, } from '../../actions/statuses'; +import { + unblockAccount, + unmuteAccount, +} from '../../actions/accounts'; +import { + blockDomain, + unblockDomain, +} from '../../actions/domain_blocks'; import { initMuteModal } from '../../actions/mutes'; +import { initBlockModal } from '../../actions/blocks'; import { initReport } from '../../actions/reports'; import { makeGetStatus } from '../../selectors'; import { ScrollContainer } from 'react-router-scroll-4'; @@ -51,53 +62,81 @@ const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, 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?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); const makeMapStateToProps = () => { const getStatus = makeGetStatus(); - const mapStateToProps = (state, props) => { - const status = getStatus(state, { id: props.params.statusId }); + const getAncestorsIds = createSelector([ + (_, { id }) => id, + state => state.getIn(['contexts', 'inReplyTos']), + ], (statusId, inReplyTos) => { let ancestorsIds = Immutable.List(); - let descendantsIds = Immutable.List(); + ancestorsIds = ancestorsIds.withMutations(mutable => { + let id = statusId; - if (status) { - ancestorsIds = ancestorsIds.withMutations(mutable => { - let id = status.get('in_reply_to_id'); + while (id) { + mutable.unshift(id); + id = inReplyTos.get(id); + } + }); - while (id) { - mutable.unshift(id); - id = state.getIn(['contexts', 'inReplyTos', id]); - } - }); + return ancestorsIds; + }); - descendantsIds = descendantsIds.withMutations(mutable => { - const ids = [status.get('id')]; + const getDescendantsIds = createSelector([ + (_, { id }) => id, + state => state.getIn(['contexts', 'replies']), + state => state.get('statuses'), + ], (statusId, contextReplies, statuses) => { + let descendantsIds = []; + const ids = [statusId]; - while (ids.length > 0) { - let id = ids.shift(); - const replies = state.getIn(['contexts', 'replies', id]); + while (ids.length > 0) { + let id = ids.shift(); + const replies = contextReplies.get(id); - if (status.get('id') !== id) { - mutable.push(id); - } + if (statusId !== id) { + descendantsIds.push(id); + } + + if (replies) { + replies.reverse().forEach(reply => { + ids.unshift(reply); + }); + } + } - if (replies) { - replies.reverse().forEach(reply => { - ids.unshift(reply); - }); - } + let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account')); + if (insertAt !== -1) { + descendantsIds.forEach((id, idx) => { + if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) { + descendantsIds.splice(idx, 1); + descendantsIds.splice(insertAt, 0, id); + insertAt += 1; } }); } + return Immutable.List(descendantsIds); + }); + + const mapStateToProps = (state, props) => { + const status = getStatus(state, { id: props.params.statusId }); + let ancestorsIds = Immutable.List(); + let descendantsIds = Immutable.List(); + + if (status) { + ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); + descendantsIds = getDescendantsIds(state, { id: status.get('id') }); + } + return { status, ancestorsIds, @@ -126,6 +165,7 @@ class Status extends ImmutablePureComponent { descendantsIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, askReplyConfirmation: PropTypes.bool, + multiColumn: PropTypes.bool, domain: PropTypes.string.isRequired, }; @@ -203,6 +243,14 @@ class Status extends ImmutablePureComponent { } } + handleBookmarkClick = (status) => { + if (status.get('bookmarked')) { + this.props.dispatch(unbookmark(status)); + } else { + this.props.dispatch(bookmark(status)); + } + } + handleDeleteClick = (status, history, withRedraft = false) => { const { dispatch, intl } = this.props; @@ -233,6 +281,22 @@ class Status extends ImmutablePureComponent { this.props.dispatch(openModal('VIDEO', { media, time })); } + handleHotkeyOpenMedia = e => { + const status = this._properStatus(); + + 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') { + this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0); + } else { + this.handleOpenMedia(status.get('media_attachments'), 0); + } + } + } + handleMuteClick = (account) => { this.props.dispatch(initMuteModal(account)); } @@ -265,19 +329,9 @@ class Status extends ImmutablePureComponent { } handleBlockClick = (status) => { - const { dispatch, intl } = this.props; + const { dispatch } = this.props; const account = status.get('account'); - - dispatch(openModal('CONFIRM', { - message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => dispatch(blockAccount(account.get('id'))), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - dispatch(blockAccount(account.get('id'))); - dispatch(initReport(account, status)); - }, - })); + dispatch(initBlockModal(account)); } handleReport = (status) => { @@ -288,6 +342,27 @@ class Status extends ImmutablePureComponent { this.props.dispatch(openModal('EMBED', { url: status.get('url') })); } + handleUnmuteClick = account => { + this.props.dispatch(unmuteAccount(account.get('id'))); + } + + handleUnblockClick = account => { + this.props.dispatch(unblockAccount(account.get('id'))); + } + + handleBlockDomainClick = domain => { + this.props.dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />, + confirm: this.props.intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => this.props.dispatch(blockDomain(domain)), + })); + } + + handleUnblockDomainClick = domain => { + this.props.dispatch(unblockDomain(domain)); + } + + handleHotkeyMoveUp = () => { this.handleMoveUp(this.props.status.get('id')); } @@ -417,13 +492,13 @@ class Status extends ImmutablePureComponent { render () { let ancestors, descendants; - const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain } = this.props; + const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props; const { fullscreen } = this.state; if (status === null) { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <MissingIndicator /> </Column> ); @@ -447,12 +522,14 @@ class Status extends ImmutablePureComponent { openProfile: this.handleHotkeyOpenProfile, toggleHidden: this.handleHotkeyToggleHidden, toggleSensitive: this.handleHotkeyToggleSensitive, + openMedia: this.handleHotkeyOpenMedia, }; return ( - <Column label={intl.formatMessage(messages.detailedStatus)}> + <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.detailedStatus)}> <ColumnHeader showBackButton + multiColumn={multiColumn} extraButton={( <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button> )} @@ -479,12 +556,17 @@ class Status extends ImmutablePureComponent { onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} + onBookmark={this.handleBookmarkClick} onDelete={this.handleDeleteClick} onDirect={this.handleDirectClick} onMention={this.handleMentionClick} onMute={this.handleMuteClick} + onUnmute={this.handleUnmuteClick} onMuteConversation={this.handleConversationMuteClick} onBlock={this.handleBlockClick} + onUnblock={this.handleUnblockClick} + onBlockDomain={this.handleBlockDomainClick} + onUnblockDomain={this.handleUnblockDomainClick} onReport={this.handleReport} onPin={this.handlePin} onEmbed={this.handleEmbed} diff --git a/app/javascript/mastodon/features/ui/components/actions_modal.js b/app/javascript/mastodon/features/ui/components/actions_modal.js index 00280f7a6..875b2b75d 100644 --- a/app/javascript/mastodon/features/ui/components/actions_modal.js +++ b/app/javascript/mastodon/features/ui/components/actions_modal.js @@ -26,7 +26,7 @@ export default class ActionsModal extends ImmutablePureComponent { return ( <li key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}> + <a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}> {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />} <div> <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div> @@ -42,7 +42,7 @@ export default class ActionsModal extends ImmutablePureComponent { <div className='status light'> <div className='boost-modal__status-header'> <div className='boost-modal__status-time'> - <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> + <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'> <RelativeTimestamp timestamp={this.props.status.get('created_at')} /> </a> </div> diff --git a/app/javascript/mastodon/features/ui/components/audio_modal.js b/app/javascript/mastodon/features/ui/components/audio_modal.js new file mode 100644 index 000000000..2300453d7 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/audio_modal.js @@ -0,0 +1,76 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Audio from 'mastodon/features/audio'; +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'; + +export default class AudioModal extends ImmutablePureComponent { + + static propTypes = { + media: ImmutablePropTypes.map.isRequired, + status: ImmutablePropTypes.map, + onClose: PropTypes.func.isRequired, + }; + + static contextTypes = { + router: PropTypes.object, + }; + + componentDidMount () { + if (this.context.router) { + const history = this.context.router.history; + + history.push(history.location.pathname, previewState); + + this.unlistenHistory = history.listen(() => { + this.props.onClose(); + }); + } + } + + componentWillUnmount () { + if (this.context.router) { + this.unlistenHistory(); + + if (this.context.router.history.location.state === previewState) { + this.context.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 } = this.props; + + return ( + <div className='modal-root__modal audio-modal'> + <div className='audio-modal__container'> + <Audio + src={media.get('url')} + alt={media.get('description')} + duration={media.getIn(['meta', 'original', 'duration'], 0)} + height={135} + preload + /> + </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> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/block_modal.js b/app/javascript/mastodon/features/ui/components/block_modal.js new file mode 100644 index 000000000..a07baeaa6 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/block_modal.js @@ -0,0 +1,103 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import { makeGetAccount } from '../../../selectors'; +import Button from '../../../components/button'; +import { closeModal } from '../../../actions/modal'; +import { blockAccount } from '../../../actions/accounts'; +import { initReport } from '../../../actions/reports'; + + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = state => ({ + account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = dispatch => { + return { + onConfirm(account) { + dispatch(blockAccount(account.get('id'))); + }, + + onBlockAndReport(account) { + dispatch(blockAccount(account.get('id'))); + dispatch(initReport(account)); + }, + + onClose() { + dispatch(closeModal()); + }, + }; +}; + +export default @connect(makeMapStateToProps, mapDispatchToProps) +@injectIntl +class BlockModal extends React.PureComponent { + + static propTypes = { + account: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + onBlockAndReport: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + componentDidMount() { + this.button.focus(); + } + + handleClick = () => { + this.props.onClose(); + this.props.onConfirm(this.props.account); + } + + handleSecondary = () => { + this.props.onClose(); + this.props.onBlockAndReport(this.props.account); + } + + handleCancel = () => { + this.props.onClose(); + } + + setRef = (c) => { + this.button = c; + } + + render () { + const { account } = this.props; + + return ( + <div className='modal-root__modal block-modal'> + <div className='block-modal__container'> + <p> + <FormattedMessage + id='confirmations.block.message' + defaultMessage='Are you sure you want to block {name}?' + values={{ name: <strong>@{account.get('acct')}</strong> }} + /> + </p> + </div> + + <div className='block-modal__action-bar'> + <Button onClick={this.handleCancel} className='block-modal__cancel-button'> + <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /> + </Button> + <Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'> + <FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' /> + </Button> + <Button onClick={this.handleClick} ref={this.setRef}> + <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' /> + </Button> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js index 70f4a1282..0e79005f0 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.js +++ b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -61,7 +61,7 @@ class BoostModal extends ImmutablePureComponent { <div className='status 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'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> </div> <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js index 9503a7a1a..0cdfd05d8 100644 --- a/app/javascript/mastodon/features/ui/components/column_loading.js +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -21,7 +21,7 @@ export default class ColumnLoading extends ImmutablePureComponent { let { title, icon } = this.props; return ( <Column> - <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} /> + <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder /> <div className='scrollable' /> </Column> ); diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 756db3c61..8bc4bfc0e 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -12,7 +12,19 @@ import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; import BundleColumnError from './bundle_column_error'; -import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; +import { + Compose, + Notifications, + HomeTimeline, + CommunityTimeline, + PublicTimeline, + HashtagTimeline, + DirectTimeline, + FavouritedStatuses, + BookmarkedStatuses, + ListTimeline, + Directory, +} from '../../ui/util/async-components'; import Icon from 'mastodon/components/icon'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; @@ -29,7 +41,9 @@ const componentMap = { 'HASHTAG': HashtagTimeline, 'DIRECT': DirectTimeline, 'FAVOURITES': FavouritedStatuses, + 'BOOKMARKS': BookmarkedStatuses, 'LIST': ListTimeline, + 'DIRECTORY': Directory, }; const messages = defineMessages({ @@ -110,6 +124,11 @@ class ColumnsArea extends ImmutablePureComponent { // React-router does this for us, but too late, feeling laggy. document.querySelector(currentLinkSelector).classList.remove('active'); document.querySelector(nextLinkSelector).classList.add('active'); + + if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') { + this.context.router.history.push(getLink(this.pendingIndex)); + this.pendingIndex = null; + } } handleAnimationEnd = () => { @@ -160,13 +179,12 @@ class ColumnsArea extends ImmutablePureComponent { const { shouldAnimate } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); - this.pendingIndex = null; if (singleColumn) { 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' 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%' }}> {links.map(this.renderView)} </ReactSwipeableViews> ) : ( diff --git a/app/javascript/mastodon/features/ui/components/document_title.js b/app/javascript/mastodon/features/ui/components/document_title.js new file mode 100644 index 000000000..cd081b20c --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/document_title.js @@ -0,0 +1,41 @@ +import { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { title } from 'mastodon/initial_state'; + +const mapStateToProps = state => ({ + unread: state.getIn(['missed_updates', 'unread']), +}); + +export default @connect(mapStateToProps) +class DocumentTitle extends PureComponent { + + static propTypes = { + unread: PropTypes.number.isRequired, + }; + + componentDidMount () { + this._sideEffects(); + } + + componentDidUpdate() { + this._sideEffects(); + } + + _sideEffects () { + const { unread } = this.props; + + if (unread > 99) { + document.title = `(*) ${title}`; + } else if (unread > 0) { + document.title = `(${unread}) ${title}`; + } else { + document.title = title; + } + } + + render () { + return null; + } + +} diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.js b/app/javascript/mastodon/features/ui/components/embed_modal.js index 982781db0..4679c9650 100644 --- a/app/javascript/mastodon/features/ui/components/embed_modal.js +++ b/app/javascript/mastodon/features/ui/components/embed_modal.js @@ -1,8 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage, injectIntl } from 'react-intl'; -import api from '../../../api'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import api from 'mastodon/api'; +import IconButton from 'mastodon/components/icon_button'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); export default @injectIntl class EmbedModal extends ImmutablePureComponent { @@ -50,13 +55,17 @@ class EmbedModal extends ImmutablePureComponent { } render () { + const { intl, onClose } = this.props; const { oembed } = this.state; return ( - <div className='modal-root__modal embed-modal'> - <h4><FormattedMessage id='status.embed' defaultMessage='Embed' /></h4> + <div className='modal-root__modal report-modal embed-modal'> + <div className='report-modal__target'> + <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> + <FormattedMessage id='status.embed' defaultMessage='Embed' /> + </div> - <div className='embed-modal__container'> + <div className='report-modal__container embed-modal__container' style={{ display: 'block' }}> <p className='hint'> <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' /> </p> diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index 7488a3598..7d1509f71 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -1,11 +1,28 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import ImageLoader from './image_loader'; import classNames from 'classnames'; import { changeUploadCompose } from '../../../actions/compose'; import { getPointerPosition } from '../../video'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import IconButton from 'mastodon/components/icon_button'; +import Button from 'mastodon/components/button'; +import Video from 'mastodon/features/video'; +import Audio from 'mastodon/features/audio'; +import Textarea from 'react-textarea-autosize'; +import UploadProgress from 'mastodon/features/compose/components/upload_progress'; +import CharacterCounter from 'mastodon/features/compose/components/character_counter'; +import { length } from 'stringz'; +import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; +import GIFV from 'mastodon/components/gifv'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, + apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' }, + placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' }, +}); const mapStateToProps = (state, { id }) => ({ media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), @@ -13,17 +30,56 @@ const mapStateToProps = (state, { id }) => ({ const mapDispatchToProps = (dispatch, { id }) => ({ - onSave: (x, y) => { - dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` })); + onSave: (description, x, y) => { + dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` })); }, }); +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 = { + src: PropTypes.string.isRequired, + width: PropTypes.number, + height: PropTypes.number, + }; + + state = { + loading: true, + }; + + componentDidMount() { + const image = new Image(); + image.addEventListener('load', () => this.setState({ loading: false })); + image.src = this.props.src; + } + + render () { + const { loading } = this.state; + + if (loading) { + return <canvas width={this.props.width} height={this.props.height} />; + } else { + return <img {...this.props} alt='' />; + } + } + +} + export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl class FocalPointModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, }; state = { @@ -32,6 +88,10 @@ class FocalPointModal extends ImmutablePureComponent { focusX: 0, focusY: 0, dragging: false, + description: '', + dirty: false, + progress: 0, + loading: true, }; componentWillMount () { @@ -57,6 +117,14 @@ class FocalPointModal extends ImmutablePureComponent { this.setState({ dragging: true }); } + handleTouchStart = e => { + document.addEventListener('touchmove', this.handleMouseMove); + document.addEventListener('touchend', this.handleTouchEnd); + + this.updatePosition(e); + this.setState({ dragging: true }); + } + handleMouseMove = e => { this.updatePosition(e); } @@ -66,7 +134,13 @@ class FocalPointModal extends ImmutablePureComponent { document.removeEventListener('mouseup', this.handleMouseUp); this.setState({ dragging: false }); - this.props.onSave(this.state.focusX, this.state.focusY); + } + + handleTouchEnd = () => { + document.removeEventListener('touchmove', this.handleMouseMove); + document.removeEventListener('touchend', this.handleTouchEnd); + + this.setState({ dragging: false }); } updatePosition = e => { @@ -74,46 +148,188 @@ class FocalPointModal extends ImmutablePureComponent { const focusX = (x - .5) * 2; const focusY = (y - .5) * -2; - this.setState({ x, y, focusX, focusY }); + this.setState({ x, y, focusX, focusY, dirty: true }); } updatePositionFromMedia = media => { - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); + const focusX = media.getIn(['meta', 'focus', 'x']); + const focusY = media.getIn(['meta', 'focus', 'y']); + const description = media.get('description') || ''; if (focusX && focusY) { const x = (focusX / 2) + .5; const y = (focusY / -2) + .5; - this.setState({ x, y, focusX, focusY }); + this.setState({ + x, + y, + focusX, + focusY, + description, + dirty: false, + }); } else { - this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 }); + this.setState({ + x: 0.5, + y: 0.5, + focusX: 0, + focusY: 0, + description, + dirty: false, + }); } } + handleChange = e => { + this.setState({ description: e.target.value, dirty: true }); + } + + handleKeyDown = (e) => { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + e.stopPropagation(); + this.setState({ description: e.target.value, dirty: true }); + this.handleSubmit(); + } + } + + handleSubmit = () => { + this.props.onSave(this.state.description, this.state.focusX, this.state.focusY); + this.props.onClose(); + } + setRef = c => { this.node = c; } - render () { + handleTextDetection = () => { const { media } = this.props; - const { x, y, dragging } = this.state; + + 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`, + }); + + let media_url = media.get('url'); + + if (window.URL && URL.createObjectURL) { + try { + media_url = URL.createObjectURL(media.get('file')); + } catch (error) { + console.error(error); + } + } + + 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 })); + } + + render () { + const { media, intl, onClose } = this.props; + const { x, y, dragging, description, dirty, detecting, progress } = this.state; const width = media.getIn(['meta', 'original', 'width']) || null; const height = media.getIn(['meta', 'original', 'height']) || null; + const focals = ['image', 'gifv'].includes(media.get('type')); + + const previewRatio = 16/9; + const previewWidth = 200; + const previewHeight = previewWidth / previewRatio; + + let descriptionLabel = null; + + if (media.get('type') === 'audio') { + descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people with hearing loss' />; + } else if (media.get('type') === 'video') { + descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people with hearing loss or visual impairment' />; + } else { + descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />; + } return ( - <div className='modal-root__modal video-modal focal-point-modal'> - <div className={classNames('focal-point', { dragging })} ref={this.setRef}> - <ImageLoader - previewSrc={media.get('preview_url')} - src={media.get('url')} - width={width} - height={height} - /> - - <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} /> - <div className='focal-point__overlay' onMouseDown={this.handleMouseDown} /> + <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}> + <div className='report-modal__target'> + <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> + <FormattedMessage id='upload_modal.edit_media' defaultMessage='Edit media' /> + </div> + + <div className='report-modal__container'> + <div className='report-modal__comment'> + {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>} + + <label className='setting-text-label' htmlFor='upload-modal__description'> + {descriptionLabel} + </label> + + <div className='setting-text__wrapper'> + <Textarea + id='upload-modal__description' + className='setting-text light' + value={detecting ? '…' : description} + onChange={this.handleChange} + onKeyDown={this.handleKeyDown} + disabled={detecting} + autoFocus + /> + + <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…' />} /> + </div> + </div> + + <div className='setting-text__toolbar'> + <button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button> + <CharacterCounter max={1500} text={detecting ? '' : description} /> + </div> + + <Button disabled={!dirty || detecting || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> + </div> + + <div className='focal-point-modal__content'> + {focals && ( + <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}> + {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />} + {media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />} + + <div className='focal-point__preview'> + <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong> + <div style={{ width: previewWidth, height: previewHeight, backgroundImage: `url(${media.get('preview_url')})`, backgroundSize: 'cover', backgroundPosition: `${x * 100}% ${y * 100}%` }} /> + </div> + + <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} /> + <div className='focal-point__overlay' /> + </div> + )} + + {media.get('type') === 'video' && ( + <Video + preview={media.get('preview_url')} + blurhash={media.get('blurhash')} + src={media.get('url')} + detailed + inline + editable + /> + )} + + {media.get('type') === 'audio' && ( + <Audio + src={media.get('url')} + duration={media.getIn(['meta', 'original', 'duration'], 0)} + height={150} + preload + editable + /> + )} + </div> </div> </div> ); diff --git a/app/javascript/mastodon/features/ui/components/follow_requests_nav_link.js b/app/javascript/mastodon/features/ui/components/follow_requests_nav_link.js index 90c953893..950ed7b27 100644 --- a/app/javascript/mastodon/features/ui/components/follow_requests_nav_link.js +++ b/app/javascript/mastodon/features/ui/components/follow_requests_nav_link.js @@ -4,12 +4,10 @@ import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { connect } from 'react-redux'; import { NavLink, withRouter } from 'react-router-dom'; import IconWithBadge from 'mastodon/components/icon_with_badge'; -import { me } from 'mastodon/initial_state'; import { List as ImmutableList } from 'immutable'; import { FormattedMessage } from 'react-intl'; const mapStateToProps = state => ({ - locked: state.getIn(['accounts', me, 'locked']), count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, }); @@ -19,22 +17,19 @@ class FollowRequestsNavLink extends React.Component { static propTypes = { dispatch: PropTypes.func.isRequired, - locked: PropTypes.bool, count: PropTypes.number.isRequired, }; componentDidMount () { - const { dispatch, locked } = this.props; + const { dispatch } = this.props; - if (locked) { - dispatch(fetchFollowRequests()); - } + dispatch(fetchFollowRequests()); } render () { - const { locked, count } = this.props; + const { count } = this.props; - if (!locked || count === 0) { + if (count === 0) { return null; } diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.js index b481983dc..3c4cff9f9 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.js +++ b/app/javascript/mastodon/features/ui/components/link_footer.js @@ -1,35 +1,72 @@ +import { connect } from 'react-redux'; import React from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state'; +import { logOut } from 'mastodon/utils/log_out'; +import { openModal } from 'mastodon/actions/modal'; -const LinkFooter = ({ withHotkeys }) => ( - <div className='getting-started__footer'> - <ul> - {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} - {withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} - <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> - <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> - <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> - <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> - <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> - <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li> - <li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li> - </ul> - - <p> - <FormattedMessage - id='getting_started.open_source_notice' - defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.' - values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }} - /> - </p> - </div> -); - -LinkFooter.propTypes = { - withHotkeys: PropTypes.bool, -}; +const messages = defineMessages({ + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, +}); + +const mapDispatchToProps = (dispatch, { intl }) => ({ + onLogout () { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + onConfirm: () => logOut(), + })); + }, +}); + +export default @injectIntl +@connect(null, mapDispatchToProps) +class LinkFooter extends React.PureComponent { + + static propTypes = { + withHotkeys: PropTypes.bool, + onLogout: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleLogoutClick = e => { + e.preventDefault(); + e.stopPropagation(); + + this.props.onLogout(); -export default LinkFooter; + return false; + } + + render () { + const { withHotkeys } = this.props; + + return ( + <div className='getting-started__footer'> + <ul> + {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} + {withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} + <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> + <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> + <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> + <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> + <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> + <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li> + <li><a href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li> + </ul> + + <p> + <FormattedMessage + id='getting_started.open_source_notice' + defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.' + values={{ github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span> }} + /> + </p> + </div> + ); + } + +}; diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index da2ac5f26..a785551c0 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -3,13 +3,13 @@ import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Video from 'mastodon/features/video'; -import ExtendedVideoPlayer from 'mastodon/components/extended_video_player'; import classNames from 'classnames'; import { defineMessages, injectIntl, FormattedMessage } 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'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -169,10 +169,8 @@ class MediaModal extends ImmutablePureComponent { ); } else if (image.get('type') === 'gifv') { return ( - <ExtendedVideoPlayer + <GIFV src={image.get('url')} - muted - controls={false} width={width} height={height} key={image.get('preview_url')} @@ -228,7 +226,7 @@ class MediaModal extends ImmutablePureComponent { {status && ( <div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}> - <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> + <a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> </div> )} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index cc2ab6c8c..5cf70a0cc 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Base from '../../../components/modal_root'; +import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; +import Base from 'mastodon/components/modal_root'; import BundleContainer from '../containers/bundle_container'; import BundleModalError from './bundle_modal_error'; import ModalLoading from './modal_loading'; @@ -8,10 +9,12 @@ import ActionsModal from './actions_modal'; import MediaModal from './media_modal'; import VideoModal from './video_modal'; import BoostModal from './boost_modal'; +import AudioModal from './audio_modal'; import ConfirmationModal from './confirmation_modal'; import FocalPointModal from './focal_point_modal'; import { MuteModal, + BlockModal, ReportModal, EmbedModal, ListEditor, @@ -21,9 +24,11 @@ import { const MODAL_COMPONENTS = { 'MEDIA': () => Promise.resolve({ default: MediaModal }), 'VIDEO': () => Promise.resolve({ default: VideoModal }), + 'AUDIO': () => Promise.resolve({ default: AudioModal }), 'BOOST': () => Promise.resolve({ default: BoostModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), 'MUTE': MuteModal, + 'BLOCK': BlockModal, 'REPORT': ReportModal, 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'EMBED': EmbedModal, @@ -47,8 +52,10 @@ export default class ModalRoot extends React.PureComponent { componentDidUpdate (prevProps, prevState, { visible }) { if (visible) { document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; } else { document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; } } diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js index ac356b42a..852830c3c 100644 --- a/app/javascript/mastodon/features/ui/components/mute_modal.js +++ b/app/javascript/mastodon/features/ui/components/mute_modal.js @@ -11,7 +11,6 @@ import { toggleHideNotifications } from '../../../actions/mutes'; const mapStateToProps = state => { return { - isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), account: state.getIn(['mutes', 'new', 'account']), notifications: state.getIn(['mutes', 'new', 'notifications']), }; @@ -38,7 +37,6 @@ export default @connect(mapStateToProps, mapDispatchToProps) class MuteModal extends React.PureComponent { static propTypes = { - isSubmitting: PropTypes.bool.isRequired, account: PropTypes.object.isRequired, notifications: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, @@ -81,11 +79,16 @@ class MuteModal extends React.PureComponent { values={{ name: <strong>@{account.get('acct')}</strong> }} /> </p> - <div> - <label htmlFor='mute-modal__hide-notifications-checkbox'> + <p className='mute-modal__explanation'> + <FormattedMessage + id='confirmations.mute.explanation' + defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.' + /> + </p> + <div className='setting-toggle'> + <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} /> + <label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'> <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' /> - {' '} - <Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} /> </label> </div> </div> diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index ef3ad2e09..0c12852f5 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -2,10 +2,11 @@ import React from 'react'; import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; import Icon from 'mastodon/components/icon'; -import { profile_directory } from 'mastodon/initial_state'; +import { profile_directory, showTrends } from 'mastodon/initial_state'; import NotificationsCounterIcon from './notifications_counter_icon'; import FollowRequestsNavLink from './follow_requests_nav_link'; import ListPanel from './list_panel'; +import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container'; const NavigationPanel = () => ( <div className='navigation-panel'> @@ -16,7 +17,9 @@ const NavigationPanel = () => ( <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> + {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>} <ListPanel /> @@ -24,7 +27,9 @@ const NavigationPanel = () => ( <a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a> <a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a> - {!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>} + + {showTrends && <div className='flex-spacer' />} + {showTrends && <TrendsContainer />} </div> ); diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index 29583d3d7..1911da8ba 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -73,9 +73,13 @@ class TabsBar extends React.PureComponent { const { intl: { formatMessage } } = this.props; return ( - <nav className='tabs-bar' ref={this.setRef}> - {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} - </nav> + <div className='tabs-bar__wrapper'> + <nav className='tabs-bar' ref={this.setRef}> + {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} + </nav> + + <div id='tabs-bar__portal' /> + </div> ); } diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js index 213d31316..f37fc796f 100644 --- a/app/javascript/mastodon/features/ui/components/video_modal.js +++ b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -4,6 +4,8 @@ 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'; export const previewState = 'previewVideoModal'; @@ -52,22 +54,25 @@ export default class VideoModal extends ImmutablePureComponent { render () { const { media, status, time, onClose } = this.props; - const link = status && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>; - return ( <div className='modal-root__modal video-modal'> - <div> + <div className='video-modal__container'> <Video preview={media.get('preview_url')} blurhash={media.get('blurhash')} src={media.get('url')} startTime={time} onCloseVideo={onClose} - link={link} 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> ); } diff --git a/app/javascript/mastodon/features/ui/containers/notifications_container.js b/app/javascript/mastodon/features/ui/containers/notifications_container.js index b60a0216f..3819da3d8 100644 --- a/app/javascript/mastodon/features/ui/containers/notifications_container.js +++ b/app/javascript/mastodon/features/ui/containers/notifications_container.js @@ -11,7 +11,7 @@ const mapStateToProps = (state, { intl }) => { const value = notification[key]; if (typeof value === 'object') { - notification[key] = intl.formatMessage(value); + notification[key] = intl.formatMessage(value, notification[`${key}_values`]); } })); diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 3df5b7bea..9f6cbf988 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -1,14 +1,14 @@ import { connect } from 'react-redux'; import StatusList from '../../../components/status_list'; -import { scrollTopTimeline } from '../../../actions/timelines'; +import { scrollTopTimeline, loadPending } from '../../../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; import { me } from '../../../initial_state'; -const makeGetStatusIds = () => createSelector([ +const makeGetStatusIds = (pending = false) => createSelector([ (state, { type }) => state.getIn(['settings', type], ImmutableMap()), - (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), + (state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()), (state) => state.get('statuses'), ], (columnSettings, statusIds, statuses) => { return statusIds.filter(id => { @@ -31,12 +31,14 @@ const makeGetStatusIds = () => createSelector([ const makeMapStateToProps = () => { const getStatusIds = makeGetStatusIds(); + const getPendingStatusIds = makeGetStatusIds(true); const mapStateToProps = (state, { timelineId }) => ({ statusIds: getStatusIds(state, { type: timelineId }), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), + numPending: getPendingStatusIds(state, { type: timelineId }).size, }); return mapStateToProps; @@ -52,6 +54,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({ dispatch(scrollTopTimeline(timelineId, false)); }, 100), + onLoadPending: () => dispatch(loadPending(timelineId)), + }); export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 791133afd..957e80737 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -15,9 +15,12 @@ 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 { submitMarkers } 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 { Compose, Status, @@ -38,6 +41,7 @@ import { FollowRequests, GenericNotFound, FavouritedStatuses, + BookmarkedStatuses, ListTimeline, Blocks, DomainBlocks, @@ -45,6 +49,7 @@ import { PinnedStatuses, Lists, Search, + Directory, } from './util/async-components'; import { me, forceSingleColumn } from '../../initial_state'; import { previewState as previewMediaState } from './components/media_modal'; @@ -62,6 +67,7 @@ const mapStateToProps = state => ({ isComposing: state.getIn(['compose', 'is_composing']), hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, + canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4, dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, }); @@ -94,6 +100,7 @@ const keyMap = { goToRequests: 'g r', toggleHidden: 'x', toggleSensitive: 'h', + openMedia: 'e', }; class SwitchingColumnsArea extends React.PureComponent { @@ -110,12 +117,25 @@ class SwitchingColumnsArea extends React.PureComponent { componentWillMount () { window.addEventListener('resize', this.handleResize, { passive: true }); + + if (this.state.mobile || forceSingleColumn) { + document.body.classList.toggle('layout-single-column', true); + document.body.classList.toggle('layout-multiple-columns', false); + } else { + document.body.classList.toggle('layout-single-column', false); + document.body.classList.toggle('layout-multiple-columns', true); + } } - componentDidUpdate (prevProps) { + componentDidUpdate (prevProps, prevState) { 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); + } } componentWillUnmount () { @@ -126,17 +146,29 @@ class SwitchingColumnsArea extends React.PureComponent { return location.state !== previewMediaState && location.state !== previewVideoState; } - handleResize = debounce(() => { + handleLayoutChange = debounce(() => { // The cached heights are no longer accurate, invalidate this.props.onLayoutChange(); - - this.setState({ mobile: isMobile(window.innerWidth) }); }, 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 => { - this.node = c.getWrappedInstance(); + if (c) { + this.node = c.getWrappedInstance(); + } } render () { @@ -160,9 +192,11 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/notifications' component={Notifications} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> + <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/search' component={Search} content={children} /> + <WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/statuses/new' component={Compose} content={children} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> @@ -204,6 +238,7 @@ class UI extends React.PureComponent { isComposing: PropTypes.bool, hasComposingText: PropTypes.bool, hasMediaAttachments: PropTypes.bool, + canUploadMore: PropTypes.bool, location: PropTypes.object, intl: PropTypes.object.isRequired, dropdownMenuIsOpen: PropTypes.bool, @@ -213,8 +248,10 @@ class UI extends React.PureComponent { draggingOver: false, }; - handleBeforeUnload = (e) => { - const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props; + handleBeforeUnload = e => { + const { intl, dispatch, isComposing, hasComposingText, hasMediaAttachments } = this.props; + + dispatch(submitMarkers()); if (isComposing && (hasComposingText || hasMediaAttachments)) { // Setting returnValue to any string causes confirmation dialog. @@ -224,6 +261,14 @@ class UI extends React.PureComponent { } } + handleWindowFocus = () => { + this.props.dispatch(focusApp()); + } + + handleWindowBlur = () => { + this.props.dispatch(unfocusApp()); + } + handleLayoutChange = () => { // The cached heights are no longer accurate, invalidate this.props.dispatch(clearHeight()); @@ -240,13 +285,14 @@ class UI extends React.PureComponent { this.dragTargets.push(e.target); } - if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) { + if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore) { this.setState({ draggingOver: true }); } } handleDragOver = (e) => { if (this.dataTransferIsText(e.dataTransfer)) return false; + e.preventDefault(); e.stopPropagation(); @@ -261,12 +307,13 @@ class UI extends React.PureComponent { handleDrop = (e) => { if (this.dataTransferIsText(e.dataTransfer)) return; + e.preventDefault(); this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length >= 1) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } } @@ -285,7 +332,7 @@ class UI extends React.PureComponent { } dataTransferIsText = (dataTransfer) => { - return (dataTransfer && Array.from(dataTransfer.types).includes('text/plain') && dataTransfer.items.length === 1); + return (dataTransfer && Array.from(dataTransfer.types).filter((type) => type === 'text/plain').length === 1); } closeUploadModal = () => { @@ -301,6 +348,8 @@ class UI extends React.PureComponent { } componentWillMount () { + window.addEventListener('focus', this.handleWindowFocus, false); + window.addEventListener('blur', this.handleWindowBlur, false); window.addEventListener('beforeunload', this.handleBeforeUnload, false); document.addEventListener('dragenter', this.handleDragEnter, false); @@ -330,7 +379,10 @@ class UI extends React.PureComponent { } componentWillUnmount () { + window.removeEventListener('focus', this.handleWindowFocus); + window.removeEventListener('blur', this.handleWindowBlur); window.removeEventListener('beforeunload', this.handleBeforeUnload); + document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('drop', this.handleDrop); @@ -489,6 +541,7 @@ class UI extends React.PureComponent { <LoadingBarContainer className='loading-bar' /> <ModalContainer /> <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> + <DocumentTitle /> </div> </HotKeys> ); diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 6e8ed163a..986efda1e 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -90,6 +90,10 @@ export function FavouritedStatuses () { return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses'); } +export function BookmarkedStatuses () { + return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses'); +} + export function Blocks () { return import(/* webpackChunkName: "features/blocks" */'../../blocks'); } @@ -106,6 +110,10 @@ export function MuteModal () { return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal'); } +export function BlockModal () { + return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal'); +} + export function ReportModal () { return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); } @@ -133,3 +141,15 @@ export function ListAdder () { export function Search () { return import(/*webpackChunkName: "features/search" */'../../search'); } + +export function Tesseract () { + return import(/*webpackChunkName: "tesseract" */'tesseract.js'); +} + +export function Audio () { + return import(/* webpackChunkName: "features/audio" */'../../audio'); +} + +export function Directory () { + return import(/* webpackChunkName: "features/directory" */'../../directory'); +} diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index b0c408527..f6aeb8c9a 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -5,7 +5,7 @@ import { fromJS, is } from 'immutable'; import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; -import { displayMedia } from '../../initial_state'; +import { displayMedia, useBlurhash } from '../../initial_state'; import Icon from 'mastodon/components/icon'; import { decode } from 'blurhash'; @@ -19,9 +19,10 @@ const messages = defineMessages({ close: { id: 'video.close', defaultMessage: 'Close video' }, fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, + download: { id: 'video.download', defaultMessage: 'Download file' }, }); -const formatTime = secondsNum => { +export const formatTime = secondsNum => { let hours = Math.floor(secondsNum / 3600); let minutes = Math.floor((secondsNum - (hours * 3600)) / 60); let seconds = secondsNum - (hours * 3600) - (minutes * 60); @@ -101,6 +102,7 @@ class Video extends React.PureComponent { onCloseVideo: PropTypes.func, detailed: PropTypes.bool, inline: PropTypes.bool, + editable: PropTypes.bool, cacheWidth: PropTypes.func, visible: PropTypes.bool, onToggleVisibility: PropTypes.func, @@ -298,6 +300,8 @@ class Video extends React.PureComponent { } _decode () { + if (!useBlurhash) return; + const hash = this.props.blurhash; const pixels = decode(hash, 32, 32); @@ -373,7 +377,7 @@ class Video extends React.PureComponent { } render () { - const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link } = this.props; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; @@ -411,7 +415,7 @@ class Video extends React.PureComponent { return ( <div role='menuitem' - className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen })} + className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable })} style={playerStyle} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} @@ -421,7 +425,7 @@ class Video extends React.PureComponent { > <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} /> - {revealed && <video + {(revealed || editable) && <video ref={this.setVideoRef} src={src} poster={preview} @@ -443,7 +447,7 @@ class Video extends React.PureComponent { onVolumeChange={this.handleVolumeChange} />} - <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}> + <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> <span className='spoiler-button__overlay__label'>{warning}</span> </button> @@ -463,10 +467,11 @@ class Video extends React.PureComponent { <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> + <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> <span className={classNames('video-player__volume__handle')} @@ -487,10 +492,16 @@ class Video extends React.PureComponent { </div> <div className='video-player__buttons right'> - {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} + {(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} + <button type='button' aria-label={intl.formatMessage(messages.download)}> + <a className='video-player__download__icon' href={this.props.src} download> + <Icon id={'download'} fixedWidth /> + </a> + </button> <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> + </div> </div> </div> diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 4e0ecef94..847d29dea 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -21,5 +21,10 @@ export const mascot = getMeta('mascot'); export const profile_directory = getMeta('profile_directory'); export const isStaff = getMeta('is_staff'); export const forceSingleColumn = !getMeta('advanced_layout'); +export const useBlurhash = getMeta('use_blurhash'); +export const usePendingItems = getMeta('use_pending_items'); +export const showTrends = getMeta('trends'); +export const title = getMeta('title'); +export const cropImages = getMeta('crop_images'); export default initialState; diff --git a/app/javascript/mastodon/load_keyboard_extensions.js b/app/javascript/mastodon/load_keyboard_extensions.js new file mode 100644 index 000000000..2dd0e45fa --- /dev/null +++ b/app/javascript/mastodon/load_keyboard_extensions.js @@ -0,0 +1,16 @@ +// On KaiOS, we may not be able to use a mouse cursor or navigate using Tab-based focus, so we install +// special left/right focus navigation keyboard listeners, at least on public pages (i.e. so folks +// can at least log in using KaiOS devices). + +function importArrowKeyNavigation() { + return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation'); +} + +export default function loadKeyboardExtensions() { + if (/KAIOS/.test(navigator.userAgent)) { + return importArrowKeyNavigation().then(arrowKeyNav => { + arrowKeyNav.register(); + }); + } + return Promise.resolve(); +} diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 28a18c92d..e65c81e64 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -4,39 +4,45 @@ "account.block": "حظر @{name}", "account.block_domain": "إخفاء كل شيئ قادم من اسم النطاق {domain}", "account.blocked": "محظور", + "account.cancel_follow_request": "إلغاء طلب المتابَعة", "account.direct": "رسالة خاصة إلى @{name}", "account.domain_blocked": "النطاق مخفي", - "account.edit_profile": "تعديل الملف الشخصي", - "account.endorse": "خاصّية على الملف الشخصي", + "account.edit_profile": "تعديل الملف التعريفي", + "account.endorse": "أوصِ به على صفحتك", "account.follow": "تابِع", - "account.followers": "متابعون", + "account.followers": "مُتابِعون", "account.followers.empty": "لا أحد يتبع هذا الحساب بعد.", "account.follows": "يتبع", "account.follows.empty": "هذا الحساب لا يتبع أحدًا بعد.", "account.follows_you": "يتابعك", "account.hide_reblogs": "إخفاء ترقيات @{name}", + "account.last_status": "آخر نشاط", "account.link_verified_on": "تم التحقق مِن مِلْكية هذا الرابط بتاريخ {date}", "account.locked_info": "تم تأمين خصوصية هذا الحساب عبر قفل. صاحب الحساب يُراجِع يدويا طلبات المتابَعة و الاشتراك بحسابه.", "account.media": "وسائط", - "account.mention": "أُذكُر/ي @{name}", + "account.mention": "أذكُر @{name}", "account.moved_to": "{name} انتقل إلى:", - "account.mute": "كتم @{name}", + "account.mute": "أكتم @{name}", "account.mute_notifications": "كتم الإخطارات من @{name}", "account.muted": "مكتوم", - "account.posts": "التبويقات", - "account.posts_with_replies": "التبويقات و الردود", - "account.report": "أبلغ/ي عن @{name}", + "account.never_active": "أبدا", + "account.posts": "تبويقات", + "account.posts_with_replies": "التبويقات والردود", + "account.report": "ابلِغ عن @{name}", "account.requested": "في انتظار الموافقة. اضْغَطْ/ي لإلغاء طلب المتابعة", - "account.share": "مشاركة حساب @{name}", - "account.show_reblogs": "عرض ترقيات @{name}", + "account.share": "شارك ملف تعريف @{name}", + "account.show_reblogs": "اعرض ترقيات @{name}", "account.unblock": "إلغاء الحظر عن @{name}", "account.unblock_domain": "فك الخْفى عن {domain}", - "account.unendorse": "إزالة ترويجه مِن الملف الشخصي", + "account.unendorse": "أزل ترويجه مِن الملف التعريفي", "account.unfollow": "إلغاء المتابعة", "account.unmute": "إلغاء الكتم عن @{name}", "account.unmute_notifications": "إلغاء كتم إخطارات @{name}", + "alert.rate_limited.message": "يرجى إعادة المحاولة بعد {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.", "alert.unexpected.title": "المعذرة!", + "autosuggest_hashtag.per_week": "{count} في الأسبوع", "boost_modal.combo": "يمكنك/ي ضغط {combo} لتخطّي هذه في المرّة القادمة", "bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.", "bundle_column_error.retry": "إعادة المحاولة", @@ -45,8 +51,9 @@ "bundle_modal_error.message": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.", "bundle_modal_error.retry": "إعادة المحاولة", "column.blocks": "الحسابات المحجوبة", - "column.community": "التَسَلْسُل الزَمني المحلي", + "column.community": "الخيط العام المحلي", "column.direct": "الرسائل المباشرة", + "column.directory": "استعراض الملفات التعريفية", "column.domain_blocks": "النطاقات المخفية", "column.favourites": "المفضلة", "column.follow_requests": "طلبات المتابعة", @@ -93,14 +100,25 @@ "confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟", "confirmations.domain_block.confirm": "إخفاء اسم النطاق كاملا", "confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.", + "confirmations.logout.confirm": "خروج", + "confirmations.logout.message": "متأكد من أنك تريد الخروج؟", "confirmations.mute.confirm": "أكتم", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟", "confirmations.redraft.confirm": "إزالة و إعادة الصياغة", - "confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته ؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.", + "confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.", "confirmations.reply.confirm": "رد", "confirmations.reply.message": "الرد في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد كتابتها. متأكد من أنك تريد المواصلة؟", "confirmations.unfollow.confirm": "إلغاء المتابعة", "confirmations.unfollow.message": "متأكد من أنك تريد إلغاء متابعة {name} ؟", + "conversation.delete": "احذف المحادثة", + "conversation.mark_as_read": "اعتبرها كمقروءة", + "conversation.open": "اعرض المحادثة", + "conversation.with": "بـ {names}", + "directory.federated": "مِن الفديفرس المعروف", + "directory.local": "مِن {domain} فقط", + "directory.new_arrivals": "الوافدون الجُدد", + "directory.recently_active": "نشط مؤخرا", "embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.", "embed.preview": "هكذا ما سوف يبدو عليه:", "emoji_button.activity": "الأنشطة", @@ -109,18 +127,18 @@ "emoji_button.food": "الطعام والشراب", "emoji_button.label": "أدرج إيموجي", "emoji_button.nature": "الطبيعة", - "emoji_button.not_found": "لا إيموجو !! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "لا إيموجو!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "أشياء", "emoji_button.people": "الناس", "emoji_button.recent": "الشائعة الاستخدام", "emoji_button.search": "ابحث...", "emoji_button.search_results": "نتائج البحث", "emoji_button.symbols": "رموز", - "emoji_button.travel": "أماكن و أسفار", + "emoji_button.travel": "الأماكن والسفر", "empty_column.account_timeline": "ليس هناك تبويقات!", - "empty_column.account_unavailable": "الملف الشخصي غير متوفر", + "empty_column.account_unavailable": "الملف التعريفي غير متوفر", "empty_column.blocks": "لم تقم بحظر أي مستخدِم بعد.", - "empty_column.community": "الخط الزمني المحلي فارغ. أكتب شيئا ما للعامة كبداية!", + "empty_column.community": "الخط العام المحلي فارغ. أكتب شيئا ما للعامة كبداية!", "empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.", "empty_column.domain_blocks": "ليس هناك نطاقات مخفية بعد.", "empty_column.favourited_statuses": "ليس لديك أية تبويقات مفضلة بعد. عندما ستقوم بالإعجاب بواحد، سيظهر هنا.", @@ -134,10 +152,14 @@ "empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.", "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", "empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "الإبلاغ عن خلل", "follow_request.authorize": "ترخيص", "follow_request.reject": "رفض", "getting_started.developers": "المُطوِّرون", - "getting_started.directory": "دليل المستخدِمين والمستخدِمات", + "getting_started.directory": "دليل الصفحات التعريفية", "getting_started.documentation": "الدليل", "getting_started.heading": "استعدّ للبدء", "getting_started.invite": "دعوة أشخاص", @@ -153,9 +175,9 @@ "hashtag.column_settings.tag_mode.any": "أي كان مِن هذه", "hashtag.column_settings.tag_mode.none": "لا شيء مِن هذه", "hashtag.column_settings.tag_toggle": "إدراج الوسوم الإضافية لهذا العمود", - "home.column_settings.basic": "أساسية", - "home.column_settings.show_reblogs": "عرض الترقيات", - "home.column_settings.show_replies": "عرض الردود", + "home.column_settings.basic": "الأساسية", + "home.column_settings.show_reblogs": "اعرض الترقيات", + "home.column_settings.show_replies": "اعرض الردود", "intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}", "intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}", "intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}", @@ -164,8 +186,8 @@ "introduction.federation.federated.text": "كافة المنشورات التي نُشِرت إلى العامة على الخوادم الأخرى للفديفرس سوف يتم عرضها على الخيط المُوحَّد.", "introduction.federation.home.headline": "الرئيسي", "introduction.federation.home.text": "سوف تُعرَض منشورات الأشخاص الذين تُتابِعهم على الخيط الرئيسي. بإمكانك متابعة أي حساب أيا كان الخادم الذي هو عليه!", - "introduction.federation.local.headline": "المحلي", - "introduction.federation.local.text": "المنشورات المُوجّهة للعامة على نفس الخادم الذي أنتم عليه ستظهر على الخيط الزمني المحلي.", + "introduction.federation.local.headline": "الخيط العام المحلي", + "introduction.federation.local.text": "المنشورات المُوجّهة للعامة على نفس الخادم الذي أنتم عليه ستظهر على الخيط العام المحلي.", "introduction.interactions.action": "إنهاء العرض التوضيحي!", "introduction.interactions.favourite.headline": "الإضافة إلى المفضلة", "introduction.interactions.favourite.text": "يمكِنك إضافة أي تبويق إلى المفضلة و إعلام صاحبه أنك أعجِبت بذاك التبويق.", @@ -175,7 +197,7 @@ "introduction.interactions.reply.text": "يمكنكم الرد على تبويقاتكم و تبويقات الآخرين على شكل سلسلة محادثة.", "introduction.welcome.action": "هيا بنا!", "introduction.welcome.headline": "الخطوات الأولى", - "introduction.welcome.text": "مرحبا بكم على الفديفرس! بعد لحظات قليلة ، سيكون بمقدوركم بث رسائل والتحدث إلى أصدقائكم عبر تشكيلة واسعة من الخوادم المختلفة. هذا الخادم ، {domain} ، يستضيف ملفكم الشخصي ، لذا يجب تذكر اسمه جيدا.", + "introduction.welcome.text": "مرحبا بكم على الفديفرس! بعد لحظات قليلة ، سيكون بمقدوركم بث رسائل والتحدث إلى أصدقائكم عبر تشكيلة واسعة من الخوادم المختلفة. هذا الخادم ، {domain} ، يستضيف صفحتكم التعريفية ، لذا يجب تذكر اسمه جيدا.", "keyboard_shortcuts.back": "للعودة", "keyboard_shortcuts.blocked": "لفتح قائمة المستخدمين المحظورين", "keyboard_shortcuts.boost": "للترقية", @@ -192,13 +214,13 @@ "keyboard_shortcuts.home": "لفتح الخيط الرئيسي", "keyboard_shortcuts.hotkey": "مفتاح الاختصار", "keyboard_shortcuts.legend": "لعرض هذا المفتاح", - "keyboard_shortcuts.local": "لفتح الخيط الزمني المحلي", + "keyboard_shortcuts.local": "لفتح الخيط العام المحلي", "keyboard_shortcuts.mention": "لذِكر الناشر", "keyboard_shortcuts.muted": "لفتح قائمة المستخدِمين المكتومين", - "keyboard_shortcuts.my_profile": "لفتح ملفك الشخصي", + "keyboard_shortcuts.my_profile": "لفتح ملفك التعريفي", "keyboard_shortcuts.notifications": "لفتح عمود الإشعارات", "keyboard_shortcuts.pinned": "لفتح قائمة التبويقات المدبسة", - "keyboard_shortcuts.profile": "لفتح رابط الناشر", + "keyboard_shortcuts.profile": "لفتح الملف التعريفي للناشر", "keyboard_shortcuts.reply": "للردّ", "keyboard_shortcuts.requests": "لفتح قائمة طلبات المتابعة", "keyboard_shortcuts.search": "للتركيز على البحث", @@ -215,15 +237,16 @@ "lists.account.add": "أضف إلى القائمة", "lists.account.remove": "احذف من القائمة", "lists.delete": "احذف القائمة", - "lists.edit": "تعديل القائمة", + "lists.edit": "عدّل القائمة", "lists.edit.submit": "تعديل العنوان", "lists.new.create": "إنشاء قائمة", "lists.new.title_placeholder": "عنوان القائمة الجديدة", "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها", "lists.subheading": "قوائمك", - "loading_indicator.label": "تحميل ...", + "load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}", + "loading_indicator.label": "تحميل...", "media_gallery.toggle_visible": "عرض / إخفاء", - "missing_indicator.label": "تعذر العثور عليه", + "missing_indicator.label": "غير موجود", "missing_indicator.sublabel": "تعذر العثور على هذا المورد", "mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟", "navigation_bar.apps": "تطبيقات الأجهزة المحمولة", @@ -233,7 +256,7 @@ "navigation_bar.direct": "الرسائل المباشِرة", "navigation_bar.discover": "اكتشف", "navigation_bar.domain_blocks": "النطاقات المخفية", - "navigation_bar.edit_profile": "تعديل الملف الشخصي", + "navigation_bar.edit_profile": "عدّل الملف التعريفي", "navigation_bar.favourites": "المفضلة", "navigation_bar.filters": "الكلمات المكتومة", "navigation_bar.follow_requests": "طلبات المتابعة", @@ -246,21 +269,20 @@ "navigation_bar.personal": "شخصي", "navigation_bar.pins": "التبويقات المثبتة", "navigation_bar.preferences": "التفضيلات", - "navigation_bar.profile_directory": "دليل المستخدِمين", "navigation_bar.public_timeline": "الخيط العام الموحد", "navigation_bar.security": "الأمان", "notification.favourite": "أُعجِب {name} بمنشورك", "notification.follow": "{name} يتابعك", "notification.mention": "{name} ذكرك", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "لقد إنتها تصويت شاركت فيه", "notification.reblog": "{name} قام بترقية تبويقك", "notifications.clear": "امسح الإخطارات", "notifications.clear_confirmation": "أمتأكد من أنك تود مسح جل الإخطارات الخاصة بك و المتلقاة إلى حد الآن ؟", "notifications.column_settings.alert": "إشعارات سطح المكتب", "notifications.column_settings.favourite": "المُفَضَّلة:", - "notifications.column_settings.filter_bar.advanced": "عرض كافة الفئات", + "notifications.column_settings.filter_bar.advanced": "اعرض كافة الفئات", "notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة", - "notifications.column_settings.filter_bar.show": "عرض", + "notifications.column_settings.filter_bar.show": "اظهِره", "notifications.column_settings.follow": "متابعُون جُدُد:", "notifications.column_settings.mention": "الإشارات:", "notifications.column_settings.poll": "نتائج استطلاع الرأي:", @@ -277,8 +299,10 @@ "notifications.group": "{count} إشعارات", "poll.closed": "انتهى", "poll.refresh": "تحديث", + "poll.total_people": "{count, plural, one {# شخص} two {# شخصين} few {# أشخاص} many {# أشخاص} other {# أشخاص}}", "poll.total_votes": "{count, plural, one {# صوت} other {# أصوات}}", "poll.vote": "صَوّت", + "poll.voted": "لقد صوّتت على هذه الإجابة", "poll_button.add_poll": "إضافة استطلاع للرأي", "poll_button.remove_poll": "إزالة استطلاع الرأي", "privacy.change": "اضبط خصوصية المنشور", @@ -290,6 +314,7 @@ "privacy.public.short": "للعامة", "privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة", "privacy.unlisted.short": "غير مدرج", + "refresh": "أنعِش", "regeneration_indicator.label": "جارٍ التحميل…", "regeneration_indicator.sublabel": "جارٍ تجهيز تغذية صفحتك الرئيسية!", "relative_time.days": "{number}ي", @@ -314,7 +339,8 @@ "search_results.accounts": "أشخاص", "search_results.hashtags": "الوُسوم", "search_results.statuses": "التبويقات", - "search_results.total": "{count, number} {count, plural, one {result} و {results}}", + "search_results.statuses_fts_disabled": "البحث في التبويقات عن طريق المحتوى ليس مفعل في خادم ماستدون هذا.", + "search_results.total": "{count, number} {count, plural, zero {} one {نتيجة} two {نتيجتين} few {نتائج} many {نتائج} other {نتائج}}", "status.admin_account": "افتح الواجهة الإدارية لـ @{name}", "status.admin_status": "افتح هذا المنشور على واجهة الإشراف", "status.block": "احجب @{name}", @@ -334,7 +360,7 @@ "status.mute": "أكتم @{name}", "status.mute_conversation": "كتم المحادثة", "status.open": "وسع هذه المشاركة", - "status.pin": "تدبيس على الملف الشخصي", + "status.pin": "دبّسه على الصفحة التعريفية", "status.pinned": "تبويق مثبَّت", "status.read_more": "اقرأ المزيد", "status.reblog": "رَقِّي", @@ -352,29 +378,38 @@ "status.show_more": "أظهر المزيد", "status.show_more_all": "توسيع الكل", "status.show_thread": "الكشف عن المحادثة", + "status.uncached_media_warning": "غير متوفر", "status.unmute_conversation": "فك الكتم عن المحادثة", - "status.unpin": "فك التدبيس من الملف الشخصي", + "status.unpin": "فك التدبيس من الصفحة التعريفية", "suggestions.dismiss": "إلغاء الاقتراح", "suggestions.header": "يمكن أن يهمك…", "tabs_bar.federated_timeline": "الموحَّد", "tabs_bar.home": "الرئيسية", - "tabs_bar.local_timeline": "المحلي", + "tabs_bar.local_timeline": "الخيط العام المحلي", "tabs_bar.notifications": "الإخطارات", "tabs_bar.search": "البحث", "time_remaining.days": "{number, plural, one {# يوم} other {# أيام}} متبقية", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.hours": "{number, plural, one {# ساعة} other {# ساعات}} متبقية", + "time_remaining.minutes": "{number, plural, one {# دقيقة} other {# دقائق}} متبقية", "time_remaining.moments": "لحظات متبقية", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون", + "time_remaining.seconds": "{number, plural, one {# ثانية} other {# ثوانٍ}} متبقية", + "trends.count_by_accounts": "{count} {rawCount, plural, zero {} one {شخص واحد} two {شخصين} few {أشخاص} many {أشخاص} other {أشخاص}} تتحدّث", + "trends.trending_now": "المتداولة الآن", "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.", "upload_area.title": "اسحب ثم أفلت للرفع", - "upload_button.label": "إضافة وسائط (JPEG، PNG، GIF، WebM، MP4، MOV)", + "upload_button.label": "إضافة وسائط ({formats})", "upload_error.limit": "لقد تم بلوغ الحد الأقصى المسموح به لإرسال الملفات.", "upload_error.poll": "لا يمكن إدراج ملفات في استطلاعات الرأي.", "upload_form.description": "وصف للمعاقين بصريا", - "upload_form.focus": "قص", + "upload_form.edit": "تعديل", "upload_form.undo": "حذف", + "upload_modal.analyzing_picture": "جارٍ فحص الصورة…", + "upload_modal.apply": "طبّق", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "upload_modal.detect_text": "اكتشف النص مِن الصورة", + "upload_modal.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.preview_label": "معاينة ({ratio})", "upload_progress.label": "يرفع...", "video.close": "إغلاق الفيديو", "video.exit_fullscreen": "الخروج من وضع الشاشة المليئة", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index b911848ee..5145a6579 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -4,6 +4,7 @@ "account.block": "Bloquiar a @{name}", "account.block_domain": "Anubrir tolo de {domain}", "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Unviar un mensaxe direutu a @{name}", "account.domain_blocked": "Dominiu anubríu", "account.edit_profile": "Editar el perfil", @@ -15,6 +16,7 @@ "account.follows.empty": "Esti usuariu entá nun sigue a naide.", "account.follows_you": "Síguete", "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Media", @@ -23,10 +25,11 @@ "account.mute": "Silenciar a @{name}", "account.mute_notifications": "Mute notifications from @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Toots", "account.posts_with_replies": "Toots y rempuestes", "account.report": "Report @{name}", - "account.requested": "Awaiting approval. Click to cancel follow request", + "account.requested": "Awaiting approval", "account.share": "Share @{name}'s profile", "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Desbloquiar a @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "Asocedió un fallu inesperáu.", "alert.unexpected.title": "¡Ups!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Pues primir {combo} pa saltar esto la próxima vegada", "bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.retry": "Try again", @@ -47,6 +53,7 @@ "column.blocks": "Usuarios bloquiaos", "column.community": "Llinia temporal llocal", "column.direct": "Mensaxes direutos", + "column.directory": "Browse profiles", "column.domain_blocks": "Dominios anubríos", "column.favourites": "Favoritos", "column.follow_requests": "Solicitúes de siguimientu", @@ -64,7 +71,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Desfixar", "column_subheading.settings": "Axustes", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "Esti toot namái va unviase a los usuarios mentaos.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "¿De xuru que quies desaniciar dafechu esta llista?", "confirmations.domain_block.confirm": "Anubrir tol dominiu", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "¿De xuru que quies silenciar a {name}?", "confirmations.redraft.confirm": "Desaniciar y reeditar", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Empotra esti estáu nun sitiu web copiando'l códigu d'embaxo.", "embed.preview": "Asina ye como va vese:", "emoji_button.activity": "Actividaes", @@ -134,6 +152,10 @@ "empty_column.mutes": "Entá nun silenciesti a dengún usuariu.", "empty_column.notifications": "Entá nun tienes dengún avisu. Interactua con otros p'aniciar la conversación.", "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Autorizar", "follow_request.reject": "Refugar", "getting_started.developers": "Desendolcadores", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Títulu nuevu de la llista", "lists.search": "Guetar ente la xente que sigues", "lists.subheading": "Les tos llistes", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargando...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Nun s'alcontró", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Toots fixaos", "navigation_bar.preferences": "Preferencies", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Llinia temporal federada", "navigation_bar.security": "Seguranza", "notification.favourite": "{name} favourited your status", @@ -277,8 +299,10 @@ "notifications.group": "{count} avisos", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Adjust status privacy", @@ -290,6 +314,7 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", "regeneration_indicator.label": "Cargando…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "Xente", "search_results.hashtags": "Etiquetes", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Amosar más", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Desfixar del perfil", "suggestions.dismiss": "Dismiss suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "El borrador va perdese si coles de Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Descripción pa discapacitaos visuales", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Desaniciar", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Xubiendo...", "video.close": "Zarrar el videu", "video.exit_fullscreen": "Colar de la pantalla completa", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 783f9eb68..2ec155362 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -1,20 +1,22 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", - "account.badges.bot": "Bot", + "account.add_or_remove_from_list": "Добави или премахни от списъците", + "account.badges.bot": "бот", "account.block": "Блокирай", - "account.block_domain": "Hide everything from {domain}", - "account.blocked": "Blocked", + "account.block_domain": "скрий всичко от (домейн)", + "account.blocked": "Блокирани", + "account.cancel_follow_request": "Откажи искането за следване", "account.direct": "Direct Message @{name}", - "account.domain_blocked": "Domain hidden", + "account.domain_blocked": "Скрит домейн", "account.edit_profile": "Редактирай профила си", - "account.endorse": "Feature on profile", + "account.endorse": "Характеристика на профила", "account.follow": "Последвай", "account.followers": "Последователи", - "account.followers.empty": "No one follows this user yet.", + "account.followers.empty": "Все още никой не следва този потребител.", "account.follows": "Следвам", - "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows.empty": "Този потребител все още не следва никого.", "account.follows_you": "Твой последовател", "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Последно активен/а", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Mute @{name}", "account.mute_notifications": "Mute notifications from @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Публикации", "account.posts_with_replies": "Toots with replies", "account.report": "Report @{name}", @@ -35,11 +38,14 @@ "account.unfollow": "Не следвай", "account.unmute": "Unmute @{name}", "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", - "bundle_column_error.retry": "Try again", + "bundle_column_error.retry": "Опитай отново", "bundle_column_error.title": "Network error", "bundle_modal_error.close": "Close", "bundle_modal_error.message": "Something went wrong while loading this component.", @@ -47,11 +53,12 @@ "column.blocks": "Blocked users", "column.community": "Local timeline", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "Favourites", "column.follow_requests": "Follow requests", "column.home": "Начало", - "column.lists": "Lists", + "column.lists": "Списъци", "column.mutes": "Muted users", "column.notifications": "Известия", "column.pins": "Pinned toot", @@ -64,7 +71,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Embed this status on your website by copying the code below.", "embed.preview": "Here is what it will look like:", "emoji_button.activity": "Activity", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Зареждане...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Предпочитания", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Публичен канал", "navigation_bar.security": "Security", "notification.favourite": "{name} хареса твоята публикация", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Adjust status privacy", @@ -290,6 +314,7 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Show more", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", "suggestions.dismiss": "Dismiss suggestion", @@ -367,15 +394,23 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Добави медия", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Отмяна", - "upload_progress.label": "Uploading...", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", "video.close": "Close video", "video.exit_fullscreen": "Exit full screen", "video.expand": "Expand video", diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json index 93e7eedf9..5cf5f7a76 100644 --- a/app/javascript/mastodon/locales/bn.json +++ b/app/javascript/mastodon/locales/bn.json @@ -1,12 +1,13 @@ { - "account.add_or_remove_from_list": "তালিকাতে আরো যুক্ত বা মুছে ফেলুন", - "account.badges.bot": "রোবট", - "account.block": "@{name} বন্ধ করুন", - "account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন", - "account.blocked": "বন্ধ করা হয়েছে", - "account.direct": "@{name}কে সরকারি লিখুন", - "account.domain_blocked": "ওয়েবসাইট সরিয়ে ফেলা হয়েছে", - "account.edit_profile": "নিজের পাতা সম্পাদনা করুন", + "account.add_or_remove_from_list": "তালিকা হতে মুছুন অথবা যুক্ত করুন", + "account.badges.bot": "বট", + "account.block": "@{name} কে ব্লক করুন", + "account.block_domain": "{domain} থেকে সব লুকান", + "account.blocked": "ব্লককৃত", + "account.cancel_follow_request": "অসুসারণ অনুরোধ বাতিল করুন", + "account.direct": "@{name} কে সরাসরি বার্তা", + "account.domain_blocked": "ডোমেন লুকানো আছে", + "account.edit_profile": "প্রোফাইল সম্পাদন করুন", "account.endorse": "নিজের পাতায় দেখান", "account.follow": "অনুসরণ করুন", "account.followers": "অনুসরণকারক", @@ -14,40 +15,46 @@ "account.follows": "যাদেরকে অনুসরণ করেন", "account.follows.empty": "এই ব্যবহারকারী কাওকে এখনো অনুসরণ করেন না।", "account.follows_you": "আপনাকে অনুসরণ করে", - "account.hide_reblogs": "@{name}র সমর্থনগুলি সরিয়ে ফেলুন", + "account.hide_reblogs": "@{name}'র সমর্থনগুলি লুকিয়ে ফেলুন", + "account.last_status": "শেষ সক্রিয় ছিল", "account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে", "account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।", - "account.media": "ছবি বা ভিডিও", + "account.media": "মিডিয়া", "account.mention": "@{name} কে উল্লেখ করুন", - "account.moved_to": "{name} চলে গেছে এখানে:", - "account.mute": "@{name}র কার্যক্রম সরিয়ে ফেলুন", + "account.moved_to": "{name} কে এখানে সরানো হয়েছে:", + "account.mute": "@{name} কে নিঃশব্দ করুন", "account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন", "account.muted": "সরানো আছে", + "account.never_active": "কখনও নয়", "account.posts": "টুট", "account.posts_with_replies": "টুট এবং মতামত", - "account.report": "@{name}কে রিপোর্ট করে দিন", + "account.report": "@{name} কে রিপোর্ট করতে", "account.requested": "অনুমতির অপেক্ষায় আছে। অনুসরণ করার অনুরোধ বাতিল করতে এখানে ক্লিক করুন", "account.share": "@{name}র পাতা অন্যদের দেখান", "account.show_reblogs": "@{name}র সমর্থনগুলো দেখুন", "account.unblock": "@{name}র কার্যকলাপ আবার দেখুন", - "account.unblock_domain": "{domain}থেকে আবার দেখুন", - "account.unendorse": "নিজের পাতায় এটা দেখতে চান না", - "account.unfollow": "অনুসরণ বন্ধ করুন", + "account.unblock_domain": "{domain} থেকে আবার দেখুন", + "account.unendorse": "আপনার নিজের পাতায় এটা না দেখাতে", + "account.unfollow": "অনুসরণ না করতে", "account.unmute": "@{name}র কার্যকলাপ আবার দেখুন", "account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন", + "alert.rate_limited.message": "{retry_time, time, medium} -এর পরে আবার প্রচেষ্টা করুন।", + "alert.rate_limited.title": "হার সীমিত", "alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।", "alert.unexpected.title": "ওহো!", + "autosuggest_hashtag.per_week": "প্রতি সপ্তাহে {count}", "boost_modal.combo": "পরেরবার আপনি {combo} চাপ দিলে এটার শেষে চলে যেতে পারবেন", "bundle_column_error.body": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।", "bundle_column_error.retry": "আবার চেষ্টা করুন", "bundle_column_error.title": "নেটওয়ার্কের সমস্যা হচ্ছে", "bundle_modal_error.close": "বন্ধ করুন", - "bundle_modal_error.message": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।", + "bundle_modal_error.message": "এই অংশটি দেখাতে যেয়ে কোনো সমস্যা হয়েছে।", "bundle_modal_error.retry": "আবার চেষ্টা করুন", - "column.blocks": "যাদের বন্ধ করে রাখা হয়েছে", + "column.blocks": "যাদের ব্লক করে রাখা হয়েছে", "column.community": "স্থানীয় সময়সারি", "column.direct": "সরাসরি লেখা", - "column.domain_blocks": "সরিয়ে ফেলা ওয়েবসাইট", + "column.directory": "প্রোফাইল ব্রাউজ করুন", + "column.domain_blocks": "লুকোনো ডোমেনগুলি", "column.favourites": "পছন্দের গুলো", "column.follow_requests": "অনুসরণের অনুমতি চেয়েছে যারা", "column.home": "বাড়ি", @@ -77,30 +84,41 @@ "compose_form.poll.remove_option": "এই বিকল্পটি মুছে ফেলুন", "compose_form.publish": "টুট", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে", "compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে", "compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি", - "compose_form.spoiler.marked": "লেখাটি সাবধানতার পেছনে লুকানো আছে", + "compose_form.spoiler.marked": "সতর্কতার পিছনে লেখানটি লুকানো আছে", "compose_form.spoiler.unmarked": "লেখাটি লুকানো নেই", - "compose_form.spoiler_placeholder": "আপনার সাবধানতা এখানে লিখুন", + "compose_form.spoiler_placeholder": "আপনার লেখা দেখার সাবধানবাণী লিখুন", "confirmation_modal.cancel": "বাতিল করুন", - "confirmations.block.block_and_report": "বন্ধ করুন এবং রিপোর্ট করুন", - "confirmations.block.confirm": "বন্ধ করুন", - "confirmations.block.message": "আপনি কি নিশ্চিত {name} কে বন্ধ করতে চান ?", + "confirmations.block.block_and_report": "ব্লক করুন এবং রিপোর্ট করুন", + "confirmations.block.confirm": "ব্লক করুন", + "confirmations.block.message": "আপনি কি নিশ্চিত {name} কে ব্লক করতে চান?", "confirmations.delete.confirm": "মুছে ফেলুন", "confirmations.delete.message": "আপনি কি নিশ্চিত যে এই লেখাটি মুছে ফেলতে চান ?", "confirmations.delete_list.confirm": "মুছে ফেলুন", "confirmations.delete_list.message": "আপনি কি নিশ্চিত যে আপনি এই তালিকাটি স্থায়িভাবে মুছে ফেলতে চান ?", - "confirmations.domain_block.confirm": "এই ওয়েবসাইট থেকে সব সরান", - "confirmations.domain_block.message": "আপনি কি সত্যি সত্যি নিশ্চিত যে {domain} ওয়েবসাইট থেকে সব সরাতে চান ? সাধারণত কিছু লক্ষ্যবস্তু বন্ধ এবং সরানোযা যথেষ্ট। নিশ্চিত করলে ওই ওয়েবসাইট থেকে কোনোকিছু কোনখানে দেখবেন না। যারা আপনাকে অনুসরণ করে ওই ওয়েবসাইট থেকে তাদেরকেও মুছে ফেলা হবে।", + "confirmations.domain_block.confirm": "এই ডোমেন থেকে সব লুকান", + "confirmations.domain_block.message": "আপনি কি সত্যিই সত্যই নিশ্চিত যে আপনি পুরো {domain}'টি ব্লক করতে চান? বেশিরভাগ ক্ষেত্রে কয়েকটি লক্ষ্যযুক্ত ব্লক বা নীরবতা যথেষ্ট এবং পছন্দসই। আপনি কোনও পাবলিক টাইমলাইন বা আপনার বিজ্ঞপ্তিগুলিতে সেই ডোমেন থেকে সামগ্রী দেখতে পাবেন না। সেই ডোমেন থেকে আপনার অনুসরণকারীদের সরানো হবে।", + "confirmations.logout.confirm": "প্রস্থান", + "confirmations.logout.message": "আপনি লগ আউট করতে চান?", "confirmations.mute.confirm": "সরিয়ে ফেলুন", + "confirmations.mute.explanation": "এটি তাদের কাছ থেকে পোস্ট এবং তাদেরকে মেনশন করা পোস্টগুলি হাইড করবে, তবুও তাদেরকে এটি আপনার পোস্ট গুলো দেখতে দিবে ও তারা আপনাকে অনুসরন করতে পারবে।.", "confirmations.mute.message": "আপনি কি নিশ্চিত {name} সরিয়ে ফেলতে চান ?", "confirmations.redraft.confirm": "মুছে ফেলুন এবং আবার সম্পাদন করুন", "confirmations.redraft.message": "আপনি কি নিশ্চিত এটি মুছে ফেলে এবং আবার সম্পাদন করতে চান ? এটাতে যা পছন্দিত, সমর্থন বা মতামত আছে সেগুলো নতুন লেখার সাথে যুক্ত থাকবে না।", "confirmations.reply.confirm": "মতামত", "confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?", - "confirmations.unfollow.confirm": "অনুসরণ বন্ধ করুন", + "confirmations.unfollow.confirm": "অনুসরণ করা বাতিল করতে", "confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?", + "conversation.delete": "কথোপকথন মুছে ফেলুন", + "conversation.mark_as_read": "পঠিত হিসেবে চিহ্নিত করুন", + "conversation.open": "কথপোকথন দেখান", + "conversation.with": "{names} এর সঙ্গে", + "directory.federated": "পরিচিত ফেডিভারসের থেকে", + "directory.local": "শুধু {domain} থেকে", + "directory.new_arrivals": "নতুন আগত", + "directory.recently_active": "সম্প্রতি সক্রিয়", "embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।", "embed.preview": "সেটা দেখতে এরকম হবে:", "emoji_button.activity": "কার্যকলাপ", @@ -119,29 +137,33 @@ "emoji_button.travel": "ভ্রমণ এবং স্থান", "empty_column.account_timeline": "এখানে কোনো টুট নেই!", "empty_column.account_unavailable": "নিজস্ব পাতা নেই", - "empty_column.blocks": "আপনি কোনো ব্যবহারকারীদের বন্ধ করেন নি।", + "empty_column.blocks": "আপনি কোনো ব্যবহারকারীদের ব্লক করেন নি।", "empty_column.community": "স্থানীয় সময়রেখাতে কিছু নেই। প্রকাশ্যভাবে কিছু লিখে লেখালেখির উদ্বোধন করে ফেলুন!", "empty_column.direct": "আপনার কাছে সরাসরি পাঠানো কোনো লেখা নেই। যদি কেও পাঠায়, সেটা এখানে দেখা যাবে।", - "empty_column.domain_blocks": "এখনো কোনো সরানো ওয়েবসাইট নেই।", + "empty_column.domain_blocks": "এখনও কোনও লুকানো ডোমেন নেই।", "empty_column.favourited_statuses": "আপনার পছন্দের কোনো টুট এখনো নেই। আপনি কোনো লেখা পছন্দের হিসেবে চিহ্নিত করলে এখানে পাওয়া যাবে।", "empty_column.favourites": "কেও এখনো এটাকে পছন্দের টুট হিসেবে চিহ্নিত করেনি। যদি করে, তখন তাদের এখানে পাওয়া যাবে।", "empty_column.follow_requests": "আপনার এখনো কোনো অনুসরণের আবেদন পাঠানো নেই। যদি পাঠায়, এখানে পাওয়া যাবে।", "empty_column.hashtag": "এই হেসটাগে এখনো কিছু নেই।", - "empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public}এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।", + "empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public} এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।", "empty_column.home.public_timeline": "প্রকাশ্য সময়রেখা", "empty_column.list": "এই তালিকাতে এখনো কিছু নেই. যখন এই তালিকায় থাকা ব্যবহারকারী নতুন কিছু লিখবে, সেগুলো এখানে পাওয়া যাবে।", "empty_column.lists": "আপনার এখনো কোনো তালিকা তৈরী নেই। যদি বা যখন তৈরী করেন, সেগুলো এখানে পাওয়া যাবে।", - "empty_column.mutes": "আপনি এখনো কোনো ব্যবহারকারীকে সরাননি।", + "empty_column.mutes": "আপনি এখনো কোনো ব্যবহারকারীকে নিঃশব্দ করেননি।", "empty_column.notifications": "আপনার এখনো কোনো প্রজ্ঞাপন নেই। কথোপকথন শুরু করতে, অন্যদের সাথে মেলামেশা করতে পারেন।", "empty_column.public": "এখানে এখনো কিছু নেই! প্রকাশ্য ভাবে কিছু লিখুন বা অন্য সার্ভার থেকে কাওকে অনুসরণ করে এই জায়গা ভরে ফেলুন", + "error.unexpected_crash.explanation": "আমাদের কোড বা ব্রাউজারের সামঞ্জস্য ইস্যুতে একটি বাগের কারণে এই পৃষ্ঠাটি সঠিকভাবে প্রদর্শিত করা যায় নি।", + "error.unexpected_crash.next_steps": "পাতাটি রিফ্রেশ করে চেষ্টা করুন। তবুও যদি না হয়, তবে আপনি অন্য একটি ব্রাউজার অথবা আপনার ডিভাইসের জন্যে এপের মাধ্যমে মাস্টডন ব্যাবহার করতে পারবেন।.", + "errors.unexpected_crash.copy_stacktrace": "স্টেকট্রেস ক্লিপবোর্ডে কপি করুন", + "errors.unexpected_crash.report_issue": "সমস্যার প্রতিবেদন করুন", "follow_request.authorize": "অনুমতি দিন", "follow_request.reject": "প্রত্যাখ্যান করুন", "getting_started.developers": "তৈরিকারকদের জন্য", - "getting_started.directory": "নিজস্ব পাতার তালিকা", + "getting_started.directory": "নিজস্ব-পাতাগুলির তালিকা", "getting_started.documentation": "নথিপত্র", "getting_started.heading": "শুরু করা", "getting_started.invite": "অন্যদের আমন্ত্রণ করুন", - "getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। আপনি তৈরিতে সাহায্য করতে পারেন অথবা সমস্যা রিপোর্ট করতে পারেন গিটহাবে {github}।", + "getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। তৈরিতে সাহায্য করতে বা কোনো সমস্যা সম্পর্কে জানাতে আমাদের গিটহাবে যেতে পারেন {github}।", "getting_started.security": "নিরাপত্তা", "getting_started.terms": "ব্যবহারের নিয়মাবলী", "hashtag.column_header.tag_mode.all": "এবং {additional}", @@ -152,13 +174,13 @@ "hashtag.column_settings.tag_mode.all": "এগুলো সব", "hashtag.column_settings.tag_mode.any": "এর ভেতরে যেকোনোটা", "hashtag.column_settings.tag_mode.none": "এগুলোর একটাও না", - "hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করুন", + "hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করতে", "home.column_settings.basic": "সাধারণ", "home.column_settings.show_reblogs": "সমর্থনগুলো দেখান", "home.column_settings.show_replies": "মতামত দেখান", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}}", "introduction.federation.action": "পরবর্তী", "introduction.federation.federated.headline": "যুক্তবিশ্ব", "introduction.federation.federated.text": "অন্যান্য যুক্তবিশ্বের সার্ভারের লেখাগুলি যুক্তবিশ্বের সময়রেখাতে আসবে ।", @@ -177,7 +199,7 @@ "introduction.welcome.headline": "প্রথম ধাপ", "introduction.welcome.text": "যুক্তবিশ্বে স্বাগতম! কিছুক্ষনের মধ্যেই আপনি আপনার লেখা বিভিন্ন সার্ভারে সম্প্রচার করতে পারবেন। কিন্তু মনে রাখবে যে এটা একটা বিশেষ সার্ভার, {domain} কারণ এখানে আপনার নিজেস্ব পাতা রাখা হচ্ছে।", "keyboard_shortcuts.back": "পেছনে যেতে", - "keyboard_shortcuts.blocked": "বন্ধ করা ব্যবহারকারীদের তালিকা দেখতে", + "keyboard_shortcuts.blocked": "ব্লক করা ব্যবহারকারীদের তালিকা খুলতে", "keyboard_shortcuts.boost": "সমর্থন করতে", "keyboard_shortcuts.column": "কোনো কলামএ কোনো লেখা ফোকাস করতে", "keyboard_shortcuts.compose": "লেখা সম্পদনার জায়গায় ফোকাস করতে", @@ -195,7 +217,7 @@ "keyboard_shortcuts.local": "স্থানীয় সময়রেখাতে যেতে", "keyboard_shortcuts.mention": "লেখককে উল্লেখ করতে", "keyboard_shortcuts.muted": "বন্ধ করা ব্যবহারকারীদের তালিকা খুলতে", - "keyboard_shortcuts.my_profile": "নিজের পাতা দেখতে", + "keyboard_shortcuts.my_profile": "আপনার নিজের পাতা দেখতে", "keyboard_shortcuts.notifications": "প্রজ্ঞাপনের কলাম খুলতে", "keyboard_shortcuts.pinned": "পিন দেওয়া টুটের তালিকা খুলতে", "keyboard_shortcuts.profile": "লেখকের পাতা দেখতে", @@ -204,14 +226,14 @@ "keyboard_shortcuts.search": "খোঁজার অংশে ফোকাস করতে", "keyboard_shortcuts.start": "\"প্রথম শুরুর\" কলাম বের করতে", "keyboard_shortcuts.toggle_hidden": "CW লেখা দেখতে বা লুকাতে", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "ভিডিও/ছবি দেখতে বা বন্ধ করতে", "keyboard_shortcuts.toot": "নতুন একটা টুট লেখা শুরু করতে", "keyboard_shortcuts.unfocus": "লেখা বা খোঁজার জায়গায় ফোকাস না করতে", "keyboard_shortcuts.up": "তালিকার উপরের দিকে যেতে", "lightbox.close": "বন্ধ", "lightbox.next": "পরবর্তী", "lightbox.previous": "পূর্ববর্তী", - "lightbox.view_context": "View context", + "lightbox.view_context": "প্রসঙ্গটি দেখতে", "lists.account.add": "তালিকাতে যুক্ত করতে", "lists.account.remove": "তালিকা থেকে বাদ দিতে", "lists.delete": "তালিকা মুছে ফেলতে", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে", "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন", "lists.subheading": "আপনার তালিকা", + "load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}", "loading_indicator.label": "আসছে...", "media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান", "missing_indicator.label": "খুঁজে পাওয়া যায়নি", @@ -230,23 +253,22 @@ "navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী", "navigation_bar.community_timeline": "স্থানীয় সময়রেখা", "navigation_bar.compose": "নতুন টুট লিখুন", - "navigation_bar.direct": "সরাসরি লেখা", + "navigation_bar.direct": "সরাসরি লেখাগুলি", "navigation_bar.discover": "ঘুরে দেখুন", - "navigation_bar.domain_blocks": "বন্ধ করা ওয়েবসাইট", - "navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করুন", + "navigation_bar.domain_blocks": "লুকানো ডোমেনগুলি", + "navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করতে", "navigation_bar.favourites": "পছন্দের", "navigation_bar.filters": "বন্ধ করা শব্দ", "navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "অনুসরণ এবং অনুসরণকারী", "navigation_bar.info": "এই সার্ভার সম্পর্কে", - "navigation_bar.keyboard_shortcuts": "চাবি ব্যবহার", + "navigation_bar.keyboard_shortcuts": "হটকীগুলি", "navigation_bar.lists": "তালিকাগুলো", "navigation_bar.logout": "বাইরে যান", - "navigation_bar.mutes": "যেসব বেভহারকারীদের কার্যক্রম বন্ধ করা আছে", + "navigation_bar.mutes": "যাদের কার্যক্রম দেখা বন্ধ আছে", "navigation_bar.personal": "নিজস্ব", "navigation_bar.pins": "পিন দেওয়া টুট", "navigation_bar.preferences": "পছন্দসমূহ", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা", "navigation_bar.security": "নিরাপত্তা", "notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন", @@ -256,18 +278,18 @@ "notification.reblog": "{name} আপনার কার্যক্রমে সমর্থন দেখিয়েছেন", "notifications.clear": "প্রজ্ঞাপনগুলো মুছে ফেলতে", "notifications.clear_confirmation": "আপনি কি নির্চিত প্রজ্ঞাপনগুলো মুছে ফেলতে চান ?", - "notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপন", + "notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপনগুলি", "notifications.column_settings.favourite": "পছন্দের:", - "notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখতে", - "notifications.column_settings.filter_bar.category": "দ্রুত ছাঁকনি বার", - "notifications.column_settings.filter_bar.show": "দেখতে", + "notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখানো", + "notifications.column_settings.filter_bar.category": "সংক্ষিপ্ত ছাঁকনি অংশ", + "notifications.column_settings.filter_bar.show": "দেখানো", "notifications.column_settings.follow": "নতুন অনুসরণকারীরা:", "notifications.column_settings.mention": "প্রজ্ঞাপনগুলো:", "notifications.column_settings.poll": "নির্বাচনের ফলাফল:", - "notifications.column_settings.push": "পুশ প্রজ্ঞাপন", + "notifications.column_settings.push": "পুশ প্রজ্ঞাপনগুলি", "notifications.column_settings.reblog": "সমর্থনগুলো:", - "notifications.column_settings.show": "কলামে দেখান", - "notifications.column_settings.sound": "শব্দ বাজাতে", + "notifications.column_settings.show": "কলামে দেখানো", + "notifications.column_settings.sound": "শব্দ বাজানো", "notifications.filter.all": "সব", "notifications.filter.boosts": "সমর্থনগুলো", "notifications.filter.favourites": "পছন্দের গুলো", @@ -276,9 +298,11 @@ "notifications.filter.polls": "নির্বাচনের ফলাফল", "notifications.group": "{count} প্রজ্ঞাপন", "poll.closed": "বন্ধ", - "poll.refresh": "আবার সতেজ করতে", + "poll.refresh": "বদলেছে কিনা দেখতে", + "poll.total_people": "{count, plural, one {# ব্যক্তি} other {# ব্যক্তি}}", "poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}", "poll.vote": "ভোট", + "poll.voted": "আপনি এই উত্তরের পক্ষে ভোট দিয়েছেন", "poll_button.add_poll": "একটা নির্বাচন যোগ করতে", "poll_button.remove_poll": "নির্বাচন বাদ দিতে", "privacy.change": "লেখার গোপনীয়তা অবস্থা ঠিক করতে", @@ -290,12 +314,13 @@ "privacy.public.short": "সর্বজনীন প্রকাশ্য", "privacy.unlisted.long": "সর্বজনীন প্রকাশ্য সময়রেখাতে না দেখাতে", "privacy.unlisted.short": "প্রকাশ্য নয়", + "refresh": "সতেজ করা", "regeneration_indicator.label": "আসছে…", "regeneration_indicator.sublabel": "আপনার বাড়ির-সময়রেখা প্রস্তূত করা হচ্ছে!", "relative_time.days": "{number} দিন", "relative_time.hours": "{number} ঘন্টা", "relative_time.just_now": "এখন", - "relative_time.minutes": "{number}ম", + "relative_time.minutes": "{number}মিঃ", "relative_time.seconds": "{number} সেকেন্ড", "reply_indicator.cancel": "বাতিল করতে", "report.forward": "এটা আরো পাঠান {target} তে", @@ -304,7 +329,7 @@ "report.placeholder": "অন্য কোনো মন্তব্য", "report.submit": "জমা দিন", "report.target": "{target} রিপোর্ট করুন", - "search.placeholder": "খুঁজতে", + "search.placeholder": "অনুসন্ধান", "search_popout.search_format": "বিস্তারিতভাবে খোঁজার পদ্ধতি", "search_popout.tips.full_text": "সাধারণ লেখা দিয়ে খুঁজলে বের হবে সেরকম আপনার লেখা, পছন্দের লেখা, সমর্থন করা লেখা, আপনাকে উল্লেখকরা কোনো লেখা, যা খুঁজছেন সেরকম কোনো ব্যবহারকারীর নাম বা কোনো হ্যাশট্যাগগুলো।", "search_popout.tips.hashtag": "হ্যাশট্যাগ", @@ -314,21 +339,22 @@ "search_results.accounts": "মানুষ", "search_results.hashtags": "হ্যাশট্যাগগুলি", "search_results.statuses": "টুট", + "search_results.statuses_fts_disabled": "তাদের সামগ্রী দ্বারা টুটগুলি অনুসন্ধান এই মস্তোডন সার্ভারে সক্ষম নয়।", "search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}", "status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন", "status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন", - "status.block": "@{name}কে বন্ধ করুন", + "status.block": "@{name} কে ব্লক করুন", "status.cancel_reblog_private": "সমর্থন বাতিল করতে", "status.cannot_reblog": "এটিতে সমর্থন দেওয়া যাবেনা", "status.copy": "লেখাটির লিংক কপি করতে", "status.delete": "মুছে ফেলতে", "status.detailed_status": "বিস্তারিত কথোপকথনের হিসেবে দেখতে", - "status.direct": "@{name} কে সরাসরি পাঠান", + "status.direct": "@{name} কে সরাসরি লেখা পাঠাতে", "status.embed": "এমবেড করতে", "status.favourite": "পছন্দের করতে", "status.filtered": "ছাঁকনিদিত", "status.load_more": "আরো দেখুন", - "status.media_hidden": "ছবি বা ভিডিও পেছনে", + "status.media_hidden": "মিডিয়া লুকানো আছে", "status.mention": "@{name}কে উল্লেখ করতে", "status.more": "আরো", "status.mute": "@{name}র কার্যক্রম সরিয়ে ফেলতে", @@ -344,7 +370,7 @@ "status.redraft": "মুছে আবার নতুন করে লিখতে", "status.reply": "মতামত জানাতে", "status.replyAll": "লেখাযুক্ত সবার কাছে মতামত জানাতে", - "status.report": "@{name}কে রিপোর্ট করতে", + "status.report": "@{name} কে রিপোর্ট করতে", "status.sensitive_warning": "সংবেদনশীল কিছু", "status.share": "অন্যদের জানান", "status.show_less": "কম দেখতে", @@ -352,29 +378,38 @@ "status.show_more": "আরো দেখাতে", "status.show_more_all": "সবগুলোতে আরো দেখতে", "status.show_thread": "আলোচনা দেখতে", + "status.uncached_media_warning": "পাওয়া যাচ্ছে না", "status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে", "status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে", - "suggestions.dismiss": "সাহায্যের জন্য পরামর্শগুলো সরাতে", + "suggestions.dismiss": "সাহায্যের পরামর্শগুলো সরাতে", "suggestions.header": "আপনি হয়তোবা এগুলোতে আগ্রহী হতে পারেন…", "tabs_bar.federated_timeline": "যুক্তবিশ্ব", "tabs_bar.home": "বাড়ি", "tabs_bar.local_timeline": "স্থানীয়", "tabs_bar.notifications": "প্রজ্ঞাপনগুলো", - "tabs_bar.search": "খুঁজতে", + "tabs_bar.search": "অনুসন্ধান", "time_remaining.days": "{number, plural, one {# day} other {# days}} বাকি আছে", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} বাকি আছে", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} বাকি আছে", + "time_remaining.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}} বাকি আছে", "time_remaining.moments": "সময় বাকি আছে", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে", + "trends.trending_now": "বর্তমানে জনপ্রিয়", "ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।", "upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে", - "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।", "upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।", "upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে", - "upload_form.focus": "সাধারণ দেখাটি পরিবর্তন করতে", + "upload_form.edit": "সম্পাদন", "upload_form.undo": "মুছে ফেলতে", + "upload_modal.analyzing_picture": "চিত্র বিশ্লেষণ করা হচ্ছে…", + "upload_modal.apply": "প্রয়োগ করুন", + "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.preview_label": "পূর্বরূপ({ratio})", "upload_progress.label": "যুক্ত করতে পাঠানো হচ্ছে...", "video.close": "ভিডিওটি বন্ধ করতে", "video.exit_fullscreen": "পূর্ণ পর্দা থেকে বাইরে বের হতে", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json new file mode 100644 index 000000000..db07f1aba --- /dev/null +++ b/app/javascript/mastodon/locales/br.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Ouzhpenn pe lemel ag ar listennadoù", + "account.badges.bot": "Robot", + "account.block": "Stankañ @{name}", + "account.block_domain": "Kuzh kement tra a {domain}", + "account.blocked": "Stanket", + "account.cancel_follow_request": "Nullañ ar pedad heuliañ", + "account.direct": "Kas ur c'hemennad da @{name}", + "account.domain_blocked": "Domani kuzhet", + "account.edit_profile": "Aozañ ar profil", + "account.endorse": "Lakaat war-wel war ar profil", + "account.follow": "Heuliañ", + "account.followers": "Heilour·ezed·ion", + "account.followers.empty": "Den na heul an implijour-mañ c'hoazh.", + "account.follows": "Koumanantoù", + "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows_you": "Ho heul", + "account.hide_reblogs": "Kuzh toudoù skignet gant @{name}", + "account.last_status": "Oberiantiz zivezhañ", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.media": "Media", + "account.mention": "Menegiñ @{name}", + "account.moved_to": "Dilojet en·he deus {name} da:", + "account.mute": "Kuzhat @{name}", + "account.mute_notifications": "Kuzh kemennoù a @{name}", + "account.muted": "Kuzhet", + "account.never_active": "Birviken", + "account.posts": "Toudoù", + "account.posts_with_replies": "Toudoù ha respontoù", + "account.report": "Disklêriañ @{name}", + "account.requested": "Awaiting approval", + "account.share": "Skignañ profil @{name}", + "account.show_reblogs": "Diskouez toudoù a @{name}", + "account.unblock": "Distankañ @{name}", + "account.unblock_domain": "Diguzh {domain}", + "account.unendorse": "Don't feature on profile", + "account.unfollow": "Diheuliañ", + "account.unmute": "Diguzhat @{name}", + "account.unmute_notifications": "Diguzhat kemennoù a @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.", + "alert.unexpected.title": "C'hem !", + "autosuggest_hashtag.per_week": "{count} bep sizhun", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Klask endro", + "bundle_column_error.title": "Fazi rouedad", + "bundle_modal_error.close": "Serriñ", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Klask endro", + "column.blocks": "Implijour·ezed·ion stanket", + "column.community": "Red-amzer lec'hel", + "column.direct": "Kemennadoù prevez", + "column.directory": "Mont a-dreuz ar profiloù", + "column.domain_blocks": "Domani kuzhet", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.pins": "Pinned toot", + "column.public": "Federated timeline", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", + "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", + "column_header.unpin": "Unpin", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media only", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Remove this 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.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.select.no_options_message": "No suggestions found", + "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "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", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "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.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll.voted": "You voted for this answer", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "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", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index acb9709d0..396e4a325 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -4,6 +4,7 @@ "account.block": "Bloqueja @{name}", "account.block_domain": "Amaga-ho tot de {domain}", "account.blocked": "Bloquejat", + "account.cancel_follow_request": "Anul·la la sol·licitud de seguiment", "account.direct": "Missatge directe @{name}", "account.domain_blocked": "Domini ocult", "account.edit_profile": "Editar el perfil", @@ -15,6 +16,7 @@ "account.follows.empty": "Aquest usuari encara no segueix a ningú.", "account.follows_you": "Et segueix", "account.hide_reblogs": "Amaga els impulsos de @{name}", + "account.last_status": "Darrer actiu", "account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}", "account.locked_info": "Aquest estat de privadesa del compte està definit com a bloquejat. El propietari revisa manualment qui pot seguir-lo.", "account.media": "Mèdia", @@ -23,6 +25,7 @@ "account.mute": "Silencia @{name}", "account.mute_notifications": "Notificacions desactivades de @{name}", "account.muted": "Silenciat", + "account.never_active": "Mai", "account.posts": "Toots", "account.posts_with_replies": "Toots i respostes", "account.report": "Informe @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Deixa de seguir", "account.unmute": "Treure silenci de @{name}", "account.unmute_notifications": "Activar notificacions de @{name}", + "alert.rate_limited.message": "Si us plau torna-ho a provar després de {retry_time, time, medium}.", + "alert.rate_limited.title": "Límit de freqüència", "alert.unexpected.message": "S'ha produït un error inesperat.", "alert.unexpected.title": "Vaja!", + "autosuggest_hashtag.per_week": "{count} per setmana", "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop", "bundle_column_error.body": "S'ha produït un error en carregar aquest component.", "bundle_column_error.retry": "Torna-ho a provar", @@ -47,6 +53,7 @@ "column.blocks": "Usuaris bloquejats", "column.community": "Línia de temps local", "column.direct": "Missatges directes", + "column.directory": "Navega els perfils", "column.domain_blocks": "Dominis ocults", "column.favourites": "Favorits", "column.follow_requests": "Peticions per seguir-te", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Estàs segur que vols suprimir permanentment aquesta llista?", "confirmations.domain_block.confirm": "Amaga tot el domini", "confirmations.domain_block.message": "Estàs segur, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar uns pocs objectius és suficient i preferible. No veuràs contingut d’aquest domini en cap de les línies públiques ni en les notificacions. Els teus seguidors d’aquest domini seran eliminats.", + "confirmations.logout.confirm": "Tancar sessió", + "confirmations.logout.message": "Segur que vols tancar la sessió?", "confirmations.mute.confirm": "Silencia", + "confirmations.mute.explanation": "Això amagarà les seves publicacions i les que els mencionen però encara els permetrà veure les teves i seguir-te.", "confirmations.mute.message": "Estàs segur que vols silenciar {name}?", "confirmations.redraft.confirm": "Esborrar i refer", "confirmations.redraft.message": "Estàs segur que vols esborrar aquest toot i tornar a redactar-lo? Perderàs totes els impulsos i favorits, i les respostes al toot original es quedaran orfes.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Responen ara es sobreescriurà el missatge que estàs editant. Estàs segur que vols continuar?", "confirmations.unfollow.confirm": "Deixa de seguir", "confirmations.unfollow.message": "Estàs segur que vols deixar de seguir {name}?", + "conversation.delete": "Elimina la conversa", + "conversation.mark_as_read": "Marca com a llegida", + "conversation.open": "Veure conversa", + "conversation.with": "Amb {names}", + "directory.federated": "Del fedivers conegut", + "directory.local": "Només de {domain}", + "directory.new_arrivals": "Arribades noves", + "directory.recently_active": "Recentment actius", "embed.instructions": "Incrusta aquest toot al lloc web copiant el codi a continuació.", "embed.preview": "Aquí tenim quin aspecte tindrá:", "emoji_button.activity": "Activitat", @@ -134,6 +152,10 @@ "empty_column.mutes": "Encara no has silenciat cap usuari.", "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 no podria ser mostrada correctament.", + "error.unexpected_crash.next_steps": "Prova recarregant la pàgina. Si això no ajuda encara pots ser capaç d'utilitzar Mastodont a través d'un navegador diferent o app nativa.", + "errors.unexpected_crash.copy_stacktrace": "Còpia stacktrace al porta-retalls", + "errors.unexpected_crash.report_issue": "Informa d'un problema", "follow_request.authorize": "Autoritzar", "follow_request.reject": "Rebutjar", "getting_started.developers": "Desenvolupadors", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Nova llista", "lists.search": "Cercar entre les persones que segueixes", "lists.subheading": "Les teves llistes", + "load_pending": "{count, plural, one {# element nou} other {# elements nous}}", "loading_indicator.label": "Carregant...", "media_gallery.toggle_visible": "Alternar visibilitat", "missing_indicator.label": "No trobat", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Toots fixats", "navigation_bar.preferences": "Preferències", - "navigation_bar.profile_directory": "Directori de perfils", "navigation_bar.public_timeline": "Línia de temps federada", "navigation_bar.security": "Seguretat", "notification.favourite": "{name} ha afavorit el teu estat", @@ -277,8 +299,10 @@ "notifications.group": "{count} notificacions", "poll.closed": "Finalitzada", "poll.refresh": "Actualitza", + "poll.total_people": "{count, plural, one {# persona} other {# persones}}", "poll.total_votes": "{count, plural, one {# vot} other {# vots}}", "poll.vote": "Vota", + "poll.voted": "Vas votar per aquesta resposta", "poll_button.add_poll": "Afegeix una enquesta", "poll_button.remove_poll": "Elimina l'enquesta", "privacy.change": "Ajusta l'estat de privacitat", @@ -290,6 +314,7 @@ "privacy.public.short": "Públic", "privacy.unlisted.long": "No publicar en línies de temps públiques", "privacy.unlisted.short": "No llistat", + "refresh": "Actualitza", "regeneration_indicator.label": "Carregant…", "regeneration_indicator.sublabel": "S'està preparant la línia de temps Inici!", "relative_time.days": "fa {number} dies", @@ -314,7 +339,8 @@ "search_results.accounts": "Gent", "search_results.hashtags": "Etiquetes", "search_results.statuses": "Toots", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.statuses_fts_disabled": "La cerca de toots pel seu contingut no està habilitada en aquest servidor Mastodon.", + "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.admin_account": "Obre l'interfície de moderació per a @{name}", "status.admin_status": "Obre aquest toot a la interfície de moderació", "status.block": "Bloqueja @{name}", @@ -352,6 +378,7 @@ "status.show_more": "Mostra més", "status.show_more_all": "Mostra més per a tot", "status.show_thread": "Mostra el fil", + "status.uncached_media_warning": "No està disponible", "status.unmute_conversation": "Activar conversació", "status.unpin": "Deslliga del perfil", "suggestions.dismiss": "Descartar suggeriment", @@ -366,15 +393,23 @@ "time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants", "time_remaining.moments": "Moments restants", "time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {persones}} parlant-hi", + "trends.trending_now": "Ara en tendència", "ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.", "upload_area.title": "Arrossega i deixa anar per a carregar", "upload_button.label": "Afegir multimèdia (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "S'ha superat el límit de càrrega d'arxius.", "upload_error.poll": "No es permet l'enviament de fitxers en les enquestes.", "upload_form.description": "Descriure els problemes visuals", - "upload_form.focus": "Modificar la previsualització", + "upload_form.edit": "Edita", "upload_form.undo": "Esborra", + "upload_modal.analyzing_picture": "Analitzant imatge…", + "upload_modal.apply": "Aplica", + "upload_modal.description_placeholder": "Uns salts ràpids de guineu marró sobre el gos gandul", + "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.preview_label": "Previsualitza ({ratio})", "upload_progress.label": "Pujant...", "video.close": "Tancar el vídeo", "video.exit_fullscreen": "Sortir de pantalla completa", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index 907032bdf..6c680f13f 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -1,9 +1,10 @@ { - "account.add_or_remove_from_list": "Aghjustà o toglie da e liste", + "account.add_or_remove_from_list": "Aghjunghje o toglie da e liste", "account.badges.bot": "Bot", "account.block": "Bluccà @{name}", "account.block_domain": "Piattà tuttu da {domain}", "account.blocked": "Bluccatu", + "account.cancel_follow_request": "Annullà a dumanda d'abbunamentu", "account.direct": "Missaghju direttu @{name}", "account.domain_blocked": "Duminiu piattatu", "account.edit_profile": "Mudificà u prufile", @@ -15,6 +16,7 @@ "account.follows.empty": "St'utilizatore ùn seguita nisunu.", "account.follows_you": "Vi seguita", "account.hide_reblogs": "Piattà spartere da @{name}", + "account.last_status": "Ultima attività", "account.link_verified_on": "A prupietà di stu ligame hè stata verificata u {date}", "account.locked_info": "U statutu di vita privata di u contu hè chjosu. U pruprietariu esamina manualmente e dumande d'abbunamentu.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Piattà @{name}", "account.mute_notifications": "Piattà nutificazione da @{name}", "account.muted": "Piattatu", + "account.never_active": "Mai", "account.posts": "Statuti", "account.posts_with_replies": "Statuti è risposte", "account.report": "Palisà @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Ùn siguità più", "account.unmute": "Ùn piattà più @{name}", "account.unmute_notifications": "Ùn piattà più nutificazione da @{name}", + "alert.rate_limited.message": "Pruvate ancu dop'à {retry_time, time, medium}.", + "alert.rate_limited.title": "Ghjettu limitatu", "alert.unexpected.message": "Un prublemu inaspettatu hè accadutu.", "alert.unexpected.title": "Uups!", + "autosuggest_hashtag.per_week": "{count} per settimana", "boost_modal.combo": "Pudete appughjà nant'à {combo} per saltà quessa a prussima volta", "bundle_column_error.body": "C'hè statu un prublemu caricandu st'elementu.", "bundle_column_error.retry": "Pruvà torna", @@ -47,6 +53,7 @@ "column.blocks": "Utilizatori bluccati", "column.community": "Linea pubblica lucale", "column.direct": "Missaghji diretti", + "column.directory": "Percorre i prufili", "column.domain_blocks": "Duminii piattati", "column.favourites": "Favuriti", "column.follow_requests": "Dumande d'abbunamentu", @@ -71,7 +78,7 @@ "compose_form.lock_disclaimer": "U vostru contu ùn hè micca {locked}. Tuttu u mondu pò seguitavi è vede i vostri statuti privati.", "compose_form.lock_disclaimer.lock": "privatu", "compose_form.placeholder": "À chè pensate?", - "compose_form.poll.add_option": "Aghjustà una scelta", + "compose_form.poll.add_option": "Aghjunghje scelta", "compose_form.poll.duration": "Durata di u scandagliu", "compose_form.poll.option_placeholder": "Scelta {number}", "compose_form.poll.remove_option": "Toglie sta scelta", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Site sicuru·a che vulete supprime sta lista?", "confirmations.domain_block.confirm": "Piattà tuttu u duminiu", "confirmations.domain_block.message": "Site sicuru·a che vulete piattà tuttu à {domain}? Saria forse abbastanza di bluccà ò piattà alcuni conti da quallà. Ùn viderete più nunda da quallà indè e linee pubbliche o e nutificazione. I vostri abbunati da stu duminiu saranu tolti.", + "confirmations.logout.confirm": "Scunnettassi", + "confirmations.logout.message": "Site sicuru·a che vulete scunnettà vi?", "confirmations.mute.confirm": "Piattà", + "confirmations.mute.explanation": "Quessu hà da piattà i statuti da sta persona è i posti chì a mintuvanu, mà ellu·a puderà sempre vede i vostri statuti è siguità vi.", "confirmations.mute.message": "Site sicuru·a che vulete piattà @{name}?", "confirmations.redraft.confirm": "Sguassà è riscrive", "confirmations.redraft.message": "Site sicuru·a chè vulete sguassà stu statutu è riscrivelu? I favuriti è spartere saranu persi, è e risposte diventeranu orfane.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Risponde avà sguasserà u missaghju chì scrivite. Site sicuru·a chì vulete cuntinuà?", "confirmations.unfollow.confirm": "Disabbunassi", "confirmations.unfollow.message": "Site sicuru·a ch'ùn vulete più siguità @{name}?", + "conversation.delete": "Sguassà a cunversazione", + "conversation.mark_as_read": "Marcà cum'è lettu", + "conversation.open": "Vede a cunversazione", + "conversation.with": "Cù {names}", + "directory.federated": "Da u fediverse cunisciutu", + "directory.local": "Solu da {domain}", + "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ì:", "emoji_button.activity": "Attività", @@ -123,8 +141,8 @@ "empty_column.community": "Ùn c'hè nunda indè a linea lucale. Scrivete puru qualcosa!", "empty_column.direct": "Ùn avete ancu nisun missaghju direttu. S'è voi mandate o ricevete unu, u vidarete quì.", "empty_column.domain_blocks": "Ùn c'hè manc'un duminiu bluccatu avà.", - "empty_column.favourited_statuses": "Ùn avete manc'unu statutu favuritu. Quandu aghjusterate unu à i vostri favuriti, sarà mustratu quì.", - "empty_column.favourites": "Nisunu hà aghjustatu stu statutu à i so favuriti. Quandu qualch'unu farà quessa, u so contu sarà mustratu quì.", + "empty_column.favourited_statuses": "Ùn avete manc'unu statutu favuritu. Quandu aghjunghjerate unu à i vostri favuriti, sarà mustratu quì.", + "empty_column.favourites": "Nisunu hà aghjuntu stu statutu à i so favuriti. Quandu qualch'unu farà quessa, u so contu sarà mustratu quì.", "empty_column.follow_requests": "Ùn avete manc'una dumanda d'abbunamentu. Quandu averete una, sarà mustrata quì.", "empty_column.hashtag": "Ùn c'hè ancu nunda quì.", "empty_column.home": "A vostr'accolta hè viota! Pudete andà nant'à {public} o pruvà a ricerca per truvà parsone da siguità.", @@ -134,6 +152,10 @@ "empty_column.mutes": "Per avà ùn avete manc'un utilizatore piattatu.", "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.next_steps": "Pruvate d'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", "follow_request.authorize": "Auturizà", "follow_request.reject": "Righjittà", "getting_started.developers": "Sviluppatori", @@ -161,7 +183,7 @@ "intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}", "introduction.federation.action": "Cuntinuà", "introduction.federation.federated.headline": "Federata", - "introduction.federation.federated.text": "I statuti pubblichi da l'altri servori di u fediverse saranu mustrati nant'à a linea pubblica federata.", + "introduction.federation.federated.text": "I statuti pubblichi da l'altri servori di u fediverse saranu mustrati nant'à a linea pubblica glubale.", "introduction.federation.home.headline": "Accolta", "introduction.federation.home.text": "I statuti da a ghjente che vo siguitate saranu affissati nant'à a linea d'accolta. Pudete seguità qualvogliasia nant'à tutti i servori!", "introduction.federation.local.headline": "Lucale", @@ -187,7 +209,7 @@ "keyboard_shortcuts.enter": "apre u statutu", "keyboard_shortcuts.favourite": "aghjunghje à i favuriti", "keyboard_shortcuts.favourites": "per apre a lista di i favuriti", - "keyboard_shortcuts.federated": "per apre a linea pubblica federata", + "keyboard_shortcuts.federated": "per apre a linea pubblica glubale", "keyboard_shortcuts.heading": "Accorte cù a tastera", "keyboard_shortcuts.home": "per apre a linea d'accolta", "keyboard_shortcuts.hotkey": "Accorta", @@ -217,10 +239,11 @@ "lists.delete": "Supprime a lista", "lists.edit": "Mudificà a lista", "lists.edit.submit": "Cambià u titulu", - "lists.new.create": "Aghjustà una lista", + "lists.new.create": "Aghjunghje", "lists.new.title_placeholder": "Titulu di a lista", "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à", "missing_indicator.label": "Micca trovu", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Persunale", "navigation_bar.pins": "Statuti puntarulati", "navigation_bar.preferences": "Preferenze", - "navigation_bar.profile_directory": "Annuariu di i prufili", "navigation_bar.public_timeline": "Linea pubblica glubale", "navigation_bar.security": "Sicurità", "notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti", @@ -277,19 +299,22 @@ "notifications.group": "{count} nutificazione", "poll.closed": "Chjosu", "poll.refresh": "Attualizà", + "poll.total_people": "{count, plural, one {# persona} other {# persone}}", "poll.total_votes": "{count, plural, one {# votu} other {# voti}}", "poll.vote": "Vutà", - "poll_button.add_poll": "Aghjustà un scandagliu", + "poll.voted": "Avete vutatu per sta risposta", + "poll_button.add_poll": "Aghjunghje", "poll_button.remove_poll": "Toglie u scandagliu", "privacy.change": "Mudificà a cunfidenzialità di u statutu", "privacy.direct.long": "Mandà solu à quelli chì so mintuvati", "privacy.direct.short": "Direttu", "privacy.private.long": "Mustrà solu à l'abbunati", "privacy.private.short": "Privatu", - "privacy.public.long": "Mustrà à tuttu u mondu nant'a linea pubblica", + "privacy.public.long": "Mustrà à tuttu u mondu nant'à e linee pubbliche", "privacy.public.short": "Pubblicu", - "privacy.unlisted.long": "Ùn mette micca nant'a linea pubblica (ma tutt'u mondu pò vede u statutu nant'à u vostru prufile)", + "privacy.unlisted.long": "Ùn mette micca nant'à e linee pubbliche", "privacy.unlisted.short": "Micca listatu", + "refresh": "Attualizà", "regeneration_indicator.label": "Caricamentu…", "regeneration_indicator.sublabel": "Priparazione di a vostra pagina d'accolta!", "relative_time.days": "{number}ghj", @@ -314,6 +339,7 @@ "search_results.accounts": "Ghjente", "search_results.hashtags": "Hashtag", "search_results.statuses": "Statuti", + "search_results.statuses_fts_disabled": "A ricerca di i cuntinuti di i statuti ùn hè micca attivata nant'à stu servore Mastodon.", "search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}", "status.admin_account": "Apre l'interfaccia di muderazione per @{name}", "status.admin_status": "Apre stu statutu in l'interfaccia di muderazione", @@ -352,6 +378,7 @@ "status.show_more": "Slibrà", "status.show_more_all": "Slibrà tuttu", "status.show_thread": "Vede u filu", + "status.uncached_media_warning": "Micca dispunibule", "status.unmute_conversation": "Ùn piattà più a cunversazione", "status.unpin": "Spuntarulà da u prufile", "suggestions.dismiss": "Righjittà a pruposta", @@ -367,14 +394,22 @@ "time_remaining.moments": "Ci fermanu qualchi mumentu", "time_remaining.seconds": "{number, plural, one {# siconda ferma} other {# siconde fermanu}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} parlanu", + "trends.trending_now": "Tindenze d'avà", "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.", "upload_area.title": "Drag & drop per caricà un fugliale", "upload_button.label": "Aghjunghje un media (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Limita di caricamentu di fugliali trapassata.", "upload_error.poll": "Ùn si pò micca caricà fugliali cù i scandagli.", "upload_form.description": "Discrive per i malvistosi", - "upload_form.focus": "Cambià a vista", + "upload_form.edit": "Mudificà", "upload_form.undo": "Sguassà", + "upload_modal.analyzing_picture": "Analisi di u ritrattu…", + "upload_modal.apply": "Affettà", + "upload_modal.description_placeholder": "Chì tempi brevi ziu, quandu solfeghji", + "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.preview_label": "Vista ({ratio})", "upload_progress.label": "Caricamentu...", "video.close": "Chjudà a video", "video.exit_fullscreen": "Caccià u pienu screnu", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 1bd2ae84d..fc807d45e 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -4,6 +4,7 @@ "account.block": "Zablokovat uživatele @{name}", "account.block_domain": "Skrýt vše z {domain}", "account.blocked": "Blokován/a", + "account.cancel_follow_request": "Zrušit požadavek o sledování", "account.direct": "Poslat přímou zprávu uživateli @{name}", "account.domain_blocked": "Doména skryta", "account.edit_profile": "Upravit profil", @@ -15,6 +16,7 @@ "account.follows.empty": "Tento uživatel ještě nikoho nesleduje.", "account.follows_you": "Sleduje vás", "account.hide_reblogs": "Skrýt boosty od uživatele @{name}", + "account.last_status": "Naposledy aktivní", "account.link_verified_on": "Vlastnictví tohoto odkazu bylo zkontrolováno {date}", "account.locked_info": "Stav soukromí tohoto účtu je nastaven na zamčeno. Jeho vlastník ručně posuzuje, kdo ho může sledovat.", "account.media": "Média", @@ -23,6 +25,7 @@ "account.mute": "Skrýt uživatele @{name}", "account.mute_notifications": "Skrýt oznámení od uživatele @{name}", "account.muted": "Skryt/a", + "account.never_active": "Nikdy", "account.posts": "Tooty", "account.posts_with_replies": "Tooty a odpovědi", "account.report": "Nahlásit uživatele @{name}", @@ -35,9 +38,12 @@ "account.unfollow": "Přestat sledovat", "account.unmute": "Odkrýt uživatele @{name}", "account.unmute_notifications": "Odkrýt oznámení od uživatele @{name}", + "alert.rate_limited.message": "Prosím zkuste to znovu za {retry_time, time, medium}.", + "alert.rate_limited.title": "Rychlost omezena", "alert.unexpected.message": "Objevila se neočekávaná chyba.", "alert.unexpected.title": "Jejda!", - "boost_modal.combo": "Příště můžete pro přeskočení kliknout na {combo}", + "autosuggest_hashtag.per_week": "{count} za týden", + "boost_modal.combo": "Příště můžete pro přeskočení stisknout {combo}", "bundle_column_error.body": "Při načítání tohoto komponentu se něco pokazilo.", "bundle_column_error.retry": "Zkuste to znovu", "bundle_column_error.title": "Chyba sítě", @@ -47,6 +53,7 @@ "column.blocks": "Blokovaní uživatelé", "column.community": "Místní časová osa", "column.direct": "Přímé zprávy", + "column.directory": "Prozkoumat profily", "column.domain_blocks": "Skryté domény", "column.favourites": "Oblíbené", "column.follow_requests": "Požadavky o sledování", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Jste si jistý/á, že chcete tento seznam navždy smazat?", "confirmations.domain_block.confirm": "Skrýt celou doménu", "confirmations.domain_block.message": "Jste si opravdu, opravdu jistý/á, že chcete blokovat celou doménu {domain}? Ve většině případů stačí zablokovat nebo skrýt pár konkrétních uživatelů, což se doporučuje. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.", + "confirmations.logout.confirm": "Odhlásit", + "confirmations.logout.message": "Jste si jistý/á, že se chcete odhlásit?", "confirmations.mute.confirm": "Skrýt", + "confirmations.mute.explanation": "Tohle skryje jeho příspěvky a příspěvky, které ho zmiňují, ale uživatel pořád bude moci vidět vaše příspěvky a sledovat vás.", "confirmations.mute.message": "Jste si jistý/á, že chcete skrýt uživatele {name}?", "confirmations.redraft.confirm": "Smazat a přepsat", "confirmations.redraft.message": "Jste si jistý/á, že chcete smazat a přepsat tento toot? Oblíbení a boosty budou ztraceny a odpovědi na původní příspěvek budou opuštěny.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Odpovězením nyní přepíšete zprávu, kterou aktuálně píšete. Jste si jistý/á, že chcete pokračovat?", "confirmations.unfollow.confirm": "Přestat sledovat", "confirmations.unfollow.message": "jste si jistý/á, že chcete přestat sledovat uživatele {name}?", + "conversation.delete": "Smazat konverzaci", + "conversation.mark_as_read": "Označit jako přečtenou", + "conversation.open": "Zobrazit konverzaci", + "conversation.with": "S {names}", + "directory.federated": "Ze známého fedivesmíru", + "directory.local": "Pouze z {domain}", + "directory.new_arrivals": "Nově příchozí", + "directory.recently_active": "Nedávno aktivní", "embed.instructions": "Pro přidání tootu na vaši webovou stránku zkopírujte níže uvedený kód.", "embed.preview": "Takhle to bude vypadat:", "emoji_button.activity": "Aktivita", @@ -134,6 +152,10 @@ "empty_column.mutes": "Ještě jste neskryl/a žádné uživatele.", "empty_column.notifications": "Ještě nemáte žádná oznámení. Začněte konverzaci komunikováním s ostatními.", "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.next_steps": "Zkuste obnovit stránku. Pokud to nepomůže, budete možná moci dále používat Mastodon pomocí jiného prohlížeče nebo nativní aplikace.", + "errors.unexpected_crash.copy_stacktrace": "Zkopírovat stacktrace do schránky", + "errors.unexpected_crash.report_issue": "Nahlásit problém", "follow_request.authorize": "Autorizovat", "follow_request.reject": "Odmítnout", "getting_started.developers": "Vývojáři", @@ -142,7 +164,7 @@ "getting_started.heading": "Začínáme", "getting_started.invite": "Pozvat lidi", "getting_started.open_source_notice": "Mastodon je otevřený software. Na GitHubu k němu můžete přispět nebo nahlásit chyby: {github}.", - "getting_started.security": "Zabezpečení", + "getting_started.security": "Nastavení účtu", "getting_started.terms": "Podmínky používání", "hashtag.column_header.tag_mode.all": "a {additional}", "hashtag.column_header.tag_mode.any": "nebo {additional}", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Název nového seznamu", "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}}", "loading_indicator.label": "Načítám…", "media_gallery.toggle_visible": "Přepínat viditelnost", "missing_indicator.label": "Nenalezeno", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Osobní", "navigation_bar.pins": "Připnuté tooty", "navigation_bar.preferences": "Předvolby", - "navigation_bar.profile_directory": "Adresář profilů", "navigation_bar.public_timeline": "Federovaná časová osa", "navigation_bar.security": "Zabezpečení", "notification.favourite": "{name} si oblíbil/a váš toot", @@ -277,8 +299,10 @@ "notifications.group": "{count} oznámení", "poll.closed": "Uzavřena", "poll.refresh": "Obnovit", + "poll.total_people": "{count, plural, one {# člověk} few {# lidé} many {# lidí} other {# lidí}}", "poll.total_votes": "{count, plural, one {# hlas} few {# hlasy} many {# hlasu} other {# hlasů}}", "poll.vote": "Hlasovat", + "poll.voted": "Pro tuto odpověď jste hlasoval/a", "poll_button.add_poll": "Přidat anketu", "poll_button.remove_poll": "Odstranit anketu", "privacy.change": "Změnit soukromí tootu", @@ -290,6 +314,7 @@ "privacy.public.short": "Veřejný", "privacy.unlisted.long": "Neodeslat na veřejné časové osy", "privacy.unlisted.short": "Neuvedený", + "refresh": "Obnovit", "regeneration_indicator.label": "Načítám…", "regeneration_indicator.sublabel": "Váš domovský proud se připravuje!", "relative_time.days": "{number} d", @@ -300,7 +325,7 @@ "reply_indicator.cancel": "Zrušit", "report.forward": "Přeposlat na {target}", "report.forward_hint": "Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii?", - "report.hint": "Toto nahlášení bude zasláno moderátorům vašeho serveru. Níže můžete uvést, proč tento účet nahlašujete:", + "report.hint": "Nahlášení bude zasláno moderátorům vašeho serveru. Níže můžete uvést, proč tento účet nahlašujete:", "report.placeholder": "Dodatečné komentáře", "report.submit": "Odeslat", "report.target": "Nahlášení uživatele {target}", @@ -314,6 +339,7 @@ "search_results.accounts": "Lidé", "search_results.hashtags": "Hashtagy", "search_results.statuses": "Tooty", + "search_results.statuses_fts_disabled": "Vyhledávání tootů podle jejich obsahu není na tomto serveru Mastodon povoleno.", "search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledku} other {výsledků}}", "status.admin_account": "Otevřít moderátorské rozhraní pro uživatele @{name}", "status.admin_status": "Otevřít tento toot v moderátorském rozhraní", @@ -352,6 +378,7 @@ "status.show_more": "Zobrazit více", "status.show_more_all": "Zobrazit více pro všechny", "status.show_thread": "Zobrazit vlákno", + "status.uncached_media_warning": "Nedostupné", "status.unmute_conversation": "Odkrýt konverzaci", "status.unpin": "Odepnout z profilu", "suggestions.dismiss": "Odmítnout návrh", @@ -367,14 +394,22 @@ "time_remaining.moments": "Zbývá několik sekund", "time_remaining.seconds": "{number, plural, one {Zbývá # sekunda} few {Zbývají # sekundy} many {Zbývá # sekundy} other {Zbývá # sekund}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {člověk} few {lidé} many {lidí} other {lidí}} hovoří", + "trends.trending_now": "Aktuální trendy", "ui.beforeunload": "Váš koncept se ztratí, pokud Mastodon opustíte.", "upload_area.title": "Přetažením nahrajete", "upload_button.label": "Přidat média (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Byl překročen limit nahraných souborů.", "upload_error.poll": "Nahrávání souborů není povoleno u anket.", "upload_form.description": "Popis pro zrakově postižené", - "upload_form.focus": "Změnit náhled", + "upload_form.edit": "Upravit", "upload_form.undo": "Smazat", + "upload_modal.analyzing_picture": "Analyzuji obrázek…", + "upload_modal.apply": "Použít", + "upload_modal.description_placeholder": "Příliš žluťoučký kůň úpěl ďábelské ódy", + "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 bod soustředění, který bude vždy zobrazen na všech náhledech.", + "upload_modal.preview_label": "Náhled ({ratio})", "upload_progress.label": "Nahrávám…", "video.close": "Zavřít video", "video.exit_fullscreen": "Ukončit celou obrazovku", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 07067e6b1..a8a952798 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -4,6 +4,7 @@ "account.block": "Blocio @{name}", "account.block_domain": "Cuddio popeth rhag {domain}", "account.blocked": "Blociwyd", + "account.cancel_follow_request": "Canslo cais dilyn", "account.direct": "Neges breifat @{name}", "account.domain_blocked": "Parth wedi ei guddio", "account.edit_profile": "Golygu proffil", @@ -15,6 +16,7 @@ "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}", + "account.last_status": "Gweithredol olaf", "account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}", "account.locked_info": "Mae'r statws preifatrwydd cyfrif hwn wedi'i osod i gloi. Mae'r perchennog yn adolygu'r sawl sy'n gallu eu dilyn.", "account.media": "Cyfryngau", @@ -23,6 +25,7 @@ "account.mute": "Tawelu @{name}", "account.mute_notifications": "Cuddio hysbysiadau o @{name}", "account.muted": "Distewyd", + "account.never_active": "Byth", "account.posts": "Tŵtiau", "account.posts_with_replies": "Tŵtiau ac atebion", "account.report": "Adrodd @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Dad-ddilyn", "account.unmute": "Dad-dawelu @{name}", "account.unmute_notifications": "Dad-dawelu hysbysiadau o @{name}", + "alert.rate_limited.message": "Ceisiwch eto ar ôl {retry_time, time, medium}.", + "alert.rate_limited.title": "Cyfradd gyfyngedig", "alert.unexpected.message": "Digwyddodd gwall annisgwyl.", "alert.unexpected.title": "Wps!", + "autosuggest_hashtag.per_week": "{count} yr wythnos", "boost_modal.combo": "Mae modd gwasgu {combo} er mwyn sgipio hyn tro nesa", "bundle_column_error.body": "Aeth rhywbeth o'i le tra'n llwytho'r elfen hon.", "bundle_column_error.retry": "Ceisiwch eto", @@ -47,6 +53,7 @@ "column.blocks": "Defnyddwyr a flociwyd", "column.community": "Ffrwd lleol", "column.direct": "Negeseuon preifat", + "column.directory": "Pori proffiliau", "column.domain_blocks": "Parthau cuddiedig", "column.favourites": "Ffefrynnau", "column.follow_requests": "Ceisiadau dilyn", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Ydych chi'n sicr eich bod eisiau dileu y rhestr hwn am byth?", "confirmations.domain_block.confirm": "Cuddio parth cyfan", "confirmations.domain_block.message": "A ydych yn hollol, hollol sicr eich bod am flocio y {domain} cyfan? Yn y nifer helaeth o achosion mae blocio neu tawelu ambell gyfrif yn ddigonol ac yn well. Ni fyddwch yn gweld cynnwys o'r parth hwnnw mewn unrhyw ffrydiau cyhoeddus na chwaith yn eich hysbysiadau. Bydd hyn yn cael gwared o'ch dilynwyr o'r parth hwnnw.", + "confirmations.logout.confirm": "Allgofnodi", + "confirmations.logout.message": "Ydych chi'n siŵr eich bod am allgofnodi?", "confirmations.mute.confirm": "Tawelu", + "confirmations.mute.explanation": "Bydd hyn yn cuddio pyst oddi wrthynt a physt sydd yn sôn amdanynt, ond bydd hyn dal yn gadael iddyn nhw gweld eich pyst a'ch dilyn.", "confirmations.mute.message": "Ydych chi'n sicr eich bod am ddistewi {name}?", "confirmations.redraft.confirm": "Dileu & ailddrafftio", "confirmations.redraft.message": "Ydych chi'n siwr eich bod eisiau dileu y tŵt hwn a'i ailddrafftio? Bydd ffefrynnau a bwstiau'n cael ei colli, a bydd ymatebion i'r tŵt gwreiddiol yn cael eu hamddifadu.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Bydd ateb nawr yn cymryd lle y neges yr ydych yn cyfansoddi ar hyn o bryd. Ydych chi'n sicr yr ydych am barhau?", "confirmations.unfollow.confirm": "Dad-ddilynwch", "confirmations.unfollow.message": "Ydych chi'n sicr eich bod am ddad-ddilyn {name}?", + "conversation.delete": "Dileu sgwrs", + "conversation.mark_as_read": "Nodi fel wedi'i ddarllen", + "conversation.open": "Gweld sgwrs", + "conversation.with": "Gyda {names}", + "directory.federated": "O ffedysawd hysbys", + "directory.local": "O {domain} yn unig", + "directory.new_arrivals": "Newydd-ddyfodiaid", + "directory.recently_active": "Yn weithredol yn ddiweddar", "embed.instructions": "Mewnblannwch y tŵt hwn ar eich gwefan drwy gopïo'r côd isod.", "embed.preview": "Dyma sut olwg fydd arno:", "emoji_button.activity": "Gweithgarwch", @@ -134,6 +152,10 @@ "empty_column.mutes": "Nid ydych wedi tawelu unrhyw ddefnyddwyr eto.", "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.next_steps": "Ceisiwch ail-lwytho y dudalen. Os nad yw hyn yn eich helpu, efallai gallech defnyddio Mastodon trwy borwr neu ap brodorol gwahanol.", + "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", "getting_started.developers": "Datblygwyr", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Teitl rhestr newydd", "lists.search": "Chwilio ymysg pobl yr ydych yn ei ddilyn", "lists.subheading": "Eich rhestrau", + "load_pending": "{count, plural, one {# eitem newydd} other {# eitemau newydd}}", "loading_indicator.label": "Llwytho...", "media_gallery.toggle_visible": "Toglo gwelededd", "missing_indicator.label": "Heb ei ganfod", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personol", "navigation_bar.pins": "Tŵtiau wedi eu pinio", "navigation_bar.preferences": "Dewisiadau", - "navigation_bar.profile_directory": "Cyfeiriadur Proffil", "navigation_bar.public_timeline": "Ffrwd y ffederasiwn", "navigation_bar.security": "Diogelwch", "notification.favourite": "hoffodd {name} eich tŵt", @@ -264,7 +286,7 @@ "notifications.column_settings.follow": "Dilynwyr newydd:", "notifications.column_settings.mention": "Crybwylliadau:", "notifications.column_settings.poll": "Canlyniadau pleidlais:", - "notifications.column_settings.push": "Hysbysiadau push", + "notifications.column_settings.push": "Hysbysiadau gwthiadwy", "notifications.column_settings.reblog": "Hybiadau:", "notifications.column_settings.show": "Dangos yn y golofn", "notifications.column_settings.sound": "Chwarae sain", @@ -277,8 +299,10 @@ "notifications.group": "{count} o hysbysiadau", "poll.closed": "Ar gau", "poll.refresh": "Adnewyddu", + "poll.total_people": "{count, plural, one {# berson} other {# o bobl}}", "poll.total_votes": "{count, plural, one {# bleidlais} other {# o bleidleisiau}}", "poll.vote": "Pleidleisio", + "poll.voted": "Pleidleisioch chi am yr ateb hon", "poll_button.add_poll": "Ychwanegu pleidlais", "poll_button.remove_poll": "Tynnu pleidlais", "privacy.change": "Addasu preifatrwdd y tŵt", @@ -290,6 +314,7 @@ "privacy.public.short": "Cyhoeddus", "privacy.unlisted.long": "Peidio a chyhoeddi i ffrydiau cyhoeddus", "privacy.unlisted.short": "Heb ei restru", + "refresh": "Adnewyddu", "regeneration_indicator.label": "Llwytho…", "regeneration_indicator.sublabel": "Mae eich ffrwd cartref yn cael ei baratoi!", "relative_time.days": "{number}dydd", @@ -314,6 +339,7 @@ "search_results.accounts": "Pobl", "search_results.hashtags": "Hanshnodau", "search_results.statuses": "Tŵtiau", + "search_results.statuses_fts_disabled": "Nid yw chwilio Tŵtiau yn ôl eu cynnwys wedi'i alluogi ar y gweinydd Mastodon hwn.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Agor rhyngwyneb goruwchwylio ar gyfer @{name}", "status.admin_status": "Agor y tŵt yn y rhyngwyneb goruwchwylio", @@ -352,6 +378,7 @@ "status.show_more": "Dangos mwy", "status.show_more_all": "Dangos mwy i bawb", "status.show_thread": "Dangos edefyn", + "status.uncached_media_warning": "Dim ar gael", "status.unmute_conversation": "Dad-dawelu sgwrs", "status.unpin": "Dadbinio o'r proffil", "suggestions.dismiss": "Diswyddo", @@ -367,14 +394,22 @@ "time_remaining.moments": "Munudau ar ôl", "time_remaining.seconds": "{number, plural, one {# eiliad} other {# o eiliadau}} ar ôl", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} yn siarad", + "trends.trending_now": "Yn tueddu nawr", "ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.", "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.", "upload_error.poll": "Nid oes modd uwchlwytho ffeiliau â phleidleisiau.", "upload_form.description": "Disgrifio i'r rheini a nam ar ei golwg", - "upload_form.focus": "Newid rhagolwg", + "upload_form.edit": "Golygu", "upload_form.undo": "Dileu", + "upload_modal.analyzing_picture": "Dadansoddi llun…", + "upload_modal.apply": "Gweithredu", + "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.preview_label": "Rhagolwg ({ratio})", "upload_progress.label": "Uwchlwytho...", "video.close": "Cau fideo", "video.exit_fullscreen": "Gadael sgrîn llawn", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index ba8ba7a28..4719d5ca5 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -4,6 +4,7 @@ "account.block": "Bloker @{name}", "account.block_domain": "Skjul alt fra {domain}", "account.blocked": "Blokeret", + "account.cancel_follow_request": "Annullér følgeranmodning", "account.direct": "Send en direkte besked til @{name}", "account.domain_blocked": "Domænet er blevet skjult", "account.edit_profile": "Rediger profil", @@ -15,14 +16,16 @@ "account.follows.empty": "Denne bruger følger endnu ikke nogen.", "account.follows_you": "Følger dig", "account.hide_reblogs": "Skjul fremhævelserne fra @{name}", + "account.last_status": "Sidst aktiv", "account.link_verified_on": "Ejerskabet af dette link blev tjekket den %{date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.locked_info": "Denne kontos privatlivsstatus er sat til låst. Ejeren bedømmer manuelt, hvem der kan følge dem.", "account.media": "Medie", "account.mention": "Nævn @{name}", "account.moved_to": "{name} er flyttet til:", "account.mute": "Dæmp @{name}", "account.mute_notifications": "Dæmp notifikationer fra @{name}", "account.muted": "Dæmpet", + "account.never_active": "Aldrig", "account.posts": "Trut", "account.posts_with_replies": "Trut og svar", "account.report": "Rapporter @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Følg ikke længere", "account.unmute": "Fjern dæmpningen af @{name}", "account.unmute_notifications": "Fjern dæmpningen af notifikationer fra @{name}", + "alert.rate_limited.message": "Prøv venligst igen efter {retry_time, time, medium}.", + "alert.rate_limited.title": "Gradsbegrænset", "alert.unexpected.message": "Der opstod en uventet fejl.", "alert.unexpected.title": "Ups!", + "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.", "bundle_column_error.retry": "Prøv igen", @@ -47,6 +53,7 @@ "column.blocks": "Blokerede brugere", "column.community": "Lokal tidslinje", "column.direct": "Direkte beskeder", + "column.directory": "Gennemse profiler", "column.domain_blocks": "Skjulte domæner", "column.favourites": "Favoritter", "column.follow_requests": "Anmodning om at følge", @@ -71,20 +78,20 @@ "compose_form.lock_disclaimer": "Din konto er ikke {locked}. Alle kan følge dig for at se dine følger-kun indlæg.", "compose_form.lock_disclaimer.lock": "låst", "compose_form.placeholder": "Hvad har du på hjertet?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Tilføj valgmulighed", + "compose_form.poll.duration": "Afstemningens varighed", + "compose_form.poll.option_placeholder": "Valgmulighed {number}", + "compose_form.poll.remove_option": "Fjern denne valgmulighed", "compose_form.publish": "Trut", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Markér medie som følsomt", "compose_form.sensitive.marked": "Medie er markeret som værende følsomt", "compose_form.sensitive.unmarked": "Mediet er ikke markeret som værende følsomt", "compose_form.spoiler.marked": "Teksten er skjult bag en advarsel", "compose_form.spoiler.unmarked": "Teksten er ikke skjult", "compose_form.spoiler_placeholder": "Skriv din advarsel her", "confirmation_modal.cancel": "Annuller", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Blokér og anmeld", "confirmations.block.confirm": "Bloker", "confirmations.block.message": "Er du sikker på, du vil blokere {name}?", "confirmations.delete.confirm": "Slet", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Er du sikker på, du vil slette denne liste?", "confirmations.domain_block.confirm": "Skjul helt domæne", "confirmations.domain_block.message": "Er du helt sikker på du vil blokere hele {domain} domænet? I de fleste tilfælde vil få specifikke blokeringer eller dæmpninger være nok og at fortrække. Du vil ikke se indhold fra det domæne hverken på offentlige tidslinjer eller i dine notifikationer. Dine følgere fra det domæne vil blive fjernet.", + "confirmations.logout.confirm": "Log ud", + "confirmations.logout.message": "Er du sikker på du vil logge ud?", "confirmations.mute.confirm": "Dæmp", + "confirmations.mute.explanation": "Dette vil skjule indlæg fra dem, samt andre indlæg der omtaler dem, men de vil stadig være i stand til at se dine indlæg og følge dig.", "confirmations.mute.message": "Er du sikker på, du vil dæmpe {name}?", "confirmations.redraft.confirm": "Slet & omskriv", "confirmations.redraft.message": "Er du sikker på, du vil slette denne status og omskrive den? Favoritter og fremhævelser vil gå tabt og svar til det oprindelige opslag vil blive forældreløse.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Hvis du svarer nu vil du overskrive den besked du er ved at skrive. Er du sikker på, du vil fortsætte?", "confirmations.unfollow.confirm": "Følg ikke længere", "confirmations.unfollow.message": "Er du sikker på, du ikke længere vil følge {name}?", + "conversation.delete": "Slet samtale", + "conversation.mark_as_read": "Marker som læst", + "conversation.open": "Vis samtale", + "conversation.with": "Med {names}", + "directory.federated": "Fra kendt fedivers", + "directory.local": "Kun fra {domain}", + "directory.new_arrivals": "Nye ankomster", + "directory.recently_active": "Senest aktiv", "embed.instructions": "Indlejre denne status på din side ved at kopiere nedenstående kode.", "embed.preview": "Det kommer til at se således ud:", "emoji_button.activity": "Aktivitet", @@ -111,14 +129,14 @@ "emoji_button.nature": "Natur", "emoji_button.not_found": "Ingen emojos!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objekter", - "emoji_button.people": "Mennesker", + "emoji_button.people": "Personer", "emoji_button.recent": "Oftest brugt", "emoji_button.search": "Søg...", "emoji_button.search_results": "Søgeresultater", "emoji_button.symbols": "Symboler", "emoji_button.travel": "Rejser & steder", "empty_column.account_timeline": "Ingen bidrag her!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "Profil utilgængelig", "empty_column.blocks": "Du har ikke blokeret nogen endnu.", "empty_column.community": "Den lokale tidslinje er tom. Skriv noget offentligt for at starte lavinen!", "empty_column.direct": "Du har endnu ingen direkte beskeder. Når du sender eller modtager en, vil den vises her.", @@ -134,10 +152,14 @@ "empty_column.mutes": "Du har endnu ikke dæmpet nogen som helst bruger.", "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.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.", + "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", "getting_started.developers": "Udviklere", - "getting_started.directory": "Profile directory", + "getting_started.directory": "Profilliste", "getting_started.documentation": "Dokumentation", "getting_started.heading": "Kom igang", "getting_started.invite": "Inviter folk", @@ -147,35 +169,35 @@ "hashtag.column_header.tag_mode.all": "og {additional}", "hashtag.column_header.tag_mode.any": "eller {additional}", "hashtag.column_header.tag_mode.none": "uden {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.select.no_options_message": "Ingen forslag fundet", + "hashtag.column_settings.select.placeholder": "Indtast hashtags…", + "hashtag.column_settings.tag_mode.all": "Alle disse", + "hashtag.column_settings.tag_mode.any": "Nogle af disse", + "hashtag.column_settings.tag_mode.none": "Ingen af disse", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", "home.column_settings.basic": "Grundlæggende", "home.column_settings.show_reblogs": "Vis fremhævelser", "home.column_settings.show_replies": "Vis svar", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "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}}", "introduction.federation.action": "Næste", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Fælles", "introduction.federation.federated.text": "Offentlige bidrag fra andre servere af fediversen vil komme til syne i den federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.federation.home.headline": "Hjem", + "introduction.federation.home.text": "Statusser fra personer du følger vil blive vist i dit hjemmefeed. Du kan følge alle på enhver server!", + "introduction.federation.local.headline": "Lokal", + "introduction.federation.local.text": "Offentlige statusser fra personer på samme server som dig vil blive vist i det lokale feed.", "introduction.interactions.action": "Slut tutorial!", "introduction.interactions.favourite.headline": "Favorisere", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.favourite.text": "Du kan gemme en status til senere (og vise forfatteren at du kunne lide den) ved at favorisere den.", "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reblog.text": "Du kan delete andres statusser med dine følgere ved at booste dem.", "introduction.interactions.reply.headline": "Svar", "introduction.interactions.reply.text": "Du kan svare andres og din egen bidrag, hvilke vil kæde dem sammen i en konversation.", "introduction.welcome.action": "Læd os gå!", "introduction.welcome.headline": "Første skridt", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "introduction.welcome.text": "Velkommen til fediverset! Om få øjeblikke vil du kunne dele statusser og tale med dine venner på en bred vifte af servere. Men denne server, {domain}, er speciel. Det er på denne server at din profil har hjemme så husk dens navn.", "keyboard_shortcuts.back": "for at navigere dig tilbage", "keyboard_shortcuts.blocked": "for at åbne listen over blokerede brugere", "keyboard_shortcuts.boost": "for at fremhæve", @@ -204,23 +226,24 @@ "keyboard_shortcuts.search": "for at fokusere søgningen", "keyboard_shortcuts.start": "for at åbne \"kom igen\" kolonnen", "keyboard_shortcuts.toggle_hidden": "for at vise/skjule tekst bag CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "for at vise/skjule medier", "keyboard_shortcuts.toot": "for at påbegynde et helt nyt trut", "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.next": "Næste", "lightbox.previous": "Forrige", - "lightbox.view_context": "View context", + "lightbox.view_context": "Vis kontekst", "lists.account.add": "Tilføj til liste", "lists.account.remove": "Fjern fra liste", "lists.delete": "Slet liste", "lists.edit": "Rediger liste", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Skift titel", "lists.new.create": "Tilføj liste", "lists.new.title_placeholder": "Ny liste titel", "lists.search": "Søg iblandt folk du følger", "lists.subheading": "Dine lister", + "load_pending": "{count, plural, one {# nyt punkt} other {# nye punkter}}", "loading_indicator.label": "Indlæser...", "media_gallery.toggle_visible": "Ændre synlighed", "missing_indicator.label": "Ikke fundet", @@ -237,7 +260,7 @@ "navigation_bar.favourites": "Favoritter", "navigation_bar.filters": "Dæmpede ord", "navigation_bar.follow_requests": "Følgeanmodninger", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Følger og følgere", "navigation_bar.info": "Om denne instans", "navigation_bar.keyboard_shortcuts": "Hurtigtast", "navigation_bar.lists": "Lister", @@ -246,50 +269,52 @@ "navigation_bar.personal": "Personligt", "navigation_bar.pins": "Fastgjorte trut", "navigation_bar.preferences": "Præferencer", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Fælles tidslinje", "navigation_bar.security": "Sikkerhed", "notification.favourite": "{name} favoriserede din status", "notification.follow": "{name} fulgte dig", "notification.mention": "{name} nævnte dig", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} fremhævede din status", + "notification.poll": "En afstemning, du stemte i, er slut", + "notification.reblog": "{name} boostede din status", "notifications.clear": "Ryd notifikationer", "notifications.clear_confirmation": "Er du sikker på, du vil rydde alle dine notifikationer permanent?", - "notifications.column_settings.alert": "Skrivebords notifikationer", + "notifications.column_settings.alert": "Skrivebordsnotifikationer", "notifications.column_settings.favourite": "Favoritter:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Vis alle kategorier", + "notifications.column_settings.filter_bar.category": "Hurtigfilter", + "notifications.column_settings.filter_bar.show": "Vis", "notifications.column_settings.follow": "Nye følgere:", - "notifications.column_settings.mention": "Omtale:", - "notifications.column_settings.poll": "Poll results:", - "notifications.column_settings.push": "Push notifikationer", - "notifications.column_settings.reblog": "Fremhævelser:", + "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.filter.all": "Alle", "notifications.filter.boosts": "Boosts", "notifications.filter.favourites": "Favoritter", "notifications.filter.follows": "Følger", - "notifications.filter.mentions": "Mentions", - "notifications.filter.polls": "Poll results", + "notifications.filter.mentions": "Statusser der nævner dig", + "notifications.filter.polls": "Afstemningsresultat", "notifications.group": "{count} notifikationer", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", - "privacy.change": "Ændre status privatliv", - "privacy.direct.long": "Post til kun de nævnte brugere", + "poll.closed": "Lukket", + "poll.refresh": "Opdatér", + "poll.total_people": "{count, plural, one {# person} other {# personer}}", + "poll.total_votes": "{count, plural, one {# stemme} other {# stemmer}}", + "poll.vote": "Stem", + "poll.voted": "Du stemte for denne valgmulighed", + "poll_button.add_poll": "Tilføj en afstemning", + "poll_button.remove_poll": "Fjern afstemning", + "privacy.change": "Skift status visningsindstillinger", + "privacy.direct.long": "Udgiv kun til nævnte brugere", "privacy.direct.short": "Direkte", - "privacy.private.long": "Post kun til følgere", + "privacy.private.long": "Udgiv kun til følgere", "privacy.private.short": "Kun for følgere", - "privacy.public.long": "Post til offentlige tidslinjer", + "privacy.public.long": "Udgiv på offentlige tidslinjer", "privacy.public.short": "Offentligt", - "privacy.unlisted.long": "Post ikke til offentlige tidslinjer", + "privacy.unlisted.long": "Udgiv ikke på offentlige tidslinjer", "privacy.unlisted.short": "Ikke listet", + "refresh": "Opdatér", "regeneration_indicator.label": "Indlæser…", "regeneration_indicator.sublabel": "Din startside er ved at blive forberedt!", "relative_time.days": "{number}d", @@ -300,31 +325,32 @@ "reply_indicator.cancel": "Annuller", "report.forward": "Videresend til {target}", "report.forward_hint": "Kontoen er fra en anden server. Vil du også sende en anonym kopi af anmeldelsen dertil?", - "report.hint": "Anmeldelsen vil blive sendt til moderatorene af din instans. Du kan give en forklaring for hvorfor du anmelder denne konto nedenfor:", + "report.hint": "Anmeldelsen vil blive sendt til moderatorene af din instans. Du kan give en forklaring på hvorfor du anmelder denne konto nedenfor:", "report.placeholder": "Yderligere kommentarer", "report.submit": "Indsend", "report.target": "Anmelder {target}", "search.placeholder": "Søg", "search_popout.search_format": "Avanceret søgeformat", - "search_popout.tips.full_text": "Simpel tekst returnerer statusser du har skrevet, favoriseret, fremhævet, eller er blevet nævnt i, lige så vel som matchende brugernavne, visningsnavne, og hashtags.", - "search_popout.tips.hashtag": "emnetag", + "search_popout.tips.full_text": "Simpel tekst returnerer statusser du har skrevet, favoriseret, boostet, eller er blevet nævnt i såvel som matchende brugernavne, profilnavne, og hashtags.", + "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", - "search_popout.tips.text": "Simpelt tekst returnerer passende visningsnavne, brugernavne og hashtags", + "search_popout.tips.text": "Simpel tekst returnerer matchende profilnavne, brugernavne og hashtags", "search_popout.tips.user": "bruger", - "search_results.accounts": "Folk", - "search_results.hashtags": "Emnetags", + "search_results.accounts": "Personer", + "search_results.hashtags": "Hashtags", "search_results.statuses": "Trut", - "search_results.total": "{count, number} {count, plural, et {result} andre {results}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", + "search_results.statuses_fts_disabled": "Denne Mastodonserver har ikke aktiveret for søgning af statusser via deres indhold.", + "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", + "status.admin_account": "Åben modereringsvisning for @{name}", + "status.admin_status": "Åben denne status i modereringsvisningen", "status.block": "Bloker @{name}", - "status.cancel_reblog_private": "Fremhæv ikke længere", - "status.cannot_reblog": "Denne post kan ikke fremhæves", - "status.copy": "Copy link to status", + "status.cancel_reblog_private": "Fjern boost", + "status.cannot_reblog": "Denne post kan ikke boostes", + "status.copy": "Kopiér link til status", "status.delete": "Slet", "status.detailed_status": "Detaljeret visning af samtale", "status.direct": "Send direkte besked til @{name}", - "status.embed": "Indlejre", + "status.embed": "Integrér", "status.favourite": "Favorit", "status.filtered": "Filtreret", "status.load_more": "Indlæs mere", @@ -337,13 +363,13 @@ "status.pin": "Fastgør til profil", "status.pinned": "Fastgjort trut", "status.read_more": "Læs mere", - "status.reblog": "Fremhæv", - "status.reblog_private": "Fremhæv til oprindeligt publikum", - "status.reblogged_by": "{name} fremhævede", - "status.reblogs.empty": "Der er endnu ingen der har fremhævet dette trut. Når der er nogen der gør, vil det blive vist her.", + "status.reblog": "Boost", + "status.reblog_private": "Boost til det oprindelige publikum", + "status.reblogged_by": "{name} boostede", + "status.reblogs.empty": "Der er endnu ingen der har boostet dette trut. Når der er nogen der gør, vil det blive vist her.", "status.redraft": "Slet og omskriv", - "status.reply": "Svar", - "status.replyAll": "Svar samtale", + "status.reply": "Besvar", + "status.replyAll": "Besvar samtale", "status.report": "Anmeld @{name}", "status.sensitive_warning": "Følsomt indhold", "status.share": "Del", @@ -351,33 +377,42 @@ "status.show_less_all": "Vis mindre for alle", "status.show_more": "Vis mere", "status.show_more_all": "Vis mere for alle", - "status.show_thread": "Show thread", - "status.unmute_conversation": "Fjern dæmpningen fra samtale", - "status.unpin": "Fjern som fastgjort fra profil", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "status.show_thread": "Vis tråd", + "status.uncached_media_warning": "Ikke tilgængelig", + "status.unmute_conversation": "Genaktivér samtale", + "status.unpin": "Frigør fra profil", + "suggestions.dismiss": "Afvis foreslag", + "suggestions.header": "Du er måske interesseret i…", "tabs_bar.federated_timeline": "Fælles", "tabs_bar.home": "Hjem", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Notifikationer", "tabs_bar.search": "Søg", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} snakker", + "time_remaining.days": "{number, plural, one {# dag} other {# dage}} tilbage", + "time_remaining.hours": "{number, plural, one {# time} other {# timer}} tilbage", + "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", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {personer}} snakker", + "trends.trending_now": "Hot lige nu", "ui.beforeunload": "Din kladde vil gå tabt hvis du forlader Mastodon.", "upload_area.title": "Træk og slip for at uploade", "upload_button.label": "Tilføj medie (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", - "upload_form.description": "Beskriv for de svagtseende", - "upload_form.focus": "Beskær", + "upload_error.limit": "Uploadgrænse overskredet.", + "upload_error.poll": "Filupload ikke tilladt sammen med afstemninger.", + "upload_form.description": "Beskriv for svagtseende", + "upload_form.edit": "Redigér", "upload_form.undo": "Slet", + "upload_modal.analyzing_picture": "Analyserer billede…", + "upload_modal.apply": "Anvend", + "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.preview_label": "Forhåndsvisning ({ratio})", "upload_progress.label": "Uploader...", "video.close": "Luk video", - "video.exit_fullscreen": "Gå ud af fuldskærm", + "video.exit_fullscreen": "Forlad fuldskærm", "video.expand": "Udvid video", "video.fullscreen": "Fuldskærm", "video.hide": "Skjul video", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 5c63af3b2..c916028be 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -4,17 +4,19 @@ "account.block": "@{name} blockieren", "account.block_domain": "Alles von {domain} verstecken", "account.blocked": "Blockiert", + "account.cancel_follow_request": "Folgeanfrage abbrechen", "account.direct": "Direktnachricht an @{name}", "account.domain_blocked": "Domain versteckt", "account.edit_profile": "Profil bearbeiten", "account.endorse": "Auf Profil hervorheben", "account.follow": "Folgen", - "account.followers": "Folger_innen", + "account.followers": "Folgende", "account.followers.empty": "Diesem Profil folgt noch niemand.", "account.follows": "Folgt", "account.follows.empty": "Dieses Profil folgt noch niemandem.", "account.follows_you": "Folgt dir", "account.hide_reblogs": "Geteilte Beiträge von @{name} verbergen", + "account.last_status": "Zuletzt aktiv", "account.link_verified_on": "Besitz dieses Links wurde geprüft am {date}", "account.locked_info": "Der Privatsphärenstatus dieses Accounts wurde auf gesperrt gesetzt. Die Person bestimmt manuell wer ihm/ihr folgen darf.", "account.media": "Medien", @@ -23,6 +25,7 @@ "account.mute": "@{name} stummschalten", "account.mute_notifications": "Benachrichtigungen von @{name} verbergen", "account.muted": "Stummgeschaltet", + "account.never_active": "Nie", "account.posts": "Beiträge", "account.posts_with_replies": "Beiträge und Antworten", "account.report": "@{name} melden", @@ -35,8 +38,11 @@ "account.unfollow": "Entfolgen", "account.unmute": "@{name} nicht mehr stummschalten", "account.unmute_notifications": "Benachrichtigungen von @{name} einschalten", + "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.", "alert.unexpected.title": "Hoppla!", + "autosuggest_hashtag.per_week": "{count} pro Woche", "boost_modal.combo": "Drücke {combo}, um dieses Fenster zu überspringen", "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.", "bundle_column_error.retry": "Erneut versuchen", @@ -47,6 +53,7 @@ "column.blocks": "Blockierte Profile", "column.community": "Lokale Zeitleiste", "column.direct": "Direktnachrichten", + "column.directory": "Profile durchsuchen", "column.domain_blocks": "Versteckte Domains", "column.favourites": "Favoriten", "column.follow_requests": "Folgeanfragen", @@ -92,8 +99,11 @@ "confirmations.delete_list.confirm": "Löschen", "confirmations.delete_list.message": "Bist du dir sicher, dass du diese Liste permanent löschen möchtest?", "confirmations.domain_block.confirm": "Die ganze Domain verbergen", - "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} blockieren willst? In den meisten Fällen reichen ein paar gezielte Blockierungen oder Stummschaltungen aus. Nach der Blockierung wirst du nichts mehr von dieser Domain in öffentlichen Zeitleisten oder Benachrichtigungen sehen. Deine Folger_innen von dieser Domain werden auch entfernt.", + "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} blockieren willst? In den meisten Fällen reichen ein paar gezielte Blockierungen oder Stummschaltungen aus. Du wirst den Inhalt von dieser Domain nicht in irgendwelchen öffentlichen Timelines oder den Benachrichtigungen finden. Deine Folgenden von dieser Domain werden entfernt.", + "confirmations.logout.confirm": "Abmelden", + "confirmations.logout.message": "Bist du sicher, dass du dich abmelden möchtest?", "confirmations.mute.confirm": "Stummschalten", + "confirmations.mute.explanation": "Dies wird Beiträge von dieser Person und Beiträge, die diese Person erwähnen, ausblenden, aber es wird der Person trotzdem erlauben, deine Beiträge zu sehen und dir zu folgen.", "confirmations.mute.message": "Bist du dir sicher, dass du {name} stummschalten möchtest?", "confirmations.redraft.confirm": "Löschen und neu erstellen", "confirmations.redraft.message": "Bist du dir sicher, dass du diesen Beitrag löschen und neu erstellen möchtest? Favorisierungen, geteilte Beiträge und Antworten werden verloren gehen.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Wenn du jetzt antwortest wird es die gesamte Nachricht verwerfen, die du gerade schreibst. Möchtest du wirklich fortfahren?", "confirmations.unfollow.confirm": "Entfolgen", "confirmations.unfollow.message": "Bist du dir sicher, dass du {name} entfolgen möchtest?", + "conversation.delete": "Unterhaltung löschen", + "conversation.mark_as_read": "Als gelesen markieren", + "conversation.open": "Unterhaltung anzeigen", + "conversation.with": "Mit {names}", + "directory.federated": "Aus dem Fediverse", + "directory.local": "Nur von {domain}", + "directory.new_arrivals": "Neue Benutzer", + "directory.recently_active": "Kürzlich aktiv", "embed.instructions": "Du kannst diesen Beitrag auf deiner Webseite einbetten, indem du den folgenden Code einfügst.", "embed.preview": "So wird es aussehen:", "emoji_button.activity": "Aktivitäten", @@ -134,6 +152,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 Browsereinkompatibilität konnte diese Seite nicht korrekt angezeigt werden.", + "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.", + "errors.unexpected_crash.copy_stacktrace": "Fehlerlog in die Zwischenablage kopieren", + "errors.unexpected_crash.report_issue": "Problem melden", "follow_request.authorize": "Erlauben", "follow_request.reject": "Ablehnen", "getting_started.developers": "Entwickler", @@ -168,7 +190,7 @@ "introduction.federation.local.text": "Öffentliche Beiträge von Leuten auf demselben Server wie du erscheinen in der lokalen Zeitleiste.", "introduction.interactions.action": "Tutorial beenden!", "introduction.interactions.favourite.headline": "Favorisieren", - "introduction.interactions.favourite.text": "Du kannst Beitrage für später speichern und ihre Autor_innen wissen lassen, dass sie dir gefallen haben, indem du sie favorisierst.", + "introduction.interactions.favourite.text": "Du kannst Beitrage für später speichern und ihre Autoren wissen lassen, dass sie dir gefallen haben, indem du sie favorisierst.", "introduction.interactions.reblog.headline": "Teilen", "introduction.interactions.reblog.text": "Du kannst Beiträge anderer mit deinen Followern teilen, indem du sie teilst.", "introduction.interactions.reply.headline": "Antworten", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Neuer Titel der Liste", "lists.search": "Suche nach Leuten denen du folgst", "lists.subheading": "Deine Listen", + "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}", "loading_indicator.label": "Wird geladen …", "media_gallery.toggle_visible": "Sichtbarkeit umschalten", "missing_indicator.label": "Nicht gefunden", @@ -237,7 +260,7 @@ "navigation_bar.favourites": "Favoriten", "navigation_bar.filters": "Stummgeschaltene Wörter", "navigation_bar.follow_requests": "Folgeanfragen", - "navigation_bar.follows_and_followers": "Folger_innen und Gefolgte", + "navigation_bar.follows_and_followers": "Folgende und Gefolgte", "navigation_bar.info": "Über diesen Server", "navigation_bar.keyboard_shortcuts": "Tastenkombinationen", "navigation_bar.lists": "Listen", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Persönlich", "navigation_bar.pins": "Angeheftete Beiträge", "navigation_bar.preferences": "Einstellungen", - "navigation_bar.profile_directory": "Profilverzeichnis", "navigation_bar.public_timeline": "Föderierte Zeitleiste", "navigation_bar.security": "Sicherheit", "notification.favourite": "{name} hat deinen Beitrag favorisiert", @@ -261,7 +283,7 @@ "notifications.column_settings.filter_bar.advanced": "Zeige alle Kategorien an", "notifications.column_settings.filter_bar.category": "Schnellfilterleiste", "notifications.column_settings.filter_bar.show": "Anzeigen", - "notifications.column_settings.follow": "Neue Folger_innen:", + "notifications.column_settings.follow": "Neue Folgende:", "notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.poll": "Ergebnisse von Umfragen:", "notifications.column_settings.push": "Push-Benachrichtigungen", @@ -271,25 +293,28 @@ "notifications.filter.all": "Alle", "notifications.filter.boosts": "Geteilte Beiträge", "notifications.filter.favourites": "Favorisierungen", - "notifications.filter.follows": "Folger_innen", + "notifications.filter.follows": "Folgt", "notifications.filter.mentions": "Erwähnungen", "notifications.filter.polls": "Ergebnisse der Umfrage", "notifications.group": "{count} Benachrichtigungen", "poll.closed": "Geschlossen", "poll.refresh": "Aktualisieren", + "poll.total_people": "{count, plural, one {# Person} other {# Personen}}", "poll.total_votes": "{count, plural, one {# Stimme} other {# Stimmen}}", "poll.vote": "Abstimmen", + "poll.voted": "Du hast dafür gestimmt", "poll_button.add_poll": "Eine Umfrage erstellen", "poll_button.remove_poll": "Umfrage entfernen", "privacy.change": "Sichtbarkeit des Beitrags anpassen", "privacy.direct.long": "Wird an erwähnte Profile gesendet", "privacy.direct.short": "Direktnachricht", - "privacy.private.long": "Wird nur für deine Folger_innen sichtbar sein", - "privacy.private.short": "Nur für Folger_innen", + "privacy.private.long": "Wird nur für deine Folgende sichtbar sein", + "privacy.private.short": "Nur für Folgende", "privacy.public.long": "Wird in öffentlichen Zeitleisten erscheinen", "privacy.public.short": "Öffentlich", "privacy.unlisted.long": "Wird in öffentlichen Zeitleisten nicht gezeigt", "privacy.unlisted.short": "Nicht gelistet", + "refresh": "Aktualisieren", "regeneration_indicator.label": "Laden…", "regeneration_indicator.sublabel": "Deine Startseite wird gerade vorbereitet!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "Personen", "search_results.hashtags": "Hashtags", "search_results.statuses": "Beiträge", + "search_results.statuses_fts_disabled": "Die Suche für Beiträge nach ihrem Inhalt ist auf diesem Mastodon-Server deaktiviert.", "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", "status.admin_account": "Öffne Moderationsoberfläche für @{name}", "status.admin_status": "Öffne Beitrag in der Moderationsoberfläche", @@ -352,6 +378,7 @@ "status.show_more": "Mehr anzeigen", "status.show_more_all": "Alle Inhaltswarnungen aufklappen", "status.show_thread": "Zeige Konversation", + "status.uncached_media_warning": "Nicht verfügbar", "status.unmute_conversation": "Stummschaltung von Konversation aufheben", "status.unpin": "Vom Profil lösen", "suggestions.dismiss": "Empfehlung ausblenden", @@ -367,14 +394,22 @@ "time_remaining.moments": "Schließt in Kürze", "time_remaining.seconds": "{number, plural, one {# Sekunde} other {# Sekunden}} verbleibend", "trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber", + "trends.trending_now": "In den Trends", "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.", "upload_area.title": "Zum Hochladen hereinziehen", - "upload_button.label": "Mediendatei hinzufügen (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Mediendatei hinzufügen ({formats})", "upload_error.limit": "Dateiupload-Limit erreicht.", "upload_error.poll": "Dateiuploads sind in Kombination mit Umfragen nicht erlaubt.", "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben", - "upload_form.focus": "Vorschaubild bearbeiten", + "upload_form.edit": "Bearbeiten", "upload_form.undo": "Löschen", + "upload_modal.analyzing_picture": "Analysiere Bild…", + "upload_modal.apply": "Übernehmen", + "upload_modal.description_placeholder": "Die heiße Zypernsonne quälte Max und Victoria ja böse auf dem Weg bis zur Küste.", + "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.preview_label": "Vorschau ({ratio})", "upload_progress.label": "Wird hochgeladen …", "video.close": "Video schließen", "video.exit_fullscreen": "Vollbild verlassen", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index faca8241d..16e3e402a 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -8,6 +8,14 @@ { "defaultMessage": "An unexpected error occurred.", "id": "alert.unexpected.message" + }, + { + "defaultMessage": "Rate limited", + "id": "alert.rate_limited.title" + }, + { + "defaultMessage": "Please retry after {retry_time, time, medium}.", + "id": "alert.rate_limited.message" } ], "path": "app/javascript/mastodon/actions/alerts.json" @@ -74,6 +82,15 @@ { "descriptors": [ { + "defaultMessage": "{count} per week", + "id": "autosuggest_hashtag.per_week" + } + ], + "path": "app/javascript/mastodon/components/autosuggest_hashtag.json" + }, + { + "descriptors": [ + { "defaultMessage": "Back", "id": "column_back_button.label" } @@ -134,6 +151,27 @@ { "descriptors": [ { + "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 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" + }, + { + "defaultMessage": "Report issue", + "id": "errors.unexpected_crash.report_issue" + }, + { + "defaultMessage": "Copy stacktrace to clipboard", + "id": "errors.unexpected_crash.copy_stacktrace" + } + ], + "path": "app/javascript/mastodon/components/error_boundary.json" + }, + { + "descriptors": [ + { "defaultMessage": "{count} {rawCount, plural, one {person} other {people}} talking", "id": "trends.count_by_accounts" } @@ -161,6 +199,15 @@ { "descriptors": [ { + "defaultMessage": "{count, plural, one {# new item} other {# new items}}", + "id": "load_pending" + } + ], + "path": "app/javascript/mastodon/components/load_pending.json" + }, + { + "descriptors": [ + { "defaultMessage": "Loading...", "id": "loading_indicator.label" } @@ -174,6 +221,10 @@ "id": "media_gallery.toggle_visible" }, { + "defaultMessage": "Not available", + "id": "status.uncached_media_warning" + }, + { "defaultMessage": "Sensitive content", "id": "status.sensitive_warning" }, @@ -204,19 +255,41 @@ "id": "poll.closed" }, { + "defaultMessage": "You voted for this answer", + "description": "Tooltip of the \"voted\" checkmark in polls", + "id": "poll.voted" + }, + { + "defaultMessage": "{count, plural, one {# person} other {# people}}", + "id": "poll.total_people" + }, + { + "defaultMessage": "{count, plural, one {# vote} other {# votes}}", + "id": "poll.total_votes" + }, + { "defaultMessage": "Vote", "id": "poll.vote" }, { "defaultMessage": "Refresh", "id": "poll.refresh" + } + ], + "path": "app/javascript/mastodon/components/poll.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Loading…", + "id": "regeneration_indicator.label" }, { - "defaultMessage": "{count, plural, one {# vote} other {# votes}}", - "id": "poll.total_votes" + "defaultMessage": "Your home feed is being prepared!", + "id": "regeneration_indicator.sublabel" } ], - "path": "app/javascript/mastodon/components/poll.json" + "path": "app/javascript/mastodon/components/regeneration_indicator.json" }, { "descriptors": [ @@ -326,6 +399,14 @@ "id": "status.favourite" }, { + "defaultMessage": "Bookmark", + "id": "status.bookmark" + }, + { + "defaultMessage": "Remove bookmark", + "id": "status.remove_bookmark" + }, + { "defaultMessage": "Expand this status", "id": "status.open" }, @@ -364,6 +445,22 @@ { "defaultMessage": "Copy link to status", "id": "status.copy" + }, + { + "defaultMessage": "Hide everything from {domain}", + "id": "account.block_domain" + }, + { + "defaultMessage": "Unhide {domain}", + "id": "account.unblock_domain" + }, + { + "defaultMessage": "Unmute @{name}", + "id": "account.unmute" + }, + { + "defaultMessage": "Unblock @{name}", + "id": "account.unblock" } ], "path": "app/javascript/mastodon/components/status_action_bar.json" @@ -388,19 +485,6 @@ { "descriptors": [ { - "defaultMessage": "Loading…", - "id": "regeneration_indicator.label" - }, - { - "defaultMessage": "Your home feed is being prepared!", - "id": "regeneration_indicator.sublabel" - } - ], - "path": "app/javascript/mastodon/components/status_list.json" - }, - { - "descriptors": [ - { "defaultMessage": "Filtered", "id": "status.filtered" }, @@ -464,10 +548,6 @@ "id": "confirmations.redraft.message" }, { - "defaultMessage": "Block", - "id": "confirmations.block.confirm" - }, - { "defaultMessage": "Reply", "id": "confirmations.reply.confirm" }, @@ -476,12 +556,12 @@ "id": "confirmations.reply.message" }, { - "defaultMessage": "Block & Report", - "id": "confirmations.block.block_and_report" + "defaultMessage": "Hide entire domain", + "id": "confirmations.domain_block.confirm" }, { - "defaultMessage": "Are you sure you want to block {name}?", - "id": "confirmations.block.message" + "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "id": "confirmations.domain_block.message" } ], "path": "app/javascript/mastodon/containers/status_container.json" @@ -519,26 +599,14 @@ "id": "confirmations.unfollow.confirm" }, { - "defaultMessage": "Block", - "id": "confirmations.block.confirm" - }, - { "defaultMessage": "Hide entire domain", "id": "confirmations.domain_block.confirm" }, { - "defaultMessage": "Block & Report", - "id": "confirmations.block.block_and_report" - }, - { "defaultMessage": "Are you sure you want to unfollow {name}?", "id": "confirmations.unfollow.message" }, { - "defaultMessage": "Are you sure you want to block {name}?", - "id": "confirmations.block.message" - }, - { "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", "id": "confirmations.domain_block.message" } @@ -569,6 +637,10 @@ "id": "account.follow" }, { + "defaultMessage": "Cancel follow request", + "id": "account.cancel_follow_request" + }, + { "defaultMessage": "Awaiting approval. Click to cancel follow request", "id": "account.requested" }, @@ -722,6 +794,31 @@ { "descriptors": [ { + "defaultMessage": "Play", + "id": "video.play" + }, + { + "defaultMessage": "Pause", + "id": "video.pause" + }, + { + "defaultMessage": "Mute sound", + "id": "video.mute" + }, + { + "defaultMessage": "Unmute sound", + "id": "video.unmute" + }, + { + "defaultMessage": "Download file", + "id": "video.download" + } + ], + "path": "app/javascript/mastodon/features/audio/index.json" + }, + { + "descriptors": [ + { "defaultMessage": "Blocked users", "id": "column.blocks" }, @@ -735,7 +832,20 @@ { "descriptors": [ { - "defaultMessage": "Media Only", + "defaultMessage": "Bookmarks", + "id": "column.bookmarks" + }, + { + "defaultMessage": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.", + "id": "empty_column.bookmarked_statuses" + } + ], + "path": "app/javascript/mastodon/features/bookmarked_statuses/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Media only", "id": "community.column_settings.media_only" } ], @@ -795,6 +905,10 @@ { "defaultMessage": "Muted words", "id": "navigation_bar.filters" + }, + { + "defaultMessage": "Logout", + "id": "navigation_bar.logout" } ], "path": "app/javascript/mastodon/features/compose/components/action_bar.json" @@ -1005,6 +1119,10 @@ "id": "search_results.statuses" }, { + "defaultMessage": "Searching toots by their content is not enabled on this Mastodon server.", + "id": "search_results.statuses_fts_disabled" + }, + { "defaultMessage": "Hashtags", "id": "search_results.hashtags" }, @@ -1051,7 +1169,7 @@ { "descriptors": [ { - "defaultMessage": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "defaultMessage": "Add media ({formats})", "id": "upload_button.label" } ], @@ -1060,25 +1178,21 @@ { "descriptors": [ { - "defaultMessage": "Uploading...", + "defaultMessage": "Uploading…", "id": "upload_progress.label" } ], - "path": "app/javascript/mastodon/features/compose/components/upload_progress.json" + "path": "app/javascript/mastodon/features/compose/components/upload_form.json" }, { "descriptors": [ { - "defaultMessage": "Describe for the visually impaired", - "id": "upload_form.description" - }, - { "defaultMessage": "Delete", "id": "upload_form.undo" }, { - "defaultMessage": "Crop", - "id": "upload_form.focus" + "defaultMessage": "Edit", + "id": "upload_form.edit" } ], "path": "app/javascript/mastodon/features/compose/components/upload.json" @@ -1086,6 +1200,19 @@ { "descriptors": [ { + "defaultMessage": "Are you sure you want to log out?", + "id": "confirmations.logout.message" + }, + { + "defaultMessage": "Log out", + "id": "confirmations.logout.confirm" + } + ], + "path": "app/javascript/mastodon/features/compose/containers/navigation_container.json" + }, + { + "descriptors": [ + { "defaultMessage": "Media is marked as sensitive", "id": "compose_form.sensitive.marked" }, @@ -1171,6 +1298,14 @@ { "defaultMessage": "Compose new toot", "id": "navigation_bar.compose" + }, + { + "defaultMessage": "Are you sure you want to log out?", + "id": "confirmations.logout.message" + }, + { + "defaultMessage": "Log out", + "id": "confirmations.logout.confirm" } ], "path": "app/javascript/mastodon/features/compose/index.json" @@ -1178,6 +1313,56 @@ { "descriptors": [ { + "defaultMessage": "More", + "id": "status.more" + }, + { + "defaultMessage": "View conversation", + "id": "conversation.open" + }, + { + "defaultMessage": "Reply", + "id": "status.reply" + }, + { + "defaultMessage": "Mark as read", + "id": "conversation.mark_as_read" + }, + { + "defaultMessage": "Delete conversation", + "id": "conversation.delete" + }, + { + "defaultMessage": "Mute conversation", + "id": "status.mute_conversation" + }, + { + "defaultMessage": "Unmute conversation", + "id": "status.unmute_conversation" + }, + { + "defaultMessage": "With {names}", + "id": "conversation.with" + } + ], + "path": "app/javascript/mastodon/features/direct_timeline/components/conversation.json" + }, + { + "descriptors": [ + { + "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" + } + ], + "path": "app/javascript/mastodon/features/direct_timeline/containers/conversation_container.json" + }, + { + "descriptors": [ + { "defaultMessage": "Direct messages", "id": "column.direct" }, @@ -1191,6 +1376,76 @@ { "descriptors": [ { + "defaultMessage": "Follow", + "id": "account.follow" + }, + { + "defaultMessage": "Unfollow", + "id": "account.unfollow" + }, + { + "defaultMessage": "Awaiting approval", + "id": "account.requested" + }, + { + "defaultMessage": "Unblock @{name}", + "id": "account.unblock" + }, + { + "defaultMessage": "Unmute @{name}", + "id": "account.unmute" + }, + { + "defaultMessage": "Are you sure you want to unfollow {name}?", + "id": "confirmations.unfollow.message" + }, + { + "defaultMessage": "Toots", + "id": "account.posts" + }, + { + "defaultMessage": "Followers", + "id": "account.followers" + }, + { + "defaultMessage": "Never", + "id": "account.never_active" + }, + { + "defaultMessage": "Last active", + "id": "account.last_status" + } + ], + "path": "app/javascript/mastodon/features/directory/components/account_card.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Browse profiles", + "id": "column.directory" + }, + { + "defaultMessage": "Recently active", + "id": "directory.recently_active" + }, + { + "defaultMessage": "New arrivals", + "id": "directory.new_arrivals" + }, + { + "defaultMessage": "From {domain} only", + "id": "directory.local" + }, + { + "defaultMessage": "From known fediverse", + "id": "directory.federated" + } + ], + "path": "app/javascript/mastodon/features/directory/index.json" + }, + { + "descriptors": [ + { "defaultMessage": "Hidden domains", "id": "column.domain_blocks" }, @@ -1221,6 +1476,10 @@ { "descriptors": [ { + "defaultMessage": "Refresh", + "id": "refresh" + }, + { "defaultMessage": "No one has favourited this toot yet. When someone does, they will show up here.", "id": "empty_column.favourites" } @@ -1282,6 +1541,15 @@ { "descriptors": [ { + "defaultMessage": "Trending now", + "id": "trends.trending_now" + } + ], + "path": "app/javascript/mastodon/features/getting_started/components/trends.json" + }, + { + "descriptors": [ + { "defaultMessage": "Home", "id": "tabs_bar.home" }, @@ -1306,6 +1574,10 @@ "id": "navigation_bar.direct" }, { + "defaultMessage": "Bookmarks", + "id": "navigation_bar.bookmarks" + }, + { "defaultMessage": "Preferences", "id": "navigation_bar.preferences" }, @@ -1556,6 +1828,10 @@ "id": "keyboard_shortcuts.enter" }, { + "defaultMessage": "to open media", + "id": "keyboard_shortcuts.open_media" + }, + { "defaultMessage": "to show/hide text behind CW", "id": "keyboard_shortcuts.toggle_hidden" }, @@ -1806,6 +2082,10 @@ "id": "notifications.column_settings.follow" }, { + "defaultMessage": "New follow requests:", + "id": "notifications.column_settings.follow_request" + }, + { "defaultMessage": "Favourites:", "id": "notifications.column_settings.favourite" }, @@ -1856,20 +2136,41 @@ { "descriptors": [ { - "defaultMessage": "{name} followed you", - "id": "notification.follow" + "defaultMessage": "Authorize", + "id": "follow_request.authorize" }, { + "defaultMessage": "Reject", + "id": "follow_request.reject" + } + ], + "path": "app/javascript/mastodon/features/notifications/components/follow_request.json" + }, + { + "descriptors": [ + { "defaultMessage": "{name} favourited your status", "id": "notification.favourite" }, { - "defaultMessage": "{name} boosted your status", - "id": "notification.reblog" + "defaultMessage": "{name} followed you", + "id": "notification.follow" + }, + { + "defaultMessage": "Your poll has ended", + "id": "notification.own_poll" }, { "defaultMessage": "A poll you have voted in has ended", "id": "notification.poll" + }, + { + "defaultMessage": "{name} boosted your status", + "id": "notification.reblog" + }, + { + "defaultMessage": "{name} has requested to follow you", + "id": "notification.follow_request" } ], "path": "app/javascript/mastodon/features/notifications/components/notification.json" @@ -1925,6 +2226,10 @@ { "descriptors": [ { + "defaultMessage": "Refresh", + "id": "refresh" + }, + { "defaultMessage": "No one has boosted this toot yet. When someone does, they will show up here.", "id": "status.reblogs.empty" } @@ -1974,6 +2279,10 @@ "id": "status.favourite" }, { + "defaultMessage": "Bookmark", + "id": "status.bookmark" + }, + { "defaultMessage": "Mute @{name}", "id": "status.mute" }, @@ -2020,6 +2329,22 @@ { "defaultMessage": "Copy link to status", "id": "status.copy" + }, + { + "defaultMessage": "Hide everything from {domain}", + "id": "account.block_domain" + }, + { + "defaultMessage": "Unhide {domain}", + "id": "account.unblock_domain" + }, + { + "defaultMessage": "Unmute @{name}", + "id": "account.unmute" + }, + { + "defaultMessage": "Unblock @{name}", + "id": "account.unblock" } ], "path": "app/javascript/mastodon/features/status/components/action_bar.json" @@ -2043,24 +2368,12 @@ "id": "confirmations.redraft.message" }, { - "defaultMessage": "Block", - "id": "confirmations.block.confirm" - }, - { "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": "Block & Report", - "id": "confirmations.block.block_and_report" - }, - { - "defaultMessage": "Are you sure you want to block {name}?", - "id": "confirmations.block.message" } ], "path": "app/javascript/mastodon/features/status/containers/detailed_status_container.json" @@ -2084,10 +2397,6 @@ "id": "confirmations.redraft.message" }, { - "defaultMessage": "Block", - "id": "confirmations.block.confirm" - }, - { "defaultMessage": "Show more for all", "id": "status.show_more_all" }, @@ -2108,15 +2417,45 @@ "id": "confirmations.reply.message" }, { - "defaultMessage": "Block & Report", - "id": "confirmations.block.block_and_report" + "defaultMessage": "Hide entire domain", + "id": "confirmations.domain_block.confirm" }, { + "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "id": "confirmations.domain_block.message" + } + ], + "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": [ + { "defaultMessage": "Are you sure you want to block {name}?", "id": "confirmations.block.message" + }, + { + "defaultMessage": "Cancel", + "id": "confirmation_modal.cancel" + }, + { + "defaultMessage": "Block & Report", + "id": "confirmations.block.block_and_report" + }, + { + "defaultMessage": "Block", + "id": "confirmations.block.confirm" } ], - "path": "app/javascript/mastodon/features/status/index.json" + "path": "app/javascript/mastodon/features/ui/components/block_modal.json" }, { "descriptors": [ @@ -2203,6 +2542,10 @@ { "descriptors": [ { + "defaultMessage": "Close", + "id": "lightbox.close" + }, + { "defaultMessage": "Embed", "id": "status.embed" }, @@ -2220,6 +2563,55 @@ { "descriptors": [ { + "defaultMessage": "Close", + "id": "lightbox.close" + }, + { + "defaultMessage": "Apply", + "id": "upload_modal.apply" + }, + { + "defaultMessage": "A quick brown fox jumps over the lazy dog", + "id": "upload_modal.description_placeholder" + }, + { + "defaultMessage": "Describe for people with hearing loss", + "id": "upload_form.audio_description" + }, + { + "defaultMessage": "Describe for people with hearing loss or visual impairment", + "id": "upload_form.video_description" + }, + { + "defaultMessage": "Describe for the visually impaired", + "id": "upload_form.description" + }, + { + "defaultMessage": "Edit media", + "id": "upload_modal.edit_media" + }, + { + "defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", + "id": "upload_modal.hint" + }, + { + "defaultMessage": "Analyzing picture…", + "id": "upload_modal.analyzing_picture" + }, + { + "defaultMessage": "Detect text from picture", + "id": "upload_modal.detect_text" + }, + { + "defaultMessage": "Preview ({ratio})", + "id": "upload_modal.preview_label" + } + ], + "path": "app/javascript/mastodon/features/ui/components/focal_point_modal.json" + }, + { + "descriptors": [ + { "defaultMessage": "Follow requests", "id": "navigation_bar.follow_requests" } @@ -2229,6 +2621,14 @@ { "descriptors": [ { + "defaultMessage": "Are you sure you want to log out?", + "id": "confirmations.logout.message" + }, + { + "defaultMessage": "Log out", + "id": "confirmations.logout.confirm" + }, + { "defaultMessage": "Invite people", "id": "getting_started.invite" }, @@ -2299,6 +2699,10 @@ "id": "confirmations.mute.message" }, { + "defaultMessage": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", + "id": "confirmations.mute.explanation" + }, + { "defaultMessage": "Hide notifications from this user?", "id": "mute_modal.hide_notifications" }, @@ -2340,20 +2744,24 @@ "id": "navigation_bar.favourites" }, { + "defaultMessage": "Bookmarks", + "id": "navigation_bar.bookmarks" + }, + { "defaultMessage": "Lists", "id": "navigation_bar.lists" }, { + "defaultMessage": "Profile directory", + "id": "getting_started.directory" + }, + { "defaultMessage": "Preferences", "id": "navigation_bar.preferences" }, { "defaultMessage": "Follows and followers", "id": "navigation_bar.follows_and_followers" - }, - { - "defaultMessage": "Profile directory", - "id": "navigation_bar.profile_directory" } ], "path": "app/javascript/mastodon/features/ui/components/navigation_panel.json" @@ -2482,6 +2890,10 @@ "id": "video.exit_fullscreen" }, { + "defaultMessage": "Download file", + "id": "video.download" + }, + { "defaultMessage": "Sensitive content", "id": "status.sensitive_warning" }, diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index ceaa2be4d..ab000fa8d 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -1,9 +1,10 @@ { "account.add_or_remove_from_list": "Προσθήκη ή Αφαίρεση από λίστες", "account.badges.bot": "Μποτ", - "account.block": "Απόκλεισε τον/την @{name}", + "account.block": "Αποκλεισμός @{name}", "account.block_domain": "Απόκρυψε τα πάντα από το {domain}", "account.blocked": "Αποκλεισμένος/η", + "account.cancel_follow_request": "Ακύρωση αιτήματος παρακολούθησης", "account.direct": "Προσωπικό μήνυμα προς @{name}", "account.domain_blocked": "Κρυμμένος τομέας", "account.edit_profile": "Επεξεργασία προφίλ", @@ -15,6 +16,7 @@ "account.follows.empty": "Αυτός ο χρήστης δεν ακολουθεί κανέναν ακόμα.", "account.follows_you": "Σε ακολουθεί", "account.hide_reblogs": "Απόκρυψη προωθήσεων από @{name}", + "account.last_status": "Τελευταία δραστηριότητα", "account.link_verified_on": "Η ιδιοκτησία αυτού του συνδέσμου ελέχθηκε την {date}", "account.locked_info": "Η κατάσταση απορρήτου αυτού του λογαριασμού είναι κλειδωμένη. Ο ιδιοκτήτης επιβεβαιώνει χειροκίνητα ποιος μπορεί να τον ακολουθήσει.", "account.media": "Πολυμέσα", @@ -23,30 +25,35 @@ "account.mute": "Σώπασε @{name}", "account.mute_notifications": "Σώπασε τις ειδοποιήσεις από @{name}", "account.muted": "Αποσιωπημένος/η", + "account.never_active": "Ποτέ", "account.posts": "Τουτ", "account.posts_with_replies": "Τουτ και απαντήσεις", "account.report": "Κατάγγειλε @{name}", "account.requested": "Εκκρεμεί έγκριση. Κάνε κλικ για να ακυρώσεις το αίτημα παρακολούθησης", - "account.share": "Μοιράσου το προφίλ του/της @{name}", - "account.show_reblogs": "Δείξε τις προωθήσεις του/της @{name}", + "account.share": "Μοίρασμα του προφίλ @{name}", + "account.show_reblogs": "Εμφάνιση προωθήσεων από @{name}", "account.unblock": "Ξεμπλόκαρε @{name}", "account.unblock_domain": "Αποκάλυψε το {domain}", "account.unendorse": "Άνευ προβολής στο προφίλ", "account.unfollow": "Διακοπή παρακολούθησης", - "account.unmute": "Διακοπή αποσιώπησης του/της @{name}", + "account.unmute": "Διακοπή αποσιώπησης @{name}", "account.unmute_notifications": "Διακοπή αποσιώπησης ειδοποιήσεων του/της @{name}", + "alert.rate_limited.message": "Παρακαλούμε δοκίμασε ξανά αφού περάσει η {retry_time, time, medium}.", + "alert.rate_limited.title": "Περιορισμός συχνότητας", "alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.", "alert.unexpected.title": "Εεπ!", + "autosuggest_hashtag.per_week": "{count} ανα εβδομάδα", "boost_modal.combo": "Μπορείς να πατήσεις {combo} για να το προσπεράσεις αυτό την επόμενη φορά", "bundle_column_error.body": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.", "bundle_column_error.retry": "Δοκίμασε ξανά", "bundle_column_error.title": "Σφάλμα δικτύου", - "bundle_modal_error.close": "Κλείσε", - "bundle_modal_error.message": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.", + "bundle_modal_error.close": "Κλείσιμο", + "bundle_modal_error.message": "Κάτι πήγε στραβά κατά τη φόρτωση του στοιχείου.", "bundle_modal_error.retry": "Δοκίμασε ξανά", "column.blocks": "Αποκλεισμένοι χρήστες", "column.community": "Τοπική ροή", "column.direct": "Προσωπικά μηνύματα", + "column.directory": "Δες προφίλ", "column.domain_blocks": "Κρυμμένοι τομείς", "column.favourites": "Αγαπημένα", "column.follow_requests": "Αιτήματα ακολούθησης", @@ -69,7 +76,7 @@ "compose_form.direct_message_warning_learn_more": "Μάθετε περισσότερα", "compose_form.hashtag_warning": "Αυτό το τουτ δεν θα εμφανίζεται κάτω από κανένα hashtag καθώς είναι αφανές. Μόνο τα δημόσια τουτ μπορούν να αναζητηθούν ανά hashtag.", "compose_form.lock_disclaimer": "Ο λογαριασμός σου δεν είναι {locked}. Οποιοσδήποτε μπορεί να σε ακολουθήσει για να δει τις δημοσιεύσεις σας προς τους ακολούθους σας.", - "compose_form.lock_disclaimer.lock": "κλειδωμένος", + "compose_form.lock_disclaimer.lock": "κλειδωμένο", "compose_form.placeholder": "Τι σκέφτεσαι;", "compose_form.poll.add_option": "Προσθήκη επιλογής", "compose_form.poll.duration": "Διάρκεια δημοσκόπησης", @@ -88,12 +95,15 @@ "confirmations.block.confirm": "Απόκλεισε", "confirmations.block.message": "Σίγουρα θες να αποκλείσεις {name};", "confirmations.delete.confirm": "Διέγραψε", - "confirmations.delete.message": "Σίγουρα θες να διαγράψεις αυτή την κατάσταση;", + "confirmations.delete.message": "Σίγουρα θες να διαγράψεις αυτή τη δημοσίευση;", "confirmations.delete_list.confirm": "Διέγραψε", "confirmations.delete_list.message": "Σίγουρα θες να διαγράψεις οριστικά αυτή τη λίστα;", "confirmations.domain_block.confirm": "Απόκρυψη ολόκληρου του τομέα", "confirmations.domain_block.message": "Σίγουρα θες να μπλοκάρεις ολόκληρο το {domain}; Συνήθως μερικά εστιασμένα μπλοκ ή αποσιωπήσεις επαρκούν και προτιμούνται. Δεν θα βλέπεις περιεχόμενο από αυτό τον κόμβο σε καμία δημόσια ροή, ούτε στις ειδοποιήσεις σου. Όσους ακόλουθους έχεις αυτό αυτό τον κόμβο θα αφαιρεθούν.", + "confirmations.logout.confirm": "Αποσύνδεση", + "confirmations.logout.message": "Σίγουρα θέλεις να αποσυνδεθείς;", "confirmations.mute.confirm": "Αποσιώπηση", + "confirmations.mute.explanation": "Αυτό θα κρύψει τις δημοσιεύσεις τους και τις δημοσιεύσεις που τους αναφέρουν, αλλά θα συνεχίσουν να μπορούν να βλέπουν τις δημοσιεύσεις σου και να σε ακολουθούν.", "confirmations.mute.message": "Σίγουρα θες να αποσιωπήσεις {name};", "confirmations.redraft.confirm": "Διαγραφή & ξαναγράψιμο", "confirmations.redraft.message": "Σίγουρα θέλεις να σβήσεις αυτή την κατάσταση και να την ξαναγράψεις; Οι αναφορές και τα αγαπημένα της θα χαθούν ενώ οι απαντήσεις προς αυτή θα μείνουν ορφανές.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Απαντώντας τώρα θα αντικαταστήσεις το κείμενο που ήδη γράφεις. Σίγουρα θέλεις να συνεχίσεις;", "confirmations.unfollow.confirm": "Διακοπή παρακολούθησης", "confirmations.unfollow.message": "Σίγουρα θες να πάψεις να ακολουθείς τον/την {name};", + "conversation.delete": "Διαγραφή συζήτησης", + "conversation.mark_as_read": "Σήμανση ως αναγνωσμένο", + "conversation.open": "Προβολή συνομιλίας", + "conversation.with": "Με {names}", + "directory.federated": "Από το γνωστό fediverse", + "directory.local": "Μόνο από {domain}", + "directory.new_arrivals": "Νέες αφίξεις", + "directory.recently_active": "Πρόσφατα ενεργοί", "embed.instructions": "Ενσωματώστε αυτή την κατάσταση στην ιστοσελίδα σας αντιγράφοντας τον παρακάτω κώδικα.", "embed.preview": "Ορίστε πως θα φαίνεται:", "emoji_button.activity": "Δραστηριότητα", @@ -113,7 +131,7 @@ "emoji_button.objects": "Αντικείμενα", "emoji_button.people": "Άνθρωποι", "emoji_button.recent": "Δημοφιλή", - "emoji_button.search": "Αναζήτηση…", + "emoji_button.search": "Αναζήτηση...", "emoji_button.search_results": "Αποτελέσματα αναζήτησης", "emoji_button.symbols": "Σύμβολα", "emoji_button.travel": "Ταξίδια & Τοποθεσίες", @@ -134,6 +152,10 @@ "empty_column.mutes": "Δεν έχεις αποσιωπήσει κανένα χρήστη ακόμα.", "empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμα. Αλληλεπίδρασε με άλλους χρήστες για να ξεκινήσεις την κουβέντα.", "empty_column.public": "Δεν υπάρχει τίποτα εδώ! Γράψε κάτι δημόσιο, ή ακολούθησε χειροκίνητα χρήστες από άλλους κόμβους για να τη γεμίσεις", + "error.unexpected_crash.explanation": "Είτε λόγω λάθους στον κώδικά μας ή λόγω ασυμβατότητας με τον browser, η σελίδα δε μπόρεσε να εμφανιστεί σωστά.", + "error.unexpected_crash.next_steps": "Δοκίμασε να ανανεώσεις τη σελίδα. Αν αυτό δε βοηθήσει, ίσως να μπορέσεις να χρησιμοποιήσεις το Mastodon μέσω διαφορετικού browser ή κάποιας εφαρμογής.", + "errors.unexpected_crash.copy_stacktrace": "Αντιγραφή μηνυμάτων κώδικα στο πρόχειρο", + "errors.unexpected_crash.report_issue": "Αναφορά προβλήματος", "follow_request.authorize": "Ενέκρινε", "follow_request.reject": "Απέρριψε", "getting_started.developers": "Ανάπτυξη", @@ -149,11 +171,11 @@ "hashtag.column_header.tag_mode.none": "χωρίς {additional}", "hashtag.column_settings.select.no_options_message": "Δεν βρέθηκαν προτάσεις", "hashtag.column_settings.select.placeholder": "Γράψε μερικές ταμπέλες…", - "hashtag.column_settings.tag_mode.all": "Όλα αυτα", + "hashtag.column_settings.tag_mode.all": "Όλα αυτά", "hashtag.column_settings.tag_mode.any": "Οποιοδήποτε από αυτά", "hashtag.column_settings.tag_mode.none": "Κανένα από αυτά", "hashtag.column_settings.tag_toggle": "Προσθήκη επιπλέον ταμπελών για την κολώνα", - "home.column_settings.basic": "Βασικά", + "home.column_settings.basic": "Βασικές ρυθμίσεις", "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", "home.column_settings.show_replies": "Εμφάνιση απαντήσεων", "intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Τίτλος νέας λίστα", "lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς", "lists.subheading": "Οι λίστες σου", + "load_pending": "{count, plural, one {# νέο} other {# νέα}}", "loading_indicator.label": "Φορτώνει...", "media_gallery.toggle_visible": "Εναλλαγή ορατότητας", "missing_indicator.label": "Δε βρέθηκε", @@ -237,7 +260,7 @@ "navigation_bar.favourites": "Αγαπημένα", "navigation_bar.filters": "Αποσιωπημένες λέξεις", "navigation_bar.follow_requests": "Αιτήματα ακολούθησης", - "navigation_bar.follows_and_followers": "Ακολουθεί και ακολουθείται", + "navigation_bar.follows_and_followers": "Ακολουθείς και σε ακολουθούν", "navigation_bar.info": "Πληροφορίες κόμβου", "navigation_bar.keyboard_shortcuts": "Συντομεύσεις", "navigation_bar.lists": "Λίστες", @@ -246,13 +269,12 @@ "navigation_bar.personal": "Προσωπικά", "navigation_bar.pins": "Καρφιτσωμένα τουτ", "navigation_bar.preferences": "Προτιμήσεις", - "navigation_bar.profile_directory": "Κατάλογος λογαριασμών", "navigation_bar.public_timeline": "Ομοσπονδιακή ροή", "navigation_bar.security": "Ασφάλεια", "notification.favourite": "Ο/Η {name} σημείωσε ως αγαπημένη την κατάστασή σου", "notification.follow": "Ο/Η {name} σε ακολούθησε", "notification.mention": "Ο/Η {name} σε ανέφερε", - "notification.poll": "Έλαβε τέλος μια από τις ψηφοφορίες που συμμετείχες", + "notification.poll": "Τελείωσε μια από τις ψηφοφορίες που συμμετείχες", "notification.reblog": "Ο/Η {name} προώθησε την κατάστασή σου", "notifications.clear": "Καθαρισμός ειδοποιήσεων", "notifications.clear_confirmation": "Σίγουρα θέλεις να καθαρίσεις όλες τις ειδοποιήσεις σου;", @@ -277,8 +299,10 @@ "notifications.group": "{count} ειδοποιήσεις", "poll.closed": "Κλειστή", "poll.refresh": "Ανανέωση", + "poll.total_people": "{count, plural, one {# άτομο} other {# άτομα}}", "poll.total_votes": "{count, plural, one {# ψήφος} other {# ψήφοι}}", "poll.vote": "Ψήφισε", + "poll.voted": "Ψηφίσατε αυτήν την απάντηση", "poll_button.add_poll": "Προσθήκη δημοσκόπησης", "poll_button.remove_poll": "Αφαίρεση δημοσκόπησης", "privacy.change": "Προσαρμογή ιδιωτικότητας δημοσίευσης", @@ -290,6 +314,7 @@ "privacy.public.short": "Δημόσιο", "privacy.unlisted.long": "Μην δημοσιεύσεις στις δημόσιες ροές", "privacy.unlisted.short": "Μη καταχωρημένα", + "refresh": "Ανανέωση", "regeneration_indicator.label": "Φορτώνει…", "regeneration_indicator.sublabel": "Η αρχική σου ροή ετοιμάζεται!", "relative_time.days": "{number}η", @@ -314,6 +339,7 @@ "search_results.accounts": "Άνθρωποι", "search_results.hashtags": "Ταμπέλες", "search_results.statuses": "Τουτ", + "search_results.statuses_fts_disabled": "Η αναζήτηση τουτ βάσει του περιεχόμενού τους δεν είναι ενεργοποιημένη σε αυτό τον κόμβο.", "search_results.total": "{count, number} {count, plural, zero {αποτελέσματα} one {αποτέλεσμα} other {αποτελέσματα}}", "status.admin_account": "Άνοιγμα λειτουργίας διαμεσολάβησης για τον/την @{name}", "status.admin_status": "Άνοιγμα αυτής της δημοσίευσης στη λειτουργία διαμεσολάβησης", @@ -352,13 +378,14 @@ "status.show_more": "Δείξε περισσότερα", "status.show_more_all": "Δείξε περισσότερα για όλα", "status.show_thread": "Εμφάνιση νήματος", + "status.uncached_media_warning": "Μη διαθέσιμα", "status.unmute_conversation": "Διέκοψε την αποσιώπηση της συζήτησης", "status.unpin": "Ξεκαρφίτσωσε από το προφίλ", "suggestions.dismiss": "Απόρριψη πρότασης", "suggestions.header": "Ίσως να ενδιαφέρεσαι για…", "tabs_bar.federated_timeline": "Ομοσπονδιακή", "tabs_bar.home": "Αρχική", - "tabs_bar.local_timeline": "Τοπικά", + "tabs_bar.local_timeline": "Τοπική", "tabs_bar.notifications": "Ειδοποιήσεις", "tabs_bar.search": "Αναζήτηση", "time_remaining.days": "απομένουν {number, plural, one {# ημέρα} other {# ημέρες}}", @@ -367,14 +394,22 @@ "time_remaining.moments": "Απομένουν στιγμές", "time_remaining.seconds": "απομένουν {number, plural, one {# δευτερόλεπτο} other {# δευτερόλεπτα}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} μιλάνε", + "trends.trending_now": "Δημοφιλή τώρα", "ui.beforeunload": "Το προσχέδιό σου θα χαθεί αν φύγεις από το Mastodon.", "upload_area.title": "Drag & drop για να ανεβάσεις", - "upload_button.label": "Πρόσθεσε πολυμέσα (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Πρόσθεσε πολυμέσα ({formats})", "upload_error.limit": "Υπέρβαση ορίου μεγέθους ανεβασμένων αρχείων.", "upload_error.poll": "Στις δημοσκοπήσεις δεν επιτρέπεται η μεταφόρτωση αρχείου.", "upload_form.description": "Περιέγραψε για όσους & όσες έχουν προβλήματα όρασης", - "upload_form.focus": "Αλλαγή προεπισκόπησης", + "upload_form.edit": "Ενημέρωση", "upload_form.undo": "Διαγραφή", + "upload_modal.analyzing_picture": "Ανάλυση εικόνας…", + "upload_modal.apply": "Εφαρμογή", + "upload_modal.description_placeholder": "Λύκος μαύρος και ισχνός του πατέρα του καημός", + "upload_modal.detect_text": "Αναγνώριση κειμένου από την εικόνα", + "upload_modal.edit_media": "Επεξεργασία Πολυμέσων", + "upload_modal.hint": "Κάνε κλικ ή σείρε τον κύκλο στην προεπισκόπηση για να επιλέξεις το σημείο εστίασης που θα είναι πάντα εμφανές σε όλες τις μικρογραφίες.", + "upload_modal.preview_label": "Προεπισκόπηση ({ratio})", "upload_progress.label": "Ανεβαίνει...", "video.close": "Κλείσε το βίντεο", "video.exit_fullscreen": "Έξοδος από πλήρη οθόνη", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 49e4ddbda..84e40a9fc 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -4,6 +4,7 @@ "account.block": "Block @{name}", "account.block_domain": "Hide everything from {domain}", "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct message @{name}", "account.domain_blocked": "Domain hidden", "account.edit_profile": "Edit profile", @@ -15,6 +16,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "Follows you", "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Mute @{name}", "account.mute_notifications": "Mute notifications from @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", "account.report": "Report @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.retry": "Try again", @@ -45,8 +51,10 @@ "bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.retry": "Try again", "column.blocks": "Blocked users", + "column.bookmarks": "Bookmarks", "column.community": "Local timeline", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "Favourites", "column.follow_requests": "Follow requests", @@ -97,7 +105,10 @@ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", @@ -105,6 +116,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Embed this status on your website by copying the code below.", "embed.preview": "Here is what it will look like:", "emoji_button.activity": "Activity", @@ -124,6 +143,7 @@ "empty_column.account_timeline": "No toots here!", "empty_column.account_unavailable": "Profile unavailable", "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", "empty_column.domain_blocks": "There are no hidden domains yet.", @@ -138,6 +158,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "getting_started.developers": "Developers", @@ -146,7 +170,7 @@ "getting_started.heading": "Getting started", "getting_started.invite": "Invite people", "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", - "getting_started.security": "Security", + "getting_started.security": "Account settings", "getting_started.terms": "Terms of service", "hashtag.column_header.tag_mode.all": "and {additional}", "hashtag.column_header.tag_mode.any": "or {additional}", @@ -201,6 +225,7 @@ "keyboard_shortcuts.muted": "to open muted users list", "keyboard_shortcuts.my_profile": "to open your profile", "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.open_media": "to open media", "keyboard_shortcuts.pinned": "to open pinned toots list", "keyboard_shortcuts.profile": "to open author's profile", "keyboard_shortcuts.reply": "to reply", @@ -225,6 +250,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -232,6 +258,7 @@ "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.apps": "Mobile apps", "navigation_bar.blocks": "Blocked users", + "navigation_bar.bookmarks": "Bookmarks", "navigation_bar.community_timeline": "Local timeline", "navigation_bar.compose": "Compose new toot", "navigation_bar.direct": "Direct messages", @@ -251,12 +278,13 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferences", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.security": "Security", "notification.favourite": "{name} favourited your status", "notification.follow": "{name} followed you", + "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", "notifications.clear": "Clear notifications", @@ -267,6 +295,7 @@ "notifications.column_settings.filter_bar.category": "Quick filter bar", "notifications.column_settings.filter_bar.show": "Show", "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.follow_request": "New follow requests:", "notifications.column_settings.mention": "Mentions:", "notifications.column_settings.poll": "Poll results:", "notifications.column_settings.push": "Push notifications", @@ -282,8 +311,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Adjust status privacy", @@ -295,6 +326,7 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not post to public timelines", "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -319,10 +351,12 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", "status.block": "Block @{name}", + "status.bookmark": "Bookmark", "status.cancel_reblog_private": "Unboost", "status.cannot_reblog": "This post cannot be boosted", "status.copy": "Copy link to status", @@ -347,6 +381,7 @@ "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", + "status.remove_bookmark": "Remove bookmark", "status.reply": "Reply", "status.replyAll": "Reply to thread", "status.report": "Report @{name}", @@ -357,6 +392,7 @@ "status.show_more": "Show more", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", "suggestions.dismiss": "Dismiss suggestion", @@ -372,16 +408,27 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "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", "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Change preview", + "upload_form.edit": "Edit", "upload_form.undo": "Delete", + "upload_form.video_description": "Describe for people with hearing loss or visual impairment", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Uploading...", "video.close": "Close video", + "video.download": "Download file", "video.exit_fullscreen": "Exit full screen", "video.expand": "Expand video", "video.fullscreen": "Full screen", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 897cb6353..0070cdb5c 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -4,6 +4,7 @@ "account.block": "Bloki @{name}", "account.block_domain": "Kaŝi ĉion de {domain}", "account.blocked": "Blokita", + "account.cancel_follow_request": "Nuligi peton de sekvado", "account.direct": "Rekte mesaĝi @{name}", "account.domain_blocked": "Domajno kaŝita", "account.edit_profile": "Redakti profilon", @@ -15,14 +16,16 @@ "account.follows.empty": "Tiu uzanto ankoraŭ ne sekvas iun.", "account.follows_you": "Sekvas vin", "account.hide_reblogs": "Kaŝi diskonigojn de @{name}", + "account.last_status": "Laste aktiva", "account.link_verified_on": "La posedanto de tiu ligilo estis kontrolita je {date}", "account.locked_info": "La privateco de tiu konto estas elektita kiel fermita. La posedanto povas mane akcepti tiun, kiu povas sekvi rin.", "account.media": "Aŭdovidaĵoj", "account.mention": "Mencii @{name}", "account.moved_to": "{name} moviĝis al:", "account.mute": "Silentigi @{name}", - "account.mute_notifications": "Silentigi sciigojn el @{name}", + "account.mute_notifications": "Silentigi sciigojn de @{name}", "account.muted": "Silentigita", + "account.never_active": "Neniam", "account.posts": "Mesaĝoj", "account.posts_with_replies": "Kun respondoj", "account.report": "Signali @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Ne plu sekvi", "account.unmute": "Malsilentigi @{name}", "account.unmute_notifications": "Malsilentigi sciigojn de @{name}", + "alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.", + "alert.rate_limited.title": "Mesaĝkvante limigita", "alert.unexpected.message": "Neatendita eraro okazis.", "alert.unexpected.title": "Ups!", + "autosuggest_hashtag.per_week": "{count} semajne", "boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje", "bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.", "bundle_column_error.retry": "Bonvolu reprovi", @@ -47,6 +53,7 @@ "column.blocks": "Blokitaj uzantoj", "column.community": "Loka tempolinio", "column.direct": "Rektaj mesaĝoj", + "column.directory": "Trarigardi profilojn", "column.domain_blocks": "Kaŝitaj domajnoj", "column.favourites": "Stelumoj", "column.follow_requests": "Petoj de sekvado", @@ -71,20 +78,20 @@ "compose_form.lock_disclaimer": "Via konta ne estas {locked}. Iu ajn povas sekvi vin por vidi viajn mesaĝojn, kiuj estas nur por sekvantoj.", "compose_form.lock_disclaimer.lock": "ŝlosita", "compose_form.placeholder": "Pri kio vi pensas?", - "compose_form.poll.add_option": "Aldoni elekto", - "compose_form.poll.duration": "Balotenketo daŭro", - "compose_form.poll.option_placeholder": "elekto {number}", - "compose_form.poll.remove_option": "Forigi ĉi tiu elekton", + "compose_form.poll.add_option": "Aldoni elekteblon", + "compose_form.poll.duration": "Balotenketa daŭro", + "compose_form.poll.option_placeholder": "Elekteblo {number}", + "compose_form.poll.remove_option": "Forigi ĉi tiu elekteblon", "compose_form.publish": "Hup", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Marki aŭdovidaĵojn kiel tiklaj", + "compose_form.sensitive.hide": "Marki la aŭdovidaĵojn kiel tiklaj", "compose_form.sensitive.marked": "Aŭdovidaĵo markita tikla", "compose_form.sensitive.unmarked": "Aŭdovidaĵo ne markita tikla", "compose_form.spoiler.marked": "Teksto kaŝita malantaŭ averto", "compose_form.spoiler.unmarked": "Teksto ne kaŝita", "compose_form.spoiler_placeholder": "Skribu vian averton ĉi tie", "confirmation_modal.cancel": "Nuligi", - "confirmations.block.block_and_report": "Bloki & Signali", + "confirmations.block.block_and_report": "Bloki kaj signali", "confirmations.block.confirm": "Bloki", "confirmations.block.message": "Ĉu vi certas, ke vi volas bloki {name}?", "confirmations.delete.confirm": "Forigi", @@ -93,7 +100,10 @@ "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.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?", "confirmations.mute.confirm": "Silentigi", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Ĉu vi certas, ke vi volas silentigi {name}?", "confirmations.redraft.confirm": "Forigi kaj reskribi", "confirmations.redraft.message": "Ĉu vi certas ke vi volas forigi tiun mesaĝon kaj reskribi ĝin? Ĉiuj diskonigoj kaj stelumoj estos perditaj, kaj respondoj al la originala mesaĝo estos senparentaj.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Respondi nun anstataŭigos la mesaĝon, kiun vi nun skribas. Ĉu vi certas, ke vi volas daŭrigi?", "confirmations.unfollow.confirm": "Ne plu sekvi", "confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?", + "conversation.delete": "Forigi konversacion", + "conversation.mark_as_read": "Marki legita", + "conversation.open": "Vidi konversacion", + "conversation.with": "Kun {names}", + "directory.federated": "El konata fediverso", + "directory.local": "Nur de {domain}", + "directory.new_arrivals": "Novaj alvenoj", + "directory.recently_active": "Lastatempe aktiva", "embed.instructions": "Enkorpigu ĉi tiun mesaĝon en vian retejon per kopio de la suba kodo.", "embed.preview": "Ĝi aperos tiel:", "emoji_button.activity": "Agadoj", @@ -134,6 +152,10 @@ "empty_column.mutes": "Vi ne ankoraŭ silentigis iun uzanton.", "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": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", + "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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Raporti problemon", "follow_request.authorize": "Rajtigi", "follow_request.reject": "Rifuzi", "getting_started.developers": "Programistoj", @@ -152,7 +174,7 @@ "hashtag.column_settings.tag_mode.all": "Ĉiuj", "hashtag.column_settings.tag_mode.any": "Iu ajn", "hashtag.column_settings.tag_mode.none": "Neniu", - "hashtag.column_settings.tag_toggle": "Inkluzivi pluajn etikedojn por ĉi tiu kolumno", + "hashtag.column_settings.tag_toggle": "Aldoni pliajn etikedojn por ĉi tiu kolumno", "home.column_settings.basic": "Bazaj agordoj", "home.column_settings.show_reblogs": "Montri diskonigojn", "home.column_settings.show_replies": "Montri respondojn", @@ -160,9 +182,9 @@ "intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}", "introduction.federation.action": "Sekva", - "introduction.federation.federated.headline": "Federacio", + "introduction.federation.federated.headline": "Fratara", "introduction.federation.federated.text": "Publikaj mesaĝoj el aliaj serviloj de la Fediverse aperos en la fratara tempolinio.", - "introduction.federation.home.headline": "Heimo", + "introduction.federation.home.headline": "Hejmo", "introduction.federation.home.text": "Mesaĝoj de homoj, kiujn vi sekvas, aperos en via hejma fluo. Vi povas sekvi iun ajn de ajna servilo!", "introduction.federation.local.headline": "Loka", "introduction.federation.local.text": "Publikaj mesaĝoj de homoj de via servilo aperos en la loka tempolinio.", @@ -211,7 +233,7 @@ "lightbox.close": "Fermi", "lightbox.next": "Sekva", "lightbox.previous": "Antaŭa", - "lightbox.view_context": "Vidi kontekston", + "lightbox.view_context": "Vidi kuntekston", "lists.account.add": "Aldoni al la listo", "lists.account.remove": "Forigi de la listo", "lists.delete": "Forigi la liston", @@ -221,11 +243,12 @@ "lists.new.title_placeholder": "Titolo de la nova listo", "lists.search": "Serĉi inter la homoj, kiujn vi sekvas", "lists.subheading": "Viaj listoj", + "load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}", "loading_indicator.label": "Ŝargado…", "media_gallery.toggle_visible": "Baskuligi videblecon", "missing_indicator.label": "Ne trovita", "missing_indicator.sublabel": "Ĉi tiu elemento ne estis trovita", - "mute_modal.hide_notifications": "Ĉu vi volas kaŝi la sciigojn el ĉi tiu uzanto?", + "mute_modal.hide_notifications": "Ĉu vi volas kaŝi la sciigojn de ĉi tiu uzanto?", "navigation_bar.apps": "Telefonaj aplikaĵoj", "navigation_bar.blocks": "Blokitaj uzantoj", "navigation_bar.community_timeline": "Loka tempolinio", @@ -246,13 +269,12 @@ "navigation_bar.personal": "Persone", "navigation_bar.pins": "Alpinglitaj mesaĝoj", "navigation_bar.preferences": "Preferoj", - "navigation_bar.profile_directory": "Profilujo", "navigation_bar.public_timeline": "Fratara tempolinio", "navigation_bar.security": "Sekureco", "notification.favourite": "{name} stelumis vian mesaĝon", "notification.follow": "{name} eksekvis vin", "notification.mention": "{name} menciis vin", - "notification.poll": "Balotenketo ke vi balotis estas finita", + "notification.poll": "Partoprenita balotenketo finiĝis", "notification.reblog": "{name} diskonigis vian mesaĝon", "notifications.clear": "Forviŝi sciigojn", "notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?", @@ -263,7 +285,7 @@ "notifications.column_settings.filter_bar.show": "Montri", "notifications.column_settings.follow": "Novaj sekvantoj:", "notifications.column_settings.mention": "Mencioj:", - "notifications.column_settings.poll": "Balotenketo rezulto:", + "notifications.column_settings.poll": "Balotenketaj rezultoj:", "notifications.column_settings.push": "Puŝsciigoj", "notifications.column_settings.reblog": "Diskonigoj:", "notifications.column_settings.show": "Montri en kolumno", @@ -273,12 +295,14 @@ "notifications.filter.favourites": "Stelumoj", "notifications.filter.follows": "Sekvoj", "notifications.filter.mentions": "Mencioj", - "notifications.filter.polls": "Balotenketoj rezultoj", + "notifications.filter.polls": "Balotenketaj rezultoj", "notifications.group": "{count} sciigoj", "poll.closed": "Finita", "poll.refresh": "Aktualigi", + "poll.total_people": "{count, plural, one {# homo} other {# homoj}}", "poll.total_votes": "{count, plural, one {# voĉdono} other {# voĉdonoj}}", "poll.vote": "Voĉdoni", + "poll.voted": "Vi elektis por ĉi tiu respondo", "poll_button.add_poll": "Aldoni balotenketon", "poll_button.remove_poll": "Forigi balotenketon", "privacy.change": "Agordi mesaĝan privatecon", @@ -290,6 +314,7 @@ "privacy.public.short": "Publika", "privacy.unlisted.long": "Ne afiŝi en publikaj tempolinioj", "privacy.unlisted.short": "Nelistigita", + "refresh": "Refreŝigu", "regeneration_indicator.label": "Ŝargado…", "regeneration_indicator.sublabel": "Via hejma fluo pretiĝas!", "relative_time.days": "{number}t", @@ -314,11 +339,12 @@ "search_results.accounts": "Homoj", "search_results.hashtags": "Kradvortoj", "search_results.statuses": "Mesaĝoj", + "search_results.statuses_fts_disabled": "Serĉi mesaĝojn laŭ enhavo ne estas ebligita en ĉi tiu Mastodon-servilo.", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}", "status.admin_account": "Malfermi la kontrolan interfacon por @{name}", "status.admin_status": "Malfermi ĉi tiun mesaĝon en la kontrola interfaco", "status.block": "Bloki @{name}", - "status.cancel_reblog_private": "Eksdiskonigi", + "status.cancel_reblog_private": "Ne plu diskonigi", "status.cannot_reblog": "Ĉi tiu mesaĝo ne diskonigeblas", "status.copy": "Kopii la ligilon al la mesaĝo", "status.delete": "Forigi", @@ -352,6 +378,7 @@ "status.show_more": "Grandigi", "status.show_more_all": "Grandigi ĉiujn", "status.show_thread": "Montri la fadenon", + "status.uncached_media_warning": "Nedisponebla", "status.unmute_conversation": "Malsilentigi la konversacion", "status.unpin": "Depingli de profilo", "suggestions.dismiss": "Forigi la proponon", @@ -361,26 +388,34 @@ "tabs_bar.local_timeline": "Loka tempolinio", "tabs_bar.notifications": "Sciigoj", "tabs_bar.search": "Serĉi", - "time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restanta", - "time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restanta", - "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restanta", - "time_remaining.moments": "Momento restanta", - "time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundoj}} restanta", + "time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restas", + "time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restas", + "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restas", + "time_remaining.moments": "Momenteto restas", + "time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundoj}} restas", "trends.count_by_accounts": "{count} {rawCount, plural, one {persono} other {personoj}} parolas", + "trends.trending_now": "Nunaj furoraĵoj", "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.", "upload_area.title": "Altreni kaj lasi por alŝuti", "upload_button.label": "Aldoni aŭdovidaĵon (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Limo de dosiera alŝutado transpasita.", - "upload_error.poll": "Alŝuto de dosiero ne permisita kun balotenketo", + "upload_error.poll": "Alŝuto de dosiero ne permesita kun balotenketo.", "upload_form.description": "Priskribi por misvidantaj homoj", - "upload_form.focus": "Antaŭvido de ŝanĝo", + "upload_form.edit": "Redakti", "upload_form.undo": "Forigi", + "upload_modal.analyzing_picture": "Bilda analizado…", + "upload_modal.apply": "Apliki", + "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.preview_label": "Antaŭvido ({ratio})", "upload_progress.label": "Alŝutado…", - "video.close": "Fermi videon", + "video.close": "Fermi la videon", "video.exit_fullscreen": "Eksigi plenekrana", - "video.expand": "Grandigi videon", + "video.expand": "Grandigi la videon", "video.fullscreen": "Igi plenekrana", - "video.hide": "Kaŝi videon", + "video.hide": "Kaŝi la videon", "video.mute": "Silentigi", "video.pause": "Paŭzi", "video.play": "Ekigi", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json new file mode 100644 index 000000000..56f8781a1 --- /dev/null +++ b/app/javascript/mastodon/locales/es-AR.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Agregar o quitar de las listas", + "account.badges.bot": "Bot", + "account.block": "Bloquear a @{name}", + "account.block_domain": "Ocultar todo de {domain}", + "account.blocked": "Bloqueado", + "account.cancel_follow_request": "Cancelar la solicitud de seguimiento", + "account.direct": "Mensaje directo a @{name}", + "account.domain_blocked": "Dominio oculto", + "account.edit_profile": "Editar perfil", + "account.endorse": "Destacar en el perfil", + "account.follow": "Seguir", + "account.followers": "Seguidores", + "account.followers.empty": "Todavía nadie sigue a este usuario.", + "account.follows": "Sigue", + "account.follows.empty": "Todavía este usuario no sigue a nadie.", + "account.follows_you": "Te sigue", + "account.hide_reblogs": "Ocultar retoots de @{name}", + "account.last_status": "Última actividad", + "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}", + "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.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.report": "Denunciar a @{name}", + "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.unblock": "Desbloquear a @{name}", + "account.unblock_domain": "Mostrar {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}", + "alert.rate_limited.message": "Por favor, reintentá después de las {retry_time, time, medium}.", + "alert.rate_limited.title": "Tarifa limitada", + "alert.unexpected.message": "Ocurrió un error.", + "alert.unexpected.title": "¡Epa!", + "autosuggest_hashtag.per_week": "{count} por semana", + "boost_modal.combo": "Podés hacer clic en {combo} para saltar esto la próxima vez", + "bundle_column_error.body": "Algo salió mal al cargar este componente.", + "bundle_column_error.retry": "Intentá de nuevo", + "bundle_column_error.title": "Error de red", + "bundle_modal_error.close": "Cerrar", + "bundle_modal_error.message": "Algo salió mal al cargar este componente.", + "bundle_modal_error.retry": "Intentá de nuevo", + "column.blocks": "Usuarios bloqueados", + "column.community": "Línea temporal local", + "column.direct": "Mensajes directos", + "column.directory": "Explorar perfiles", + "column.domain_blocks": "Dominios ocultos", + "column.favourites": "Favoritos", + "column.follow_requests": "Solicitudes de seguimiento", + "column.home": "Principal", + "column.lists": "Listas", + "column.mutes": "Usuarios silenciados", + "column.notifications": "Notificaciones", + "column.pins": "Toots fijados", + "column.public": "Línea temporal federada", + "column_back_button.label": "Volver", + "column_header.hide_settings": "Ocultar configuración", + "column_header.moveLeft_settings": "Mover columna a la izquierda", + "column_header.moveRight_settings": "Mover columna a la derecha", + "column_header.pin": "Fijar", + "column_header.show_settings": "Mostrar configuración", + "column_header.unpin": "Dejar de fijar", + "column_subheading.settings": "Configuración", + "community.column_settings.media_only": "Sólo medios", + "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.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.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.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", + "compose_form.spoiler_placeholder": "Escribí tu advertencia acá", + "confirmation_modal.cancel": "Cancelar", + "confirmations.block.block_and_report": "Bloquear y denunciar", + "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_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.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?", + "confirmations.mute.confirm": "Silenciar", + "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.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.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", + "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", + "emoji_button.custom": "Personalizado", + "emoji_button.flags": "Banderas", + "emoji_button.food": "Comida y bebida", + "emoji_button.label": "Insertar emoji", + "emoji_button.nature": "Naturaleza", + "emoji_button.not_found": "¡¡No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objetos", + "emoji_button.people": "Gente", + "emoji_button.recent": "Usados frecuentemente", + "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_unavailable": "Perfil no disponible", + "empty_column.blocks": "Todavía no bloqueaste a ningún usuario.", + "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.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á.", + "empty_column.hashtag": "Todavía no hay nada con esta etiqueta.", + "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.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.", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copiar stacktrace al portapapeles", + "errors.unexpected_crash.report_issue": "Informar problema", + "follow_request.authorize": "Autorizar", + "follow_request.reject": "Rechazar", + "getting_started.developers": "Desarrolladores", + "getting_started.directory": "Directorio de perfiles", + "getting_started.documentation": "Documentación", + "getting_started.heading": "Introducción", + "getting_started.invite": "Invitar usuarios", + "getting_started.open_source_notice": "Mastodon es software libre. Podés contribuir o informar errores en {github}.", + "getting_started.security": "Seguridad", + "getting_started.terms": "Términos del servicio", + "hashtag.column_header.tag_mode.all": "y {additional}", + "hashtag.column_header.tag_mode.any": "o {additional}", + "hashtag.column_header.tag_mode.none": "sin {additional}", + "hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias", + "hashtag.column_settings.select.placeholder": "Introducí etiquetas…", + "hashtag.column_settings.tag_mode.all": "Todas estas", + "hashtag.column_settings.tag_mode.any": "Cualquiera de estas", + "hashtag.column_settings.tag_mode.none": "Ninguna de estas", + "hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionales para esta columna", + "home.column_settings.basic": "Básico", + "home.column_settings.show_reblogs": "Mostrar retoots", + "home.column_settings.show_replies": "Mostrar respuestas", + "intervals.full.days": "{number, plural, one {# día} other {# días}}", + "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.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íss 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.interactions.action": "¡Terminar tutorial!", + "introduction.interactions.favourite.headline": "Favorito", + "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.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.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.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.favourite": "para marcar como favorito", + "keyboard_shortcuts.favourites": "para abrir la lista de favoritos", + "keyboard_shortcuts.federated": "para abrir la línea temporal federada", + "keyboard_shortcuts.heading": "Atajos de teclado", + "keyboard_shortcuts.home": "para abrir la línea temporal principal", + "keyboard_shortcuts.hotkey": "Combinación", + "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.my_profile": "para abrir tu perfil", + "keyboard_shortcuts.notifications": "para abrir la columna de notificaciones", + "keyboard_shortcuts.pinned": "para abrir 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", + "keyboard_shortcuts.search": "para enfocar la búsqueda", + "keyboard_shortcuts.start": "para abrir la columna \"Introducción\"", + "keyboard_shortcuts.toggle_hidden": "para mostrar/ocultar el texto detrás de la advertencia de contenido", + "keyboard_shortcuts.toggle_sensitivity": "para mostrar/ocultar los medios", + "keyboard_shortcuts.toot": "para comenzar un toot nuevo", + "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.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", + "lists.edit": "Editar lista", + "lists.edit.submit": "Cambiar título", + "lists.new.create": "Agregar lista", + "lists.new.title_placeholder": "Nuevo título de lista", + "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", + "missing_indicator.label": "No se encontró", + "missing_indicator.sublabel": "No se encontró este recurso", + "mute_modal.hide_notifications": "¿Querés ocultar las notificaciones de este usuario?", + "navigation_bar.apps": "Aplicaciones móviles", + "navigation_bar.blocks": "Usuarios bloqueados", + "navigation_bar.community_timeline": "Línea temporal local", + "navigation_bar.compose": "Redactar un nuevo toot", + "navigation_bar.direct": "Mensajes directos", + "navigation_bar.discover": "Descubrir", + "navigation_bar.domain_blocks": "Dominios ocultos", + "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.info": "Acerca de este servidor", + "navigation_bar.keyboard_shortcuts": "Atajos", + "navigation_bar.lists": "Listas", + "navigation_bar.logout": "Cerrar sesión", + "navigation_bar.mutes": "Usuarios silenciados", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Toots fijados", + "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.follow": "{name} te empezó a seguir", + "notification.mention": "{name} te mencionó", + "notification.poll": "Finalizó una encuesta en la que votaste", + "notification.reblog": "{name} retooteó tu estado", + "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", + "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías", + "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido", + "notifications.column_settings.filter_bar.show": "Mostrar", + "notifications.column_settings.follow": "Nuevos seguidores:", + "notifications.column_settings.mention": "Menciones:", + "notifications.column_settings.poll": "Resultados de la encuesta:", + "notifications.column_settings.push": "Notificaciones push", + "notifications.column_settings.reblog": "Retoots:", + "notifications.column_settings.show": "Mostrar en columna", + "notifications.column_settings.sound": "Reproducir sonido", + "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.group": "{count} notificaciones", + "poll.closed": "Cerrada", + "poll.refresh": "Refrescar", + "poll.total_people": "{count, plural, one {# persona} other {# personas}}", + "poll.total_votes": "{count, plural, one {# voto} other {# votos}}", + "poll.vote": "Votar", + "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.direct.short": "Directo", + "privacy.private.long": "Enviar toot 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.short": "Público", + "privacy.unlisted.long": "No enviar toot a las líneas temporales públicas", + "privacy.unlisted.short": "No listado", + "refresh": "Refrescar", + "regeneration_indicator.label": "Cargando…", + "regeneration_indicator.sublabel": "¡Se está preparando tu línea temporal principal!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "recién", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancelar", + "report.forward": "Reenviar a {target}", + "report.forward_hint": "La cuenta es de otro servidor. ¿Querés enviar una copia anonimizada del informe también ahí?", + "report.hint": "La denuncia se enviará a los moderadores de tu servidor. Podés proporcionar una explicación de por qué estás denunciando esta cuenta, a continuación:", + "report.placeholder": "Comentarios adicionales", + "report.submit": "Enviar", + "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.hashtag": "etiqueta", + "search_popout.tips.status": "estado", + "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.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.block": "Bloquear a @{name}", + "status.cancel_reblog_private": "Quitar retoot", + "status.cannot_reblog": "No se puede retootear este toot", + "status.copy": "Copiar enlace al estado", + "status.delete": "Eliminar", + "status.detailed_status": "Vista de conversación detallada", + "status.direct": "Mensaje directo a @{name}", + "status.embed": "Insertar", + "status.favourite": "Favorito", + "status.filtered": "Filtrado", + "status.load_more": "Cargar más", + "status.media_hidden": "Medios ocultos", + "status.mention": "Mencionar a @{name}", + "status.more": "Más", + "status.mute": "Silenciar a @{name}", + "status.mute_conversation": "Silenciar conversación", + "status.open": "Expandir este estado", + "status.pin": "Pin en el perfil", + "status.pinned": "Toot fijado", + "status.read_more": "Leer más", + "status.reblog": "Retootear", + "status.reblog_private": "Retootear a la audiencia original", + "status.reblogged_by": "{name} retooteó", + "status.reblogs.empty": "Todavía nadie retooteó este toot. Cuando alguien lo haga, se mostrará acá.", + "status.redraft": "Eliminar toot original y editarlo", + "status.reply": "Responder", + "status.replyAll": "Responder al hilo", + "status.report": "Denunciar a @{name}", + "status.sensitive_warning": "Contenido sensible", + "status.share": "Compartir", + "status.show_less": "Mostrar menos", + "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 hilo", + "status.uncached_media_warning": "No disponible", + "status.unmute_conversation": "Dejar de silenciar conversación", + "status.unpin": "Desmarcar del perfil", + "suggestions.dismiss": "Descartar sugerencia", + "suggestions.header": "Es posible que te interese…", + "tabs_bar.federated_timeline": "Federado", + "tabs_bar.home": "Principal", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notificaciones", + "tabs_bar.search": "Buscar", + "time_remaining.days": "{number, plural,one {queda # día} other {quedan # días}}", + "time_remaining.hours": "{number, plural,one {queda # hora} other {quedan # horas}}", + "time_remaining.minutes": "{number, plural,one {queda # minuto} other {quedan # minutos}}", + "time_remaining.moments": "Momentos restantes", + "time_remaining.seconds": "{number, plural,one {queda # segundo} other {quedan # segundos}}", + "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando", + "trends.trending_now": "Tendencia ahora", + "ui.beforeunload": "Tu borrador se perderá si abandonás Mastodon.", + "upload_area.title": "Para subir, arrastrá y soltá", + "upload_button.label": "Agregar medios ({formats})", + "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.description": "Agregar descripción para los usuarios con dificultades visuales", + "upload_form.edit": "Editar", + "upload_form.undo": "Eliminar", + "upload_modal.analyzing_picture": "Analizando imagen…", + "upload_modal.apply": "Aplicar", + "upload_modal.description_placeholder": "El veloz murciélago hindú comía feliz cardillo y kiwi. La cigüeña tocaba el saxofón detrás del palenque de paja.", + "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.preview_label": "Previsualización ({ratio})", + "upload_progress.label": "Subiendo…", + "video.close": "Cerrar video", + "video.exit_fullscreen": "Salir de pantalla completa", + "video.expand": "Expandir vídeo", + "video.fullscreen": "Pantalla completa", + "video.hide": "Ocultar video", + "video.mute": "Silenciar sonido", + "video.pause": "Pausar", + "video.play": "Reproducir", + "video.unmute": "Dejar de silenciar sonido" +} diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index fc9e7bb16..c213a03e1 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -1,28 +1,31 @@ { - "account.add_or_remove_from_list": "Agregar o eliminar de las listas", + "account.add_or_remove_from_list": "Agregar o eliminar de listas", "account.badges.bot": "Bot", - "account.block": "Bloquear", + "account.block": "Bloquear a @{name}", "account.block_domain": "Ocultar todo de {domain}", "account.blocked": "Bloqueado", - "account.direct": "Direct Message @{name}", + "account.cancel_follow_request": "Cancelar la solicitud de seguimiento", + "account.direct": "Mensaje directo a @{name}", "account.domain_blocked": "Dominio oculto", "account.edit_profile": "Editar perfil", "account.endorse": "Mostrar en perfil", "account.follow": "Seguir", "account.followers": "Seguidores", - "account.followers.empty": "Nadie sigue a este usuario todavía.", + "account.followers.empty": "Todavía nadie sigue a este usuario.", "account.follows": "Sigue", "account.follows.empty": "Este usuario todavía no sigue a nadie.", "account.follows_you": "Te sigue", "account.hide_reblogs": "Ocultar retoots de @{name}", - "account.link_verified_on": "El proprietario de este link fue verificado el {date}", + "account.last_status": "Última actividad", + "account.link_verified_on": "El proprietario de este link fue comprobado el {date}", "account.locked_info": "El estado de privacidad de esta cuenta està configurado como bloqueado. El proprietario debe revisar manualmente quien puede seguirle.", - "account.media": "Media", + "account.media": "Multimedia", "account.mention": "Mencionar a @{name}", "account.moved_to": "{name} se ha mudado 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.report": "Reportar a @{name}", @@ -35,9 +38,12 @@ "account.unfollow": "Dejar de seguir", "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}", + "alert.rate_limited.message": "Por favor reintente después de {retry_time, time, medium}.", + "alert.rate_limited.title": "Tarifa limitada", "alert.unexpected.message": "Hubo un error inesperado.", - "alert.unexpected.title": "Oops!", - "boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez", + "alert.unexpected.title": "¡Ups!", + "autosuggest_hashtag.per_week": "{count} por semana", + "boost_modal.combo": "Puedes hacer clic en {combo} para saltar este aviso la próxima vez", "bundle_column_error.body": "Algo salió mal al cargar este componente.", "bundle_column_error.retry": "Inténtalo de nuevo", "bundle_column_error.title": "Error de red", @@ -47,17 +53,18 @@ "column.blocks": "Usuarios bloqueados", "column.community": "Línea de tiempo local", "column.direct": "Mensajes directos", - "column.domain_blocks": "Dominios ocultos", + "column.directory": "Buscar perfiles", + "column.domain_blocks": "Dominios ocultados", "column.favourites": "Favoritos", "column.follow_requests": "Solicitudes de seguimiento", "column.home": "Inicio", "column.lists": "Listas", "column.mutes": "Usuarios silenciados", "column.notifications": "Notificaciones", - "column.pins": "Toot fijado", - "column.public": "Historia federada", + "column.pins": "Toots fijados", + "column.public": "Línea de tiempo federada", "column_back_button.label": "Atrás", - "column_header.hide_settings": "Ocultar ajustes", + "column_header.hide_settings": "Ocultar configuración", "column_header.moveLeft_settings": "Mover columna a la izquierda", "column_header.moveRight_settings": "Mover columna a la derecha", "column_header.pin": "Fijar", @@ -71,36 +78,47 @@ "compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.", "compose_form.lock_disclaimer.lock": "bloqueado", "compose_form.placeholder": "¿En qué estás pensando?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Añadir una opción", + "compose_form.poll.duration": "Duración de la encuesta", + "compose_form.poll.option_placeholder": "Elección {number}", + "compose_form.poll.remove_option": "Eliminar esta opción", "compose_form.publish": "Tootear", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Marcar multimedia como sensible", "compose_form.sensitive.marked": "Material marcado como sensible", "compose_form.sensitive.unmarked": "Material no marcado como sensible", "compose_form.spoiler.marked": "Texto oculto tras la advertencia", "compose_form.spoiler.unmarked": "Texto no oculto", "compose_form.spoiler_placeholder": "Advertencia de contenido", "confirmation_modal.cancel": "Cancelar", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Bloquear y Reportar", "confirmations.block.confirm": "Bloquear", "confirmations.block.message": "¿Estás seguro de que quieres bloquear a {name}?", "confirmations.delete.confirm": "Eliminar", "confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Eliminar", "confirmations.delete_list.message": "¿Seguro que quieres borrar esta lista permanentemente?", "confirmations.domain_block.confirm": "Ocultar dominio entero", "confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio {domain} entero? En general unos cuantos bloqueos y silenciados concretos es suficiente y preferible.", + "confirmations.logout.confirm": "Cerrar sesión", + "confirmations.logout.message": "¿Estás seguro de querer cerrar la sesión?", "confirmations.mute.confirm": "Silenciar", + "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.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "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", "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", + "conversation.delete": "Borrar conversación", + "conversation.mark_as_read": "Marcar como leído", + "conversation.open": "Ver conversación", + "conversation.with": "Con {names}", + "directory.federated": "Desde el fediverso conocido", + "directory.local": "Sólo de {domain}", + "directory.new_arrivals": "Recién llegados", + "directory.recently_active": "Recientemente activo", "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.", "embed.preview": "Así es como se verá:", "emoji_button.activity": "Actividad", @@ -117,8 +135,8 @@ "emoji_button.search_results": "Resultados de búsqueda", "emoji_button.symbols": "Símbolos", "emoji_button.travel": "Viajes y lugares", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", + "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.", "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!", "empty_column.direct": "Aún no tienes ningún mensaje directo. Cuando envíes o recibas uno, se mostrará aquí.", @@ -134,57 +152,61 @@ "empty_column.mutes": "Aún no has silenciado a ningún usuario.", "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copiar el seguimiento de pila en el portapapeles", + "errors.unexpected_crash.report_issue": "Informar de un problema/error", "follow_request.authorize": "Autorizar", "follow_request.reject": "Rechazar", "getting_started.developers": "Desarrolladores", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", + "getting_started.directory": "Directorio de perfil", + "getting_started.documentation": "Documentación", "getting_started.heading": "Primeros pasos", "getting_started.invite": "Invitar usuarios", "getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}.", "getting_started.security": "Seguridad", "getting_started.terms": "Términos de servicio", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_header.tag_mode.all": "y {additional}", + "hashtag.column_header.tag_mode.any": "o {additional}", + "hashtag.column_header.tag_mode.none": "sin {additional}", + "hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias", + "hashtag.column_settings.select.placeholder": "Introduzca hashtags…", + "hashtag.column_settings.tag_mode.all": "Cualquiera de estos", + "hashtag.column_settings.tag_mode.any": "Cualquiera de estos", + "hashtag.column_settings.tag_mode.none": "Ninguno de estos", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", "home.column_settings.basic": "Básico", "home.column_settings.show_reblogs": "Mostrar retoots", "home.column_settings.show_replies": "Mostrar respuestas", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", - "introduction.federation.action": "Next", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "intervals.full.days": "{number, plural, one {# día} other {# días}}", + "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.text": "Los mensajes públicos de otros servidores del fediverso aparecerán en la cronología federada.", + "introduction.federation.home.headline": "Inicio", + "introduction.federation.home.text": "Los posts de personas que sigues aparecerán en tu cronología. ¡Puedes seguir a cualquiera en cualquier servidor!", "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "introduction.federation.local.text": "Los posts públicos de personas en el mismo servidor que aparecerán en la cronología local.", + "introduction.interactions.action": "¡Terminar tutorial!", + "introduction.interactions.favourite.headline": "Favorito", + "introduction.interactions.favourite.text": "Puedes guardar un toot para más tarde, y hacer saber al autor que te gustó, dándole a favorito.", + "introduction.interactions.reblog.headline": "Retootear", + "introduction.interactions.reblog.text": "Puedes compartir los toots de otras personas con tus seguidores retooteando los mismos.", + "introduction.interactions.reply.headline": "Responder", + "introduction.interactions.reply.text": "Puedes responder a tus propios toots y los de otras personas, que se encadenarán juntos en una conversación.", + "introduction.welcome.action": "¡Vamos!", + "introduction.welcome.headline": "Primeros pasos", + "introduction.welcome.text": "¡Bienvenido al fediverso! En unos momentos, podrás transmitir mensajes y hablar con tus amigos a través de una amplia variedad de servidores. Pero este servidor, {domain}, es especial, alberga tu perfil, así que recuerda su nombre.", "keyboard_shortcuts.back": "volver atrás", "keyboard_shortcuts.blocked": "abrir una lista de usuarios bloqueados", "keyboard_shortcuts.boost": "retootear", "keyboard_shortcuts.column": "enfocar un estado en una de las columnas", "keyboard_shortcuts.compose": "enfocar el área de texto de redacción", - "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.description": "Descripción", "keyboard_shortcuts.direct": "abrir la columna de mensajes directos", "keyboard_shortcuts.down": "mover hacia abajo en la lista", - "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.enter": "abrir estado", "keyboard_shortcuts.favourite": "añadir a favoritos", "keyboard_shortcuts.favourites": "abrir la lista de favoritos", "keyboard_shortcuts.federated": "abrir el timeline federado", @@ -204,23 +226,24 @@ "keyboard_shortcuts.search": "para poner el foco en la búsqueda", "keyboard_shortcuts.start": "abrir la columna \"comenzar\"", "keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios", "keyboard_shortcuts.toot": "para comenzar un nuevo toot", "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.next": "Siguiente", "lightbox.previous": "Anterior", - "lightbox.view_context": "View context", + "lightbox.view_context": "Ver contexto", "lists.account.add": "Añadir a lista", "lists.account.remove": "Quitar de lista", - "lists.delete": "Delete list", + "lists.delete": "Borrar lista", "lists.edit": "Editar lista", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Cambiar título", "lists.new.create": "Añadir lista", "lists.new.title_placeholder": "Título de la nueva lista", "lists.search": "Buscar entre la gente a la que sigues", "lists.subheading": "Tus listas", + "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.toggle_visible": "Cambiar visibilidad", "missing_indicator.label": "No encontrado", @@ -237,7 +260,7 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palabras silenciadas", "navigation_bar.follow_requests": "Solicitudes para seguirte", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Siguiendo y seguidores", "navigation_bar.info": "Información adicional", "navigation_bar.keyboard_shortcuts": "Atajos", "navigation_bar.lists": "Listas", @@ -246,41 +269,42 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Toots fijados", "navigation_bar.preferences": "Preferencias", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Historia federada", "navigation_bar.security": "Seguridad", "notification.favourite": "{name} marcó tu estado como favorito", "notification.follow": "{name} te empezó a seguir", "notification.mention": "{name} te ha mencionado", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "Una encuesta en la que has votado ha terminado", "notification.reblog": "{name} ha retooteado tu estado", "notifications.clear": "Limpiar notificaciones", "notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?", "notifications.column_settings.alert": "Notificaciones de escritorio", "notifications.column_settings.favourite": "Favoritos:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías", + "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido", + "notifications.column_settings.filter_bar.show": "Mostrar", "notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.mention": "Menciones:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.push": "Notificaciones push", "notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", - "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.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.group": "{count} notificaciones", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.closed": "Cerrada", + "poll.refresh": "Actualizar", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural, one {# voto} other {# votos}}", + "poll.vote": "Votar", + "poll.voted": "Has votado a favor de esta respuesta", + "poll_button.add_poll": "Añadir una encuesta", + "poll_button.remove_poll": "Eliminar encuesta", "privacy.change": "Ajustar privacidad", "privacy.direct.long": "Sólo mostrar a los usuarios mencionados", "privacy.direct.short": "Directo", @@ -289,7 +313,8 @@ "privacy.public.long": "Mostrar en la historia federada", "privacy.public.short": "Público", "privacy.unlisted.long": "No mostrar en la historia federada", - "privacy.unlisted.short": "Sin federar", + "privacy.unlisted.short": "No listado", + "refresh": "Actualizar", "regeneration_indicator.label": "Cargando…", "regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!", "relative_time.days": "{number}d", @@ -308,19 +333,20 @@ "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": "status", + "search_popout.tips.status": "estado", "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag", "search_popout.tips.user": "usuario", "search_results.accounts": "Gente", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Buscar toots por su contenido no está disponible en este servidor de Mastodon.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", + "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.cancel_reblog_private": "Des-impulsar", "status.cannot_reblog": "Este toot no puede retootearse", - "status.copy": "Copy link to status", + "status.copy": "Copiar enlace al estado", "status.delete": "Borrar", "status.detailed_status": "Vista de conversación detallada", "status.direct": "Mensaje directo a @{name}", @@ -336,7 +362,7 @@ "status.open": "Expandir estado", "status.pin": "Fijar", "status.pinned": "Toot fijado", - "status.read_more": "Read more", + "status.read_more": "Leer más", "status.reblog": "Retootear", "status.reblog_private": "Implusar a la audiencia original", "status.reblogged_by": "Retooteado por {name}", @@ -351,30 +377,39 @@ "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": "Show thread", + "status.show_thread": "Ver hilo", + "status.uncached_media_warning": "No disponible", "status.unmute_conversation": "Dejar de silenciar conversación", "status.unpin": "Dejar de fijar", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Descartar sugerencia", + "suggestions.header": "Es posible que te interese…", "tabs_bar.federated_timeline": "Federado", "tabs_bar.home": "Inicio", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificaciones", "tabs_bar.search": "Buscar", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}", + "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}", + "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}", + "time_remaining.moments": "Momentos restantes", + "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}", + "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando", + "trends.trending_now": "Tendencia ahora", "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.", "upload_area.title": "Arrastra y suelta para subir", "upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.limit": "Límite de subida de archivos excedido.", + "upload_error.poll": "Subida de archivos no permitida con encuestas.", "upload_form.description": "Describir para los usuarios con dificultad visual", - "upload_form.focus": "Recortar", + "upload_form.edit": "Editar", "upload_form.undo": "Borrar", + "upload_modal.analyzing_picture": "Analizando imagen…", + "upload_modal.apply": "Aplicar", + "upload_modal.description_placeholder": "Un rápido zorro marrón salta sobre el perro perezoso", + "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.preview_label": "Vista previa ({ratio})", "upload_progress.label": "Subiendo…", "video.close": "Cerrar video", "video.exit_fullscreen": "Salir de pantalla completa", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json new file mode 100644 index 000000000..6b97393d2 --- /dev/null +++ b/app/javascript/mastodon/locales/et.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Lisa või Eemalda nimekirjadest", + "account.badges.bot": "Robot", + "account.block": "Blokeeri @{name}", + "account.block_domain": "Peida kõik domeenist {domain}", + "account.blocked": "Blokeeritud", + "account.cancel_follow_request": "Tühista jälgimistaotlus", + "account.direct": "Otsesõnum @{name}", + "account.domain_blocked": "Domeen peidetud", + "account.edit_profile": "Muuda profiili", + "account.endorse": "Too profiilil esile", + "account.follow": "Jälgi", + "account.followers": "Jälgijad", + "account.followers.empty": "Keegi ei jälgi seda kasutajat veel.", + "account.follows": "Jälgib", + "account.follows.empty": "See kasutaja ei jälgi veel kedagi.", + "account.follows_you": "Jälgib sind", + "account.hide_reblogs": "Peida upitused kasutajalt @{name}", + "account.last_status": "Viimati aktiivne", + "account.link_verified_on": "Selle lingi autorsust kontrolliti {date}", + "account.locked_info": "Selle konto privaatsussätteks on lukustatud. Omanik vaatab manuaalselt üle, kes teda jägida saab.", + "account.media": "Meedia", + "account.mention": "Maini @{name}", + "account.moved_to": "{name} on kolinud:", + "account.mute": "Vaigista @{name}", + "account.mute_notifications": "Vaigista teated kasutajalt @{name}", + "account.muted": "Vaigistatud", + "account.never_active": "Mitte kunagi", + "account.posts": "Tuututused", + "account.posts_with_replies": "Tuututused ja vastused", + "account.report": "Raporteeri @{name}", + "account.requested": "Ootab kinnitust. Klõpsa jälgimise soovi tühistamiseks", + "account.share": "Jaga @{name} profiili", + "account.show_reblogs": "Näita kasutaja @{name} upitusi", + "account.unblock": "Eemalda blokeering @{name}", + "account.unblock_domain": "Tee {domain} nähtavaks", + "account.unendorse": "Ära kuva profiilil", + "account.unfollow": "Ära jälgi", + "account.unmute": "Ära vaigista @{name}", + "account.unmute_notifications": "Ära vaigista teateid kasutajalt @{name}", + "alert.rate_limited.message": "Palun proovi uuesti pärast {retry_time, time, medium}.", + "alert.rate_limited.title": "Piiratud", + "alert.unexpected.message": "Tekkis ootamatu viga.", + "alert.unexpected.title": "Oih!", + "autosuggest_hashtag.per_week": "{count} nädalas", + "boost_modal.combo": "Saad vajutada {combo}, et see järgmine kord vahele jätta", + "bundle_column_error.body": "Mindagi läks valesti selle komponendi laadimisel.", + "bundle_column_error.retry": "Proovi uuesti", + "bundle_column_error.title": "Võrgu viga", + "bundle_modal_error.close": "Sulge", + "bundle_modal_error.message": "Selle komponendi laadimisel läks midagi viltu.", + "bundle_modal_error.retry": "Proovi uuesti", + "column.blocks": "Blokeeritud kasutajad", + "column.community": "Kohalik ajajoon", + "column.direct": "Otsesõnumid", + "column.directory": "Sirvi profiile", + "column.domain_blocks": "Peidetud domeenid", + "column.favourites": "Lemmikud", + "column.follow_requests": "Jälgimistaotlused", + "column.home": "Kodu", + "column.lists": "Nimekirjad", + "column.mutes": "Vaigistatud kasutajad", + "column.notifications": "Teated", + "column.pins": "Kinnitatud upitused", + "column.public": "Föderatiivne ajajoon", + "column_back_button.label": "Tagasi", + "column_header.hide_settings": "Peida sätted", + "column_header.moveLeft_settings": "Liiguta tulp vasakule", + "column_header.moveRight_settings": "Liiguta tulp paremale", + "column_header.pin": "Kinnita", + "column_header.show_settings": "Näita sätteid", + "column_header.unpin": "Eemalda kinnitus", + "column_subheading.settings": "Sätted", + "community.column_settings.media_only": "Ainult meedia", + "compose_form.direct_message_warning": "See tuut saadetakse ainult mainitud kasutajatele.", + "compose_form.direct_message_warning_learn_more": "Vaata veel", + "compose_form.hashtag_warning": "Seda tuuti ei kuvata ühegi sildi all, sest see on kirjendamata. Ainult avalikud tuutid on sildi järgi otsitavad.", + "compose_form.lock_disclaimer": "Sinu konto ei ole {locked}. Igaüks saab sind jälgida ja näha su ainult-jälgijatele postitusi.", + "compose_form.lock_disclaimer.lock": "lukus", + "compose_form.placeholder": "Millest mõtled?", + "compose_form.poll.add_option": "Lisa valik", + "compose_form.poll.duration": "Küsitluse kestus", + "compose_form.poll.option_placeholder": "Valik {number}", + "compose_form.poll.remove_option": "Eemalda see valik", + "compose_form.publish": "Tuut", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Märgista meedia tundlikuks", + "compose_form.sensitive.marked": "Meedia on sensitiivseks märgitud", + "compose_form.sensitive.unmarked": "Meedia ei ole sensitiivseks märgitud", + "compose_form.spoiler.marked": "Tekst on hoiatuse taha peidetud", + "compose_form.spoiler.unmarked": "Tekst ei ole peidetud", + "compose_form.spoiler_placeholder": "Kirjuta oma hoiatus siia", + "confirmation_modal.cancel": "Katkesta", + "confirmations.block.block_and_report": "Blokeeri & Teata", + "confirmations.block.confirm": "Blokeeri", + "confirmations.block.message": "Oled kindel, et soovid blokkida {name}?", + "confirmations.delete.confirm": "Kustuta", + "confirmations.delete.message": "Oled kindel, et soovid selle staatuse kustutada?", + "confirmations.delete_list.confirm": "Kustuta", + "confirmations.delete_list.message": "Oled kindel, et soovid selle nimekirja püsivalt kustutada?", + "confirmations.domain_block.confirm": "Peida terve domeen", + "confirmations.domain_block.message": "Oled ikka päris kindel, et soovid blokeerida terve {domain}? Enamikul juhtudel piisab mõnest sihitud blokist või vaigistusest, mis on eelistatav. Sa ei näe selle domeeni sisu üheski avalikus ajajoones või teadetes. Sinu jälgijad sellest domeenist eemaldatakse.", + "confirmations.logout.confirm": "Välju", + "confirmations.logout.message": "Kas oled kindel, et soovid välja logida?", + "confirmations.mute.confirm": "Vaigista", + "confirmations.mute.explanation": "See peidab postitusi temalt ning postitusi, kus mainitakse neid, kuid see lubab neil ikka näha Teie postitusi ning Teid jälgida.", + "confirmations.mute.message": "Oled kindel, et soovid {name} vaigistada?", + "confirmations.redraft.confirm": "Kustuta & taasalusta", + "confirmations.redraft.message": "Oled kindel, et soovid selle staatuse kustutada ja alustada uuesti? Lemmikud ja upitused lähevad kaotsi ja vastused originaaalpostitusele jäävad orvuks.", + "confirmations.reply.confirm": "Vasta", + "confirmations.reply.message": "Kohene vastamine kirjutab üle sõnumi, mida hetkel koostad. Oled kindel, et soovid jätkata?", + "confirmations.unfollow.confirm": "Ära jälgi", + "confirmations.unfollow.message": "Oled kindel, et ei soovi jälgida {name}?", + "conversation.delete": "Kustuta vestlus", + "conversation.mark_as_read": "Märgi loetuks", + "conversation.open": "Vaata vestlust", + "conversation.with": "Koos {names}", + "directory.federated": "Teatud fediversumist", + "directory.local": "Ainult domeenilt {domain}", + "directory.new_arrivals": "Uustulijad", + "directory.recently_active": "Hiljuti aktiivne", + "embed.instructions": "Manusta see staatus oma veebilehele, kopeerides alloleva koodi.", + "embed.preview": "Nii näeb see välja:", + "emoji_button.activity": "Tegevus", + "emoji_button.custom": "Mugandatud", + "emoji_button.flags": "Lipud", + "emoji_button.food": "Toit & Jook", + "emoji_button.label": "Sisesta emoji", + "emoji_button.nature": "Loodus", + "emoji_button.not_found": "Ei ole emojosi!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objektid", + "emoji_button.people": "Inimesed", + "emoji_button.recent": "Tihti kasutatud", + "emoji_button.search": "Otsi...", + "emoji_button.search_results": "Otsitulemused", + "emoji_button.symbols": "Sümbolid", + "emoji_button.travel": "Reisimine & Kohad", + "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.", + "empty_column.community": "Kohalik ajajoon on tühi. Kirjuta midagi avalikult, et pall veerema saada!", + "empty_column.direct": "Sul ei veel otsesõnumeid. Kui saadad või võtad mõne vastu, ilmuvad nad siia.", + "empty_column.domain_blocks": "Siin ei ole veel peidetud domeene.", + "empty_column.favourited_statuses": "Sul pole veel lemmikuid tuute. Kui märgid mõne, näed neid siin.", + "empty_column.favourites": "Keegi pole veel seda tuuti lemmikuks märkinud. Kui seegi seda teeb, näed seda siin.", + "empty_column.follow_requests": "Sul pole veel ühtegi jälgimise taotlust. Kui saad mõne, näed seda siin.", + "empty_column.hashtag": "Selle sildiga pole veel midagi.", + "empty_column.home": "Sinu kodu ajajoon on tühi! Külasta {public} või kasuta otsingut alustamaks ja kohtamaks teisi kasutajaid.", + "empty_column.home.public_timeline": "avalik ajajoon", + "empty_column.list": "Siin nimstus pole veel midagi. Kui selle nimistu liikmed postitavad uusi staatusi, näed neid siin.", + "empty_column.lists": "Sul ei ole veel ühtegi nimekirja. Kui lood mõne, näed seda siin.", + "empty_column.mutes": "Sa pole veel ühtegi kasutajat vaigistanud.", + "empty_column.notifications": "Sul ei ole veel teateid. Suhtle 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.next_steps": "Proovige lehekülge uuesti avada. Kui see ei aita, võite proovida kasutada Mastodoni mõne muu veebilehitseja või äppi kaudu.", + "errors.unexpected_crash.copy_stacktrace": "Kopeeri stacktrace lõikelauale", + "errors.unexpected_crash.report_issue": "Teavita veast", + "follow_request.authorize": "Autoriseeri", + "follow_request.reject": "Hülga", + "getting_started.developers": "Arendajad", + "getting_started.directory": "Profiili kataloog", + "getting_started.documentation": "Dokumentatsioon", + "getting_started.heading": "Alustamine", + "getting_started.invite": "Kutsu inimesi", + "getting_started.open_source_notice": "Mastodon on avatud lähtekoodiga tarkvara. Saad panustada või teatada probleemidest GitHubis {github}.", + "getting_started.security": "Turvalisus", + "getting_started.terms": "Kasutustingimused", + "hashtag.column_header.tag_mode.all": "ja {additional}", + "hashtag.column_header.tag_mode.any": "või {additional}", + "hashtag.column_header.tag_mode.none": "ilma {additional}", + "hashtag.column_settings.select.no_options_message": "Soovitusi ei leitud", + "hashtag.column_settings.select.placeholder": "Sisesta sildid…", + "hashtag.column_settings.tag_mode.all": "Kõik need", + "hashtag.column_settings.tag_mode.any": "Mõni neist", + "hashtag.column_settings.tag_mode.none": "Mitte ükski neist", + "hashtag.column_settings.tag_toggle": "Kaasa lisamärked selle tulba jaoks", + "home.column_settings.basic": "Peamine", + "home.column_settings.show_reblogs": "Näita upitusi", + "home.column_settings.show_replies": "Näita vastuseid", + "intervals.full.days": "{number, plural, one {# päev} other {# päevad}}", + "intervals.full.hours": "{number, plural, one {# tund} other {# tundi}}", + "intervals.full.minutes": "{number, plural, one {# minut} other {# minutit}}", + "introduction.federation.action": "Järgmine", + "introduction.federation.federated.headline": "Föderatiivne", + "introduction.federation.federated.text": "Avalikud postitused teistest föderatsiooni serveritest kuvatakse föderatiivsel ajajoonel.", + "introduction.federation.home.headline": "Kodu", + "introduction.federation.home.text": "Inimest postitused keda jälgid kuvatakse sinu koduajajoonel. Saad jälgida igaüht igas serveris!", + "introduction.federation.local.headline": "Kohalik", + "introduction.federation.local.text": "Samas serveris olevate inimeste postitused kuvatakse kohalikul ajajoonel.", + "introduction.interactions.action": "Välju õpetusest!", + "introduction.interactions.favourite.headline": "Lemmik", + "introduction.interactions.favourite.text": "Saad tuuti salvestada ja anda autorile teada, et meeldis märkides selle lemmikuks.", + "introduction.interactions.reblog.headline": "Upita", + "introduction.interactions.reblog.text": "Saad jagada teiste inimeste tuute oma jälgijatega upitades neid.", + "introduction.interactions.reply.headline": "Vasta", + "introduction.interactions.reply.text": "Saad vastata teiste ja enda tuutidele, mis ühendab nad kokku aruteluks.", + "introduction.welcome.action": "Lähme!", + "introduction.welcome.headline": "Esimesed sammud", + "introduction.welcome.text": "Teretulemast fediversumisse! Mõne aja pärast saad avaldada sõnumeid ja rääkida oma sõpradega läbi laia valiku serverite. Aga see server, {domain}, on eriline—ta majutab sinu profiili. Seega jäta ta nimi meelde.", + "keyboard_shortcuts.back": "tagasiminekuks", + "keyboard_shortcuts.blocked": "avamaks blokeeritud kasutajate nimistut", + "keyboard_shortcuts.boost": "upitamiseks", + "keyboard_shortcuts.column": "fokuseerimaks staatust ühele tulpadest", + "keyboard_shortcuts.compose": "fokuseerimaks tekstikoostamise alale", + "keyboard_shortcuts.description": "Kirjeldus", + "keyboard_shortcuts.direct": "avamaks otsesõnumite tulpa", + "keyboard_shortcuts.down": "liikumaks nimstus alla", + "keyboard_shortcuts.enter": "staatuse avamiseks", + "keyboard_shortcuts.favourite": "lemmikuks märkimiseks", + "keyboard_shortcuts.favourites": "avamaks lemmikute nimistut", + "keyboard_shortcuts.federated": "avamaks föderatsiooni ajajoont", + "keyboard_shortcuts.heading": "Klaviatuuri kiirkäsud", + "keyboard_shortcuts.home": "avamaks kodu ajajoont", + "keyboard_shortcuts.hotkey": "Kiirklahv", + "keyboard_shortcuts.legend": "selle legendi kuvamiseks", + "keyboard_shortcuts.local": "avamaks kohalikku ajajoont", + "keyboard_shortcuts.mention": "autori mainimiseks", + "keyboard_shortcuts.muted": "avamaks vaigistatud kasutajate nimistut", + "keyboard_shortcuts.my_profile": "avamaks profiili", + "keyboard_shortcuts.notifications": "avamaks teadete tulpa", + "keyboard_shortcuts.pinned": "avamaks kinnitatud tuutide nimistut", + "keyboard_shortcuts.profile": "avamaks autori profiili", + "keyboard_shortcuts.reply": "vastamiseks", + "keyboard_shortcuts.requests": "avamaks jälgimistaotluste nimistut", + "keyboard_shortcuts.search": "otsingu fokuseerimiseks", + "keyboard_shortcuts.start": "avamaks \"Alusta\" tulpa", + "keyboard_shortcuts.toggle_hidden": "näitamaks/peitmaks teksti CW taga", + "keyboard_shortcuts.toggle_sensitivity": "et peita/näidata meediat", + "keyboard_shortcuts.toot": "alustamaks täiesti uut tuuti", + "keyboard_shortcuts.unfocus": "tekstiala/otsingu koostamise mittefokuseerimiseks", + "keyboard_shortcuts.up": "liikumaks nimistus üles", + "lightbox.close": "Sulge", + "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", + "lists.edit": "Muuda nimistut", + "lists.edit.submit": "Muuda pealkiri", + "lists.new.create": "Lisa nimistu", + "lists.new.title_placeholder": "Uus nimistu pealkiri", + "lists.search": "Otsi sinu poolt jälgitavate inimese hulgast", + "lists.subheading": "Sinu nimistud", + "load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}", + "loading_indicator.label": "Laeb..", + "media_gallery.toggle_visible": "Lülita nähtavus", + "missing_indicator.label": "Ei leitud", + "missing_indicator.sublabel": "Seda ressurssi ei leitud", + "mute_modal.hide_notifications": "Kas peita teated sellelt kasutajalt?", + "navigation_bar.apps": "Mobiilrakendused", + "navigation_bar.blocks": "Blokeeritud kasutajad", + "navigation_bar.community_timeline": "Kohalik ajajoon", + "navigation_bar.compose": "Koosta uus tuut", + "navigation_bar.direct": "Otsesõnumid", + "navigation_bar.discover": "Avasta", + "navigation_bar.domain_blocks": "Peidetud domeenid", + "navigation_bar.edit_profile": "Muuda profiili", + "navigation_bar.favourites": "Lemmikud", + "navigation_bar.filters": "Vaigistatud sõnad", + "navigation_bar.follow_requests": "Jälgimistaotlused", + "navigation_bar.follows_and_followers": "Jälgitud ja jälgijad", + "navigation_bar.info": "Selle serveri kohta", + "navigation_bar.keyboard_shortcuts": "Kiirklahvid", + "navigation_bar.lists": "Nimistud", + "navigation_bar.logout": "Logi välja", + "navigation_bar.mutes": "Vaigistatud kasutajad", + "navigation_bar.personal": "Isiklik", + "navigation_bar.pins": "Kinnitatud tuutid", + "navigation_bar.preferences": "Eelistused", + "navigation_bar.public_timeline": "Föderatiivne ajajoon", + "navigation_bar.security": "Turvalisus", + "notification.favourite": "{name} märkis su staatuse lemmikuks", + "notification.follow": "{name} jälgib sind", + "notification.mention": "{name} mainis sind", + "notification.poll": "Küsitlus, milles osalesid, on lõppenud", + "notification.reblog": "{name} upitas su staatust", + "notifications.clear": "Puhasta teated", + "notifications.clear_confirmation": "Oled kindel, et soovid püsivalt kõik oma teated puhastada?", + "notifications.column_settings.alert": "Töölauateated", + "notifications.column_settings.favourite": "Lemmikud:", + "notifications.column_settings.filter_bar.advanced": "Kuva kõik kategooriad", + "notifications.column_settings.filter_bar.category": "Kiirfiltri riba", + "notifications.column_settings.filter_bar.show": "Kuva", + "notifications.column_settings.follow": "Uued jälgijad:", + "notifications.column_settings.mention": "Mainimised:", + "notifications.column_settings.poll": "Küsitluse tulemused:", + "notifications.column_settings.push": "Push teated", + "notifications.column_settings.reblog": "Upitused:", + "notifications.column_settings.show": "Kuva tulbas", + "notifications.column_settings.sound": "Mängi heli", + "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.group": "{count} teated", + "poll.closed": "Suletud", + "poll.refresh": "Värskenda", + "poll.total_people": "{count, plural,one {# inimene} other {# inimest}}", + "poll.total_votes": "{count, plural, one {# hääl} other {# hääli}}", + "poll.vote": "Hääleta", + "poll.voted": "Teie hääletasite selle poolt", + "poll_button.add_poll": "Lisa küsitlus", + "poll_button.remove_poll": "Eemalda küsitlus", + "privacy.change": "Muuda staatuse privaatsust", + "privacy.direct.long": "Postita ainult mainitud kasutajatele", + "privacy.direct.short": "Otsene", + "privacy.private.long": "Postita ainult jälgijatele", + "privacy.private.short": "Ainult jälgijatele", + "privacy.public.long": "Postita avalikele ajajoontele", + "privacy.public.short": "Avalik", + "privacy.unlisted.long": "Ära postita avalikele ajajoontele", + "privacy.unlisted.short": "Määramata", + "refresh": "Värskenda", + "regeneration_indicator.label": "Laeb…", + "regeneration_indicator.sublabel": "Sinu kodu voog on ettevalmistamisel!", + "relative_time.days": "{number}p", + "relative_time.hours": "{number}t", + "relative_time.just_now": "nüüd", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Tühista", + "report.forward": "Edasta kasutajale {target}", + "report.forward_hint": "See kasutaja on teisest serverist. Kas saadan anonümiseeritud koopia sellest teatest sinna ka?", + "report.hint": "See teade saadetakse sinu serveri moderaatoritele. Te saate lisada selgituse selle kohta, miks selle kasutaja kohta teate esitasite, siin:", + "report.placeholder": "Lisaks kommentaarid", + "report.submit": "Saada", + "report.target": "Teatamine {target} kohta", + "search.placeholder": "Otsi", + "search_popout.search_format": "Täiustatud otsiformaat", + "search_popout.tips.full_text": "Lihtne tekst toob esile staatused mida olete kirjutanud, lisanud lemmikuks, upitanud või olete seal mainitud, ning lisaks veel kattuvad kasutajanimed, kuvanimed ja sildid.", + "search_popout.tips.hashtag": "silt", + "search_popout.tips.status": "staatus", + "search_popout.tips.text": "Lihtne tekst toob esile kattuvad kuvanimed, kasutajanimed ning sildid", + "search_popout.tips.user": "kasutaja", + "search_results.accounts": "Inimesed", + "search_results.hashtags": "Sildid", + "search_results.statuses": "Tuudid", + "search_results.statuses_fts_disabled": "Tuutsude otsimine nende sisu järgi ei ole sellel Mastodoni serveril sisse lülitatud.", + "search_results.total": "{count, number} {count, plural, one {tulemus} other {tulemust}}", + "status.admin_account": "Ava moderaatoriliides kasutajale @{name}", + "status.admin_status": "Ava see staatus moderaatoriliites", + "status.block": "Blokeeri @{name}", + "status.cancel_reblog_private": "Äraupita", + "status.cannot_reblog": "Seda postitust ei saa upitada", + "status.copy": "Kopeeri link staatusesse", + "status.delete": "Kustuta", + "status.detailed_status": "Detailne vestluskuva", + "status.direct": "Otsesõnum @{name}", + "status.embed": "Sängita", + "status.favourite": "Lemmik", + "status.filtered": "Filtreeritud", + "status.load_more": "Lae veel", + "status.media_hidden": "Meedia peidetud", + "status.mention": "Mainimine @{name}", + "status.more": "Veel", + "status.mute": "Vaigista @{name}", + "status.mute_conversation": "Vaigista vestlus", + "status.open": "Laienda see staatus", + "status.pin": "Kinnita profiilile", + "status.pinned": "Kinnitatud tuut", + "status.read_more": "Loe veel", + "status.reblog": "Upita", + "status.reblog_private": "Upita algsele publikule", + "status.reblogged_by": "{name} upitatud", + "status.reblogs.empty": "Keegi pole seda tuuti veel upitanud. Kui keegi upitab, näed seda siin.", + "status.redraft": "Kustuta & alga uuesti", + "status.reply": "Vasta", + "status.replyAll": "Vasta lõimele", + "status.report": "Raport @{name}", + "status.sensitive_warning": "Tundlik sisu", + "status.share": "Jaga", + "status.show_less": "Näita vähem", + "status.show_less_all": "Näita vähem kõigile", + "status.show_more": "Näita veel", + "status.show_more_all": "Näita enam kõigile", + "status.show_thread": "Kuva lõim", + "status.uncached_media_warning": "Pole saadaval", + "status.unmute_conversation": "Ära vaigista vestlust", + "status.unpin": "Kinnita profiililt lahti", + "suggestions.dismiss": "Eira soovitust", + "suggestions.header": "Sind võib huvitada…", + "tabs_bar.federated_timeline": "Föderatiivne", + "tabs_bar.home": "Kodu", + "tabs_bar.local_timeline": "Kohalik", + "tabs_bar.notifications": "Teated", + "tabs_bar.search": "Otsi", + "time_remaining.days": "{number, plural, one {# päev} other {# päeva}} left", + "time_remaining.hours": "{number, plural, one {# tund} other {# tundi}} left", + "time_remaining.minutes": "{number, plural, one {# minut} other {# minutit}} left", + "time_remaining.moments": "Hetked jäänud", + "time_remaining.seconds": "{number, plural, one {# sekund} other {# sekundit}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {inimene} other {inimesed}} talking", + "trends.trending_now": "Praegu populaarne", + "ui.beforeunload": "Sinu mustand läheb kaotsi, kui lahkud Mastodonist.", + "upload_area.title": "Lohista & aseta üleslaadimiseks", + "upload_button.label": "Lisa meedia (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "Faili üleslaadimise limiit ületatud.", + "upload_error.poll": "Küsitlustes pole faili üleslaadimine lubatud.", + "upload_form.description": "Kirjelda vaegnägijatele", + "upload_form.edit": "Redigeeri", + "upload_form.undo": "Kustuta", + "upload_modal.analyzing_picture": "Analüüsime pilti…", + "upload_modal.apply": "Rakenda", + "upload_modal.description_placeholder": "Kiire pruun rebane hüppab üle laisa koera", + "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.preview_label": "Eelvaade ({ratio})", + "upload_progress.label": "Laeb üles....", + "video.close": "Sulge video", + "video.exit_fullscreen": "Välju täisekraanist", + "video.expand": "Suurenda video", + "video.fullscreen": "Täisekraan", + "video.hide": "Peida video", + "video.mute": "Vaigista heli", + "video.pause": "Paus", + "video.play": "Mängi", + "video.unmute": "Taasta heli" +} diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index 5e37f5862..9e383e85a 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -1,9 +1,10 @@ { - "account.add_or_remove_from_list": "Gehitu edo kendu zerrendetatik", + "account.add_or_remove_from_list": "Gehitu edo Kendu zerrendetatik", "account.badges.bot": "Bot-a", "account.block": "Blokeatu @{name}", "account.block_domain": "Ezkutatu {domain} domeinuko guztia", "account.blocked": "Blokeatuta", + "account.cancel_follow_request": "Ezeztatu jarraitzeko eskaria", "account.direct": "Mezu zuzena @{name}(r)i", "account.domain_blocked": "Ezkutatutako domeinua", "account.edit_profile": "Aldatu profila", @@ -15,16 +16,18 @@ "account.follows.empty": "Erabiltzaile honek ez du inor jarraitzen oraindik.", "account.follows_you": "Jarraitzen dizu", "account.hide_reblogs": "Ezkutatu @{name}(r)en bultzadak", + "account.last_status": "Azkenekoz aktiboa", "account.link_verified_on": "Esteka honen jabetzaren egiaztaketa data: {date}", "account.locked_info": "Kontu honen pribatutasun egoera blokeatuta gisa ezarri da. Jabeak eskuz erabakitzen du nork jarraitu diezaioken.", "account.media": "Multimedia", "account.mention": "Aipatu @{name}", - "account.moved_to": "{name} hona lekualdatu da:", + "account.moved_to": "{name} hona migratu da:", "account.mute": "Mututu @{name}", "account.mute_notifications": "Mututu @{name}(r)en jakinarazpenak", "account.muted": "Mutututa", + "account.never_active": "Inoiz ez", "account.posts": "Tootak", - "account.posts_with_replies": "Toot eta erantzunak", + "account.posts_with_replies": "Toot-ak eta erantzunak", "account.report": "Salatu @{name}", "account.requested": "Onarpenaren zain. Klikatu jarraitzeko eskaera ezeztatzeko", "account.share": "@{name}(e)ren profila elkarbanatu", @@ -32,11 +35,14 @@ "account.unblock": "Desblokeatu @{name}", "account.unblock_domain": "Berriz erakutsi {domain}", "account.unendorse": "Ez nabarmendu profilean", - "account.unfollow": "Jarraitzeari utzi", + "account.unfollow": "Utzi jarraitzeari", "account.unmute": "Desmututu @{name}", "account.unmute_notifications": "Desmututu @{name}(r)en jakinarazpenak", + "alert.rate_limited.message": "Saiatu {retry_time, time, medium} barru.", + "alert.rate_limited.title": "Abiadura mugatua", "alert.unexpected.message": "Ustekabeko errore bat gertatu da.", "alert.unexpected.title": "Ene!", + "autosuggest_hashtag.per_week": "{count} asteko", "boost_modal.combo": "{combo} sakatu dezakezu hurrengoan hau saltatzeko", "bundle_column_error.body": "Zerbait okerra gertatu da osagai hau kargatzean.", "bundle_column_error.retry": "Saiatu berriro", @@ -47,6 +53,7 @@ "column.blocks": "Blokeatutako erabiltzaileak", "column.community": "Denbora-lerro lokala", "column.direct": "Mezu zuzenak", + "column.directory": "Arakatu profilak", "column.domain_blocks": "Ezkutatutako domeinuak", "column.favourites": "Gogokoak", "column.follow_requests": "Jarraitzeko eskariak", @@ -54,7 +61,7 @@ "column.lists": "Zerrendak", "column.mutes": "Mutututako erabiltzaileak", "column.notifications": "Jakinarazpenak", - "column.pins": "Pinned toot", + "column.pins": "Finkatutako toot-ak", "column.public": "Federatutako denbora-lerroa", "column_back_button.label": "Atzera", "column_header.hide_settings": "Ezkutatu ezarpenak", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Ziur behin betiko ezabatu nahi duzula zerrenda hau?", "confirmations.domain_block.confirm": "Ezkutatu domeinu osoa", "confirmations.domain_block.message": "Ziur, erabat ziur, {domain} domeinu osoa blokeatu nahi duzula? Gehienetan gutxi batzuk blokeatu edo mututzearekin nahikoa da. Ez duzu domeinu horretako edukirik ikusiko denbora lerroetan edo jakinarazpenetan. Domeinu horretako zure jarraitzaileak kenduko dira ere.", + "confirmations.logout.confirm": "Amaitu saioa", + "confirmations.logout.message": "Ziur saioa amaitu nahi duzula?", "confirmations.mute.confirm": "Mututu", + "confirmations.mute.explanation": "Honek horko mezuak eta aipamena egiten dietenak ezkutatuko ditu, baina beraiek zure mezuak ikusi ahal izango dituzte eta zuri jarraitu.", "confirmations.mute.message": "Ziur {name} mututu nahi duzula?", "confirmations.redraft.confirm": "Ezabatu eta berridatzi", "confirmations.redraft.message": "Ziur mezu hau ezabatu eta berridatzi nahi duzula? Gogokoak eta bultzadak galduko dira eta jaso dituen erantzunak umezurtz geratuko dira.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Orain erantzuteak idazten ari zaren mezua gainidatziko du. Ziur jarraitu nahi duzula?", "confirmations.unfollow.confirm": "Utzi jarraitzeari", "confirmations.unfollow.message": "Ziur {name} jarraitzeari utzi nahi diozula?", + "conversation.delete": "Ezabatu elkarrizketa", + "conversation.mark_as_read": "Markatu irakurrita bezala", + "conversation.open": "Ikusi elkarrizketa", + "conversation.with": "Hauekin: {names}", + "directory.federated": "Fedibertso ezagunekoak", + "directory.local": "{domain} domeinukoak soilik", + "directory.new_arrivals": "Iritsi berriak", + "directory.recently_active": "Duela gutxi aktibo", "embed.instructions": "Txertatu mezu hau zure webgunean beheko kodea kopatuz.", "embed.preview": "Hau da izango duen itxura:", "emoji_button.activity": "Jarduera", @@ -134,6 +152,10 @@ "empty_column.mutes": "Ez duzu erabiltzailerik mututu oraindik.", "empty_column.notifications": "Ez duzu jakinarazpenik oraindik. Jarri besteekin harremanetan elkarrizketa abiatzeko.", "empty_column.public": "Ez dago ezer hemen! Idatzi zerbait publikoki edo jarraitu eskuz beste zerbitzari batzuetako erabiltzaileak hau betetzen joateko", + "error.unexpected_crash.explanation": "Gure kodean arazoren bat dela eta, edo nabigatzailearekin bateragarritasun arazoren bat dela eta, orri hau ezin izan da ongi bistaratu.", + "error.unexpected_crash.next_steps": "Saiatu orria berritzen. Horrek ez badu laguntzen, agian Mastodon erabiltzeko aukera duzu oraindik ere beste nabigatzaile bat edo aplikazio natibo bat erabilita.", + "errors.unexpected_crash.copy_stacktrace": "Kopiatu irteera arbelera", + "errors.unexpected_crash.report_issue": "Eman arazoaren berri", "follow_request.authorize": "Baimendu", "follow_request.reject": "Ukatu", "getting_started.developers": "Garatzaileak", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Zerrenda berriaren izena", "lists.search": "Bilatu jarraitzen dituzun pertsonen artean", "lists.subheading": "Zure zerrendak", + "load_pending": "{count, plural, one {eleentuberri #} other {# elementu berri}}", "loading_indicator.label": "Kargatzen...", "media_gallery.toggle_visible": "Txandakatu ikusgaitasuna", "missing_indicator.label": "Ez aurkitua", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Pertsonala", "navigation_bar.pins": "Finkatutako toot-ak", "navigation_bar.preferences": "Hobespenak", - "navigation_bar.profile_directory": "Profilen direktorioa", "navigation_bar.public_timeline": "Federatutako denbora-lerroa", "navigation_bar.security": "Segurtasuna", "notification.favourite": "{name}(e)k zure mezua gogoko du", @@ -277,8 +299,10 @@ "notifications.group": "{count} jakinarazpen", "poll.closed": "Itxita", "poll.refresh": "Berritu", + "poll.total_people": "{count, plural, one {pertsona #} other {# pertsona}}", "poll.total_votes": "{count, plural, one {boto #} other {# boto}}", "poll.vote": "Bozkatu", + "poll.voted": "Erantzun honi eman diozu botoa", "poll_button.add_poll": "Gehitu inkesta bat", "poll_button.remove_poll": "Kendu inkesta", "privacy.change": "Doitu mezuaren pribatutasuna", @@ -290,6 +314,7 @@ "privacy.public.short": "Publikoa", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Zerrendatu gabea", + "refresh": "Berritu", "regeneration_indicator.label": "Kargatzen…", "regeneration_indicator.sublabel": "Zure hasiera-jarioa prestatzen ari da!", "relative_time.days": "{number}e", @@ -314,6 +339,7 @@ "search_results.accounts": "Jendea", "search_results.hashtags": "Traolak", "search_results.statuses": "Toot-ak", + "search_results.statuses_fts_disabled": "Mastodon zerbitzari honek ez du Toot-en edukiaren bilaketa gaitu.", "search_results.total": "{count, number} {count, plural, one {emaitza} other {emaitzak}}", "status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea", "status.admin_status": "Ireki mezu hau moderazio interfazean", @@ -352,6 +378,7 @@ "status.show_more": "Erakutsi gehiago", "status.show_more_all": "Erakutsi denetarik gehiago", "status.show_thread": "Erakutsi haria", + "status.uncached_media_warning": "Ez eskuragarri", "status.unmute_conversation": "Desmututu elkarrizketa", "status.unpin": "Desfinkatu profiletik", "suggestions.dismiss": "Errefusatu proposamena", @@ -367,14 +394,22 @@ "time_remaining.moments": "Amaitzekotan", "time_remaining.seconds": "{number, plural, one {segundo #} other {# segundo}} amaitzeko", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} hitz egiten", + "trends.trending_now": "Joera orain", "ui.beforeunload": "Zure zirriborroa galduko da Mastodon uzten baduzu.", "upload_area.title": "Arrastatu eta jaregin igotzeko", "upload_button.label": "Gehitu multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Fitxategi igoera muga gaindituta.", "upload_error.poll": "Ez da inkestetan fitxategiak igotzea onartzen.", "upload_form.description": "Deskribatu ikusmen arazoak dituztenentzat", - "upload_form.focus": "Aldatu aurrebista", + "upload_form.edit": "Editatu", "upload_form.undo": "Ezabatu", + "upload_modal.analyzing_picture": "Irudia aztertzen…", + "upload_modal.apply": "Aplikatu", + "upload_modal.description_placeholder": "Azeri marroi azkar batek txakur alferraren gainetik salto egiten du", + "upload_modal.detect_text": "Antzeman testua iruditik", + "upload_modal.edit_media": "Editatu media", + "upload_modal.hint": "Sakatu eta jaregin aurrebistako zirkulua iruditxoetan beti ikusgai egongo den puntu fokala hautatzeko.", + "upload_modal.preview_label": "Aurreikusi ({ratio})", "upload_progress.label": "Igotzen...", "video.close": "Itxi bideoa", "video.exit_fullscreen": "Irten pantaila osotik", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index def140df6..b651edd32 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -3,7 +3,8 @@ "account.badges.bot": "ربات", "account.block": "مسدودسازی @{name}", "account.block_domain": "پنهانسازی همه چیز از سرور {domain}", - "account.blocked": "مسدودشده", + "account.blocked": "مسدود شده", + "account.cancel_follow_request": "لغو درخواست پیگیری", "account.direct": "پیغام خصوصی به @{name}", "account.domain_blocked": "دامین پنهانشده", "account.edit_profile": "ویرایش نمایه", @@ -15,7 +16,8 @@ "account.follows.empty": "این کاربر هنوز هیچ کسی را پی نمیگیرد.", "account.follows_you": "پیگیر شماست", "account.hide_reblogs": "پنهان کردن بازبوقهای @{name}", - "account.link_verified_on": "مالکیت این نشانی در تایخ {date} بررسی شد", + "account.last_status": "آخرین فعالیت", + "account.link_verified_on": "مالکیت این نشانی در تاریخ {date} بررسی شد", "account.locked_info": "این حساب خصوصی است. صاحب این حساب تصمیم میگیرد که چه کسی میتواند پیگیرش باشد.", "account.media": "عکس و ویدیو", "account.mention": "نامبردن از @{name}", @@ -23,6 +25,7 @@ "account.mute": "بیصدا کردن @{name}", "account.mute_notifications": "بیصداکردن اعلانها از طرف @{name}", "account.muted": "بیصداشده", + "account.never_active": "هرگز", "account.posts": "نوشتهها", "account.posts_with_replies": "نوشتهها و پاسخها", "account.report": "گزارش @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "پایان پیگیری", "account.unmute": "باصدا کردن @{name}", "account.unmute_notifications": "باصداکردن اعلانها از طرف @{name}", + "alert.rate_limited.message": "لطفاً پس از {retry_time, time, medium} دوباره تلاش کنید.", + "alert.rate_limited.title": "محدودیت تعداد", "alert.unexpected.message": "خطای پیشبینینشدهای رخ داد.", "alert.unexpected.title": "ای وای!", + "autosuggest_hashtag.per_week": "{count} در هفته", "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید", "bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.", "bundle_column_error.retry": "تلاش دوباره", @@ -47,6 +53,7 @@ "column.blocks": "کاربران مسدودشده", "column.community": "نوشتههای محلی", "column.direct": "پیغامهای خصوصی", + "column.directory": "مرور نمایهها", "column.domain_blocks": "دامینهای پنهانشده", "column.favourites": "پسندیدهها", "column.follow_requests": "درخواستهای پیگیری", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "آیا واقعاً میخواهید این فهرست را برای همیشه پاک کنید؟", "confirmations.domain_block.confirm": "پنهانسازی کل دامین", "confirmations.domain_block.message": "آیا جدی جدی میخواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقتها مسدودکردن یا بیصداکردن چند حساب کاربری خاص کافی است و توصیه میشود. پس از این کار شما هیچ نوشتهای را از این دامین در فهرست نوشتههای عمومی یا اعلانهایتان نخواهید دید. پیگیران شما از این دامین هم حذف خواهد شد.", + "confirmations.logout.confirm": "خروج", + "confirmations.logout.message": "آیا مطمئنید که میخواهید خارج شوید؟", "confirmations.mute.confirm": "بیصدا کن", + "confirmations.mute.explanation": "این کار نوشتههای آنها و نوشتههایی را که از آنها نام برده پنهان میکند، ولی آنها همچنان اجازه دارند نوشتههای شما را ببینند و شما را پی بگیرند.", "confirmations.mute.message": "آیا واقعاً میخواهید {name} را بیصدا کنید؟", "confirmations.redraft.confirm": "پاککردن و بازنویسی", "confirmations.redraft.message": "آیا واقعاً میخواهید این نوشته را پاک کنید و آن را از نو بنویسید؟ با این کار بازبوقها و پسندیدهشدنهای آن از دست میرود و پاسخها به آن بیمرجع میشود.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "اگر الان پاسخ دهید، چیزی که در حال نوشتنش بودید پاک خواهد شد. آیا همین را میخواهید؟", "confirmations.unfollow.confirm": "لغو پیگیری", "confirmations.unfollow.message": "آیا واقعاً میخواهید به پیگیری از {name} پایان دهید؟", + "conversation.delete": "حذف گفتگو", + "conversation.mark_as_read": "علامتگذاری به عنوان خوانده شده", + "conversation.open": "دیدن گفتگو", + "conversation.with": "با {names}", + "directory.federated": "از سرورهای همسایه", + "directory.local": "تنها از {domain}", + "directory.new_arrivals": "تازهواردان", + "directory.recently_active": "کاربران فعال اخیر", "embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.", "embed.preview": "نوشتهٔ جاگذاریشده این گونه به نظر خواهد رسید:", "emoji_button.activity": "فعالیت", @@ -126,7 +144,7 @@ "empty_column.favourited_statuses": "شما هنوز هیچ بوقی را نپسندیدهاید. وقتی بوقی را بپسندید، اینجا نمایش خواهد یافت.", "empty_column.favourites": "هنوز هیچ کسی این بوق را نپسندیده است. وقتی کسی آن را بپسندد، نامش اینجا نمایش خواهد یافت.", "empty_column.follow_requests": "شما هنوز هیچ درخواست پیگیریای ندارید. وقتی چنین درخواستی بگیرید، اینجا نمایش خواهد یافت.", - "empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.", + "empty_column.hashtag": "هنوز هیچ چیزی با این برچسب (هشتگ) نیست.", "empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.", "empty_column.home.public_timeline": "فهرست نوشتههای همهجا", "empty_column.list": "در این فهرست هنوز چیزی نیست. وقتی اعضای این فهرست چیزی بنویسند، اینجا ظاهر خواهد شد.", @@ -134,6 +152,10 @@ "empty_column.mutes": "شما هنوز هیچ کاربری را بیصدا نکردهاید.", "empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشتههای دیگران واکنش نشان دهید تا گفتگو آغاز شود.", "empty_column.public": "اینجا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران سرورهای دیگر را پی بگیرید تا اینجا پر شود", + "error.unexpected_crash.explanation": "به خاطر اشکالی در کدهای ما یا ناسازگاری با مرورگر شما، این صفحه به درستی نمایش نیافت.", + "error.unexpected_crash.next_steps": "لطفاً صفحه را دوباره باز کنید. اگر آن هم کمکی نکرد، همچنان شاید بتوانید با ماستدون از راه یکی از اپهای آن کار کنید.", + "errors.unexpected_crash.copy_stacktrace": "کپی جزئیات اشکال", + "errors.unexpected_crash.report_issue": "گزارش اشکال", "follow_request.authorize": "اجازه دهید", "follow_request.reject": "اجازه ندهید", "getting_started.developers": "برای برنامهنویسان", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "نام فهرست تازه", "lists.search": "بین کسانی که پی میگیرید بگردید", "lists.subheading": "فهرستهای شما", + "load_pending": "{count, plural, one {# مورد تازه} other {# مورد تازه}}", "loading_indicator.label": "بارگیری...", "media_gallery.toggle_visible": "تغییر پیدایی", "missing_indicator.label": "پیدا نشد", @@ -246,7 +269,6 @@ "navigation_bar.personal": "شخصی", "navigation_bar.pins": "نوشتههای ثابت", "navigation_bar.preferences": "ترجیحات", - "navigation_bar.profile_directory": "فهرست گزیدهٔ کاربران", "navigation_bar.public_timeline": "نوشتههای همهجا", "navigation_bar.security": "امنیت", "notification.favourite": "{name} نوشتهٔ شما را پسندید", @@ -277,8 +299,10 @@ "notifications.group": "{count} اعلان", "poll.closed": "پایانیافته", "poll.refresh": "بهروزرسانی", + "poll.total_people": "{count, plural, one {# نفر} other {# نفر}}", "poll.total_votes": "{count, plural, one {# رأی} other {# رأی}}", "poll.vote": "رأی", + "poll.voted": "شما به این گزینه رأی دادید", "poll_button.add_poll": "افزودن نظرسنجی", "poll_button.remove_poll": "حذف نظرسنجی", "privacy.change": "تنظیم حریم خصوصی نوشتهها", @@ -290,6 +314,7 @@ "privacy.public.short": "عمومی", "privacy.unlisted.long": "عمومی، ولی فهرست نکن", "privacy.unlisted.short": "فهرستنشده", + "refresh": "بهروزرسانی", "regeneration_indicator.label": "در حال باز شدن…", "regeneration_indicator.sublabel": "این فهرست دارد آماده میشود!", "relative_time.days": "{number} روز", @@ -307,13 +332,14 @@ "search.placeholder": "جستجو", "search_popout.search_format": "راهنمای جستجوی پیشرفته", "search_popout.tips.full_text": "جستجوی متنی ساده میتواند بوقهایی که شما نوشتهاید، پسندیدهاید، بازبوقیدهاید، یا در آنها از شما نام برده شده است را پیدا کند. همچنین نامهای کاربری، نام نمایشیافته، و هشتگها را هم شامل میشود.", - "search_popout.tips.hashtag": "هشتگ", + "search_popout.tips.hashtag": "برچسب", "search_popout.tips.status": "نوشته", - "search_popout.tips.text": "جستجوی متنی ساده برای نامها، نامهای کاربری، و هشتگها", + "search_popout.tips.text": "جستجوی متنی ساده برای نامها، نامهای کاربری، و برچسبها", "search_popout.tips.user": "کاربر", "search_results.accounts": "افراد", - "search_results.hashtags": "هشتگها", + "search_results.hashtags": "برچسبها", "search_results.statuses": "بوقها", + "search_results.statuses_fts_disabled": "جستجوی محتوای بوقها در این سرور ماستدون ممکن نیست.", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", "status.admin_account": "محیط مدیریت مربوط به @{name} را باز کن", "status.admin_status": "این نوشته را در محیط مدیریت باز کن", @@ -352,6 +378,7 @@ "status.show_more": "نمایش", "status.show_more_all": "نمایش بیشتر همه", "status.show_thread": "نمایش گفتگو", + "status.uncached_media_warning": "ناموجود", "status.unmute_conversation": "باصداکردن گفتگو", "status.unpin": "برداشتن نوشتهٔ ثابت نمایه", "suggestions.dismiss": "پیشنهاد را نادیده بگیر", @@ -367,14 +394,22 @@ "time_remaining.moments": "زمان باقیمانده", "time_remaining.seconds": "{number, plural, one {# ثانیه} other {# ثانیه}} باقی مانده", "trends.count_by_accounts": "{count} {rawCount, plural, one {نفر نوشته است} other {نفر نوشتهاند}}", + "trends.trending_now": "پرطرفدار", "ui.beforeunload": "اگر از ماستدون خارج شوید پیشنویس شما پاک خواهد شد.", "upload_area.title": "برای بارگذاری به اینجا بکشید", "upload_button.label": "افزودن عکس و ویدیو (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "از حد مجاز باگذاری فراتر رفتید.", "upload_error.poll": "باگذاری پرونده در نظرسنجیها ممکن نیست.", "upload_form.description": "نوشتهٔ توضیحی برای کمبینایان و نابینایان", - "upload_form.focus": "تغییر پیشنمایش", + "upload_form.edit": "ویرایش", "upload_form.undo": "حذف", + "upload_modal.analyzing_picture": "در حال پردازش تصویر…", + "upload_modal.apply": "اجرا", + "upload_modal.description_placeholder": "مردی با بیل مادرزنش را کشت", + "upload_modal.detect_text": "پیدا کردن نوشته از درون تصویر", + "upload_modal.edit_media": "ویرایش تصویر", + "upload_modal.hint": "حتی اگر تصویر بریده یا کوچک شود، نقطهٔ کانونی آن همیشه دیده خواهد شد. نقطهٔ کانونی را با کلیک یا جابهجا کردن آن تنظیم کنید.", + "upload_modal.preview_label": "پیشنمایش ({ratio})", "upload_progress.label": "بارگذاری...", "video.close": "بستن ویدیو", "video.exit_fullscreen": "خروج از حالت تمام صفحه", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 4eca05ca5..60fa0c296 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -4,17 +4,19 @@ "account.block": "Estä @{name}", "account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}", "account.blocked": "Estetty", + "account.cancel_follow_request": "Peruuta seurauspyyntö", "account.direct": "Viesti käyttäjälle @{name}", "account.domain_blocked": "Verkko-osoite piilotettu", - "account.edit_profile": "Muokkaa", + "account.edit_profile": "Muokkaa profiilia", "account.endorse": "Suosittele profiilissasi", "account.follow": "Seuraa", - "account.followers": "Seuraajia", + "account.followers": "Seuraajaa", "account.followers.empty": "Tällä käyttäjällä ei ole vielä seuraajia.", "account.follows": "Seuraa", "account.follows.empty": "Tämä käyttäjä ei vielä seuraa ketään.", "account.follows_you": "Seuraa sinua", "account.hide_reblogs": "Piilota buustaukset käyttäjältä @{name}", + "account.last_status": "Aktiivinen viimeksi", "account.link_verified_on": "Tämän linkin omistaja tarkistettiin {date}", "account.locked_info": "Tämän tili on yksityinen. Käyttäjä vahvistaa itse kuka voi seurata häntä.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Mykistä @{name}", "account.mute_notifications": "Mykistä ilmoitukset käyttäjältä @{name}", "account.muted": "Mykistetty", + "account.never_active": "Ei koskaan", "account.posts": "Tuuttaukset", "account.posts_with_replies": "Tuuttaukset ja vastaukset", "account.report": "Raportoi @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Lakkaa seuraamasta", "account.unmute": "Poista käyttäjän @{name} mykistys", "account.unmute_notifications": "Poista mykistys käyttäjän @{name} ilmoituksilta", + "alert.rate_limited.message": "Yritä uudestaan {retry_time, time, medium} jälkeen.", + "alert.rate_limited.title": "Määrää rajoitettu", "alert.unexpected.message": "Tapahtui odottamaton virhe.", "alert.unexpected.title": "Hups!", + "autosuggest_hashtag.per_week": "{count} viikossa", "boost_modal.combo": "Ensi kerralla voit ohittaa tämän painamalla {combo}", "bundle_column_error.body": "Jokin meni vikaan komponenttia ladattaessa.", "bundle_column_error.retry": "Yritä uudestaan", @@ -47,6 +53,7 @@ "column.blocks": "Estetyt käyttäjät", "column.community": "Paikallinen aikajana", "column.direct": "Viestit", + "column.directory": "Selaa profiileja", "column.domain_blocks": "Piilotetut verkkotunnukset", "column.favourites": "Suosikit", "column.follow_requests": "Seuraamispyynnöt", @@ -71,20 +78,20 @@ "compose_form.lock_disclaimer": "Tilisi ei ole {locked}. Kuka tahansa voi seurata tiliäsi ja nähdä vain seuraajille rajaamasi julkaisut.", "compose_form.lock_disclaimer.lock": "lukittu", "compose_form.placeholder": "Mitä mietit?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Lisää valinta", + "compose_form.poll.duration": "Äänestyksen kesto", + "compose_form.poll.option_placeholder": "Valinta numero", + "compose_form.poll.remove_option": "Poista tämä valinta", "compose_form.publish": "Tuuttaa", - "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.publish_loud": "Julkista!", + "compose_form.sensitive.hide": "Valitse tämä arkaluontoisena", "compose_form.sensitive.marked": "Media on merkitty arkaluontoiseksi", "compose_form.sensitive.unmarked": "Mediaa ei ole merkitty arkaluontoiseksi", "compose_form.spoiler.marked": "Teksti on piilotettu varoituksen taakse", "compose_form.spoiler.unmarked": "Teksti ei ole piilotettu", "compose_form.spoiler_placeholder": "Sisältövaroitus", "confirmation_modal.cancel": "Peruuta", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Estä ja raportoi", "confirmations.block.confirm": "Estä", "confirmations.block.message": "Haluatko varmasti estää käyttäjän {name}?", "confirmations.delete.confirm": "Poista", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Haluatko varmasti poistaa tämän listan kokonaan?", "confirmations.domain_block.confirm": "Piilota koko verkko-osoite", "confirmations.domain_block.message": "Haluatko aivan varmasti estää koko verkko-osoitteen {domain}? Useimmiten jokunen kohdistettu esto ja mykistys riittää, ja se on suositeltavampi tapa toimia.", + "confirmations.logout.confirm": "Kirjaudu ulos", + "confirmations.logout.message": "Oletko varma, että haluat kirjautua ulos?", "confirmations.mute.confirm": "Mykistä", + "confirmations.mute.explanation": "Tämä piilottaa päivitykset heiltä ja päivitykset, joissa hänet mainitaan, mutta sallii silti heidän nähdä sinun päivityksesi ja seurata sinua.", "confirmations.mute.message": "Haluatko varmasti mykistää käyttäjän {name}?", "confirmations.redraft.confirm": "Poista & palauta muokattavaksi", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Jos vastaat nyt, vastaus korvaa tällä hetkellä työstämäsi viestin. Oletko varma, että haluat jatkaa?", "confirmations.unfollow.confirm": "Lakkaa seuraamasta", "confirmations.unfollow.message": "Haluatko varmasti lakata seuraamasta käyttäjää {name}?", + "conversation.delete": "Poista keskustelu", + "conversation.mark_as_read": "Merkitse luetuksi", + "conversation.open": "Näytä keskustelu", + "conversation.with": "{names} kanssa", + "directory.federated": "Koko tunnettu fediverse", + "directory.local": "Vain palvelimelta {domain}", + "directory.new_arrivals": "Äskettäin saapuneet", + "directory.recently_active": "Hiljattain aktiiviset", "embed.instructions": "Upota statuspäivitys sivullesi kopioimalla alla oleva koodi.", "embed.preview": "Se tulee näyttämään tältä:", "emoji_button.activity": "Aktiviteetit", @@ -118,7 +136,7 @@ "emoji_button.symbols": "Symbolit", "emoji_button.travel": "Matkailu", "empty_column.account_timeline": "Ei ole 'toots' täällä!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "Profiilia ei löydy", "empty_column.blocks": "Et ole vielä estänyt yhtään käyttäjää.", "empty_column.community": "Paikallinen aikajana on tyhjä. Homma lähtee käyntiin, kun kirjoitat jotain julkista!", "empty_column.direct": "Sinulla ei ole vielä yhtään viestiä yksittäiselle käyttäjälle. Kun lähetät tai vastaanotat sellaisen, se näkyy täällä.", @@ -134,11 +152,15 @@ "empty_column.mutes": "Et ole mykistänyt vielä yhtään käyttäjää.", "empty_column.notifications": "Sinulle ei ole vielä ilmoituksia. Aloita keskustelu juttelemalla muille.", "empty_column.public": "Täällä ei ole mitään! Saat sisältöä, kun kirjoitat jotain julkisesti tai käyt seuraamassa muiden instanssien käyttäjiä", + "error.unexpected_crash.explanation": "Sivua ei voi näyttää oikein, johtuen bugista tai ongelmasta selaimen yhteensopivuudessa.", + "error.unexpected_crash.next_steps": "Kokeile päivittää sivu. Jos tämä ei auta, saatat yhä pystyä käyttämään Mastodonia toisen selaimen tai sovelluksen kautta.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Ilmoita ongelmasta", "follow_request.authorize": "Valtuuta", "follow_request.reject": "Hylkää", "getting_started.developers": "Kehittäjille", "getting_started.directory": "Profiili hakemisto", - "getting_started.documentation": "Documentation", + "getting_started.documentation": "Documentaatio", "getting_started.heading": "Aloitus", "getting_started.invite": "Kutsu ihmisiä", "getting_started.open_source_notice": "Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHubissa: {github}.", @@ -147,8 +169,8 @@ "hashtag.column_header.tag_mode.all": "ja {additional}", "hashtag.column_header.tag_mode.any": "tai {additional}", "hashtag.column_header.tag_mode.none": "ilman {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.select.no_options_message": "Ehdostuta ei löydetty", + "hashtag.column_settings.select.placeholder": "Laita häshtägejä…", "hashtag.column_settings.tag_mode.all": "Kaikki", "hashtag.column_settings.tag_mode.any": "Kaikki", "hashtag.column_settings.tag_mode.none": "Ei mikään", @@ -156,26 +178,26 @@ "home.column_settings.basic": "Perusasetukset", "home.column_settings.show_reblogs": "Näytä buustaukset", "home.column_settings.show_replies": "Näytä vastaukset", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "Päivä päiviä", + "intervals.full.hours": "Tunti tunteja", + "intervals.full.minutes": "Minuuti minuuteja", "introduction.federation.action": "Seuraava", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "introduction.federation.federated.headline": "Federaatioitettu", + "introduction.federation.federated.text": "Julkisia viestejä muiden serverien that is not a word aikoo tulla federoituun aikajanaan.", + "introduction.federation.home.headline": "Koti", + "introduction.federation.home.text": "Viestit muilta pelaajilta jota seuraat aikovat tulla koti sivuusi. Voit seurata ketä vain missä vain serverillä!", + "introduction.federation.local.headline": "Paikallinen", + "introduction.federation.local.text": "Julkiset viestit muilta pelaajilta samalla serverillä tulevat sinun paikalliseen aikajanaan.", + "introduction.interactions.action": "Suorita harjoitus!", + "introduction.interactions.favourite.headline": "Lempi", + "introduction.interactions.favourite.text": "Toot is not a word.", + "introduction.interactions.reblog.headline": "Nopeutus", + "introduction.interactions.reblog.text": "Toot is not a word", + "introduction.interactions.reply.headline": "Vastaa", + "introduction.interactions.reply.text": "TOOT IS NOT A WORD", + "introduction.welcome.action": "Mennään!", + "introduction.welcome.headline": "Ensimmäiset askeleet", + "introduction.welcome.text": "Tervetuloa fediverseen! Muutaman hetken kuluttua voit lähettää viestejä ja jutella ystävillesi useiden palvelinten halki. Mutta tämä palvelin {domain}, on erityinen — sinun profiilisi sijaitsee sillä, joten muista sen nimi.", "keyboard_shortcuts.back": "liiku taaksepäin", "keyboard_shortcuts.blocked": "avaa lista estetyistä käyttäjistä", "keyboard_shortcuts.boost": "buustaa", @@ -204,83 +226,85 @@ "keyboard_shortcuts.search": "siirry hakukenttään", "keyboard_shortcuts.start": "avaa \"Aloitus\" -sarake", "keyboard_shortcuts.toggle_hidden": "näytä/piilota sisältövaroituksella merkitty teksti", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "näytä/piilota media", "keyboard_shortcuts.toot": "ala kirjoittaa uutta tuuttausta", "keyboard_shortcuts.unfocus": "siirry pois tekstikentästä tai hakukentästä", "keyboard_shortcuts.up": "siirry listassa ylöspäin", "lightbox.close": "Sulje", "lightbox.next": "Seuraava", "lightbox.previous": "Edellinen", - "lightbox.view_context": "View context", + "lightbox.view_context": "Näytä kontekstissa", "lists.account.add": "Lisää listaan", "lists.account.remove": "Poista listasta", "lists.delete": "Poista lista", "lists.edit": "Muokkaa listaa", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Vaihda otsikko", "lists.new.create": "Lisää lista", "lists.new.title_placeholder": "Uuden listan nimi", "lists.search": "Etsi seuraamistasi henkilöistä", "lists.subheading": "Omat listat", + "load_pending": "{count, plural, one {# uusi kappale} other {# uutta kappaletta}}", "loading_indicator.label": "Ladataan...", "media_gallery.toggle_visible": "Säädä näkyvyyttä", "missing_indicator.label": "Ei löytynyt", "missing_indicator.sublabel": "Tätä resurssia ei löytynyt", "mute_modal.hide_notifications": "Piilota tältä käyttäjältä tulevat ilmoitukset?", - "navigation_bar.apps": "Mobiiliapplikaatiot", + "navigation_bar.apps": "Mobiilisovellukset", "navigation_bar.blocks": "Estetyt käyttäjät", "navigation_bar.community_timeline": "Paikallinen aikajana", "navigation_bar.compose": "Kirjoita uusi tuuttaus", "navigation_bar.direct": "Viestit", - "navigation_bar.discover": "Discover", + "navigation_bar.discover": "Löydä uutta", "navigation_bar.domain_blocks": "Piilotetut verkkotunnukset", "navigation_bar.edit_profile": "Muokkaa profiilia", "navigation_bar.favourites": "Suosikit", "navigation_bar.filters": "Mykistetyt sanat", "navigation_bar.follow_requests": "Seuraamispyynnöt", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Seurattavat ja seuraajat", "navigation_bar.info": "Tietoa tästä instanssista", "navigation_bar.keyboard_shortcuts": "Näppäinkomennot", "navigation_bar.lists": "Listat", "navigation_bar.logout": "Kirjaudu ulos", "navigation_bar.mutes": "Mykistetyt käyttäjät", - "navigation_bar.personal": "Personal", + "navigation_bar.personal": "Henkilökohtaiset", "navigation_bar.pins": "Kiinnitetyt tuuttaukset", "navigation_bar.preferences": "Asetukset", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Yleinen aikajana", "navigation_bar.security": "Tunnukset", "notification.favourite": "{name} tykkäsi tilastasi", "notification.follow": "{name} seurasi sinua", "notification.mention": "{name} mainitsi sinut", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "Kysely, johon osallistuit, on päättynyt", "notification.reblog": "{name} buustasi tilaasi", "notifications.clear": "Tyhjennä ilmoitukset", "notifications.clear_confirmation": "Haluatko varmasti poistaa kaikki ilmoitukset pysyvästi?", "notifications.column_settings.alert": "Työpöytäilmoitukset", "notifications.column_settings.favourite": "Tykkäykset:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Näytä kaikki kategoriat", + "notifications.column_settings.filter_bar.category": "Pikasuodatuspalkki", + "notifications.column_settings.filter_bar.show": "Näytä", "notifications.column_settings.follow": "Uudet seuraajat:", "notifications.column_settings.mention": "Maininnat:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Kyselyn tulokset:", "notifications.column_settings.push": "Push-ilmoitukset", "notifications.column_settings.reblog": "Buustit:", "notifications.column_settings.show": "Näytä sarakkeessa", "notifications.column_settings.sound": "Äänimerkki", - "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.group": "{count} notifications", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "notifications.filter.all": "Kaikki", + "notifications.filter.boosts": "Buustit", + "notifications.filter.favourites": "Suosikit", + "notifications.filter.follows": "Seuraa", + "notifications.filter.mentions": "Maininnat", + "notifications.filter.polls": "Kyselyn tulokset", + "notifications.group": "{count} ilmoitusta", + "poll.closed": "Suljettu", + "poll.refresh": "Päivitä", + "poll.total_people": "{count, plural, one {# henkilö} other {# henkilöä}}", + "poll.total_votes": "{count, plural, one {# ääni} other {# ääntä}}", + "poll.vote": "Äänestä", + "poll.voted": "Äänestit tätä vastausta", + "poll_button.add_poll": "Lisää kysely", + "poll_button.remove_poll": "Poista kysely", "privacy.change": "Säädä tuuttauksen näkyvyyttä", "privacy.direct.long": "Julkaise vain mainituille käyttäjille", "privacy.direct.short": "Suora viesti", @@ -290,6 +314,7 @@ "privacy.public.short": "Julkinen", "privacy.unlisted.long": "Älä julkaise julkisilla aikajanoilla", "privacy.unlisted.short": "Listaamaton julkinen", + "refresh": "Päivitä", "regeneration_indicator.label": "Ladataan…", "regeneration_indicator.sublabel": "Kotinäkymääsi valmistellaan!", "relative_time.days": "{number} pv", @@ -307,22 +332,23 @@ "search.placeholder": "Hae", "search_popout.search_format": "Tarkennettu haku", "search_popout.tips.full_text": "Tekstihaku palauttaa tilapäivitykset, jotka olet kirjoittanut, lisännyt suosikkeihisi, boostannut tai joissa sinut mainitaan, sekä tekstin sisältävät käyttäjänimet, nimimerkit ja hastagit.", - "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.hashtag": "hashtagit", "search_popout.tips.status": "tila", "search_popout.tips.text": "Tekstihaku palauttaa hakua vastaavat nimimerkit, käyttäjänimet ja hastagit", "search_popout.tips.user": "käyttäjä", "search_results.accounts": "Ihmiset", "search_results.hashtags": "Hashtagit", "search_results.statuses": "Tuuttaukset", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", + "search_results.statuses_fts_disabled": "Tuuttausten haku sisällön perusteella ei ole käytössä tällä Mastodon-serverillä.", + "search_results.total": "{count, number} {count, plural, one {tulos} other {tulosta}}", + "status.admin_account": "Avaa moderaattorinäkymä tilistä @{name}", + "status.admin_status": "Avaa tilapäivitys moderaattorinäkymässä", "status.block": "Estä @{name}", "status.cancel_reblog_private": "Peru buustaus", "status.cannot_reblog": "Tätä julkaisua ei voi buustata", - "status.copy": "Copy link to status", + "status.copy": "Kopioi linkki tilapäivitykseen", "status.delete": "Poista", - "status.detailed_status": "Detailed conversation view", + "status.detailed_status": "Yksityiskohtainen keskustelunäkymä", "status.direct": "Viesti käyttäjälle @{name}", "status.embed": "Upota", "status.favourite": "Tykkää", @@ -340,7 +366,7 @@ "status.reblog": "Buustaa", "status.reblog_private": "Buustaa alkuperäiselle yleisölle", "status.reblogged_by": "{name} buustasi", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", + "status.reblogs.empty": "Kukaan ei ole vielä buustannut tätä tuuttausta. Kun joku tekee niin, näkyy kyseinen henkilö tässä.", "status.redraft": "Poista & palauta muokattavaksi", "status.reply": "Vastaa", "status.replyAll": "Vastaa ketjuun", @@ -351,30 +377,39 @@ "status.show_less_all": "Näytä vähemmän kaikista", "status.show_more": "Näytä lisää", "status.show_more_all": "Näytä lisää kaikista", - "status.show_thread": "Show thread", + "status.show_thread": "Näytä ketju", + "status.uncached_media_warning": "Ei saatavilla", "status.unmute_conversation": "Poista keskustelun mykistys", "status.unpin": "Irrota profiilista", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Hylkää ehdotus", + "suggestions.header": "Saatat olla kiinnostunut myös…", "tabs_bar.federated_timeline": "Yleinen", "tabs_bar.home": "Koti", "tabs_bar.local_timeline": "Paikallinen", "tabs_bar.notifications": "Ilmoitukset", "tabs_bar.search": "Hae", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "time_remaining.days": "{number, plural, one {# päivä} other {# päivää}} jäljellä", + "time_remaining.hours": "{number, plural, one {# tunti} other {# tuntia}} jäljellä", + "time_remaining.minutes": "{number, plural, one {# minuutti} other {# minuuttia}} jäljellä", + "time_remaining.moments": "Hetki jäljellä", + "time_remaining.seconds": "{number, plural, one {# sekunti} other {# sekuntia}} jäljellä", + "trends.count_by_accounts": "{count} {rawCount, plural, one {henkilö} other {henkilöä}} keskustelee", + "trends.trending_now": "Suosittua nyt", "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.", "upload_area.title": "Lataa raahaamalla ja pudottamalla tähän", "upload_button.label": "Lisää mediaa", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.limit": "Tiedostolatauksien raja ylitetty.", + "upload_error.poll": "Tiedon lataaminen ei ole sallittua kyselyissä.", "upload_form.description": "Anna kuvaus näkörajoitteisia varten", - "upload_form.focus": "Rajaa", + "upload_form.edit": "Muokkaa", "upload_form.undo": "Peru", + "upload_modal.analyzing_picture": "Analysoidaan kuvaa…", + "upload_modal.apply": "Käytä", + "upload_modal.description_placeholder": "Eräänä jäätävänä ja pimeänä yönä gorilla ratkaisi sudokun kahdessa minuutissa", + "upload_modal.detect_text": "Tunnista teksti kuvasta", + "upload_modal.edit_media": "Muokkaa mediaa", + "upload_modal.hint": "Klikkaa tai vedä ympyrä esikatselussa valitaksesi keskipiste, joka näkyy aina pienoiskuvissa.", + "upload_modal.preview_label": "Esikatselu ({ratio})", "upload_progress.label": "Ladataan...", "video.close": "Sulje video", "video.exit_fullscreen": "Poistu koko näytön tilasta", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 06bb70e02..a92f23415 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -3,11 +3,12 @@ "account.badges.bot": "Robot", "account.block": "Bloquer @{name}", "account.block_domain": "Tout masquer venant de {domain}", - "account.blocked": "Bloqué", + "account.blocked": "Bloqué·e", + "account.cancel_follow_request": "Annuler la demande de suivi", "account.direct": "Envoyer un message direct à @{name}", "account.domain_blocked": "Domaine caché", "account.edit_profile": "Modifier le profil", - "account.endorse": "Figure sur le profil", + "account.endorse": "Recommander sur le profil", "account.follow": "Suivre", "account.followers": "Abonné⋅e⋅s", "account.followers.empty": "Personne ne suit cet utilisateur·rice pour l’instant.", @@ -15,6 +16,7 @@ "account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.", "account.follows_you": "Vous suit", "account.hide_reblogs": "Masquer les partages de @{name}", + "account.last_status": "Dernière activité", "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}", "account.locked_info": "Ce compte est verrouillé. Son propriétaire approuve manuellement qui peut le ou la suivre.", "account.media": "Média", @@ -22,7 +24,8 @@ "account.moved_to": "{name} a déménagé vers :", "account.mute": "Masquer @{name}", "account.mute_notifications": "Ignorer les notifications de @{name}", - "account.muted": "Silencé", + "account.muted": "Silencé·e", + "account.never_active": "Jamais", "account.posts": "Pouets", "account.posts_with_replies": "Pouets et réponses", "account.report": "Signaler @{name}", @@ -31,13 +34,16 @@ "account.show_reblogs": "Afficher les partages de @{name}", "account.unblock": "Débloquer @{name}", "account.unblock_domain": "Ne plus masquer {domain}", - "account.unendorse": "Ne figure pas sur le profil", + "account.unendorse": "Ne plus recommander sur le profil", "account.unfollow": "Ne plus suivre", "account.unmute": "Ne plus masquer @{name}", "account.unmute_notifications": "Réactiver les notifications de @{name}", + "alert.rate_limited.message": "Veuillez réessayer après {retry_time, time, medium}.", + "alert.rate_limited.title": "Débit limité", "alert.unexpected.message": "Une erreur inattendue s’est produite.", "alert.unexpected.title": "Oups !", - "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois", + "autosuggest_hashtag.per_week": "{count} par semaine", + "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci, la prochaine fois", "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.", "bundle_column_error.retry": "Réessayer", "bundle_column_error.title": "Erreur réseau", @@ -46,10 +52,11 @@ "bundle_modal_error.retry": "Réessayer", "column.blocks": "Comptes bloqués", "column.community": "Fil public local", - "column.direct": "Messages directs", + "column.direct": "Messages privés", + "column.directory": "Parcourir les profils", "column.domain_blocks": "Domaines cachés", "column.favourites": "Favoris", - "column.follow_requests": "Demandes de suivi", + "column.follow_requests": "Demandes d'abonnement", "column.home": "Accueil", "column.lists": "Listes", "column.mutes": "Comptes masqués", @@ -62,12 +69,12 @@ "column_header.moveRight_settings": "Déplacer la colonne vers la droite", "column_header.pin": "Épingler", "column_header.show_settings": "Afficher les paramètres", - "column_header.unpin": "Retirer", + "column_header.unpin": "Désépingler", "column_subheading.settings": "Paramètres", "community.column_settings.media_only": "Média uniquement", "compose_form.direct_message_warning": "Ce pouet sera uniquement envoyé aux personnes mentionnées. Cependant, l’administration de votre instance et des instances réceptrices pourront inspecter ce message.", "compose_form.direct_message_warning_learn_more": "En savoir plus", - "compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur \"non listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par hashtag.", + "compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur « non listé ». Seuls les pouets avec une visibilité « publique » peuvent être recherchés par hashtag.", "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.", "compose_form.lock_disclaimer.lock": "verrouillé", "compose_form.placeholder": "Qu’avez-vous en tête ?", @@ -79,7 +86,7 @@ "compose_form.publish_loud": "{publish} !", "compose_form.sensitive.hide": "Marquer le média comme sensible", "compose_form.sensitive.marked": "Média marqué comme sensible", - "compose_form.sensitive.unmarked": "Média non marqué comme sensible", + "compose_form.sensitive.unmarked": "Le média n’est pas marqué comme sensible", "compose_form.spoiler.marked": "Le texte est caché derrière un avertissement", "compose_form.spoiler.unmarked": "Le texte n’est pas caché", "compose_form.spoiler_placeholder": "Écrivez ici votre avertissement", @@ -93,14 +100,25 @@ "confirmations.delete_list.message": "Êtes-vous sûr·e de vouloir supprimer définitivement cette liste ?", "confirmations.domain_block.confirm": "Masquer le domaine entier", "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables. Vous ne verrez plus de contenu provenant de ce domaine, ni dans fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s.", + "confirmations.logout.confirm": "Déconnexion", + "confirmations.logout.message": "Êtes-vous sûr·e de vouloir vous déconnecter ?", "confirmations.mute.confirm": "Masquer", - "confirmations.mute.message": "Confirmez-vous le masquage de {name} ?", + "confirmations.mute.explanation": "Cela masquera ses messages et les messages le ou la mentionnant, mais cela lui permettra quand même de voir vos messages et de vous suivre.", + "confirmations.mute.message": "Êtes-vous sûr·e de vouloir masquer {name} ?", "confirmations.redraft.confirm": "Effacer et ré-écrire", "confirmations.redraft.message": "Êtes-vous sûr·e de vouloir effacer ce statut pour le ré-écrire ? Ses partages ainsi que ses mises en favori seront perdu·e·s et ses réponses seront orphelines.", "confirmations.reply.confirm": "Répondre", - "confirmations.reply.message": "Répondre maintenant écrasera le message que vous êtes en train de composer. Voulez-vous vraiment continuer ?", + "confirmations.reply.message": "Répondre maintenant écrasera le message que vous composez actuellement. Êtes-vous sûr·e de vouloir continuer ?", "confirmations.unfollow.confirm": "Ne plus suivre", - "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?", + "confirmations.unfollow.message": "Êtes-vous sûr·e de vouloir arrêter de suivre {name} ?", + "conversation.delete": "Supprimer la conversation", + "conversation.mark_as_read": "Marquer comme lu", + "conversation.open": "Afficher la conversation", + "conversation.with": "Avec {names}", + "directory.federated": "Du fédiverse connu", + "directory.local": "De {domain} seulement", + "directory.new_arrivals": "Nouveaux·elles arrivant·e·s", + "directory.recently_active": "Récemment actif·ve·s", "embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.", "embed.preview": "Il apparaîtra comme cela :", "emoji_button.activity": "Activités", @@ -111,7 +129,7 @@ "emoji_button.nature": "Nature", "emoji_button.not_found": "Pas d’émoji !! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objets", - "emoji_button.people": "Personnages", + "emoji_button.people": "Personnes", "emoji_button.recent": "Fréquemment utilisés", "emoji_button.search": "Recherche…", "emoji_button.search_results": "Résultats de la recherche", @@ -131,9 +149,13 @@ "empty_column.home.public_timeline": "le fil public", "empty_column.list": "Il n’y a rien dans cette liste pour l’instant. Dès que des personnes de cette liste publieront de nouveaux statuts, ils apparaîtront ici.", "empty_column.lists": "Vous n’avez pas encore de liste. Lorsque vous en créerez une, elle apparaîtra ici.", - "empty_column.mutes": "Vous n’avez pas encore mis d'utilisateur·rice·s en silence.", + "empty_column.mutes": "Vous n’avez pas encore silencié d’utilisateur·rice·s.", "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres personnes pour débuter la conversation.", - "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des personnes d’autres instances pour remplir le fil public", + "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des personnes d’autres serveurs pour remplir le fil public", + "error.unexpected_crash.explanation": "En raison d’un bug dans notre code ou d’un problème de compatibilité avec votre navigateur, cette page n’a pas pu être affichée correctement.", + "error.unexpected_crash.next_steps": "Essayez de rafraîchir la page. Si cela n’aide pas, vous pouvez toujours utiliser Mastodon via un autre navigateur ou une application native.", + "errors.unexpected_crash.copy_stacktrace": "Copier la trace d'appels dans le presse-papier", + "errors.unexpected_crash.report_issue": "Signaler le problème", "follow_request.authorize": "Accepter", "follow_request.reject": "Rejeter", "getting_started.developers": "Développeur·euse·s", @@ -141,7 +163,7 @@ "getting_started.documentation": "Documentation", "getting_started.heading": "Pour commencer", "getting_started.invite": "Inviter des gens", - "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", + "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer ou faire des rapports de bogues via {github} sur GitHub.", "getting_started.security": "Sécurité", "getting_started.terms": "Conditions d’utilisation", "hashtag.column_header.tag_mode.all": "et {additional}", @@ -152,8 +174,8 @@ "hashtag.column_settings.tag_mode.all": "Tous ces éléments", "hashtag.column_settings.tag_mode.any": "Au moins un de ces éléments", "hashtag.column_settings.tag_mode.none": "Aucun de ces éléments", - "hashtag.column_settings.tag_toggle": "Inclure des tags additionnels dans cette colonne", - "home.column_settings.basic": "Basique", + "hashtag.column_settings.tag_toggle": "Inclure des mots-clés additionnels dans cette colonne", + "home.column_settings.basic": "Base", "home.column_settings.show_reblogs": "Afficher les partages", "home.column_settings.show_replies": "Afficher les réponses", "intervals.full.days": "{number, plural, one {# jour} other {# jours}}", @@ -168,7 +190,7 @@ "introduction.federation.local.text": "Les messages publics de personnes se trouvant sur le même serveur que vous apparaîtront sur le fil public local.", "introduction.interactions.action": "Finir le tutoriel !", "introduction.interactions.favourite.headline": "Favoris", - "introduction.interactions.favourite.text": "Vous pouvez garder un pouet pour plus tard, et faire savoir à son auteur·ice que vous l'avez aimé, en le favorisant.", + "introduction.interactions.favourite.text": "Vous pouvez garder un pouet pour plus tard et faire savoir à son auteur·ice que vous l’avez aimé, en l'ajoutant aux favoris.", "introduction.interactions.reblog.headline": "Repartager", "introduction.interactions.reblog.text": "Vous pouvez partager les pouets d'autres personnes avec vos abonné·e·s en les repartageant.", "introduction.interactions.reply.headline": "Répondre", @@ -177,16 +199,16 @@ "introduction.welcome.headline": "Premiers pas", "introduction.welcome.text": "Bienvenue dans le fediverse ! Dans quelques instants, vous pourrez diffuser des messages et parler à vos amis sur une grande variété de serveurs. Mais ce serveur, {domain}, est spécial - il héberge votre profil, alors souvenez-vous de son nom.", "keyboard_shortcuts.back": "pour revenir en arrière", - "keyboard_shortcuts.blocked": "pour ouvrir une liste d’utilisateur·rice·s bloqué·e·s", + "keyboard_shortcuts.blocked": "pour ouvrir la liste des comptes bloqués", "keyboard_shortcuts.boost": "pour partager", "keyboard_shortcuts.column": "pour focaliser un statut dans l’une des colonnes", - "keyboard_shortcuts.compose": "pour centrer la zone de rédaction", + "keyboard_shortcuts.compose": "pour focaliser la zone de rédaction", "keyboard_shortcuts.description": "Description", - "keyboard_shortcuts.direct": "pour ouvrir une colonne des messages directs", + "keyboard_shortcuts.direct": "pour ouvrir la colonne des messages directs", "keyboard_shortcuts.down": "pour descendre dans la liste", "keyboard_shortcuts.enter": "pour ouvrir le statut", "keyboard_shortcuts.favourite": "pour ajouter aux favoris", - "keyboard_shortcuts.favourites": "pour ouvrir une liste de favoris", + "keyboard_shortcuts.favourites": "pour ouvrir la liste des pouets favoris", "keyboard_shortcuts.federated": "pour ouvrir le fil public global", "keyboard_shortcuts.heading": "Raccourcis clavier", "keyboard_shortcuts.home": "pour ouvrir l’accueil", @@ -194,7 +216,7 @@ "keyboard_shortcuts.legend": "pour afficher cette légende", "keyboard_shortcuts.local": "pour ouvrir le fil public local", "keyboard_shortcuts.mention": "pour mentionner l’auteur·rice", - "keyboard_shortcuts.muted": "pour ouvrir la liste des utilisateur·rice·s rendu·e·s muet·te·s", + "keyboard_shortcuts.muted": "pour ouvrir la liste des utilisateur·rice·s muté·e·s", "keyboard_shortcuts.my_profile": "pour ouvrir votre profil", "keyboard_shortcuts.notifications": "pour ouvrir votre colonne de notifications", "keyboard_shortcuts.pinned": "pour ouvrir une liste des pouets épinglés", @@ -202,7 +224,7 @@ "keyboard_shortcuts.reply": "pour répondre", "keyboard_shortcuts.requests": "pour ouvrir la liste de demandes de suivi", "keyboard_shortcuts.search": "pour cibler la recherche", - "keyboard_shortcuts.start": "pour ouvrir la colonne \"pour commencer\"", + "keyboard_shortcuts.start": "pour ouvrir la colonne « pour commencer »", "keyboard_shortcuts.toggle_hidden": "pour afficher/cacher un texte derrière CW", "keyboard_shortcuts.toggle_sensitivity": "pour afficher/cacher les médias", "keyboard_shortcuts.toot": "pour démarrer un tout nouveau pouet", @@ -214,13 +236,14 @@ "lightbox.view_context": "Voir le contexte", "lists.account.add": "Ajouter à la liste", "lists.account.remove": "Supprimer de la liste", - "lists.delete": "Effacer la liste", + "lists.delete": "Supprimer la liste", "lists.edit": "Éditer la liste", - "lists.edit.submit": "Changer le titre", + "lists.edit.submit": "Modifier le titre", "lists.new.create": "Ajouter une liste", "lists.new.title_placeholder": "Titre de la nouvelle liste", "lists.search": "Rechercher parmi les gens que vous suivez", "lists.subheading": "Vos listes", + "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}", "loading_indicator.label": "Chargement…", "media_gallery.toggle_visible": "Modifier la visibilité", "missing_indicator.label": "Non trouvé", @@ -229,7 +252,7 @@ "navigation_bar.apps": "Applications mobiles", "navigation_bar.blocks": "Comptes bloqués", "navigation_bar.community_timeline": "Fil public local", - "navigation_bar.compose": "Rédiger un nouveau toot", + "navigation_bar.compose": "Rédiger un nouveau pouet", "navigation_bar.direct": "Messages directs", "navigation_bar.discover": "Découvrir", "navigation_bar.domain_blocks": "Domaines cachés", @@ -238,7 +261,7 @@ "navigation_bar.filters": "Mots silenciés", "navigation_bar.follow_requests": "Demandes de suivi", "navigation_bar.follows_and_followers": "Abonnements et abonné⋅e·s", - "navigation_bar.info": "Plus d’informations", + "navigation_bar.info": "À propos de ce serveur", "navigation_bar.keyboard_shortcuts": "Raccourcis clavier", "navigation_bar.lists": "Listes", "navigation_bar.logout": "Déconnexion", @@ -246,12 +269,11 @@ "navigation_bar.personal": "Personnel", "navigation_bar.pins": "Pouets épinglés", "navigation_bar.preferences": "Préférences", - "navigation_bar.profile_directory": "Annuaire des profils", "navigation_bar.public_timeline": "Fil public global", "navigation_bar.security": "Sécurité", "notification.favourite": "{name} a ajouté à ses favoris :", "notification.follow": "{name} vous suit", - "notification.mention": "{name} vous a mentionné :", + "notification.mention": "{name} vous a mentionné·e :", "notification.poll": "Un sondage auquel vous avez participé vient de se terminer", "notification.reblog": "{name} a partagé votre statut :", "notifications.clear": "Nettoyer les notifications", @@ -263,8 +285,8 @@ "notifications.column_settings.filter_bar.show": "Afficher", "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e·s :", "notifications.column_settings.mention": "Mentions :", - "notifications.column_settings.poll": "Résultats du sondage :", - "notifications.column_settings.push": "Notifications", + "notifications.column_settings.poll": "Résultats des sondage :", + "notifications.column_settings.push": "Notifications push", "notifications.column_settings.reblog": "Partages :", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Émettre un son", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Fermé", "poll.refresh": "Actualiser", + "poll.total_people": "{count, plural, one {# personne} other {# personnes}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Voter", + "poll.voted": "Vous avez voté pour cette réponse", "poll_button.add_poll": "Ajouter un sondage", "poll_button.remove_poll": "Supprimer le sondage", "privacy.change": "Ajuster la confidentialité du message", @@ -290,8 +314,9 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Ne pas afficher dans les fils publics", "privacy.unlisted.short": "Non listé", + "refresh": "Actualiser", "regeneration_indicator.label": "Chargement…", - "regeneration_indicator.sublabel": "Le flux de votre page principale est en cours de préparation !", + "regeneration_indicator.sublabel": "Votre fil principal est en cours de préparation !", "relative_time.days": "{number} j", "relative_time.hours": "{number} h", "relative_time.just_now": "à l’instant", @@ -299,14 +324,14 @@ "relative_time.seconds": "{number} s", "reply_indicator.cancel": "Annuler", "report.forward": "Transférer à {target}", - "report.forward_hint": "Le compte provient d’un autre serveur. Envoyez également une copie anonyme du rapport ?", + "report.forward_hint": "Le compte provient d’un autre serveur. Envoyer également une copie anonyme du rapport ?", "report.hint": "Le rapport sera envoyé aux modérateur·rice·s de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous :", "report.placeholder": "Commentaires additionnels", "report.submit": "Envoyer", - "report.target": "Signalement", + "report.target": "Signalement de {target}", "search.placeholder": "Rechercher", "search_popout.search_format": "Recherche avancée", - "search_popout.tips.full_text": "Les textes simples retournent les pouets que vous avez écris, mis en favori, épinglés, ou ayant été mentionnés, ainsi que les identifiants, les noms affichés, et les hashtags des personnes et messages correspondant.", + "search_popout.tips.full_text": "Les textes simples retournent les pouets que vous avez écris, mis en favori, épinglés, ou vous mentionnant, ainsi que les identifiants, les noms affichés, et les hashtags des personnes et messages correspondant.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "statuts", "search_popout.tips.text": "Un texte simple renvoie les noms affichés, les identifiants et les hashtags correspondants", @@ -314,12 +339,13 @@ "search_results.accounts": "Comptes", "search_results.hashtags": "Hashtags", "search_results.statuses": "Pouets", + "search_results.statuses_fts_disabled": "La recherche de pouets par leur contenu n'est pas activée sur ce serveur Mastodon.", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", - "status.admin_account": "Ouvrir l'interface de modération pour @{name}", - "status.admin_status": "Ouvrir ce statut dans l'interface de modération", + "status.admin_account": "Ouvrir l’interface de modération pour @{name}", + "status.admin_status": "Ouvrir ce statut dans l’interface de modération", "status.block": "Bloquer @{name}", "status.cancel_reblog_private": "Dé-booster", - "status.cannot_reblog": "Cette publication ne peut être boostée", + "status.cannot_reblog": "Ce pouet ne peut pas être partagé", "status.copy": "Copier le lien vers le pouet", "status.delete": "Effacer", "status.detailed_status": "Vue détaillée de la conversation", @@ -352,10 +378,11 @@ "status.show_more": "Déplier", "status.show_more_all": "Tout déplier", "status.show_thread": "Lire le fil", + "status.uncached_media_warning": "Indisponible", "status.unmute_conversation": "Ne plus masquer la conversation", "status.unpin": "Retirer du profil", "suggestions.dismiss": "Rejeter la suggestion", - "suggestions.header": "Vous pourriez être intéressé par…", + "suggestions.header": "Vous pourriez être intéressé·e par…", "tabs_bar.federated_timeline": "Fil public global", "tabs_bar.home": "Accueil", "tabs_bar.local_timeline": "Fil public local", @@ -367,14 +394,22 @@ "time_remaining.moments": "Encore quelques instants", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} restantes", "trends.count_by_accounts": "{count} {rawCount, plural, one {personne} other {personnes}} discutent", + "trends.trending_now": "Tendance en ce moment", "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.", "upload_area.title": "Glissez et déposez pour envoyer", - "upload_button.label": "Joindre un média (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Joindre un média ({formats})", "upload_error.limit": "Taille maximale d'envoi de fichier dépassée.", - "upload_error.poll": "L'envoi de fichiers n'est pas autorisé avec les sondages.", + "upload_error.poll": "L’envoi de fichiers n’est pas autorisé avec les sondages.", "upload_form.description": "Décrire pour les malvoyant·e·s", - "upload_form.focus": "Modifier l’aperçu", + "upload_form.edit": "Modifier", "upload_form.undo": "Supprimer", + "upload_modal.analyzing_picture": "Analyse de l’image en cours…", + "upload_modal.apply": "Appliquer", + "upload_modal.description_placeholder": "Buvez de ce whisky que le patron juge fameux", + "upload_modal.detect_text": "Détecter le texte de l’image", + "upload_modal.edit_media": "Modifier le média", + "upload_modal.hint": "Cliquez ou faites glisser le cercle sur l’aperçu pour choisir le point focal qui sera toujours visible sur toutes les miniatures.", + "upload_modal.preview_label": "Aperçu ({ratio})", "upload_progress.label": "Envoi en cours…", "video.close": "Fermer la vidéo", "video.exit_fullscreen": "Quitter le plein écran", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json new file mode 100644 index 000000000..2dd0dbbad --- /dev/null +++ b/app/javascript/mastodon/locales/ga.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.bot": "Bot", + "account.block": "Block @{name}", + "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", + "account.direct": "Direct message @{name}", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Edit profile", + "account.endorse": "Feature on profile", + "account.follow": "Follow", + "account.followers": "Followers", + "account.followers.empty": "No one follows this user yet.", + "account.follows": "Follows", + "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.media": "Media", + "account.mention": "Mention @{name}", + "account.moved_to": "{name} has moved to:", + "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", + "account.never_active": "Never", + "account.posts": "Toots", + "account.posts_with_replies": "Toots and replies", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", + "account.unblock": "Unblock @{name}", + "account.unblock_domain": "Unhide {domain}", + "account.unendorse": "Don't feature on profile", + "account.unfollow": "Unfollow", + "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.direct": "Direct messages", + "column.directory": "Browse profiles", + "column.domain_blocks": "Hidden domains", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.pins": "Pinned toot", + "column.public": "Federated timeline", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", + "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", + "column_header.unpin": "Unpin", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media only", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Remove this 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.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.select.no_options_message": "No suggestions found", + "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "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", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "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.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll.voted": "You voted for this answer", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "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", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 9b19d6f11..0125754b1 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -4,6 +4,7 @@ "account.block": "Bloquear @{name}", "account.block_domain": "Ocultar calquer contido de {domain}", "account.blocked": "Bloqueada", + "account.cancel_follow_request": "Cancelar petición de seguemento", "account.direct": "Mensaxe directa @{name}", "account.domain_blocked": "Dominio agochado", "account.edit_profile": "Editar perfil", @@ -15,6 +16,7 @@ "account.follows.empty": "Esta usuaria aínda non segue a ninguén.", "account.follows_you": "Séguete", "account.hide_reblogs": "Ocultar repeticións de @{name}", + "account.last_status": "Último activo", "account.link_verified_on": "A propiedade de esta ligazón foi comprobada en {date}", "account.locked_info": "O estado da intimidade de esta conta estableceuse en pechado. A persoa dona da conta revisa quen pode seguila.", "account.media": "Medios", @@ -23,6 +25,7 @@ "account.mute": "Acalar @{name}", "account.mute_notifications": "Acalar as notificacións de @{name}", "account.muted": "Acalada", + "account.never_active": "Nunca", "account.posts": "Toots", "account.posts_with_replies": "Toots e respostas", "account.report": "Informar sobre @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Non seguir", "account.unmute": "Non acalar @{name}", "account.unmute_notifications": "Desbloquear as notificacións de @{name}", + "alert.rate_limited.message": "Por favor inténteo tras {retry_time, time, medium}.", + "alert.rate_limited.title": "Taxa limitada", "alert.unexpected.message": "Aconteceu un fallo non agardado.", "alert.unexpected.title": "Vaia!", + "autosuggest_hashtag.per_week": "{count} por semana", "boost_modal.combo": "Pulse {combo} para saltar esto a próxima vez", "bundle_column_error.body": "Houbo un fallo mentras se cargaba este compoñente.", "bundle_column_error.retry": "Inténteo de novo", @@ -47,6 +53,7 @@ "column.blocks": "Usuarias bloqueadas", "column.community": "Liña temporal local", "column.direct": "Mensaxes directas", + "column.directory": "Ver perfiles", "column.domain_blocks": "Dominios agochados", "column.favourites": "Favoritas", "column.follow_requests": "Peticións de seguimento", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Estás seguro de que queres eliminar permanentemente esta lista?", "confirmations.domain_block.confirm": "Agochar un dominio completo", "confirmations.domain_block.message": "Realmente está segura de que quere bloquear por completo o dominio {domain}? Normalmente é suficiente, e preferible, bloquear de xeito selectivo varios elementos. Non verá contidos de ese dominio en ningunha liña temporal ou nas notificacións. As súas seguidoras en ese dominio serán eliminadas.", + "confirmations.logout.confirm": "Desconectar", + "confirmations.logout.message": "Seguro que desexa desconectar?", "confirmations.mute.confirm": "Acalar", + "confirmations.mute.explanation": "Esto ocultará as publicacións delas e as que as mencionen, pero poderán seguir lendo as túas publicacións e seguirte.", "confirmations.mute.message": "Está segura de que quere acalar a {name}?", "confirmations.redraft.confirm": "Eliminar e reescribir", "confirmations.redraft.message": "Está segura de querer eliminar este estado e voltalo a escribir? Perderá réplicas e favoritas, e as respostas ao orixinal quedarán orfas.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Respostando agora sobreescribirá a mensaxe que está a compoñer. Segura de querer proceder?", "confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.message": "Quere deixar de seguir a {name}?", + "conversation.delete": "Eliminar conversa", + "conversation.mark_as_read": "Marcar como lido", + "conversation.open": "Ver conversa", + "conversation.with": "Con {names}", + "directory.federated": "Desde o fediverso coñecido", + "directory.local": "Só desde {domain}", + "directory.new_arrivals": "Novas achegas", + "directory.recently_active": "Activo recentemente", "embed.instructions": "Copie o código inferior para incrustar no seu sitio web este estado.", "embed.preview": "Así será mostrado:", "emoji_button.activity": "Actividade", @@ -134,6 +152,10 @@ "empty_column.mutes": "Non acalou ningunha usuaria polo de agora.", "empty_column.notifications": "Aínda non ten notificacións. Interactúe con outras para iniciar unha conversa.", "empty_column.public": "Nada por aquí! Escriba algo de xeito público, ou siga manualmente usuarias de outros servidores para ir enchéndoa", + "error.unexpected_crash.explanation": "Non se mostra correctamente a páxina debido a un fallo no código ou problema de compatibilidade do navegador.", + "error.unexpected_crash.next_steps": "Intenta actualizar a páxina. Se esto non axuda podes tamén utilizar Mastodon en outro navegador ou app nativa.", + "errors.unexpected_crash.copy_stacktrace": "Copiar trazas ao portaretallos", + "errors.unexpected_crash.report_issue": "Informar de problema", "follow_request.authorize": "Autorizar", "follow_request.reject": "Rexeitar", "getting_started.developers": "Desenvolvedoras", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Novo título da lista", "lists.search": "Procurar entre a xente que segues", "lists.subheading": "As túas listas", + "load_pending": "{count, plural, one {# novo elemento} other {# novos elementos}}", "loading_indicator.label": "Cargando...", "media_gallery.toggle_visible": "Ocultar", "missing_indicator.label": "Non atopado", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Persoal", "navigation_bar.pins": "Mensaxes fixadas", "navigation_bar.preferences": "Preferencias", - "navigation_bar.profile_directory": "Directorio de perfil", "navigation_bar.public_timeline": "Liña temporal federada", "navigation_bar.security": "Seguridade", "notification.favourite": "{name} marcou como favorito o seu estado", @@ -277,8 +299,10 @@ "notifications.group": "{count} notificacións", "poll.closed": "Pechado", "poll.refresh": "Actualizar", + "poll.total_people": "{count, plural,one {# persoa}other {# persoas}}", "poll.total_votes": "{count, plural, one {# voto} outros {# votos}}", "poll.vote": "Votar", + "poll.voted": "Votou por esta opción", "poll_button.add_poll": "Engadir sondaxe", "poll_button.remove_poll": "Eliminar sondaxe", "privacy.change": "Axustar a intimidade do estado", @@ -290,6 +314,7 @@ "privacy.public.short": "Pública", "privacy.unlisted.long": "Non publicar en liñas temporais públicas", "privacy.unlisted.short": "Non listada", + "refresh": "Actualizar", "regeneration_indicator.label": "Cargando…", "regeneration_indicator.sublabel": "Estase a preparar a súa liña temporal de inicio!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "Xente", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Non está activada neste servidor Mastodon a busca de toots polo seu contido.", "search_results.total": "{count, number} {count,plural,one {result} outros {results}}", "status.admin_account": "Abrir interface de moderación para @{name}", "status.admin_status": "Abrir este estado na interface de moderación", @@ -352,6 +378,7 @@ "status.show_more": "Mostrar máis", "status.show_more_all": "Mostrar máis para todas", "status.show_thread": "Mostrar fío", + "status.uncached_media_warning": "Non dispoñible", "status.unmute_conversation": "Non acalar a conversa", "status.unpin": "Despegar do perfil", "suggestions.dismiss": "Rexeitar suxestión", @@ -367,14 +394,22 @@ "time_remaining.moments": "Está rematando", "time_remaining.seconds": "{number, plural, one {# segundo} other {# segundos}} restantes", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} outras {people}} conversando", + "trends.trending_now": "Tendencias actuais", "ui.beforeunload": "O borrador perderase se sae de Mastodon.", "upload_area.title": "Arrastre e solte para subir", - "upload_button.label": "Engadir medios (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Engadir medios ({formats})", "upload_error.limit": "Excedeu o límite de subida de ficheiros.", "upload_error.poll": "Non se poden subir ficheiros nas sondaxes.", "upload_form.description": "Describa para deficientes visuais", - "upload_form.focus": "Cambiar vista previa", + "upload_form.edit": "Editar", "upload_form.undo": "Eliminar", + "upload_modal.analyzing_picture": "Analizando imaxe…", + "upload_modal.apply": "Aplicar", + "upload_modal.description_placeholder": "Un raposo moi feitiño salta sobre o can preguiceiro", + "upload_modal.detect_text": "Detectar texto na imaxe", + "upload_modal.edit_media": "Editar medios", + "upload_modal.hint": "Prema ou arrastre o círculo na vista previa para escolle o punto focal que se verá na vista de todas as miniaturas.", + "upload_modal.preview_label": "Vista previa ({ratio})", "upload_progress.label": "Subindo...", "video.close": "Pechar video", "video.exit_fullscreen": "Saír da pantalla completa", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 248be3c7b..70aec3279 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -1,21 +1,23 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", - "account.badges.bot": "Bot", + "account.add_or_remove_from_list": "הוסף או הסר מהרשימות", + "account.badges.bot": "בוט", "account.block": "חסימת @{name}", "account.block_domain": "להסתיר הכל מהקהילה {domain}", - "account.blocked": "Blocked", + "account.blocked": "חסום", + "account.cancel_follow_request": "בטל בקשת מעקב", "account.direct": "Direct Message @{name}", - "account.domain_blocked": "Domain hidden", + "account.domain_blocked": "הדומיין חסוי", "account.edit_profile": "עריכת פרופיל", - "account.endorse": "Feature on profile", + "account.endorse": "הצג בפרופיל", "account.follow": "מעקב", "account.followers": "עוקבים", - "account.followers.empty": "No one follows this user yet.", + "account.followers.empty": "אף אחד לא עוקב אחר המשתמש הזה עדיין.", "account.follows": "נעקבים", - "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows.empty": "משתמש זה לא עוקב אחר אף אחד עדיין.", "account.follows_you": "במעקב אחריך", "account.hide_reblogs": "להסתיר הידהודים מאת @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.last_status": "פעילות אחרונה", + "account.link_verified_on": "בעלות על הקישור הזה נבדקה לאחרונה ב{date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "מדיה", "account.mention": "אזכור של @{name}", @@ -23,6 +25,7 @@ "account.mute": "להשתיק את @{name}", "account.mute_notifications": "להסתיר התראות מאת @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "הודעות", "account.posts_with_replies": "Toots with replies", "account.report": "לדווח על @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "הפסקת מעקב", "account.unmute": "הפסקת השתקת @{name}", "account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "אירעה שגיאה בלתי צפויה.", "alert.unexpected.title": "אופס!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה", "bundle_column_error.body": "משהו השתבש בעת הצגת הרכיב הזה.", "bundle_column_error.retry": "לנסות שוב", @@ -47,6 +53,7 @@ "column.blocks": "חסימות", "column.community": "ציר זמן מקומי", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "חיבובים", "column.follow_requests": "בקשות מעקב", @@ -64,7 +71,7 @@ "column_header.show_settings": "הצגת העדפות", "column_header.unpin": "שחרור קיבוע", "column_subheading.settings": "אפשרויות", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.domain_block.confirm": "הסתר קהילה שלמה", "confirmations.domain_block.message": "באמת באמת לחסום את כל קהילת {domain}? ברב המקרים השתקות נבחרות של מספר משתמשים מסויימים צריכה להספיק.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "להשתיק", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "להשתיק את {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "להפסיק מעקב", "confirmations.unfollow.message": "להפסיק מעקב אחרי {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "ניתן להטמיע את ההודעה באתרך ע\"י העתקת הקוד שלהלן.", "embed.preview": "דוגמא כיצד זה יראה:", "emoji_button.activity": "פעילות", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב.", "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "קבלה", "follow_request.reject": "דחיה", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "טוען...", "media_gallery.toggle_visible": "נראה\\בלתי נראה", "missing_indicator.label": "לא נמצא", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "חיצרוצים מקובעים", "navigation_bar.preferences": "העדפות", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "ציר זמן בין-קהילתי", "navigation_bar.security": "Security", "notification.favourite": "חצרוצך חובב על ידי {name}", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "שינוי פרטיות ההודעה", @@ -290,6 +314,7 @@ "privacy.public.short": "פומבי", "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים", "privacy.unlisted.short": "לא לפיד הכללי", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "הראה יותר", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "הסרת השתקת שיחה", "status.unpin": "לשחרר מקיבוע באודות", "suggestions.dismiss": "Dismiss suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.", "upload_area.title": "ניתן להעלות על ידי Drag & drop", "upload_button.label": "הוספת מדיה", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "תיאור לכבדי ראיה", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "ביטול", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "עולה...", "video.close": "סגירת וידאו", "video.exit_fullscreen": "יציאה ממסך מלא", diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json index ac58514d4..c50567aac 100644 --- a/app/javascript/mastodon/locales/hi.json +++ b/app/javascript/mastodon/locales/hi.json @@ -1,207 +1,229 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", - "account.badges.bot": "Bot", - "account.block": "Block @{name}", - "account.block_domain": "Hide everything from {domain}", - "account.blocked": "Blocked", - "account.direct": "Direct message @{name}", - "account.domain_blocked": "Domain hidden", - "account.edit_profile": "Edit profile", - "account.endorse": "Feature on profile", - "account.follow": "Follow", - "account.followers": "Followers", - "account.followers.empty": "No one follows this user yet.", - "account.follows": "Follows", - "account.follows.empty": "This user doesn't follow anyone yet.", - "account.follows_you": "Follows you", - "account.hide_reblogs": "Hide boosts from @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", - "account.media": "Media", - "account.mention": "Mention @{name}", - "account.moved_to": "{name} has moved to:", - "account.mute": "Mute @{name}", - "account.mute_notifications": "Mute notifications from @{name}", - "account.muted": "Muted", - "account.posts": "Toots", - "account.posts_with_replies": "Toots and replies", - "account.report": "Report @{name}", - "account.requested": "Awaiting approval. Click to cancel follow request", - "account.share": "Share @{name}'s profile", - "account.show_reblogs": "Show boosts from @{name}", - "account.unblock": "Unblock @{name}", - "account.unblock_domain": "Unhide {domain}", - "account.unendorse": "Don't feature on profile", - "account.unfollow": "Unfollow", - "account.unmute": "Unmute @{name}", - "account.unmute_notifications": "Unmute notifications from @{name}", - "alert.unexpected.message": "An unexpected error occurred.", - "alert.unexpected.title": "Oops!", - "boost_modal.combo": "You can press {combo} to skip this next time", - "bundle_column_error.body": "Something went wrong while loading this component.", - "bundle_column_error.retry": "Try again", - "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", - "column.blocks": "Blocked users", - "column.community": "Local timeline", - "column.direct": "Direct messages", - "column.domain_blocks": "Hidden domains", - "column.favourites": "Favourites", - "column.follow_requests": "Follow requests", - "column.home": "Home", - "column.lists": "Lists", - "column.mutes": "Muted users", - "column.notifications": "Notifications", - "column.pins": "Pinned toot", - "column.public": "Federated timeline", - "column_back_button.label": "Back", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", - "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "account.add_or_remove_from_list": "सूची में जोड़ें या हटाए", + "account.badges.bot": "बॉट", + "account.block": "@{name} को ब्लॉक करें", + "account.block_domain": "{domain} के सारी चीज़े छुपाएं", + "account.blocked": "ब्लॉक", + "account.cancel_follow_request": "फ़ॉलो रिक्वेस्ट रद्द करें", + "account.direct": "प्रत्यक्ष संदेश @{name}", + "account.domain_blocked": "छिपा हुआ डोमेन", + "account.edit_profile": "प्रोफ़ाइल संपादित करें", + "account.endorse": "प्रोफ़ाइल पर दिखाए", + "account.follow": "फॉलो करें", + "account.followers": "फॉलोवर", + "account.followers.empty": "कोई भी इस यूज़र् को फ़ॉलो नहीं करता है", + "account.follows": "फॉलो करें", + "account.follows.empty": "यह यूज़र् अभी तक किसी को फॉलो नहीं करता है।", + "account.follows_you": "आपको फॉलो करता है", + "account.hide_reblogs": "@{name} के बूस्ट छुपाएं", + "account.last_status": "अंतिम सक्रिय", + "account.link_verified_on": "इस लिंक का स्वामित्व {date} को चेक किया गया था", + "account.locked_info": "यह खाता गोपनीयता स्थिति लॉक करने के लिए सेट है। मालिक मैन्युअल रूप से समीक्षा करता है कि कौन उनको फॉलो कर सकता है।", + "account.media": "मीडिया", + "account.mention": "उल्लेख @{name}", + "account.moved_to": "{name} स्थानांतरित हो गया:", + "account.mute": "म्यूट @{name}", + "account.mute_notifications": "@{name} के नोटिफिकेशन म्यूट करे", + "account.muted": "म्यूट है", + "account.never_active": "कभी नहीं दिखे", + "account.posts": "टूट्स", + "account.posts_with_replies": "टूट्स एवं जवाब", + "account.report": "रिपोर्ट @{name}", + "account.requested": "मंजूरी का इंतजार। फॉलो रिक्वेस्ट को रद्द करने के लिए क्लिक करें", + "account.share": "@{name} की प्रोफाइल शेयर करे", + "account.show_reblogs": "@{name} के बूस्ट दिखाए", + "account.unblock": "@{name} को अनब्लॉक करें", + "account.unblock_domain": "{domain} दिखाए", + "account.unendorse": "प्रोफ़ाइल पर न दिखाए", + "account.unfollow": "अनफॉलो करें", + "account.unmute": "अनम्यूट @{name}", + "account.unmute_notifications": "@{name} के नोटिफिकेशन अनम्यूट करे", + "alert.rate_limited.message": "कृप्या {retry_time, time, medium} के बाद दुबारा कोशिश करें", + "alert.rate_limited.title": "सीमित दर", + "alert.unexpected.message": "एक अप्रत्याशित त्रुटि हुई है!", + "alert.unexpected.title": "उफ़!", + "autosuggest_hashtag.per_week": "{count} हर सप्ताह", + "boost_modal.combo": "अगली बार स्किप करने के लिए आप {combo} दबा सकते है", + "bundle_column_error.body": "इस कॉम्पोनेन्ट को लोड करते वक्त कुछ गलत हो गया", + "bundle_column_error.retry": "दुबारा कोशिश करें", + "bundle_column_error.title": "नेटवर्क त्रुटि", + "bundle_modal_error.close": "बंद", + "bundle_modal_error.message": "इस कॉम्पोनेन्ट को लोड करते वक्त कुछ गलत हो गया", + "bundle_modal_error.retry": "दुबारा कोशिश करें", + "column.blocks": "ब्लॉक्ड यूज़र्स", + "column.community": "लोकल टाइम्लाइन", + "column.direct": "सीधा संदेश", + "column.directory": "प्रोफाइल्स खोजें", + "column.domain_blocks": "छुपे डोमेन्स", + "column.favourites": "पसंदीदा", + "column.follow_requests": "फॉलो रिक्वेस्ट्स", + "column.home": "होम", + "column.lists": "सूचियाँ", + "column.mutes": "म्यूट किये हुए यूजर", + "column.notifications": "नोटिफिकेशन्स", + "column.pins": "पिनड टूट्स", + "column.public": "फ़ेडरेटेड टाइम्लाइन", + "column_back_button.label": "पीछे जाए", + "column_header.hide_settings": "सेटिंग्स छुपाए", + "column_header.moveLeft_settings": "कॉलम को बाएं ले जाये", + "column_header.moveRight_settings": "कॉलम को दाएं ले जाये", + "column_header.pin": "पिन", + "column_header.show_settings": "सेटिंग्स दिखाएँ", + "column_header.unpin": "अनपिन", + "column_subheading.settings": "सेटिंग्स", + "community.column_settings.media_only": "सिर्फ़ मीडिया", "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", - "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "compose_form.lock_disclaimer.lock": "locked", + "compose_form.direct_message_warning_learn_more": "और जानें", + "compose_form.hashtag_warning": "यह टूट् किसी भी हैशटैग के तहत सूचीबद्ध नहीं होगा क्योंकि यह अनलिस्टेड है। हैशटैग द्वारा केवल सार्वजनिक टूट्स खोजे जा सकते हैं।", + "compose_form.lock_disclaimer": "आपका खाता {locked} नहीं है। आपको केवल फॉलोवर्स को दिखाई दिए जाने वाले पोस्ट देखने के लिए कोई भी फॉलो कर सकता है।", + "compose_form.lock_disclaimer.lock": "लॉक्ड", "compose_form.placeholder": "What is on your mind?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", - "compose_form.publish": "Toot", + "compose_form.poll.add_option": "विकल्प जोड़े", + "compose_form.poll.duration": "चुनाव की अवधि", + "compose_form.poll.option_placeholder": "कुल विकल्प {number}", + "compose_form.poll.remove_option": "इस विकल्प को हटाएँ", + "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.spoiler.marked": "Text is hidden behind warning", - "compose_form.spoiler.unmarked": "Text is not hidden", - "compose_form.spoiler_placeholder": "Write your warning here", - "confirmation_modal.cancel": "Cancel", - "confirmations.block.block_and_report": "Block & Report", - "confirmations.block.confirm": "Block", - "confirmations.block.message": "Are you sure you want to block {name}?", - "confirmations.delete.confirm": "Delete", - "confirmations.delete.message": "Are you sure you want to delete this status?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", - "confirmations.mute.confirm": "Mute", - "confirmations.mute.message": "Are you sure you want to mute {name}?", - "confirmations.redraft.confirm": "Delete & redraft", - "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "confirmations.unfollow.confirm": "Unfollow", - "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", - "emoji_button.activity": "Activity", - "emoji_button.custom": "Custom", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", - "emoji_button.label": "Insert emoji", - "emoji_button.nature": "Nature", - "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.recent": "Frequently used", - "emoji_button.search": "Search...", - "emoji_button.search_results": "Search results", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.blocks": "You haven't blocked any users yet.", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", - "empty_column.hashtag": "There is nothing in this hashtag yet.", - "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", - "empty_column.home.public_timeline": "the public timeline", - "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "compose_form.sensitive.hide": "मीडिया को संवेदनशील के रूप में चिह्नित करें", + "compose_form.sensitive.marked": "मीडिया संवेदनशील के रूप में चिह्नित है", + "compose_form.sensitive.unmarked": "मीडिया संवेदनशील के रूप में चिह्नित नहीं है", + "compose_form.spoiler.marked": "चेतावनी के पीछे टेक्स्ट छिपा है", + "compose_form.spoiler.unmarked": "टेक्स्ट छिपा नहीं है", + "compose_form.spoiler_placeholder": "अपनी चेतावनी यहाँ लिखें", + "confirmation_modal.cancel": "रद्द करें", + "confirmations.block.block_and_report": "ब्लॉक एवं रिपोर्ट", + "confirmations.block.confirm": "ब्लॉक", + "confirmations.block.message": "क्या आप वाकई {name} को ब्लॉक करना चाहते हैं?", + "confirmations.delete.confirm": "मिटाए", + "confirmations.delete.message": "क्या आप वाकई इस स्टेटस को हटाना चाहते हैं?", + "confirmations.delete_list.confirm": "मिटाए", + "confirmations.delete_list.message": "क्या आप वाकई इस लिस्ट को हमेशा के लिये मिटाना चाहते हैं?", + "confirmations.domain_block.confirm": "संपूर्ण डोमेन छिपाएं", + "confirmations.domain_block.message": "क्या आप वास्तव में, वास्तव में आप पूरे {domain} को ब्लॉक करना चाहते हैं? ज्यादातर मामलों में कुछ लक्षित ब्लॉक या म्यूट पर्याप्त और बेहतर हैं। आप किसी भी सार्वजनिक समय-सीमा या अपनी सूचनाओं में उस डोमेन की सामग्री नहीं देखेंगे। उस डोमेन से आपके फॉलोवर्स को हटा दिया जाएगा।", + "confirmations.logout.confirm": "लॉग आउट करें", + "confirmations.logout.message": "आप सुनिश्चित हैं कि लॉगआउट करना चाहते हैं?", + "confirmations.mute.confirm": "शांत", + "confirmations.mute.explanation": "यह उनसे और पोस्टों का उल्लेख करते हुए उनसे छिपाएगा, लेकिन यह अभी भी उन्हें आपकी पोस्ट देखने और आपको फॉलो करने की अनुमति देगा।", + "confirmations.mute.message": "क्या आप वाकई {name} को शांत करना चाहते हैं?", + "confirmations.redraft.confirm": "मिटायें और पुनःप्रारूपण करें", + "confirmations.redraft.message": "क्या आप वाकई इस स्टेटस को हटाना चाहते हैं और इसे फिर से ड्राफ्ट करना चाहते हैं? पसंदीदा और बूस्ट खो जाएंगे, और मूल पोस्ट के उत्तर अनाथ हो जाएंगे।", + "confirmations.reply.confirm": "उत्तर दें", + "confirmations.reply.message": "अब उत्तर देना उस संदेश को अधिलेखित कर देगा जो आप वर्तमान में बना रहे हैं। क्या आप सुनिश्चित रूप से आगे बढ़ना चाहते हैं?", + "confirmations.unfollow.confirm": "अनफॉलो करें", + "confirmations.unfollow.message": "क्या आप वाकई {name} को अनफॉलो करना चाहते हैं?", + "conversation.delete": "वार्तालाप हटाएँ", + "conversation.mark_as_read": "पढ़ा गया के रूप में चिह्नित करें", + "conversation.open": "वार्तालाप देखें", + "conversation.with": "{names} के साथ", + "directory.federated": "ज्ञात फेडीवर्स से", + "directory.local": "केवल {domain} से", + "directory.new_arrivals": "नए आगंतुक", + "directory.recently_active": "हाल में ही सक्रिय", + "embed.instructions": "अपने वेबसाइट पर, निचे दिए कोड को कॉपी करके, इस स्टेटस को एम्बेड करें", + "embed.preview": "यह ऐसा दिखेगा :", + "emoji_button.activity": "गतिविधि", + "emoji_button.custom": "निजीकृत", + "emoji_button.flags": "झंडे", + "emoji_button.food": "भोजन एवं पेय", + "emoji_button.label": "इमोजी डाले", + "emoji_button.nature": "प्रकृति", + "emoji_button.not_found": "कोई इमोजी नहीं मिला! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "वस्तुएं", + "emoji_button.people": "लोग", + "emoji_button.recent": "अक्सर इस्तेमाल किया जाने वाला", + "emoji_button.search": "खोजें", + "emoji_button.search_results": "खोज परिणाम", + "emoji_button.symbols": "प्रतीक", + "emoji_button.travel": "यात्रा एवं स्थान", + "empty_column.account_timeline": "सन्नाटा! यहां कोई टूट्स नहीं!", + "empty_column.account_unavailable": "प्रोफाइल उपलब्ध नहीं", + "empty_column.blocks": "आप अभी तक किसी भी यूजर के द्वारा ब्लॉक्ड नहीं हो।", + "empty_column.community": "लोकल टाइम्लाइन खाली है, कुछ देखने के लिये सार्वजनिक रूप से कुछ लिखें!", + "empty_column.direct": "आपके पास कोई सीधा सन्देश नहीं है, जब आप कोई भेजेंगे प्राप्त करेंगे तो यहाँ दिखेगा।", + "empty_column.domain_blocks": "अभी तक कोई छुपा हुआ डोमेन नहीं है।", + "empty_column.favourited_statuses": "आपके पास अभी कोई भी चहिता टूट नहीं है. जब आप किसी टूट को पसंद (स्टार) करेंगे, तब वो यहाँ दिखेगा।", + "empty_column.favourites": "अभी तक किसी ने भी इस टूट को पसंद (स्टार) नहीं किया है. जब भी कोई इसे पसंद करेगा, उनका नाम यहाँ दिखेगा।", + "empty_column.follow_requests": "अभी तक किसी ने भी आपका अनुसरण करने की विनती नहीं की है. जब भी कोई आपको विनती भेजेगा, वो यहाँ दिखेगी.", + "empty_column.hashtag": "यह हैशटैग अभी तक खाली है।", + "empty_column.home": "आपकी मुख्य कालक्रम अभी खली है. अन्य उपयोगकर्ताओं से मिलने के लिए और अपनी गतिविधियां शुरू करने के लिए या तो {public} पर जाएं या खोज का उपयोग करें।", + "empty_column.home.public_timeline": "सार्वजनिक कालक्रम", + "empty_column.list": "यह सूची अभी खाली है. जब इसके सदस्य कोई अभिव्यक्ति देंगे, तो वो यहां दिखाई देंगी.", "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", "empty_column.mutes": "You haven't muted any users yet.", "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", - "follow_request.authorize": "Authorize", - "follow_request.reject": "Reject", - "getting_started.developers": "Developers", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", - "getting_started.heading": "Getting started", - "getting_started.invite": "Invite people", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", - "getting_started.security": "Security", - "getting_started.terms": "Terms of service", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "समस्या सूचित करें", + "follow_request.authorize": "अधिकार दें", + "follow_request.reject": "अस्वीकार करें", + "getting_started.developers": "डेवॅलपर्स", + "getting_started.directory": "प्रोफ़ाइल निर्देशिका", + "getting_started.documentation": "प्रलेखन", + "getting_started.heading": "पहले कदम रखें", + "getting_started.invite": "दोस्तों को आमंत्रित करें", + "getting_started.open_source_notice": "मास्टोडॉन एक मुक्त स्रोत सॉफ्टवेयर है. आप गिटहब {github} पर इस सॉफ्टवेयर में योगदान या किसी भी समस्या को सूचित कर सकते है.", + "getting_started.security": "अकाउंट सेटिंग्स", + "getting_started.terms": "सेवा की शर्तें", + "hashtag.column_header.tag_mode.all": "और {additional}", + "hashtag.column_header.tag_mode.any": "या {additional}", + "hashtag.column_header.tag_mode.none": "बिना {additional}", + "hashtag.column_settings.select.no_options_message": "कोई सुझाव नहीं मिला", + "hashtag.column_settings.select.placeholder": "हैशटैग लिखें...", + "hashtag.column_settings.tag_mode.all": "यह सभी", + "hashtag.column_settings.tag_mode.any": "इनमें से कोई भी", + "hashtag.column_settings.tag_mode.none": "इनमें से कोई भी नहीं", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", - "home.column_settings.basic": "Basic", - "home.column_settings.show_reblogs": "Show boosts", - "home.column_settings.show_replies": "Show replies", + "home.column_settings.basic": "बुनियादी", + "home.column_settings.show_reblogs": "बूस्ट दिखाए", + "home.column_settings.show_replies": "जवाबों को दिखाए", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", - "introduction.federation.action": "Next", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.action": "अगला", + "introduction.federation.federated.headline": "फ़ेडरेटेड", "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "होम", "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", + "introduction.federation.local.headline": "लोकल", "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.headline": "पसंदीदा", "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.headline": "बूस्ट", "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.headline": "जवाब", "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", + "introduction.welcome.action": "आइए शुरू करते हैं!", + "introduction.welcome.headline": "पहले कदम", "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", "keyboard_shortcuts.back": "to navigate back", "keyboard_shortcuts.blocked": "to open blocked users list", "keyboard_shortcuts.boost": "to boost", "keyboard_shortcuts.column": "to focus a status in one of the columns", - "keyboard_shortcuts.compose": "to focus the compose textarea", - "keyboard_shortcuts.description": "Description", - "keyboard_shortcuts.direct": "to open direct messages column", - "keyboard_shortcuts.down": "to move down in the list", - "keyboard_shortcuts.enter": "to open status", - "keyboard_shortcuts.favourite": "to favourite", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.compose": "कंपोज़ टेक्स्ट-एरिया पर ध्यान केंद्रित करने के लिए", + "keyboard_shortcuts.description": "विवरण", + "keyboard_shortcuts.direct": "सीधा सन्देश कॉलम को खोलने के लिए", + "keyboard_shortcuts.down": "सूची में शामिल करने के लिए", + "keyboard_shortcuts.enter": "स्टेटस खोलने के लिए", + "keyboard_shortcuts.favourite": "पसंदीदा के लिए", + "keyboard_shortcuts.favourites": "पसंदीदा सूची खोलने के लिए", + "keyboard_shortcuts.federated": "फ़ैडरेटेड टाइम्लाइन खोलने के लिए", "keyboard_shortcuts.heading": "Keyboard Shortcuts", - "keyboard_shortcuts.home": "to open home timeline", - "keyboard_shortcuts.hotkey": "Hotkey", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.home": "होम टाइम्लाइन खोलने के लिए", + "keyboard_shortcuts.hotkey": "हॉट-की", + "keyboard_shortcuts.legend": "इस लीजेंड को दिखाने के लिए", + "keyboard_shortcuts.local": "लोकल टाइम्लाइन खोलने के लिए", + "keyboard_shortcuts.mention": "लेखक को मेन्शन करने के लिए", + "keyboard_shortcuts.muted": "म्यूटेड यूजर की लिस्ट खोलने के लिए", + "keyboard_shortcuts.my_profile": "आपकी प्रोफाइल खोलने के लिए", + "keyboard_shortcuts.notifications": "नोटिफिकेशन कॉलम खोलने के लिए", + "keyboard_shortcuts.pinned": "पिनड टूट्स की लिस्ट खोलने के लिए", + "keyboard_shortcuts.profile": "लेखक की प्रोफाइल खोलने के लिए", + "keyboard_shortcuts.reply": "जवाब के लिए", + "keyboard_shortcuts.requests": "फॉलो रिक्वेस्ट लिस्ट खोलने के लिए", + "keyboard_shortcuts.search": "गहरी खोज के लिए", "keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", @@ -221,15 +243,16 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", - "loading_indicator.label": "Loading...", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "लोड हो रहा है...", "media_gallery.toggle_visible": "Toggle visibility", - "missing_indicator.label": "Not found", - "missing_indicator.sublabel": "This resource could not be found", + "missing_indicator.label": "नहीं मिला", + "missing_indicator.sublabel": "यह संसाधन नहीं मिल सका।", "mute_modal.hide_notifications": "Hide notifications from this user?", - "navigation_bar.apps": "Mobile apps", - "navigation_bar.blocks": "Blocked users", - "navigation_bar.community_timeline": "Local timeline", - "navigation_bar.compose": "Compose new toot", + "navigation_bar.apps": "मोबाइल एप्लिकेशंस", + "navigation_bar.blocks": "ब्लॉक्ड यूज़र्स", + "navigation_bar.community_timeline": "लोकल टाइम्लाइन", + "navigation_bar.compose": "नया टूट् लिखें", "navigation_bar.direct": "Direct messages", "navigation_bar.discover": "Discover", "navigation_bar.domain_blocks": "Hidden domains", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferences", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.security": "Security", "notification.favourite": "{name} favourited your status", @@ -258,53 +280,56 @@ "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", "notifications.column_settings.alert": "Desktop notifications", "notifications.column_settings.favourite": "Favourites:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", - "notifications.column_settings.follow": "New followers:", - "notifications.column_settings.mention": "Mentions:", - "notifications.column_settings.poll": "Poll results:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Show in column", - "notifications.column_settings.sound": "Play sound", - "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.group": "{count} notifications", - "poll.closed": "Closed", - "poll.refresh": "Refresh", + "notifications.column_settings.filter_bar.advanced": "सभी श्रेणियाँ दिखाएं", + "notifications.column_settings.filter_bar.category": "फ़िल्टर बार", + "notifications.column_settings.filter_bar.show": "दिखाएँ", + "notifications.column_settings.follow": "नए फ़ॉलोअर्स", + "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.filter.all": "सभी", + "notifications.filter.boosts": "बूस्ट", + "notifications.filter.favourites": "पसंदीदा", + "notifications.filter.follows": "फॉलो", + "notifications.filter.mentions": "उल्लेख", + "notifications.filter.polls": "चुनाव परिणाम", + "notifications.group": "{count} सूचनाएँ", + "poll.closed": "बंद कर दिया", + "poll.refresh": "रीफ्रेश करें", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", + "poll.vote": "वोट", + "poll.voted": "आपने इसी उत्तर का चुनाव किया है।", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Adjust status privacy", "privacy.direct.long": "Post to mentioned users only", - "privacy.direct.short": "Direct", + "privacy.direct.short": "सीधा", "privacy.private.long": "Post to followers only", "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", - "privacy.public.short": "Public", + "privacy.public.long": "सार्वजनिक टाइम्लाइन पर भेजें", + "privacy.public.short": "सार्वजनिक", "privacy.unlisted.long": "Do not show in public timelines", - "privacy.unlisted.short": "Unlisted", - "regeneration_indicator.label": "Loading…", + "privacy.unlisted.short": "अनलिस्टेड", + "refresh": "रीफ्रेश करें", + "regeneration_indicator.label": "लोड हो रहा है...", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "अभी", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", - "reply_indicator.cancel": "Cancel", + "reply_indicator.cancel": "रद्द करें", "report.forward": "Forward to {target}", "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Additional comments", - "report.submit": "Submit", + "report.submit": "सबमिट करें", "report.target": "Report {target}", - "search.placeholder": "Search", + "search.placeholder": "खोजें", "search_popout.search_format": "Advanced search format", "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -337,45 +363,54 @@ "status.pin": "Pin on profile", "status.pinned": "Pinned toot", "status.read_more": "Read more", - "status.reblog": "Boost", + "status.reblog": "बूस्ट", "status.reblog_private": "Boost to original audience", "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", - "status.reply": "Reply", + "status.reply": "जवाब", "status.replyAll": "Reply to thread", "status.report": "Report @{name}", - "status.sensitive_warning": "Sensitive content", - "status.share": "Share", - "status.show_less": "Show less", + "status.sensitive_warning": "संवेदनशील विषय वस्तु", + "status.share": "शेयर करें", + "status.show_less": "कम दिखाएँ", "status.show_less_all": "Show less for all", - "status.show_more": "Show more", + "status.show_more": "और दिखाएँ", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "अनुपलब्ध", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", "suggestions.dismiss": "Dismiss suggestion", "suggestions.header": "You might be interested in…", - "tabs_bar.federated_timeline": "Federated", - "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Local", - "tabs_bar.notifications": "Notifications", - "tabs_bar.search": "Search", + "tabs_bar.federated_timeline": "फ़ेडरेटेड", + "tabs_bar.home": "होम", + "tabs_bar.local_timeline": "लोकल", + "tabs_bar.notifications": "सूचनाएँ", + "tabs_bar.search": "खोजें", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", - "upload_form.undo": "Delete", - "upload_progress.label": "Uploading...", + "upload_form.edit": "संशोधन करें", + "upload_form.undo": "मिटाए", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "लागू करें", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "upload_modal.detect_text": "Detect text from picture", + "upload_modal.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.preview_label": "Preview ({ratio})", + "upload_progress.label": "अपलोडिंग...", "video.close": "Close video", "video.exit_fullscreen": "Exit full screen", "video.expand": "Expand video", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 6f9b5343a..09298a7ad 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -4,6 +4,7 @@ "account.block": "Blokiraj @{name}", "account.block_domain": "Sakrij sve sa {domain}", "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domain hidden", "account.edit_profile": "Uredi profil", @@ -15,6 +16,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "te slijedi", "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Utišaj @{name}", "account.mute_notifications": "Mute notifications from @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Postovi", "account.posts_with_replies": "Toots with replies", "account.report": "Prijavi @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Prestani slijediti", "account.unmute": "Poništi utišavanje @{name}", "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put", "bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.retry": "Try again", @@ -47,6 +53,7 @@ "column.blocks": "Blokirani korisnici", "column.community": "Lokalni timeline", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "Favoriti", "column.follow_requests": "Zahtjevi za slijeđenje", @@ -64,7 +71,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Postavke", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.domain_block.confirm": "Sakrij cijelu domenu", "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Utišaj", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Embed this status on your website by copying the code below.", "embed.preview": "Here is what it will look like:", "emoji_button.activity": "Aktivnost", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.", "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Autoriziraj", "follow_request.reject": "Odbij", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Preklopi vidljivost", "missing_indicator.label": "Nije nađen", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Postavke", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federalni timeline", "navigation_bar.security": "Security", "notification.favourite": "{name} je lajkao tvoj status", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Podesi status privatnosti", @@ -290,6 +314,7 @@ "privacy.public.short": "Javno", "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima", "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Pokaži više", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Poništi utišavanje razgovora", "status.unpin": "Unpin from profile", "suggestions.dismiss": "Dismiss suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Povuci i spusti kako bi uploadao", "upload_button.label": "Dodaj media", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Poništi", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Uploadam...", "video.close": "Close video", "video.exit_fullscreen": "Exit full screen", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 7be8b7d67..72dd6b986 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -1,107 +1,125 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", + "account.add_or_remove_from_list": "Hozzáadás vagy eltávolítás a listáról", "account.badges.bot": "Bot", "account.block": "@{name} letiltása", "account.block_domain": "Minden elrejtése innen: {domain}", - "account.blocked": "Blocked", - "account.direct": "Direct Message @{name}", - "account.domain_blocked": "Domain hidden", + "account.blocked": "Letiltva", + "account.cancel_follow_request": "Követési kérelem törlése", + "account.direct": "Közvetlen üzenet @{name} számára", + "account.domain_blocked": "Rejtett domain", "account.edit_profile": "Profil szerkesztése", - "account.endorse": "Feature on profile", + "account.endorse": "Kiemelés a profilodon", "account.follow": "Követés", - "account.followers": "Követők", - "account.followers.empty": "No one follows this user yet.", - "account.follows": "Követve", - "account.follows.empty": "This user doesn't follow anyone yet.", - "account.follows_you": "Követnek téged", - "account.hide_reblogs": "Rejtsd el a tülkölést @{name}-tól/től", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.followers": "Követő", + "account.followers.empty": "Ezt a felhasználót még senki sem követi.", + "account.follows": "Követések", + "account.follows.empty": "Ez a felhasználó még senkit sem követ.", + "account.follows_you": "Követ téged", + "account.hide_reblogs": "@{name} megtolásainak némítása", + "account.last_status": "Utoljára aktív", + "account.link_verified_on": "A linket ellenőriztük: {date}", + "account.locked_info": "Ez a fiók zárt. A tulajdonos engedélyezi, hogy ki követheti őt.", "account.media": "Média", "account.mention": "@{name} említése", "account.moved_to": "{name} átköltözött:", "account.mute": "@{name} némítása", - "account.mute_notifications": "@{name} értesítések némítása", - "account.muted": "Muted", - "account.posts": "Státuszok", - "account.posts_with_replies": "Toots with replies", + "account.mute_notifications": "@{name} értesítéseinek némítása", + "account.muted": "Némítva", + "account.never_active": "Soha", + "account.posts": "Tülkölés", + "account.posts_with_replies": "Tülkölés és válaszok", "account.report": "@{name} jelentése", - "account.requested": "Engedélyre vár. Kattintson a követési kérés visszavonására", + "account.requested": "Engedélyre vár. Kattints a követési kérés visszavonásához", "account.share": "@{name} profiljának megosztása", - "account.show_reblogs": "@{name} kedvenceinek mutatása", - "account.unblock": "@{name} kiblokkolása", - "account.unblock_domain": "{domain} mutatása", - "account.unendorse": "Don't feature on profile", - "account.unfollow": "Követés abbahagyása", - "account.unmute": "@{name} kinémítása", - "account.unmute_notifications": "@{name} értesítéseinek kinémítása", - "alert.unexpected.message": "An unexpected error occurred.", - "alert.unexpected.title": "Oops!", - "boost_modal.combo": "Megnyomhatod {combo}, hogy átugord következő alkalommal", + "account.show_reblogs": "@{name} megtolásainak mutatása", + "account.unblock": "@{name} letiltásának feloldása", + "account.unblock_domain": "{domain} elrejtésének feloldása", + "account.unendorse": "Kiemelés törlése a profilodról", + "account.unfollow": "Követés vége", + "account.unmute": "@{name} némítás feloldása", + "account.unmute_notifications": "@{name} némított értesítéseinek feloldása", + "alert.rate_limited.message": "Próbáld újra {retry_time, time, medium} után.", + "alert.rate_limited.title": "Forgalomkorlátozás", + "alert.unexpected.message": "Váratlan hiba történt.", + "alert.unexpected.title": "Hoppá!", + "autosuggest_hashtag.per_week": "{count}/hét", + "boost_modal.combo": "Hogy átugord ezt következő alkalommal, használd {combo}", "bundle_column_error.body": "Hiba történt a komponens betöltése közben.", - "bundle_column_error.retry": "Próbálja újra", + "bundle_column_error.retry": "Próbáld újra", "bundle_column_error.title": "Hálózati hiba", - "bundle_modal_error.close": "Bezár", + "bundle_modal_error.close": "Bezárás", "bundle_modal_error.message": "Hiba történt a komponens betöltésekor.", - "bundle_modal_error.retry": "Próbálja újra", + "bundle_modal_error.retry": "Próbáld újra", "column.blocks": "Letiltott felhasználók", "column.community": "Helyi idővonal", - "column.direct": "Direct messages", - "column.domain_blocks": "Hidden domains", + "column.direct": "Közvetlen üzenetek", + "column.directory": "Profilok böngészése", + "column.domain_blocks": "Rejtett domainek", "column.favourites": "Kedvencek", - "column.follow_requests": "Követési kérések", + "column.follow_requests": "Követési kérelmek", "column.home": "Kezdőlap", "column.lists": "Listák", "column.mutes": "Némított felhasználók", "column.notifications": "Értesítések", - "column.pins": "Kitűzött tülkölések", + "column.pins": "Kitűzött tülkök", "column.public": "Nyilvános idővonal", "column_back_button.label": "Vissza", "column_header.hide_settings": "Beállítások elrejtése", "column_header.moveLeft_settings": "Oszlop elmozdítása balra", - "column_header.moveRight_settings": "oszlop elmozdítása jobbra", - "column_header.pin": "Kitűz", + "column_header.moveRight_settings": "Oszlop elmozdítása jobbra", + "column_header.pin": "Kitűzés", "column_header.show_settings": "Beállítások mutatása", "column_header.unpin": "Kitűzés eltávolítása", "column_subheading.settings": "Beállítások", - "community.column_settings.media_only": "Media Only", - "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", - "compose_form.hashtag_warning": "Ezen tülkölés nem fog megjelenni semmilyen hashtag alatt mivel listázatlan. Csak a publikus tülkölések kereshetőek hashtag-el.", - "compose_form.lock_disclaimer": "Az ön fiókja nincs {locked}. Bárki követni tud, hogy megtekintse a kizárt követőknek szánt üzeneteid.", + "community.column_settings.media_only": "Csak média", + "compose_form.direct_message_warning": "Ezt a tülköt csak a benne megemlített felhasználók láthatják majd.", + "compose_form.direct_message_warning_learn_more": "Tudj meg többet", + "compose_form.hashtag_warning": "Ez a tülköd nem fog megjelenni semmilyen hashtag alatt mivel listázatlan. Csak nyilvános tülkök kereshetőek hashtaggel.", + "compose_form.lock_disclaimer": "A fiókod nincs {locked}. Bárki követni tud, hogy megtekintse a kizárólag követőknek szánt üzeneteidet.", "compose_form.lock_disclaimer.lock": "lezárva", - "compose_form.placeholder": "Mire gondolsz?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.placeholder": "Mi jár a fejedben?", + "compose_form.poll.add_option": "Lehetőség hozzáadása", + "compose_form.poll.duration": "Szavazás időtartama", + "compose_form.poll.option_placeholder": "{number}. lehetőség", + "compose_form.poll.remove_option": "Lehetőség törlése", "compose_form.publish": "Tülk", "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.spoiler.marked": "Text is hidden behind warning", - "compose_form.spoiler.unmarked": "Text is not hidden", - "compose_form.spoiler_placeholder": "Figyelmeztetését írja ide", - "confirmation_modal.cancel": "Bezár", - "confirmations.block.block_and_report": "Block & Report", - "confirmations.block.confirm": "Letilt", - "confirmations.block.message": "Biztos benne, hogy le szeretné tiltani {name}?", - "confirmations.delete.confirm": "Töröl", - "confirmations.delete.message": "Biztos benne, hogy törölni szeretné ezt a státuszt?", - "confirmations.delete_list.confirm": "Töröl", - "confirmations.delete_list.message": "Biztos benne, hogy véglegesen törölni szeretné ezt a listát?", - "confirmations.domain_block.confirm": "Egész domain elrejtése", - "confirmations.domain_block.message": "Nagyon biztos abban, hogy le szeretné tiltani az egész {domain}-t? A legtöbb esetben néhány célszerű tiltás vagy némítás elegendő és kívánatosabb megoldás.", - "confirmations.mute.confirm": "Némít", - "confirmations.mute.message": "Biztos benne, hogy némítani szeretné {name}?", - "confirmations.redraft.confirm": "Delete & redraft", - "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "compose_form.sensitive.hide": "Média megjelölése szenzitívként", + "compose_form.sensitive.marked": "A médiát érzékenynek jelölték", + "compose_form.sensitive.unmarked": "A médiát nem jelölték érzékenynek", + "compose_form.spoiler.marked": "A szöveg figyelmeztetés mögé van rejtve", + "compose_form.spoiler.unmarked": "A szöveg nem rejtett", + "compose_form.spoiler_placeholder": "Írd ide a figyelmeztetést", + "confirmation_modal.cancel": "Mégse", + "confirmations.block.block_and_report": "Letiltás és jelentés", + "confirmations.block.confirm": "Letiltás", + "confirmations.block.message": "Biztos, hogy letiltod: {name}?", + "confirmations.delete.confirm": "Törlés", + "confirmations.delete.message": "Biztos, hogy törölni szeretnéd ezt a tülkölést?", + "confirmations.delete_list.confirm": "Törlés", + "confirmations.delete_list.message": "Biztos, hogy véglegesen törölni szeretnéd ezt a listát?", + "confirmations.domain_block.confirm": "Teljes domain elrejtése", + "confirmations.domain_block.message": "Biztos, hogy le szeretnéd tiltani a teljes {domain} domaint? A legtöbb esetben néhány célzott tiltás vagy némítás elegendő, és kívánatosabb megoldás. Semmilyen tartalmat nem fogsz látni ebből a domainből se az idővonalakon, se az értesítésekben. Az ebben a domainben lévő követőidet is eltávolítjuk.", + "confirmations.logout.confirm": "Kijelentkezés", + "confirmations.logout.message": "Biztos, hogy kijelentkezel?", + "confirmations.mute.confirm": "Némítás", + "confirmations.mute.explanation": "Ez elrejti a tőlük érkező bejegyzéseket, valamint az őket megemlítőket, de ők továbbra is láthatják a te bejegyzéseid, és követhetnek is téged.", + "confirmations.mute.message": "Biztos, hogy némítod: {name}?", + "confirmations.redraft.confirm": "Törlés és újraírás", + "confirmations.redraft.message": "Biztos, hogy ezt a tülköt szeretnéd törölni és újraírni? Minden megtolást és kedvencnek jelölést elvesztesz, az eredetire adott válaszok pedig elárvulnak.", + "confirmations.reply.confirm": "Válasz", + "confirmations.reply.message": "Ha most válaszolsz, ez felülírja a most szerkesztés alatt álló üzenetet. Mégis ezt szeretnéd?", "confirmations.unfollow.confirm": "Követés visszavonása", - "confirmations.unfollow.message": "Biztos benne, hogy vissza szeretné vonni {name} követését?", - "embed.instructions": "Ágyazza be ezen státuszt weboldalába az alábbi kód másolásával.", + "confirmations.unfollow.message": "Biztos, hogy vissza szeretnéd vonni {name} követését?", + "conversation.delete": "Beszélgetés törlése", + "conversation.mark_as_read": "Megjelölés olvasottként", + "conversation.open": "Beszélgetés megtekintése", + "conversation.with": "{names}-el/al", + "directory.federated": "Az ismert fediverzumból", + "directory.local": "Csak innen: {domain}", + "directory.new_arrivals": "Új csatlakozók", + "directory.recently_active": "Nemrég aktív", + "embed.instructions": "Ágyazd be ezt a tülköt a weboldaladba az alábbi kód kimásolásával.", "embed.preview": "Így fog kinézni:", "emoji_button.activity": "Aktivitás", "emoji_button.custom": "Egyéni", @@ -109,7 +127,7 @@ "emoji_button.food": "Étel és Ital", "emoji_button.label": "Emoji beszúrása", "emoji_button.nature": "Természet", - "emoji_button.not_found": "Nincsenek emojok!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "Nincsenek emodzsik!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Tárgyak", "emoji_button.people": "Emberek", "emoji_button.recent": "Gyakran használt", @@ -117,272 +135,289 @@ "emoji_button.search_results": "Keresési találatok", "emoji_button.symbols": "Szimbólumok", "emoji_button.travel": "Utazás és Helyek", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.blocks": "You haven't blocked any users yet.", - "empty_column.community": "A helyi idővonal üres. Írj egy publikus stástuszt, hogy elindítsd a labdát!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", - "empty_column.hashtag": "Jelenleg nem található semmi ezen hashtaggel.", - "empty_column.home": "A hazai idővonala üres! Látogasd meg a {public} vagy használd a keresőt, hogy ismerj meg más felhasználókat.", - "empty_column.home.public_timeline": "publikus idővonal", - "empty_column.list": "A lista jelenleg üres. Mikor a listatagok új státuszt posztolnak itt meg fognak jelenni.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", - "empty_column.notifications": "Jelenleg nincsenek értesítései. Lépj kapcsolatba másokkal, hogy indítsd el a beszélgetést.", - "empty_column.public": "Jelenleg semmi nincs itt! Írj valamit publikusan vagy kövess más szervereken levő felhasználókat, hogy megtöltsd", - "follow_request.authorize": "Engedélyez", - "follow_request.reject": "Visszautasít", - "getting_started.developers": "Developers", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", + "empty_column.account_timeline": "Itt nincs tülkölés!", + "empty_column.account_unavailable": "A profil nem érhető el", + "empty_column.blocks": "Még senkit sem tiltottál le.", + "empty_column.community": "A helyi idővonal üres. Tülkölj egyet nyilvánosan, hogy elindítsd az eseményeket!", + "empty_column.direct": "Még nincs egy közvetlen üzeneted sem. Ha küldesz vagy kapsz egyet, itt fog megjelenni.", + "empty_column.domain_blocks": "Még nem rejtettél el egyetlen domaint sem.", + "empty_column.favourited_statuses": "Még nincs egy kedvenc tülköd sem. Ha kedvencnek jelölsz egyet, itt fog megjelenni.", + "empty_column.favourites": "Még senki sem jelölte ezt a tülköt kedvencként. Ha valaki mégis megteszi, itt fogjuk mutatni.", + "empty_column.follow_requests": "Még nincs egy követési kérésed sem. Ha kapsz egyet, itt fogjuk feltüntetni.", + "empty_column.hashtag": "Jelenleg nem található semmi ezzel a hashtaggel.", + "empty_column.home": "A saját idővonalad üres! Látogasd meg a {public} oldalt vagy használd a keresőt, hogy megismerj másokat.", + "empty_column.home.public_timeline": "a nyilvános idővonal", + "empty_column.list": "A lista jelenleg üres. Ha a listatagok tülkölnek, itt fognak megjelenni.", + "empty_column.lists": "Még nem hoztál létre listát. Ha csinálsz egyet, itt látszik majd.", + "empty_column.mutes": "Még egy felhasználót sem némítottál le.", + "empty_column.notifications": "Jelenleg nincsenek értesítéseid. Lépj kapcsolatba másokkal, hogy elindítsd a beszélgetést.", + "empty_column.public": "Jelenleg itt nincs semmi! Írj valamit nyilvánosan vagy kövess más szervereken levő felhasználókat, hogy megtöltsd", + "error.unexpected_crash.explanation": "Egy hiba vagy böngésző inkompatibilitás miatt ez az oldal nem jeleníthető meg rendesen.", + "error.unexpected_crash.next_steps": "Próbáld frissíteni az oldalt. Ha ez nem segít, egy másik böngészőn vagy appon keresztül még mindig használhatod a Mastodont.", + "errors.unexpected_crash.copy_stacktrace": "Veremkiíratás vágólapra másolása", + "errors.unexpected_crash.report_issue": "Probléma jelentése", + "follow_request.authorize": "Engedélyezés", + "follow_request.reject": "Elutasítás", + "getting_started.developers": "Fejlesztőknek", + "getting_started.directory": "Profilok", + "getting_started.documentation": "Dokumentáció", "getting_started.heading": "Első lépések", - "getting_started.invite": "Invite people", - "getting_started.open_source_notice": "Mastodon egy nyílt forráskódú szoftver. Hozzájárulás vagy problémák jelentése a GitHub-on {github}.", - "getting_started.security": "Security", - "getting_started.terms": "Terms of service", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", - "hashtag.column_settings.tag_toggle": "Include additional tags in this column", - "home.column_settings.basic": "Alap", - "home.column_settings.show_reblogs": "Ismétlések mutatása", + "getting_started.invite": "Mások meghívása", + "getting_started.open_source_notice": "A Mastodon nyílt forráskódú szoftver. Közreműködhetsz vagy problémákat jelenthetsz a GitHubon: {github}.", + "getting_started.security": "Fiókbeállítások", + "getting_started.terms": "Felhasználási feltételek", + "hashtag.column_header.tag_mode.all": "és {additional}", + "hashtag.column_header.tag_mode.any": "vagy {additional}", + "hashtag.column_header.tag_mode.none": "{additional} nélkül", + "hashtag.column_settings.select.no_options_message": "Nincs javaslat", + "hashtag.column_settings.select.placeholder": "Addj meg hashtageket…", + "hashtag.column_settings.tag_mode.all": "Mindegyik", + "hashtag.column_settings.tag_mode.any": "Bármelyik", + "hashtag.column_settings.tag_mode.none": "Egyik sem", + "hashtag.column_settings.tag_toggle": "Új címkék felvétele ehhez az oszlophoz", + "home.column_settings.basic": "Alapvető", + "home.column_settings.show_reblogs": "Megtolások mutatása", "home.column_settings.show_replies": "Válaszok mutatása", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", - "introduction.federation.action": "Next", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "keyboard_shortcuts.back": "vissza navigálás", - "keyboard_shortcuts.blocked": "to open blocked users list", - "keyboard_shortcuts.boost": "ismétlés", - "keyboard_shortcuts.column": "összpontosítson egy státuszra az egyik oszlopban", - "keyboard_shortcuts.compose": "fókuszálja a szerkesztési szövegdobozt", + "intervals.full.days": "{number, plural, one {# nap} other {# nap}}", + "intervals.full.hours": "{number, plural, one {# óra} other {# óra}}", + "intervals.full.minutes": "{number, plural, one {# perc} other {# perc}}", + "introduction.federation.action": "Következő", + "introduction.federation.federated.headline": "Föderációs", + "introduction.federation.federated.text": "A fediverzum más szervereiről származó nyilvános tülkök a föderációs idővonalon jelennek meg.", + "introduction.federation.home.headline": "Kezdőlap", + "introduction.federation.home.text": "A saját idővonaladon az általad követett emberek tülkjei jelennek meg. Bárkit követhetsz, bármelyik kiszolgálón.", + "introduction.federation.local.headline": "Helyi", + "introduction.federation.local.text": "A helyi idővonalon a veled közös kiszolgálón lévő emberek nyilvános tülkjei jelennek meg.", + "introduction.interactions.action": "Oktatóanyag befejezése!", + "introduction.interactions.favourite.headline": "Kedvenc", + "introduction.interactions.favourite.text": "A kedvenc funkcióval elrakhatsz későbbre egy tülköt, illetve közölheted a szerzővel, hogy tetszett a megosztása.", + "introduction.interactions.reblog.headline": "Megtolás", + "introduction.interactions.reblog.text": "A saját követőiddel mások tülkjeit is megoszthatod úgy, hogy megtolod őket.", + "introduction.interactions.reply.headline": "Válasz", + "introduction.interactions.reply.text": "Saját vagy mások tülkjeire válaszolva egy beszélgetési láncot alakíthatsz ki.", + "introduction.welcome.action": "Csapjunk bele!", + "introduction.welcome.headline": "Első lépések", + "introduction.welcome.text": "Üdv a fediverzumban! Pár pillanat múlva már küldheted is üzeneteidet barátaidnak bármely szerveren. Ez a szerver {domain} viszont különleges. Ez tartja nyilván a profilod, szóval jegyezd meg a nevét.", + "keyboard_shortcuts.back": "visszafelé navigálás", + "keyboard_shortcuts.blocked": "letiltott felhasználók listájának megnyitása", + "keyboard_shortcuts.boost": "megtolás", + "keyboard_shortcuts.column": "fókuszálás egy tülkre az egyik oszlopban", + "keyboard_shortcuts.compose": "fókuszálás a szerkesztési szövegdobozra", "keyboard_shortcuts.description": "Leírás", - "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.direct": "közvetlen üzenetek megnyitása", "keyboard_shortcuts.down": "lefele navigálás a listában", - "keyboard_shortcuts.enter": "státusz megnyitása", - "keyboard_shortcuts.favourite": "kedvenccé tétel", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", - "keyboard_shortcuts.heading": "Billentyű rövidítések", - "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.enter": "tülk megnyitása", + "keyboard_shortcuts.favourite": "kedvencnek jelölés", + "keyboard_shortcuts.favourites": "kedvenc lista megnyitása", + "keyboard_shortcuts.federated": "föderációs idővonal megnyitása", + "keyboard_shortcuts.heading": "Billentyűparancsok", + "keyboard_shortcuts.home": "saját idővonal megnyitása", "keyboard_shortcuts.hotkey": "Gyorsbillentyű", "keyboard_shortcuts.legend": "jelmagyarázat megjelenítése", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "szerző megjelenítése", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "válaszolás", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "kereső kiemelése", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "új tülk megkezdése", + "keyboard_shortcuts.local": "helyi idővonal megnyitása", + "keyboard_shortcuts.mention": "szerző megemlítése", + "keyboard_shortcuts.muted": "némított felhasználók listájának megnyitása", + "keyboard_shortcuts.my_profile": "profilod megnyitása", + "keyboard_shortcuts.notifications": "értesítések megnyitása", + "keyboard_shortcuts.pinned": "kitűzött tülkök listájának megnyitása", + "keyboard_shortcuts.profile": "szerző profiljának megnyitása", + "keyboard_shortcuts.reply": "válasz", + "keyboard_shortcuts.requests": "követési kérések listájának megnyitása", + "keyboard_shortcuts.search": "fókuszálás a keresőre", + "keyboard_shortcuts.start": "\"Első lépések\" megnyitása", + "keyboard_shortcuts.toggle_hidden": "tartalmi figyelmeztetéssel ellátott szöveg mutatása/elrejtése", + "keyboard_shortcuts.toggle_sensitivity": "média mutatása/elrejtése", + "keyboard_shortcuts.toot": "új tülk írása", "keyboard_shortcuts.unfocus": "tülk szerkesztés/keresés fókuszpontból való kivétele", - "keyboard_shortcuts.up": "fennebb helyezés a listában", + "keyboard_shortcuts.up": "felfelé mozdítás a listában", "lightbox.close": "Bezárás", "lightbox.next": "Következő", "lightbox.previous": "Előző", - "lightbox.view_context": "View context", + "lightbox.view_context": "Kontextus megtekintése", "lists.account.add": "Hozzáadás a listához", - "lists.account.remove": "Eltávolít a listából", + "lists.account.remove": "Eltávolítás a listából", "lists.delete": "Lista törlése", "lists.edit": "Lista szerkesztése", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Cím megváltoztatása", "lists.new.create": "Lista hozzáadása", - "lists.new.title_placeholder": "Új lista cím", - "lists.search": "Keresés a követtett személyek között", + "lists.new.title_placeholder": "Új lista címe", + "lists.search": "Keresés a követett személyek között", "lists.subheading": "Listáid", + "load_pending": "{count, plural, one {# új elem} other {# új elem}}", "loading_indicator.label": "Betöltés...", - "media_gallery.toggle_visible": "Láthatóság váltása", + "media_gallery.toggle_visible": "Láthatóság állítása", "missing_indicator.label": "Nincs találat", - "missing_indicator.sublabel": "Ezen forrás nem található", - "mute_modal.hide_notifications": "Értesítések elrejtése ezen felhasználótól?", - "navigation_bar.apps": "Mobile apps", - "navigation_bar.blocks": "Tiltott felhasználók", + "missing_indicator.sublabel": "Ez az erőforrás nem található", + "mute_modal.hide_notifications": "Rejtsük el a felhasználótól származó értesítéseket?", + "navigation_bar.apps": "Mobil appok", + "navigation_bar.blocks": "Letiltott felhasználók", "navigation_bar.community_timeline": "Helyi idővonal", - "navigation_bar.compose": "Compose new toot", - "navigation_bar.direct": "Direct messages", - "navigation_bar.discover": "Discover", - "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.compose": "Új tülk írása", + "navigation_bar.direct": "Közvetlen üzenetek", + "navigation_bar.discover": "Felfedezés", + "navigation_bar.domain_blocks": "Rejtett domainek", "navigation_bar.edit_profile": "Profil szerkesztése", "navigation_bar.favourites": "Kedvencek", - "navigation_bar.filters": "Muted words", - "navigation_bar.follow_requests": "Követési kérések", - "navigation_bar.follows_and_followers": "Follows and followers", - "navigation_bar.info": "Ezen szerverről", + "navigation_bar.filters": "Némított szavak", + "navigation_bar.follow_requests": "Követési kérelmek", + "navigation_bar.follows_and_followers": "Követettek és követők", + "navigation_bar.info": "Erről a kiszolgálóról", "navigation_bar.keyboard_shortcuts": "Gyorsbillentyűk", "navigation_bar.lists": "Listák", "navigation_bar.logout": "Kijelentkezés", "navigation_bar.mutes": "Némított felhasználók", - "navigation_bar.personal": "Personal", + "navigation_bar.personal": "Személyes", "navigation_bar.pins": "Kitűzött tülkök", "navigation_bar.preferences": "Beállítások", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.public_timeline": "Nyilvános időfolyam", - "navigation_bar.security": "Security", - "notification.favourite": "{name} kedvencnek jelölte az állapotod", + "navigation_bar.public_timeline": "Föderációs idővonal", + "navigation_bar.security": "Biztonság", + "notification.favourite": "{name} kedvencnek jelölte egy tülködet", "notification.follow": "{name} követ téged", "notification.mention": "{name} megemlített", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} rebloggolta az állapotod", + "notification.poll": "Egy szavazás, melyben részt vettél, véget ért", + "notification.reblog": "{name} megtolta a tülködet", "notifications.clear": "Értesítések törlése", - "notifications.clear_confirmation": "Biztos benne, hogy véglegesen törölni akarja az összes értesítését?", - "notifications.column_settings.alert": "Asztali gépi értesítések", + "notifications.clear_confirmation": "Biztos, hogy véglegesen törölni akarod az összes értesítésed?", + "notifications.column_settings.alert": "Asztali értesítések", "notifications.column_settings.favourite": "Kedvencek:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Minden kategória mutatása", + "notifications.column_settings.filter_bar.category": "Gyorskereső mező", + "notifications.column_settings.filter_bar.show": "Mutat", "notifications.column_settings.follow": "Új követők:", - "notifications.column_settings.mention": "Megemítéseim:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.mention": "Megemlítések:", + "notifications.column_settings.poll": "Szavazás eredménye:", "notifications.column_settings.push": "Push értesítések", - "notifications.column_settings.reblog": "Rebloggolások:", + "notifications.column_settings.reblog": "Megtolások:", "notifications.column_settings.show": "Oszlopban mutatás", "notifications.column_settings.sound": "Hang lejátszása", - "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.group": "{count} notifications", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", - "privacy.change": "Státusz láthatóságának módosítása", - "privacy.direct.long": "Posztolás csak az említett felhasználóknak", - "privacy.direct.short": "Egyenesen", - "privacy.private.long": "Posztolás csak követőknek", + "notifications.filter.all": "Mind", + "notifications.filter.boosts": "Megtolások", + "notifications.filter.favourites": "Kedvencnek jelölések", + "notifications.filter.follows": "Követések", + "notifications.filter.mentions": "Megemlítések", + "notifications.filter.polls": "Szavazások eredményei", + "notifications.group": "{count} értesítés", + "poll.closed": "Lezárva", + "poll.refresh": "Frissítés", + "poll.total_people": "{count, plural, one {# személy} other {# személy}}", + "poll.total_votes": "{count, plural, one {# szavazat} other {# szavazat}}", + "poll.vote": "Szavazás", + "poll.voted": "Erre a válaszra szavaztál", + "poll_button.add_poll": "Új szavazás", + "poll_button.remove_poll": "Szavazás törlése", + "privacy.change": "Tülk láthatóságának módosítása", + "privacy.direct.long": "Tülk csak az említett felhasználóknak", + "privacy.direct.short": "Közvetlen", + "privacy.private.long": "Tülk csak követőknek", "privacy.private.short": "Csak követőknek", - "privacy.public.long": "Posztolás a publikus idővonalakra", - "privacy.public.short": "Publikus", - "privacy.unlisted.long": "Do not show in public timelines", + "privacy.public.long": "Tülk a nyilvános idővonalra", + "privacy.public.short": "Nyilvános", + "privacy.unlisted.long": "Ne mutassuk nyilvános idővonalon", "privacy.unlisted.short": "Listázatlan", + "refresh": "Frissítés", "regeneration_indicator.label": "Töltődik…", - "regeneration_indicator.sublabel": "Your home feed is being prepared!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", + "regeneration_indicator.sublabel": "A saját idővonalad épp készül!", + "relative_time.days": "{number}n", + "relative_time.hours": "{number}ó", "relative_time.just_now": "most", - "relative_time.minutes": "{number}m", - "relative_time.seconds": "{number}s", + "relative_time.minutes": "{number}p", + "relative_time.seconds": "{number}mp", "reply_indicator.cancel": "Mégsem", - "report.forward": "Forward to {target}", - "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", - "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", - "report.placeholder": "További kommentek", - "report.submit": "Submit", - "report.target": "Reporting", + "report.forward": "Továbbítás: {target}", + "report.forward_hint": "Ez a fiók egy másik kiszolgálóról van. Oda is elküldöd a jelentés egy anonimizált másolatát?", + "report.hint": "A bejelentést a szervered moderátorainak küldjük el. Megmagyarázhatod, miért jelented az alábbi problémát:", + "report.placeholder": "További megjegyzések", + "report.submit": "Küldés", + "report.target": "{target} jelentése", "search.placeholder": "Keresés", - "search_popout.search_format": "Fejlett keresés", - "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.search_format": "Speciális keresés", + "search_popout.tips.full_text": "Egyszerű szöveg. Illeszkedő, általad írt tülköket, kedvencnek jelöléseket, megtolást, megemlítést, felhasználói nevet, megjelenített nevet, hashtageket ad majd vissza.", "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.status": "állapot", + "search_popout.tips.text": "Egyszerű szöveg. Illeszkedő megjelenített nevet, felhasználói nevet, hashtageket ad majd vissza", "search_popout.tips.user": "felhasználó", - "search_results.accounts": "People", - "search_results.hashtags": "Hashtags", - "search_results.statuses": "Toots", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Unboost", - "status.cannot_reblog": "Ezen státusz nem rebloggolható", - "status.copy": "Copy link to status", + "search_results.accounts": "Emberek", + "search_results.hashtags": "Hashtagek", + "search_results.statuses": "Tülkök", + "search_results.statuses_fts_disabled": "Ezen a szerveren nem engedélyezett a tülkök tartalom szerinti keresése.", + "search_results.total": "{count, number} {count, plural, one {találat} other {találat}}", + "status.admin_account": "Moderáció megnyitása @{name} felhasználóhoz", + "status.admin_status": "Tülk megnyitása moderációra", + "status.block": "@{name} letiltása", + "status.cancel_reblog_private": "Megtolás törlése", + "status.cannot_reblog": "Ez a tülk nem tolható meg", + "status.copy": "Link másolása tülkbe", "status.delete": "Törlés", - "status.detailed_status": "Detailed conversation view", - "status.direct": "Direct message @{name}", - "status.embed": "Beágyaz", + "status.detailed_status": "Részletes beszélgetési nézet", + "status.direct": "Közvetlen üzenet @{name} számára", + "status.embed": "Beágyazás", "status.favourite": "Kedvenc", - "status.filtered": "Filtered", + "status.filtered": "Megszűrt", "status.load_more": "Többet", "status.media_hidden": "Média elrejtve", - "status.mention": "Említés", + "status.mention": "@{name} említése", "status.more": "Többet", "status.mute": "@{name} némítása", "status.mute_conversation": "Beszélgetés némítása", - "status.open": "Státusz kinagyítása", + "status.open": "Tülk kibontása", "status.pin": "Kitűzés a profilra", - "status.pinned": "Pinned toot", - "status.read_more": "Read more", - "status.reblog": "Reblog", - "status.reblog_private": "Boost to original audience", - "status.reblogged_by": "{name} reblogolta", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", - "status.redraft": "Delete & re-draft", + "status.pinned": "Kitűzött tülk", + "status.read_more": "Bővebben", + "status.reblog": "Megtolás", + "status.reblog_private": "Megtolás az eredeti közönségnek", + "status.reblogged_by": "{name} megtolta", + "status.reblogs.empty": "Senki sem tolta még meg ezt a tülköt. Ha valaki megteszi, itt fog megjelenni.", + "status.redraft": "Törlés és újraírás", "status.reply": "Válasz", - "status.replyAll": "Válaszolj a beszélgetésre", - "status.report": "Report @{name}", + "status.replyAll": "Válasz a beszélgetésre", + "status.report": "@{name} jelentése", "status.sensitive_warning": "Érzékeny tartalom", "status.share": "Megosztás", - "status.show_less": "Kevesebb", - "status.show_less_all": "Show less for all", + "status.show_less": "Kevesebb megjelenítése", + "status.show_less_all": "Kevesebbet mindenhol", "status.show_more": "Többet", - "status.show_more_all": "Show more for all", - "status.show_thread": "Show thread", - "status.unmute_conversation": "Beszélgetés némításának elvonása", - "status.unpin": "Kitűzés eltávolítása a profilról", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", - "tabs_bar.federated_timeline": "Federált", + "status.show_more_all": "Többet mindenhol", + "status.show_thread": "Szál mutatása", + "status.uncached_media_warning": "Nem érhető el", + "status.unmute_conversation": "Beszélgetés némításának kikapcsolása", + "status.unpin": "Kitűzés eltávolítása a profilodról", + "suggestions.dismiss": "Javaslat elvetése", + "suggestions.header": "Esetleg érdekelhet…", + "tabs_bar.federated_timeline": "Föderációs", "tabs_bar.home": "Kezdőlap", - "tabs_bar.local_timeline": "Local", + "tabs_bar.local_timeline": "Helyi", "tabs_bar.notifications": "Értesítések", - "tabs_bar.search": "Search", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", - "ui.beforeunload": "A piszkozata el fog vesztődni ha elhagyja Mastodon-t.", - "upload_area.title": "Húzza ide a feltöltéshez", - "upload_button.label": "Média hozzáadása", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", - "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", + "tabs_bar.search": "Keresés", + "time_remaining.days": "{number, plural, one {# nap} other {# nap}} van hátra", + "time_remaining.hours": "{number, plural, one {# óra} other {# óra}} van hátra", + "time_remaining.minutes": "{number, plural, one {# perc} other {# perc}} van hátra", + "time_remaining.moments": "Pillanatok vannak hátra", + "time_remaining.seconds": "{number, plural, one {# másodperc} other {# másodperc}} van hátra", + "trends.count_by_accounts": "{count} {rawCount, plural, one {résztvevő} other {résztvevő}} beszélget", + "trends.trending_now": "Most felkapott", + "ui.beforeunload": "A piszkozatod el fog veszni, ha elhagyod a Mastodont.", + "upload_area.title": "Húzd ide a feltöltéshez", + "upload_button.label": "Média hozzáadása ({formats})", + "upload_error.limit": "Túllépted a fájlfeltöltési korlátot.", + "upload_error.poll": "Szavazásnál nem lehet fájlt feltölteni.", + "upload_form.description": "Leírás látáskorlátozottak számára", + "upload_form.edit": "Szerkesztés", "upload_form.undo": "Mégsem", - "upload_progress.label": "Uploading...", - "video.close": "Close video", - "video.exit_fullscreen": "Exit full screen", - "video.expand": "Expand video", - "video.fullscreen": "Full screen", - "video.hide": "Hide video", - "video.mute": "Mute sound", + "upload_modal.analyzing_picture": "Kép elemzése…", + "upload_modal.apply": "Alkalmaz", + "upload_modal.description_placeholder": "A gyors, barna róka átugrik a lusta kutya fölött", + "upload_modal.detect_text": "Szöveg felismerése a képről", + "upload_modal.edit_media": "Média szerkesztése", + "upload_modal.hint": "Kattints vagy húzd a kört az előnézetben arra a fókuszpontra, mely minden megjelenített bélyegképen látható kell, legyen.", + "upload_modal.preview_label": "Előnézet ({ratio})", + "upload_progress.label": "Feltöltés...", + "video.close": "Videó bezárása", + "video.exit_fullscreen": "Kilépés teljes képernyőből", + "video.expand": "Videó nagyítása", + "video.fullscreen": "Teljes képernyő", + "video.hide": "Videó elrejtése", + "video.mute": "Hang némítása", "video.pause": "Szünet", "video.play": "Lejátszás", - "video.unmute": "Hang kinémítása" + "video.unmute": "Hang némításának vége" } diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index b2dc16a48..a4c155dfe 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -1,20 +1,22 @@ { "account.add_or_remove_from_list": "Add or Remove from lists", - "account.badges.bot": "Bot", + "account.badges.bot": "Բոտ", "account.block": "Արգելափակել @{name}֊ին", "account.block_domain": "Թաքցնել ամենը հետեւյալ տիրույթից՝ {domain}", "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domain hidden", "account.edit_profile": "Խմբագրել անձնական էջը", "account.endorse": "Feature on profile", "account.follow": "Հետեւել", - "account.followers": "Հետեւվողներ", + "account.followers": "Հետեւողներ", "account.followers.empty": "No one follows this user yet.", "account.follows": "Հետեւում է", "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "Հետեւում է քեզ", "account.hide_reblogs": "Թաքցնել @{name}֊ի տարածածները", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Մեդիա", @@ -23,6 +25,7 @@ "account.mute": "Լռեցնել @{name}֊ին", "account.mute_notifications": "Անջատել ծանուցումները @{name}֊ից", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Գրառումներ", "account.posts_with_replies": "Toots with replies", "account.report": "Բողոքել @{name}֊ից", @@ -35,8 +38,11 @@ "account.unfollow": "Չհետեւել", "account.unmute": "Ապալռեցնել @{name}֊ին", "account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", - "alert.unexpected.title": "Oops!", + "alert.unexpected.title": "Վա՜յ", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Կարող ես սեղմել {combo}՝ սա հաջորդ անգամ բաց թողնելու համար", "bundle_column_error.body": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանվեց։", "bundle_column_error.retry": "Կրկին փորձել", @@ -47,6 +53,7 @@ "column.blocks": "Արգելափակված օգտատերեր", "column.community": "Տեղական հոսք", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "Հավանածներ", "column.follow_requests": "Հետեւելու հայցեր", @@ -64,9 +71,9 @@ "column_header.show_settings": "Ցուցադրել կարգավորումները", "column_header.unpin": "Հանել", "column_subheading.settings": "Կարգավորումներ", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.direct_message_warning_learn_more": "Իմանալ ավելին", "compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։", "compose_form.lock_disclaimer": "Քո հաշիվը {locked} չէ։ Յուրաքանչյուր ոք կարող է հետեւել քեզ եւ տեսնել միայն հետեւողների համար նախատեսված գրառումները։", "compose_form.lock_disclaimer.lock": "փակ", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Վստա՞հ ես, որ ուզում ես մշտապես ջնջել այս ցանկը։", "confirmations.domain_block.confirm": "Թաքցնել ամբողջ տիրույթը", "confirmations.domain_block.message": "Հաստատ֊հաստա՞տ վստահ ես, որ ուզում ես արգելափակել ամբողջ {domain} տիրույթը։ Սովորաբար մի երկու թիրախավորված արգելափակում կամ լռեցում բավական է ու նախընտրելի։", + "confirmations.logout.confirm": "Ելք", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Լռեցնել", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Վստա՞հ ես, որ ուզում ես {name}֊ին լռեցնել։", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Ապահետեւել", "confirmations.unfollow.message": "Վստա՞հ ես, որ ուզում ես այլեւս չհետեւել {name}֊ին։", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Այս թութը քո կայքում ներդնելու համար կարող ես պատճենել ներքոհիշյալ կոդը։", "embed.preview": "Ահա, թե ինչ տեսք կունենա այն՝", "emoji_button.activity": "Զբաղմունքներ", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "Ոչ մի ծանուցում դեռ չունես։ Բզիր մյուսներին՝ խոսակցությունը սկսելու համար։", "empty_column.public": "Այստեղ բան չկա՛։ Հրապարակային մի բան գրիր կամ հետեւիր այլ հանգույցներից էակների՝ այն լցնելու համար։", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Վավերացնել", "follow_request.reject": "Մերժել", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Նոր ցանկի վերնագիր", "lists.search": "Փնտրել քո հետեւած մարդկանց մեջ", "lists.subheading": "Քո ցանկերը", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Բեռնվում է…", "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել", "missing_indicator.label": "Չգտնվեց", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Անձնական", "navigation_bar.pins": "Ամրացված թթեր", "navigation_bar.preferences": "Նախապատվություններ", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Դաշնային հոսք", "navigation_bar.security": "Անվտանգություն", "notification.favourite": "{name} հավանեց թութդ", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Կարգավորել թթի գաղտնիությունը", @@ -290,6 +314,7 @@ "privacy.public.short": "Հրապարակային", "privacy.unlisted.long": "Չթթել հրապարակային հոսքերում", "privacy.unlisted.short": "Ծածուկ", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}օր", @@ -311,9 +336,10 @@ "search_popout.tips.status": "թութ", "search_popout.tips.text": "Հասարակ տեքստը կվերադարձնի համընկնող անուններ, օգտանուններ ու պիտակներ", "search_popout.tips.user": "օգտատեր", - "search_results.accounts": "People", + "search_results.accounts": "Մարդիկ", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {արդյունք} other {արդյունք}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Ավելին", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Ապալռեցնել խոսակցությունը", "status.unpin": "Հանել անձնական էջից", "suggestions.dismiss": "Dismiss suggestion", @@ -360,21 +387,29 @@ "tabs_bar.home": "Հիմնական", "tabs_bar.local_timeline": "Տեղական", "tabs_bar.notifications": "Ծանուցումներ", - "tabs_bar.search": "Search", + "tabs_bar.search": "Փնտրել", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։", "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար", "upload_button.label": "Ավելացնել մեդիա", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Նկարագրություն ավելացրու տեսողական խնդիրներ ունեցողների համար", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Հետարկել", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Վերբեռնվում է…", "video.close": "Փակել տեսագրությունը", "video.exit_fullscreen": "Անջատել լիաէկրան դիտումը", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 07ce0eb98..167c2a766 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -1,29 +1,32 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", + "account.add_or_remove_from_list": "Tambah atau Hapus dari daftar", "account.badges.bot": "Bot", "account.block": "Blokir @{name}", "account.block_domain": "Sembunyikan segalanya dari {domain}", "account.blocked": "Terblokir", + "account.cancel_follow_request": "Batalkan permintaan ikuti", "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domain disembunyikan", "account.edit_profile": "Ubah profil", - "account.endorse": "Feature on profile", + "account.endorse": "Tampilkan di profil", "account.follow": "Ikuti", "account.followers": "Pengikut", - "account.followers.empty": "No one follows this user yet.", + "account.followers.empty": "Tidak ada satupun yang mengkuti pengguna ini saat ini.", "account.follows": "Mengikuti", - "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows.empty": "Pengguna ini belum mengikuti siapapun.", "account.follows_you": "Mengikuti anda", "account.hide_reblogs": "Sembunyikan boosts dari @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.last_status": "Terakhir aktif", + "account.link_verified_on": "Kepemilikan tautan ini telah dicek pada {date}", + "account.locked_info": "Status privasi akun ini disetel untuk dikunci. Pemilik secara manual meninjau siapa yang dapat mengikuti mereka.", "account.media": "Media", "account.mention": "Balasan @{name}", "account.moved_to": "{name} telah pindah ke:", "account.mute": "Bisukan @{name}", "account.mute_notifications": "Sembunyikan notifikasi dari @{name}", "account.muted": "Dibisukan", - "account.posts": "Toots", + "account.never_active": "Tak pernah", + "account.posts": "Toot", "account.posts_with_replies": "Postingan dengan balasan", "account.report": "Laporkan @{name}", "account.requested": "Menunggu persetujuan. Klik untuk membatalkan permintaan", @@ -31,23 +34,27 @@ "account.show_reblogs": "Tampilkan boost dari @{name}", "account.unblock": "Hapus blokir @{name}", "account.unblock_domain": "Tampilkan {domain}", - "account.unendorse": "Don't feature on profile", + "account.unendorse": "Jangan tampilkan di profil", "account.unfollow": "Berhenti mengikuti", "account.unmute": "Berhenti membisukan @{name}", "account.unmute_notifications": "Munculkan notifikasi dari @{name}", - "alert.unexpected.message": "An unexpected error occurred.", + "alert.rate_limited.message": "Tolong ulangi setelah {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "Terjadi kesalahan yang tidak terduga.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per minggu", "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini", "bundle_column_error.body": "Kesalahan terjadi saat memuat komponen ini.", "bundle_column_error.retry": "Coba lagi", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "Kesalahan jaringan", "bundle_modal_error.close": "Tutup", "bundle_modal_error.message": "Kesalahan terjadi saat memuat komponen ini.", "bundle_modal_error.retry": "Coba lagi", "column.blocks": "Pengguna diblokir", "column.community": "Linimasa Lokal", - "column.direct": "Direct messages", - "column.domain_blocks": "Hidden domains", + "column.direct": "Pesan langsung", + "column.directory": "Jelajahi profil", + "column.domain_blocks": "Topik tersembunyi", "column.favourites": "Favorit", "column.follow_requests": "Permintaan mengikuti", "column.home": "Beranda", @@ -64,43 +71,54 @@ "column_header.show_settings": "Tampilkan pengaturan", "column_header.unpin": "Lepaskan", "column_subheading.settings": "Pengaturan", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Hanya media", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.direct_message_warning_learn_more": "Pelajari selengkapnya", "compose_form.hashtag_warning": "Toot ini tidak akan ada dalam daftar tagar manapun karena telah di set sebagai tidak terdaftar. Hanya postingan publik yang bisa dicari dengan tagar.", "compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.", "compose_form.lock_disclaimer.lock": "terkunci", "compose_form.placeholder": "Apa yang ada di pikiran anda?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Tambahkan pilihan", + "compose_form.poll.duration": "Durasi jajak pendapat", + "compose_form.poll.option_placeholder": "Pilihan {number}", + "compose_form.poll.remove_option": "Hapus opsi ini", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Tandai sebagai media sensitif", "compose_form.sensitive.marked": "Sumber ini telah ditandai sebagai sumber sensitif.", "compose_form.sensitive.unmarked": "Sumber ini tidak ditandai sebagai sumber sensitif", "compose_form.spoiler.marked": "Teks disembunyikan dibalik peringatan", "compose_form.spoiler.unmarked": "Teks tidak tersembunyi", "compose_form.spoiler_placeholder": "Peringatan konten", "confirmation_modal.cancel": "Batal", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Blokir & Laporkan", "confirmations.block.confirm": "Blokir", "confirmations.block.message": "Apa anda yakin ingin memblokir {name}?", "confirmations.delete.confirm": "Hapus", "confirmations.delete.message": "Apa anda yakin untuk menghapus status ini?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Hapus", "confirmations.delete_list.message": "Apakah anda yakin untuk menghapus daftar ini secara permanen?", "confirmations.domain_block.confirm": "Sembunyikan keseluruhan domain", "confirmations.domain_block.message": "Apakah anda benar benar yakin untuk memblokir keseluruhan {domain}? Dalam kasus tertentu beberapa pemblokiran atau penyembunyian lebih baik.", + "confirmations.logout.confirm": "Keluar", + "confirmations.logout.message": "Apakah anda yakin ingin keluar?", "confirmations.mute.confirm": "Bisukan", + "confirmations.mute.explanation": "Ini akan menyembunyikan pos dari mereka dan pos yang menyebut mereka, tapi ini tetap mengizinkan mereka melihat posmu dan mengikutimu.", "confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?", - "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.confirm": "Hapus dan konsep ulang", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.reply.confirm": "Balas", + "confirmations.reply.message": "Membalas sekarang akan menimpa pesan yang sedang Anda buat. Anda yakin ingin melanjutkan?", "confirmations.unfollow.confirm": "Berhenti mengikuti", "confirmations.unfollow.message": "Apakah anda ingin berhenti mengikuti {name}?", + "conversation.delete": "Hapus percakapan", + "conversation.mark_as_read": "Tandai sudah dibaca", + "conversation.open": "Lihat percakapan", + "conversation.with": "Dengan {names}", + "directory.federated": "Dari fediverse yang dikenal", + "directory.local": "Dari {domain} saja", + "directory.new_arrivals": "Yang baru datang", + "directory.recently_active": "Baru-baru ini aktif", "embed.instructions": "Sematkan status ini di website anda dengan menyalin kode di bawah ini.", "embed.preview": "Seperti ini nantinya:", "emoji_button.activity": "Aktivitas", @@ -117,170 +135,176 @@ "emoji_button.search_results": "Hasil pencarian", "emoji_button.symbols": "Simbol", "emoji_button.travel": "Tempat Wisata", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.account_timeline": "Tidak ada toot di sini!", + "empty_column.account_unavailable": "Profil tidak tersedia", + "empty_column.blocks": "Anda belum memblokir siapapun.", "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.direct": "Anda belum memiliki pesan langsung. Ketika Anda mengirim atau menerimanya, maka akan muncul di sini.", + "empty_column.domain_blocks": "Tidak ada topik tersembunyi.", + "empty_column.favourited_statuses": "Anda belum memiliki toot favorit. Ketika Anda mengirim atau menerimanya, maka akan muncul di sini.", + "empty_column.favourites": "Tidak ada seorangpun yang memfavoritkan toot ini. Ketika seseorang melakukannya, maka akan muncul disini.", + "empty_column.follow_requests": "Anda belum memiliki permintaan mengikuti. Ketika Anda menerimanya, maka akan muncul disini.", "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.", "empty_column.home": "Linimasa anda kosong! Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.", "empty_column.home.public_timeline": "linimasa publik", "empty_column.list": "Tidak ada postingan di list ini. Ketika anggota dari list ini memposting status baru, status tersebut akan tampil disini.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", + "empty_column.lists": "Anda belum memiliki daftar. Ketika Anda membuatnya, maka akan muncul disini.", + "empty_column.mutes": "Anda belum membisukan siapapun.", "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.", "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisi ini", + "error.unexpected_crash.explanation": "Karena kutu pada kode kami atau isu kompatibilitas peramban, halaman tak dapat ditampilkan dengan benar.", + "error.unexpected_crash.next_steps": "Coba segarkan halaman. Jika tak membantu, Anda masih bisa memakai Mastodon dengan peramban berbeda atau aplikasi native.", + "errors.unexpected_crash.copy_stacktrace": "Salin stacktrace ke papan klip", + "errors.unexpected_crash.report_issue": "Laporkan masalah", "follow_request.authorize": "Izinkan", "follow_request.reject": "Tolak", - "getting_started.developers": "Developers", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", + "getting_started.developers": "Pengembang", + "getting_started.directory": "Direktori profil", + "getting_started.documentation": "Dokumentasi", "getting_started.heading": "Mulai", - "getting_started.invite": "Invite people", + "getting_started.invite": "Undang orang", "getting_started.open_source_notice": "Mastodon adalah perangkat lunak yang bersifat terbuka. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Github {github}.", - "getting_started.security": "Security", - "getting_started.terms": "Terms of service", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", + "getting_started.security": "Keamanan", + "getting_started.terms": "Ketentuan layanan", + "hashtag.column_header.tag_mode.all": "dan {additional}", + "hashtag.column_header.tag_mode.any": "atau {additional}", + "hashtag.column_header.tag_mode.none": "tanpa {additional}", + "hashtag.column_settings.select.no_options_message": "Tidak ada saran yang ditemukan", + "hashtag.column_settings.select.placeholder": "Masukkan tagar…", + "hashtag.column_settings.tag_mode.all": "Semua ini", + "hashtag.column_settings.tag_mode.any": "Semua ini", + "hashtag.column_settings.tag_mode.none": "Tak satu pun", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", "home.column_settings.basic": "Dasar", "home.column_settings.show_reblogs": "Tampilkan boost", "home.column_settings.show_replies": "Tampilkan balasan", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", - "introduction.federation.action": "Next", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "intervals.full.days": "{number, plural, other {# hari}}", + "intervals.full.hours": "{number, plural, other {# jam}}", + "intervals.full.minutes": "{number, plural, other {# menit}}", + "introduction.federation.action": "Selanjutnya", + "introduction.federation.federated.headline": "Gabungan", + "introduction.federation.federated.text": "Pos publik dari server fediverse lain akan muncul di linimasa gabungan.", + "introduction.federation.home.headline": "Beranda", + "introduction.federation.home.text": "Pos dari orang yang Anda ikuti akan muncul di beranda. Anda dapat mengikuti siapa pun dari server mana pun!", + "introduction.federation.local.headline": "Lokal", + "introduction.federation.local.text": "Pos publik dari orang yang ada di server sama denganmu akan muncul di linimasa lokal.", "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.favourite.headline": "Favorit", + "introduction.interactions.favourite.text": "Anda dapat menyimpan toot untuk dibaca nanti, biarkan penulis tahu Anda menyukainya, dengan memfavoritkannya.", "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "introduction.interactions.reblog.text": "Anda dapat membagikan toot orang lain kepada pengikut Anda dengan mem-boost-nya.", + "introduction.interactions.reply.headline": "Balas", + "introduction.interactions.reply.text": "Anda dapat membalas toot Anda dan orang lain, yang akan menjalin dalam satu percakapan.", + "introduction.welcome.action": "Ayo!", + "introduction.welcome.headline": "Langkah pertama", + "introduction.welcome.text": "Selamat datang di fediverse! Beberapa saat lagi, Anda dapat menyiarkan pesan dan berbincang dengan teman lintas server. Namun server ini, {domain}, spesial--ia menyimpan profil Anda, jadi ingatlah namanya.", "keyboard_shortcuts.back": "untuk kembali", - "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.blocked": "buka daftar pengguna terblokir", "keyboard_shortcuts.boost": "untuk menyebarkan", "keyboard_shortcuts.column": "untuk fokus kepada sebuah status di sebuah kolom", "keyboard_shortcuts.compose": "untuk fokus ke area penulisan", "keyboard_shortcuts.description": "Deskripsi", - "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.direct": "buka kolom pesan langsung", "keyboard_shortcuts.down": "untuk pindah ke bawah dalam sebuah daftar", "keyboard_shortcuts.enter": "untuk membuka status", "keyboard_shortcuts.favourite": "untuk memfavoritkan", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.favourites": "buka daftar favorit", + "keyboard_shortcuts.federated": "buka linimasa gabungan", "keyboard_shortcuts.heading": "Pintasan keyboard", - "keyboard_shortcuts.home": "to open home timeline", - "keyboard_shortcuts.hotkey": "Hotkey", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.home": "buka linimasa beranda", + "keyboard_shortcuts.hotkey": "Pintasan", + "keyboard_shortcuts.legend": "tampilkan legenda ini", + "keyboard_shortcuts.local": "buka linimasa lokal", + "keyboard_shortcuts.mention": "sebut pencipta", + "keyboard_shortcuts.muted": "buka daftar pengguna terbisukan", + "keyboard_shortcuts.my_profile": "buka profil Anda", + "keyboard_shortcuts.notifications": "buka kolom notifikasi", + "keyboard_shortcuts.pinned": "buka daftar toot tersemat", + "keyboard_shortcuts.profile": "buka profil pencipta", + "keyboard_shortcuts.reply": "balas", + "keyboard_shortcuts.requests": "buka daftar permintaan ikuti", "keyboard_shortcuts.search": "untuk fokus mencari", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "to start a brand new toot", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", - "keyboard_shortcuts.up": "to move up in the list", + "keyboard_shortcuts.start": "buka kolom \"memulai\"", + "keyboard_shortcuts.toggle_hidden": "tampilkan/sembunyikan teks di belakang CW", + "keyboard_shortcuts.toggle_sensitivity": "tampilkan/sembunyikan media", + "keyboard_shortcuts.toot": "mulai toot baru", + "keyboard_shortcuts.unfocus": "untuk tidak fokus pada area teks/pencarian", + "keyboard_shortcuts.up": "untuk memindah ke atas pada daftar", "lightbox.close": "Tutup", - "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", - "lists.edit": "Edit list", - "lists.edit.submit": "Change title", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lightbox.next": "Selanjutnya", + "lightbox.previous": "Sebelumnya", + "lightbox.view_context": "Lihat konteks", + "lists.account.add": "Tambah ke daftar", + "lists.account.remove": "Hapus dari daftar", + "lists.delete": "Hapus daftar", + "lists.edit": "Sunting daftar", + "lists.edit.submit": "Ubah judul", + "lists.new.create": "Tambah daftar", + "lists.new.title_placeholder": "Judul daftar baru", + "lists.search": "Cari di antara orang yang Anda ikuti", + "lists.subheading": "Daftar Anda", + "load_pending": "{count, plural, other {# item baru}}", "loading_indicator.label": "Tunggu sebentar...", "media_gallery.toggle_visible": "Tampil/Sembunyikan", "missing_indicator.label": "Tidak ditemukan", - "missing_indicator.sublabel": "This resource could not be found", - "mute_modal.hide_notifications": "Hide notifications from this user?", - "navigation_bar.apps": "Mobile apps", + "missing_indicator.sublabel": "Sumber daya tak bisa ditemukan", + "mute_modal.hide_notifications": "Sembunyikan notifikasi dari pengguna ini?", + "navigation_bar.apps": "Aplikasi mobile", "navigation_bar.blocks": "Pengguna diblokir", "navigation_bar.community_timeline": "Linimasa lokal", - "navigation_bar.compose": "Compose new toot", - "navigation_bar.direct": "Direct messages", - "navigation_bar.discover": "Discover", - "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.compose": "Tulis toot baru", + "navigation_bar.direct": "Pesan langsung", + "navigation_bar.discover": "Temukan", + "navigation_bar.domain_blocks": "Domain tersembunyi", "navigation_bar.edit_profile": "Ubah profil", "navigation_bar.favourites": "Favorit", - "navigation_bar.filters": "Muted words", + "navigation_bar.filters": "Kata yang dibisukan", "navigation_bar.follow_requests": "Permintaan mengikuti", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Ikuti dan pengikut", "navigation_bar.info": "Informasi selengkapnya", "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", - "navigation_bar.lists": "Lists", + "navigation_bar.lists": "Daftar", "navigation_bar.logout": "Keluar", "navigation_bar.mutes": "Pengguna dibisukan", "navigation_bar.personal": "Personal", - "navigation_bar.pins": "Pinned toots", + "navigation_bar.pins": "Toot tersemat", "navigation_bar.preferences": "Pengaturan", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Linimasa gabungan", - "navigation_bar.security": "Security", + "navigation_bar.security": "Keamanan", "notification.favourite": "{name} menyukai status anda", "notification.follow": "{name} mengikuti anda", - "notification.mention": "{name} mentioned you", - "notification.poll": "A poll you have voted in has ended", + "notification.mention": "{name} menyebut Anda", + "notification.poll": "Japat yang Anda ikuti telah berakhir", "notification.reblog": "{name} mem-boost status anda", "notifications.clear": "Hapus notifikasi", "notifications.clear_confirmation": "Apa anda yakin hendak menghapus semua notifikasi anda?", "notifications.column_settings.alert": "Notifikasi desktop", "notifications.column_settings.favourite": "Favorit:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Tampilkan semua kategori", + "notifications.column_settings.filter_bar.category": "Bilah penyaring cepat", + "notifications.column_settings.filter_bar.show": "Tampilkan", "notifications.column_settings.follow": "Pengikut baru:", "notifications.column_settings.mention": "Balasan:", - "notifications.column_settings.poll": "Poll results:", - "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.poll": "Hasil japat:", + "notifications.column_settings.push": "Notifikasi dorong", "notifications.column_settings.reblog": "Boost:", "notifications.column_settings.show": "Tampilkan dalam kolom", "notifications.column_settings.sound": "Mainkan suara", - "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.group": "{count} notifications", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "notifications.filter.all": "Semua", + "notifications.filter.boosts": "Boost", + "notifications.filter.favourites": "Favorit", + "notifications.filter.follows": "Diikuti", + "notifications.filter.mentions": "Sebutan", + "notifications.filter.polls": "Hasil japat", + "notifications.group": "{count} notifikasi", + "poll.closed": "Ditutup", + "poll.refresh": "Segarkan", + "poll.total_people": "{count, plural, other {# orang}}", + "poll.total_votes": "{count, plural, other {# suara}}", + "poll.vote": "Memilih", + "poll.voted": "Anda memilih jawaban ini", + "poll_button.add_poll": "Tambah japat", + "poll_button.remove_poll": "Hapus japat", "privacy.change": "Tentukan privasi status", "privacy.direct.long": "Kirim hanya ke pengguna yang disebut", "privacy.direct.short": "Langsung", @@ -290,99 +314,110 @@ "privacy.public.short": "Publik", "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik", "privacy.unlisted.short": "Tak Terdaftar", - "regeneration_indicator.label": "Loading…", + "refresh": "Segarkan", + "regeneration_indicator.label": "Memuat…", "regeneration_indicator.sublabel": "Linimasa anda sedang disiapkan!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", - "relative_time.just_now": "now", - "relative_time.minutes": "{number}m", - "relative_time.seconds": "{number}s", + "relative_time.days": "{number}h", + "relative_time.hours": "{number}j", + "relative_time.just_now": "sekarang", + "relative_time.minutes": "{number}b", + "relative_time.seconds": "{number}d", "reply_indicator.cancel": "Batal", - "report.forward": "Forward to {target}", - "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.forward": "Teruskan ke {target}", + "report.forward_hint": "Akun dari server lain. Kirim salinan laporan scr anonim ke sana?", "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Komentar tambahan", "report.submit": "Kirim", "report.target": "Melaporkan", "search.placeholder": "Pencarian", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.search_format": "Format pencarian mahir", + "search_popout.tips.full_text": "Teks simpel menampilkan status yang Anda tulis, favoritkan, boost-kan, atau status yang menyebut Anda, serta nama pengguna, nama yang ditampilkan, dan tagar yang cocok.", "search_popout.tips.hashtag": "tagar", "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", - "search_results.accounts": "People", - "search_results.hashtags": "Hashtags", - "search_results.statuses": "Toots", + "search_popout.tips.text": "Teks sederhana menampilkan nama yang ditampilkan, nama pengguna, dan tagar yang cocok", + "search_popout.tips.user": "pengguna", + "search_results.accounts": "Orang", + "search_results.hashtags": "Tagar", + "search_results.statuses": "Toot", + "search_results.statuses_fts_disabled": "Pencarian toot berdasarkan konten tidak diaktifkan di server Mastadon ini.", "search_results.total": "{count, number} {count, plural, one {hasil} other {hasil}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Unboost", - "status.cannot_reblog": "This post cannot be boosted", - "status.copy": "Copy link to status", + "status.admin_account": "Buka antar muka moderasi untuk @{name}", + "status.admin_status": "Buka status ini dalam antar muka moderasi", + "status.block": "Blokir @{name}", + "status.cancel_reblog_private": "Batalkan boost", + "status.cannot_reblog": "Pos ini tak dapat di-boost", + "status.copy": "Salin tautan ke status", "status.delete": "Hapus", - "status.detailed_status": "Detailed conversation view", - "status.direct": "Direct message @{name}", - "status.embed": "Embed", + "status.detailed_status": "Tampilan detail percakapan", + "status.direct": "Pesan langsung @{name}", + "status.embed": "Tanam", "status.favourite": "Difavoritkan", - "status.filtered": "Filtered", + "status.filtered": "Disaring", "status.load_more": "Tampilkan semua", "status.media_hidden": "Media disembunyikan", "status.mention": "Balasan @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", - "status.mute_conversation": "Mute conversation", + "status.more": "Lebih banyak", + "status.mute": "Bisukan @{name}", + "status.mute_conversation": "Bisukan percakapan", "status.open": "Tampilkan status ini", - "status.pin": "Pin on profile", - "status.pinned": "Pinned toot", - "status.read_more": "Read more", + "status.pin": "Sematkan pada profil", + "status.pinned": "Toot tersemat", + "status.read_more": "Baca lebih banyak", "status.reblog": "Boost", - "status.reblog_private": "Boost to original audience", + "status.reblog_private": "Boost ke audiens asli", "status.reblogged_by": "di-boost {name}", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", - "status.redraft": "Delete & re-draft", + "status.reblogs.empty": "Belum ada yang mem-boost toot ini. Ketika seseorang melakukannya, maka akan muncul di sini.", + "status.redraft": "Hapus & redraf", "status.reply": "Balas", "status.replyAll": "Balas ke semua", "status.report": "Laporkan @{name}", "status.sensitive_warning": "Konten sensitif", - "status.share": "Share", + "status.share": "Bagikan", "status.show_less": "Tampilkan lebih sedikit", "status.show_less_all": "Show less for all", "status.show_more": "Tampilkan semua", "status.show_more_all": "Show more for all", - "status.show_thread": "Show thread", - "status.unmute_conversation": "Unmute conversation", - "status.unpin": "Unpin from profile", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "status.show_thread": "Tampilkan utas", + "status.uncached_media_warning": "Tak tersedia", + "status.unmute_conversation": "Bunyikan percakapan", + "status.unpin": "Hapus sematan dari profil", + "suggestions.dismiss": "Hentikan saran", + "suggestions.header": "Anda mungkin tertarik dg…", "tabs_bar.federated_timeline": "Gabungan", "tabs_bar.home": "Beranda", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Notifikasi", - "tabs_bar.search": "Search", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "tabs_bar.search": "Cari", + "time_remaining.days": "{number, plural, other {# hari}} tersisa", + "time_remaining.hours": "{number, plural, other {# jam}} tersisa", + "time_remaining.minutes": "{number, plural, other {# menit}} tersisa", + "time_remaining.moments": "Momen tersisa", + "time_remaining.seconds": "{number, plural, other {# detik}} tersisa", + "trends.count_by_accounts": "{count} {rawCount, plural, other {orang}} berbicara", + "trends.trending_now": "Sedang tren sekarang", "ui.beforeunload": "Naskah anda akan hilang jika anda keluar dari Mastodon.", "upload_area.title": "Seret & lepaskan untuk mengunggah", "upload_button.label": "Tambahkan media", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.limit": "Batas unggah berkas terlampaui.", + "upload_error.poll": "Unggah berkas tak diizinkan di japat ini.", "upload_form.description": "Deskripsikan untuk mereka yang tidak bisa melihat dengan jelas", - "upload_form.focus": "Potong", + "upload_form.edit": "Sunting", "upload_form.undo": "Undo", + "upload_modal.analyzing_picture": "Analisis gambar…", + "upload_modal.apply": "Terapkan", + "upload_modal.description_placeholder": "Muharjo seorang xenofobia universal yang takut pada warga jazirah, contohnya Qatar", + "upload_modal.detect_text": "Deteksi teks pada gambar", + "upload_modal.edit_media": "Sunting media", + "upload_modal.hint": "Klik atau seret lingkaran pada pratinjau untuk memilih titik fokus yang akan ditampilkan pada semua gambar kecil.", + "upload_modal.preview_label": "Pratinjau ({ratio})", "upload_progress.label": "Mengunggah...", - "video.close": "Close video", + "video.close": "Tutup video", "video.exit_fullscreen": "Keluar dari layar penuh", "video.expand": "Perbesar video", - "video.fullscreen": "Full screen", - "video.hide": "Hide video", - "video.mute": "Mute sound", - "video.pause": "Pause", - "video.play": "Play", - "video.unmute": "Unmute sound" + "video.fullscreen": "Layar penuh", + "video.hide": "Sembunyikan video", + "video.mute": "Bisukan suara", + "video.pause": "Jeda", + "video.play": "Putar", + "video.unmute": "Bunyikan suara" } diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index c3f8707d1..076835218 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -4,6 +4,7 @@ "account.block": "Blokusar @{name}", "account.block_domain": "Hide everything from {domain}", "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domain hidden", "account.edit_profile": "Modifikar profilo", @@ -15,6 +16,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "Sequas tu", "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Celar @{name}", "account.mute_notifications": "Mute notifications from @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Mesaji", "account.posts_with_replies": "Toots with replies", "account.report": "Denuncar @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Ne plus sequar", "account.unmute": "Ne plus celar @{name}", "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo", "bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.retry": "Try again", @@ -47,6 +53,7 @@ "column.blocks": "Blokusita uzeri", "column.community": "Lokala tempolineo", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "Favorati", "column.follow_requests": "Demandi di sequado", @@ -64,7 +71,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Embed this status on your website by copying the code below.", "embed.preview": "Here is what it will look like:", "emoji_button.activity": "Activity", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.", "empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.", + "error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", + "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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Yurizar", "follow_request.reject": "Refuzar", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Kargante...", "media_gallery.toggle_visible": "Chanjar videbleso", "missing_indicator.label": "Ne trovita", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferi", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federata tempolineo", "navigation_bar.security": "Security", "notification.favourite": "{name} favorizis tua mesajo", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Aranjar privateso di mesaji", @@ -290,6 +314,7 @@ "privacy.public.short": "Publike", "privacy.unlisted.long": "Ne montrar en publika tempolinei", "privacy.unlisted.short": "Ne enlistigota", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Montrar plue", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", "suggestions.dismiss": "Dismiss suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Tranar faligar por kargar", "upload_button.label": "Adjuntar kontenajo", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Desfacar", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Kargante...", "video.close": "Close video", "video.exit_fullscreen": "Exit full screen", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index d5e88c4f3..757882523 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -4,7 +4,8 @@ "account.block": "Blocca @{name}", "account.block_domain": "Nascondi tutto da {domain}", "account.blocked": "Bloccato", - "account.direct": "Invia messaggio diretto a @{name}", + "account.cancel_follow_request": "Annulla richiesta di seguito", + "account.direct": "Invia messaggio privato a @{name}", "account.domain_blocked": "Dominio nascosto", "account.edit_profile": "Modifica profilo", "account.endorse": "Metti in evidenza sul profilo", @@ -15,6 +16,7 @@ "account.follows.empty": "Questo utente non segue ancora nessuno.", "account.follows_you": "Ti segue", "account.hide_reblogs": "Nascondi condivisioni da @{name}", + "account.last_status": "Ultima attività", "account.link_verified_on": "La proprietà di questo link è stata controllata il {date}", "account.locked_info": "Il livello di privacy di questo account è impostato a \"bloccato\". Il proprietario esamina manualmente le richieste di seguirlo.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Silenzia @{name}", "account.mute_notifications": "Silenzia notifiche da @{name}", "account.muted": "Silenziato", + "account.never_active": "Mai", "account.posts": "Toot", "account.posts_with_replies": "Toot e risposte", "account.report": "Segnala @{name}", @@ -35,18 +38,22 @@ "account.unfollow": "Non seguire", "account.unmute": "Non silenziare @{name}", "account.unmute_notifications": "Non silenziare più le notifiche da @{name}", + "alert.rate_limited.message": "Riprova dopo {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "Si è verificato un errore inatteso.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per settimana", "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta", "bundle_column_error.body": "E' avvenuto un errore durante il caricamento di questo componente.", "bundle_column_error.retry": "Riprova", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "Errore di rete", "bundle_modal_error.close": "Chiudi", "bundle_modal_error.message": "C'è stato un errore mentre questo componente veniva caricato.", "bundle_modal_error.retry": "Riprova", "column.blocks": "Utenti bloccati", "column.community": "Timeline locale", "column.direct": "Messaggi diretti", + "column.directory": "Sfoglia profili", "column.domain_blocks": "Domini nascosti", "column.favourites": "Apprezzati", "column.follow_requests": "Richieste di amicizia", @@ -66,25 +73,25 @@ "column_subheading.settings": "Impostazioni", "community.column_settings.media_only": "Solo media", "compose_form.direct_message_warning": "Questo toot sarà mandato solo a tutti gli utenti menzionati.", - "compose_form.direct_message_warning_learn_more": "Per saperne di piu'", + "compose_form.direct_message_warning_learn_more": "Per saperne di più", "compose_form.hashtag_warning": "Questo toot non è listato, quindi non sarà trovato nelle ricerche per hashtag. Solo i toot pubblici possono essere cercati per hashtag.", "compose_form.lock_disclaimer": "Il tuo account non è {bloccato}. Chiunque può decidere di seguirti per vedere i tuoi post per soli seguaci.", "compose_form.lock_disclaimer.lock": "bloccato", "compose_form.placeholder": "A cosa stai pensando?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Aggiungi una scelta", + "compose_form.poll.duration": "Durata del sondaggio", + "compose_form.poll.option_placeholder": "Scelta {number}", + "compose_form.poll.remove_option": "Rimuovi questa scelta", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Segna media come sensibile", "compose_form.sensitive.marked": "Questo media è contrassegnato come sensibile", "compose_form.sensitive.unmarked": "Questo media non è contrassegnato come sensibile", "compose_form.spoiler.marked": "Il testo è nascosto dall'avviso", "compose_form.spoiler.unmarked": "Il testo non è nascosto", "compose_form.spoiler_placeholder": "Content warning", "confirmation_modal.cancel": "Annulla", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Blocca & Segnala", "confirmations.block.confirm": "Blocca", "confirmations.block.message": "Sei sicuro di voler bloccare {name}?", "confirmations.delete.confirm": "Cancella", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Sei sicuro di voler cancellare definitivamente questa lista?", "confirmations.domain_block.confirm": "Nascondi intero dominio", "confirmations.domain_block.message": "Sei davvero sicuro che vuoi bloccare l'intero {domain}? Nella maggior parte dei casi, pochi blocchi o silenziamenti mirati sono sufficienti e preferibili. Non vedrai nessun contenuto di quel dominio né nelle timeline pubbliche né nelle notifiche. I tuoi seguaci di quel dominio saranno eliminati.", + "confirmations.logout.confirm": "Esci", + "confirmations.logout.message": "Sei sicuro di voler uscire?", "confirmations.mute.confirm": "Silenzia", + "confirmations.mute.explanation": "I post scritti da loro e quelli che li menzionano saranno nascosti, ma loro continueranno a vedere i tuoi post e a poterti seguire.", "confirmations.mute.message": "Sei sicuro di voler silenziare {name}?", "confirmations.redraft.confirm": "Cancella e riscrivi", "confirmations.redraft.message": "Sei sicuro di voler cancellare questo stato e riscriverlo? Perderai tutte le risposte, condivisioni e preferiti.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Se rispondi ora, il messaggio che stai componendo sarà sovrascritto. Sei sicuro di voler continuare?", "confirmations.unfollow.confirm": "Smetti di seguire", "confirmations.unfollow.message": "Sei sicuro che non vuoi più seguire {name}?", + "conversation.delete": "Elimina conversazione", + "conversation.mark_as_read": "Segna come letto", + "conversation.open": "Visualizza conversazione", + "conversation.with": "Con {names}", + "directory.federated": "Da un fediverso noto", + "directory.local": "Solo da {domain}", + "directory.new_arrivals": "Nuovi arrivi", + "directory.recently_active": "Attivo di recente", "embed.instructions": "Inserisci questo status nel tuo sito copiando il codice qui sotto.", "embed.preview": "Ecco come apparirà:", "emoji_button.activity": "Attività", @@ -118,10 +136,10 @@ "emoji_button.symbols": "Simboli", "emoji_button.travel": "Viaggi e luoghi", "empty_column.account_timeline": "Non ci sono toot qui!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "Profilo non disponibile", "empty_column.blocks": "Non hai ancora bloccato nessun utente.", "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!", - "empty_column.direct": "Non hai ancora nessun messaggio diretto. Quando ne manderai o riceverai qualcuno, apparirà qui.", + "empty_column.direct": "Non hai ancora nessun messaggio privato. Quando ne manderai o riceverai qualcuno, apparirà qui.", "empty_column.domain_blocks": "Non vi sono domini nascosti.", "empty_column.favourited_statuses": "Non hai ancora segnato nessun toot come apprezzato. Quando lo farai, comparirà qui.", "empty_column.favourites": "Nessuno ha ancora segnato questo toot come apprezzato. Quando qualcuno lo farà, apparirà qui.", @@ -134,10 +152,14 @@ "empty_column.mutes": "Non hai ancora silenziato nessun utente.", "empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.", "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio", + "error.unexpected_crash.explanation": "A causa di un bug nel nostro codice o di un problema di compatibilità del browser, questa pagina non può essere visualizzata correttamente.", + "error.unexpected_crash.next_steps": "Prova ad aggiornare la pagina. Se non funziona, potresti ancora essere in grado di utilizzare Mastodon attraverso un browser diverso o un'app nativa.", + "errors.unexpected_crash.copy_stacktrace": "Copia stacktrace negli appunti", + "errors.unexpected_crash.report_issue": "Segnala il problema", "follow_request.authorize": "Autorizza", "follow_request.reject": "Rifiuta", "getting_started.developers": "Sviluppatori", - "getting_started.directory": "Directory del profilo", + "getting_started.directory": "Directory dei profili", "getting_started.documentation": "Documentazione", "getting_started.heading": "Come iniziare", "getting_started.invite": "Invita qualcuno", @@ -156,15 +178,15 @@ "home.column_settings.basic": "Semplice", "home.column_settings.show_reblogs": "Mostra post condivisi", "home.column_settings.show_replies": "Mostra risposte", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}", + "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}", "introduction.federation.action": "Avanti", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Federato", "introduction.federation.federated.text": "I post pubblici provenienti da altri server del fediverse saranno mostrati nella timeline federata.", "introduction.federation.home.headline": "Home", "introduction.federation.home.text": "I post scritti da persone che segui saranno mostrati nella timeline home. Puoi seguire chiunque su qualunque server!", - "introduction.federation.local.headline": "Local", + "introduction.federation.local.headline": "Locale", "introduction.federation.local.text": "I post pubblici scritti da persone sul tuo stesso server saranno mostrati nella timeline locale.", "introduction.interactions.action": "Finisci il tutorial!", "introduction.interactions.favourite.headline": "Apprezza", @@ -204,23 +226,24 @@ "keyboard_shortcuts.search": "per spostare il focus sulla ricerca", "keyboard_shortcuts.start": "per aprire la colonna \"Come iniziare\"", "keyboard_shortcuts.toggle_hidden": "per mostrare/nascondere il testo dei CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrare/nascondere media", "keyboard_shortcuts.toot": "per iniziare a scrivere un toot completamente nuovo", "keyboard_shortcuts.unfocus": "per uscire dall'area di composizione o dalla ricerca", "keyboard_shortcuts.up": "per spostarsi in alto nella lista", "lightbox.close": "Chiudi", "lightbox.next": "Successivo", "lightbox.previous": "Precedente", - "lightbox.view_context": "View context", + "lightbox.view_context": "Mostra contesto", "lists.account.add": "Aggiungi alla lista", "lists.account.remove": "Togli dalla lista", - "lists.delete": "Delete list", + "lists.delete": "Elimina lista", "lists.edit": "Modifica lista", "lists.edit.submit": "Cambia titolo", "lists.new.create": "Aggiungi lista", "lists.new.title_placeholder": "Titolo della nuova lista", "lists.search": "Cerca tra le persone che segui", "lists.subheading": "Le tue liste", + "load_pending": "{count, plural, one {# nuovo oggetto} other {# nuovi oggetti}}", "loading_indicator.label": "Caricamento...", "media_gallery.toggle_visible": "Imposta visibilità", "missing_indicator.label": "Non trovato", @@ -237,22 +260,21 @@ "navigation_bar.favourites": "Apprezzati", "navigation_bar.filters": "Parole silenziate", "navigation_bar.follow_requests": "Richieste di amicizia", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Seguiti e seguaci", "navigation_bar.info": "Informazioni su questo server", "navigation_bar.keyboard_shortcuts": "Tasti di scelta rapida", "navigation_bar.lists": "Liste", "navigation_bar.logout": "Esci", "navigation_bar.mutes": "Utenti silenziati", - "navigation_bar.personal": "Personal", + "navigation_bar.personal": "Personale", "navigation_bar.pins": "Toot fissati in cima", "navigation_bar.preferences": "Impostazioni", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Timeline federata", "navigation_bar.security": "Sicurezza", "notification.favourite": "{name} ha apprezzato il tuo post", "notification.follow": "{name} ha iniziato a seguirti", "notification.mention": "{name} ti ha menzionato", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "Un sondaggio in cui hai votato è terminato", "notification.reblog": "{name} ha condiviso il tuo post", "notifications.clear": "Cancella notifiche", "notifications.clear_confirmation": "Vuoi davvero cancellare tutte le notifiche?", @@ -263,7 +285,7 @@ "notifications.column_settings.filter_bar.show": "Mostra", "notifications.column_settings.follow": "Nuovi seguaci:", "notifications.column_settings.mention": "Menzioni:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Risultati del sondaggio:", "notifications.column_settings.push": "Notifiche push", "notifications.column_settings.reblog": "Post condivisi:", "notifications.column_settings.show": "Mostra in colonna", @@ -273,27 +295,30 @@ "notifications.filter.favourites": "Apprezzati", "notifications.filter.follows": "Seguaci", "notifications.filter.mentions": "Menzioni", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "Risultati del sondaggio", "notifications.group": "{count} notifiche", "poll.closed": "Chiuso", "poll.refresh": "Aggiorna", + "poll.total_people": "{count, plural, one {# persona} other {# persone}}", "poll.total_votes": "{count, plural, one {# voto} other {# voti}}", "poll.vote": "Vota", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.voted": "Hai votato per questa risposta", + "poll_button.add_poll": "Aggiungi un sondaggio", + "poll_button.remove_poll": "Rimuovi sondaggio", "privacy.change": "Modifica privacy del post", "privacy.direct.long": "Invia solo a utenti menzionati", - "privacy.direct.short": "Diretto", + "privacy.direct.short": "Diretto in privato", "privacy.private.long": "Invia solo ai seguaci", "privacy.private.short": "Privato", "privacy.public.long": "Invia alla timeline pubblica", "privacy.public.short": "Pubblico", "privacy.unlisted.long": "Non mostrare sulla timeline pubblica", "privacy.unlisted.short": "Non elencato", + "refresh": "Aggiorna", "regeneration_indicator.label": "Caricamento in corso…", "regeneration_indicator.sublabel": "Stiamo preparando il tuo home feed!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", + "relative_time.days": "{number}g", + "relative_time.hours": "{number}o", "relative_time.just_now": "ora", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", @@ -307,13 +332,14 @@ "search.placeholder": "Cerca", "search_popout.search_format": "Formato di ricerca avanzato", "search_popout.tips.full_text": "Testo semplice per trovare gli status che hai scritto, segnato come apprezzati, condiviso o in cui sei stato citato, e inoltre i nomi utente, nomi visualizzati e hashtag che lo contengono.", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", + "search_popout.tips.hashtag": "etichetta", + "search_popout.tips.status": "stato", "search_popout.tips.text": "Testo semplice per trovare nomi visualizzati, nomi utente e hashtag che lo contengono", "search_popout.tips.user": "utente", "search_results.accounts": "Gente", "search_results.hashtags": "Hashtag", "search_results.statuses": "Toot", + "search_results.statuses_fts_disabled": "La ricerca di toot per il loro contenuto non è abilitata su questo server Mastodon.", "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", "status.admin_account": "Apri interfaccia di moderazione per @{name}", "status.admin_status": "Apri questo status nell'interfaccia di moderazione", @@ -323,7 +349,7 @@ "status.copy": "Copia link allo status", "status.delete": "Elimina", "status.detailed_status": "Vista conversazione dettagliata", - "status.direct": "Messaggio diretto @{name}", + "status.direct": "Messaggio privato @{name}", "status.embed": "Incorpora", "status.favourite": "Apprezzato", "status.filtered": "Filtrato", @@ -352,6 +378,7 @@ "status.show_more": "Mostra di più", "status.show_more_all": "Mostra di più per tutti", "status.show_thread": "Mostra thread", + "status.uncached_media_warning": "Non disponibile", "status.unmute_conversation": "Annulla silenzia conversazione", "status.unpin": "Non fissare in cima al profilo", "suggestions.dismiss": "Elimina suggerimento", @@ -367,14 +394,22 @@ "time_remaining.moments": "Restano pochi istanti", "time_remaining.seconds": "{number, plural, one {# secondo} other {# secondi}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona ne sta} other {persone ne stanno}} parlando", + "trends.trending_now": "Di tendenza ora", "ui.beforeunload": "La bozza andrà persa se esci da Mastodon.", "upload_area.title": "Trascina per caricare", "upload_button.label": "Aggiungi file multimediale", "upload_error.limit": "Limite al caricamento di file superato.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.poll": "Caricamento file non consentito nei sondaggi.", "upload_form.description": "Descrizione per utenti con disabilità visive", - "upload_form.focus": "Modifica anteprima", + "upload_form.edit": "Modifica", "upload_form.undo": "Cancella", + "upload_modal.analyzing_picture": "Analisi immagine…", + "upload_modal.apply": "Applica", + "upload_modal.description_placeholder": "Ma la volpe col suo balzo ha raggiunto il quieto Fido", + "upload_modal.detect_text": "Rileva testo dall'immagine", + "upload_modal.edit_media": "Modifica media", + "upload_modal.hint": "Clicca o trascina il cerchio sull'anteprima per scegliere il punto focale che sarà sempre visualizzato su tutte le miniature.", + "upload_modal.preview_label": "Anteprima ({ratio})", "upload_progress.label": "Sto caricando...", "video.close": "Chiudi video", "video.exit_fullscreen": "Esci da modalità a schermo intero", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index bdc1f98fb..fdcbe53a2 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -4,6 +4,7 @@ "account.block": "@{name}さんをブロック", "account.block_domain": "{domain}全体を非表示", "account.blocked": "ブロック済み", + "account.cancel_follow_request": "フォローリクエストを取り消す", "account.direct": "@{name}さんにダイレクトメッセージ", "account.domain_blocked": "ドメイン非表示中", "account.edit_profile": "プロフィール編集", @@ -15,6 +16,7 @@ "account.follows.empty": "まだ誰もフォローしていません。", "account.follows_you": "フォローされています", "account.hide_reblogs": "@{name}さんからのブーストを非表示", + "account.last_status": "最後の活動", "account.link_verified_on": "このリンクの所有権は{date}に確認されました", "account.locked_info": "このアカウントは承認制アカウントです。相手が承認するまでフォローは完了しません。", "account.media": "メディア", @@ -23,6 +25,7 @@ "account.mute": "@{name}さんをミュート", "account.mute_notifications": "@{name}さんからの通知を受け取らない", "account.muted": "ミュート済み", + "account.never_active": "活動なし", "account.posts": "投稿", "account.posts_with_replies": "投稿と返信", "account.report": "@{name}さんを通報", @@ -35,8 +38,11 @@ "account.unfollow": "フォロー解除", "account.unmute": "@{name}さんのミュートを解除", "account.unmute_notifications": "@{name}さんからの通知を受け取るようにする", + "alert.rate_limited.message": "{retry_time, time, medium} 以降に再度実行してください。", + "alert.rate_limited.title": "制限に達しました", "alert.unexpected.message": "不明なエラーが発生しました。", "alert.unexpected.title": "エラー!", + "autosuggest_hashtag.per_week": "{count} 回 / 週", "boost_modal.combo": "次からは{combo}を押せばスキップできます", "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。", "bundle_column_error.retry": "再試行", @@ -47,6 +53,7 @@ "column.blocks": "ブロックしたユーザー", "column.community": "ローカルタイムライン", "column.direct": "ダイレクトメッセージ", + "column.directory": "ディレクトリ", "column.domain_blocks": "非表示にしたドメイン", "column.favourites": "お気に入り", "column.follow_requests": "フォローリクエスト", @@ -97,7 +104,10 @@ "confirmations.delete_list.message": "本当にこのリストを完全に削除しますか?", "confirmations.domain_block.confirm": "ドメイン全体を非表示", "confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。公開タイムラインにそのドメインのコンテンツが表示されなくなり、通知も届かなくなります。そのドメインのフォロワーはアンフォローされます。", + "confirmations.logout.confirm": "ログアウト", + "confirmations.logout.message": "本当にログアウトしますか?", "confirmations.mute.confirm": "ミュート", + "confirmations.mute.explanation": "これにより相手のトゥートと返信は見えなくなりますが、引き続きあなたをフォローしトゥートを見ることはできます。", "confirmations.mute.message": "本当に{name}さんをミュートしますか?", "confirmations.redraft.confirm": "削除して下書きに戻す", "confirmations.redraft.message": "本当にこのトゥートを削除して下書きに戻しますか? このトゥートへのお気に入り登録やブーストは失われ、返信は孤立することになります。", @@ -105,6 +115,14 @@ "confirmations.reply.message": "今返信すると現在作成中のメッセージが上書きされます。本当に実行しますか?", "confirmations.unfollow.confirm": "フォロー解除", "confirmations.unfollow.message": "本当に{name}さんのフォローを解除しますか?", + "conversation.delete": "会話を削除", + "conversation.mark_as_read": "既読にする", + "conversation.open": "会話を表示", + "conversation.with": "{names}", + "directory.federated": "既知の連合より", + "directory.local": "{domain} のみ", + "directory.new_arrivals": "新着順", + "directory.recently_active": "最近の活動順", "embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。", "embed.preview": "表示例:", "emoji_button.activity": "活動", @@ -138,6 +156,10 @@ "empty_column.mutes": "まだ誰もミュートしていません。", "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう", + "error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。", + "error.unexpected_crash.next_steps": "ページの再読み込みをお試しください。それでも解決しない場合、別のブラウザかアプリを使えば使用できることがあります。", + "errors.unexpected_crash.copy_stacktrace": "スタックトレースをクリップボードにコピー", + "errors.unexpected_crash.report_issue": "問題を報告", "follow_request.authorize": "許可", "follow_request.reject": "拒否", "getting_started.developers": "開発", @@ -146,7 +168,7 @@ "getting_started.heading": "スタート", "getting_started.invite": "招待", "getting_started.open_source_notice": "Mastodonはオープンソースソフトウェアです。誰でもGitHub ( {github} ) から開発に参加したり、問題を報告したりできます。", - "getting_started.security": "セキュリティ", + "getting_started.security": "アカウント設定", "getting_started.terms": "プライバシーポリシー", "hashtag.column_header.tag_mode.all": "と {additional}", "hashtag.column_header.tag_mode.any": "か {additional}", @@ -225,6 +247,7 @@ "lists.new.title_placeholder": "新規リスト名", "lists.search": "フォローしている人の中から検索", "lists.subheading": "あなたのリスト", + "load_pending": "{count} 件の新着", "loading_indicator.label": "読み込み中...", "media_gallery.toggle_visible": "表示切り替え", "missing_indicator.label": "見つかりません", @@ -250,7 +273,6 @@ "navigation_bar.personal": "個人用", "navigation_bar.pins": "固定したトゥート", "navigation_bar.preferences": "ユーザー設定", - "navigation_bar.profile_directory": "ディレクトリ", "navigation_bar.public_timeline": "連合タイムライン", "navigation_bar.misc": "その他", "navigation_bar.security": "セキュリティ", @@ -282,8 +304,10 @@ "notifications.group": "{count} 件の通知", "poll.closed": "終了", "poll.refresh": "更新", + "poll.total_people": "{count}人", "poll.total_votes": "{count}票", "poll.vote": "投票", + "poll.voted": "この項目に投票しました", "poll_button.add_poll": "アンケートを追加", "poll_button.remove_poll": "アンケートを削除", "privacy.change": "公開範囲を変更", @@ -295,6 +319,7 @@ "privacy.public.short": "公開", "privacy.unlisted.long": "公開TLで表示しない", "privacy.unlisted.short": "未収載", + "refresh": "更新", "regeneration_indicator.label": "読み込み中…", "regeneration_indicator.sublabel": "ホームタイムラインは準備中です!", "relative_time.days": "{number}日前", @@ -319,6 +344,7 @@ "search_results.accounts": "人々", "search_results.hashtags": "ハッシュタグ", "search_results.statuses": "トゥート", + "search_results.statuses_fts_disabled": "このサーバーではトゥート本文の検索は利用できません。", "search_results.total": "{count, number}件の結果", "status.admin_account": "@{name} のモデレーション画面を開く", "status.admin_status": "このトゥートをモデレーション画面で開く", @@ -357,6 +383,7 @@ "status.show_more": "もっと見る", "status.show_more_all": "全て見る", "status.show_thread": "スレッドを表示", + "status.uncached_media_warning": "利用できません", "status.unmute_conversation": "会話のミュートを解除", "status.unpin": "プロフィールへの固定を解除", "suggestions.dismiss": "隠す", @@ -372,14 +399,22 @@ "time_remaining.moments": "まもなく終了", "time_remaining.seconds": "残り{number}秒", "trends.count_by_accounts": "{count}人がトゥート", + "trends.trending_now": "トレンドタグ", "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。", "upload_area.title": "ドラッグ&ドロップでアップロード", - "upload_button.label": "メディアを追加 (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "メディアを追加 ({formats})", "upload_error.limit": "アップロードできる上限を超えています。", "upload_error.poll": "アンケートではファイルをアップロードできません。", "upload_form.description": "視覚障害者のための説明", - "upload_form.focus": "プレビューを変更", + "upload_form.edit": "編集", "upload_form.undo": "削除", + "upload_modal.analyzing_picture": "画像を解析中…", + "upload_modal.apply": "適用", + "upload_modal.description_placeholder": "あのイーハトーヴォのすきとおった風", + "upload_modal.detect_text": "画像からテキストを検出", + "upload_modal.edit_media": "メディアを編集", + "upload_modal.hint": "サムネイルの焦点にしたい場所をクリックするか円形の枠をその場所にドラッグしてください。", + "upload_modal.preview_label": "プレビュー ({ratio})", "upload_progress.label": "アップロード中...", "video.close": "動画を閉じる", "video.exit_fullscreen": "全画面を終了する", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index ff7059aea..35789374a 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -4,6 +4,7 @@ "account.block": "დაბლოკე @{name}", "account.block_domain": "დაიმალოს ყველაფერი დომენიდან {domain}", "account.blocked": "დაიბლოკა", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "პირდაპირი წერილი @{name}-ს", "account.domain_blocked": "დომენი დამალულია", "account.edit_profile": "პროფილის ცვლილება", @@ -15,6 +16,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "მოგყვებათ", "account.hide_reblogs": "დაიმალოს ბუსტები @{name}-სგან", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "მედია", @@ -23,6 +25,7 @@ "account.mute": "გააჩუმე @{name}", "account.mute_notifications": "გააჩუმე შეტყობინებები @{name}-სგან", "account.muted": "გაჩუმებული", + "account.never_active": "Never", "account.posts": "ტუტები", "account.posts_with_replies": "ტუტები და პასუხები", "account.report": "დაარეპორტე @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "ნუღარ მიჰყვები", "account.unmute": "ნუღარ აჩუმებ @{name}-ს", "account.unmute_notifications": "ნუღარ აჩუმებ შეტყობინებებს @{name}-სგან", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "წარმოიშვა მოულოდნელი შეცდომა.", "alert.unexpected.title": "უპს!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "შეგიძლიათ დააჭიროთ {combo}-ს რათა შემდეგ ჯერზე გამოტოვოთ ეს", "bundle_column_error.body": "ამ კომპონენტის ჩატვირთვისას რაღაც აირია.", "bundle_column_error.retry": "სცადეთ კიდევ ერთხელ", @@ -47,6 +53,7 @@ "column.blocks": "დაბლოკილი მომხმარებლები", "column.community": "ლოკალური თაიმლაინი", "column.direct": "პირდაპირი წერილები", + "column.directory": "Browse profiles", "column.domain_blocks": "დამალული დომენები", "column.favourites": "ფავორიტები", "column.follow_requests": "დადევნების მოთხოვნები", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "დარწმუნებული ხართ, გსურთ სამუდამოდ გააუქმოთ ეს სია?", "confirmations.domain_block.confirm": "მთელი დომენის დამალვა", "confirmations.domain_block.message": "ნაღდად, ნაღდად, დარწმუნებული ხართ, გსურთ დაბლოკოთ მთელი {domain}? უმეტეს შემთხვევაში რამდენიმე გამიზნული ბლოკი ან გაჩუმება საკმარისი და უკეთესია. კონტენტს ამ დომენიდან ვერ იხილავთ ვერც ერთ ღია თაიმლაინზე ან თქვენს შეტყობინებებში. ამ დომენიდან არსებული მიმდევრები ამოიშლება.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "გაჩუმება", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "დარწმუნებული ხართ, გსურთ გააჩუმოთ {name}?", "confirmations.redraft.confirm": "გაუქმება და გადანაწილება", "confirmations.redraft.message": "დარწმუნებული ხართ, გსურთ გააუქმოთ ეს სტატუსი და გადაანაწილოთ? დაკარგავთ ყველა პასუხს, ბუსტს და მასზედ არსებულ ფავორიტს.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "ნუღარ მიჰყვები", "confirmations.unfollow.message": "დარწმუნებული ხართ, აღარ გსურთ მიჰყვებოდეთ {name}-ს?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "ეს სტატუსი ჩასვით თქვენს ვებ-საიტზე შემდეგი კოდის კოპირებით.", "embed.preview": "ესაა თუ როგორც გამოჩნდება:", "emoji_button.activity": "აქტივობა", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "ჯერ შეტყობინებები არ გაქვთ. საუბრის დასაწყებად იურთიერთქმედეთ სხვებთან.", "empty_column.public": "აქ არაფერია! შესავსებად, დაწერეთ რაიმე ღიად ან ხელით გაჰყევით მომხმარებლებს სხვა ინსტანციებისგან", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "ავტორიზაცია", "follow_request.reject": "უარყოფა", "getting_started.developers": "დეველოპერები", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "ახალი სიის სათაური", "lists.search": "ძებნა ადამიანებს შორის რომელთაც მიჰყვებით", "lists.subheading": "თქვენი სიები", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "იტვირთება...", "media_gallery.toggle_visible": "ხილვადობის ჩართვა", "missing_indicator.label": "არაა ნაპოვნი", @@ -246,7 +269,6 @@ "navigation_bar.personal": "პირადი", "navigation_bar.pins": "აპინული ტუტები", "navigation_bar.preferences": "პრეფერენსიები", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "ფედერალური თაიმლაინი", "navigation_bar.security": "უსაფრთხოება", "notification.favourite": "{name}-მა თქვენი სტატუსი აქცია ფავორიტად", @@ -277,8 +299,10 @@ "notifications.group": "{count} შეტყობინება", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "სტატუსის კონფიდენციალურობის მითითება", @@ -290,6 +314,7 @@ "privacy.public.short": "საჯარო", "privacy.unlisted.long": "არ დაიპოსტოს საჯარო თაიმლაინებზე", "privacy.unlisted.short": "ჩამოუთვლელი", + "refresh": "Refresh", "regeneration_indicator.label": "იტვირთება…", "regeneration_indicator.sublabel": "თქვენი სახლის ლენტა მზადდება!", "relative_time.days": "{number}დღ", @@ -314,6 +339,7 @@ "search_results.accounts": "ხალხი", "search_results.hashtags": "ჰეშტეგები", "search_results.statuses": "ტუტები", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "აჩვენე მეტი", "status.show_more_all": "აჩვენე მეტი ყველაზე", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "საუბარზე გაჩუმების მოშორება", "status.unpin": "პროფილიდან პინის მოშორება", "suggestions.dismiss": "Dismiss suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} საუბრობს", + "trends.trending_now": "Trending now", "ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.", "upload_area.title": "გადმოწიეთ და ჩააგდეთ ასატვირთათ", "upload_button.label": "მედიის დამატება", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "აღწერილობა ვიზუალურად უფასურისთვის", - "upload_form.focus": "კროპი", + "upload_form.edit": "Edit", "upload_form.undo": "გაუქმება", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "იტვირთება...", "video.close": "ვიდეოს დახურვა", "video.exit_fullscreen": "სრულ ეკრანზე ჩვენების გათიშვა", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index b9bd7cac3..ba13c7ddf 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -4,6 +4,7 @@ "account.block": "Бұғаттау @{name}", "account.block_domain": "Домендегі барлығын бұғатта {domain}", "account.blocked": "Бұғатталды", + "account.cancel_follow_request": "Жазылуға сұранымды қайтару", "account.direct": "Жеке хат @{name}", "account.domain_blocked": "Домен жабық", "account.edit_profile": "Профильді өңдеу", @@ -15,6 +16,7 @@ "account.follows.empty": "Ешкімге жазылмапты.", "account.follows_you": "Сізге жазылыпты", "account.hide_reblogs": "@{name} атты қолданушының әрекеттерін жасыру", + "account.last_status": "Соңғы белсенділік", "account.link_verified_on": "Сілтеме меншігі расталған күн {date}", "account.locked_info": "Бұл қолданушы өзі туралы мәліметтерді жасырған. Тек жазылғандар ғана көре алады.", "account.media": "Медиа", @@ -23,6 +25,7 @@ "account.mute": "Үнсіз қылу @{name}", "account.mute_notifications": "@{name} туралы ескертпелерді жасыру", "account.muted": "Үнсіз", + "account.never_active": "Ешқашан", "account.posts": "Жазбалар", "account.posts_with_replies": "Жазбалар мен жауаптар", "account.report": "Шағымдану @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Оқымау", "account.unmute": "@{name} ескертпелерін қосу", "account.unmute_notifications": "@{name} ескертпелерін көрсету", + "alert.rate_limited.message": "Қайтадан көріңіз {retry_time, time, medium} кейін.", + "alert.rate_limited.title": "Бағалау шектеулі", "alert.unexpected.message": "Бір нәрсе дұрыс болмады.", "alert.unexpected.title": "Өй!", + "autosuggest_hashtag.per_week": "{count} аптасына", "boost_modal.combo": "Келесіде өткізіп жіберу үшін басыңыз {combo}", "bundle_column_error.body": "Бұл компонентті жүктеген кезде бір қате пайда болды.", "bundle_column_error.retry": "Қайтадан көріңіз", @@ -47,6 +53,7 @@ "column.blocks": "Бұғатталғандар", "column.community": "Жергілікті желі", "column.direct": "Жеке хаттар", + "column.directory": "Профильдерді аралау", "column.domain_blocks": "Жасырылған домендер", "column.favourites": "Таңдаулылар", "column.follow_requests": "Жазылу сұранымдары", @@ -77,14 +84,14 @@ "compose_form.poll.remove_option": "Бұл жауапты өшір", "compose_form.publish": "Түрт", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Сезімтал ретінде белгіле", "compose_form.sensitive.marked": "Медиа нәзік деп белгіленген", "compose_form.sensitive.unmarked": "Медиа нәзік деп белгіленбеген", "compose_form.spoiler.marked": "Мәтін ескертумен жасырылған", "compose_form.spoiler.unmarked": "Мәтін жасырылмаған", "compose_form.spoiler_placeholder": "Ескертуіңізді осында жазыңыз", "confirmation_modal.cancel": "Қайтып алу", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Блок және Шағым", "confirmations.block.confirm": "Бұғаттау", "confirmations.block.message": "{name} атты қолданушыны бұғаттайтыныңызға сенімдісіз бе?", "confirmations.delete.confirm": "Өшіру", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Бұл тізімді жоясыз ба шынымен?", "confirmations.domain_block.confirm": "Бұл доменді бұғатта", "confirmations.domain_block.message": "Бұл домендегі {domain} жазбаларды шынымен бұғаттайсыз ба? Кейде үнсіз қылып тастау да жеткілікті.", + "confirmations.logout.confirm": "Шығу", + "confirmations.logout.message": "Шығатыныңызға сенімдісіз бе?", "confirmations.mute.confirm": "Үнсіз қылу", + "confirmations.mute.explanation": "Олардың посттары же олар туралы меншндар сізге көрінбейді, бірақ олар сіздің посттарды көре алады және жазыла алады.", "confirmations.mute.message": "{name} атты қолданушы үнсіз болсын ба?", "confirmations.redraft.confirm": "Өшіруді құптау", "confirmations.redraft.message": "Бұл жазбаны өшіріп, нобайларға жібереміз бе? Барлық жауаптар мен лайктарды жоғалтасыз.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Жауабыңыз жазып жатқан жазбаңыздың үстіне кетеді. Жалғастырамыз ба?", "confirmations.unfollow.confirm": "Оқымау", "confirmations.unfollow.message": "\"{name} атты қолданушыға енді жазылғыңыз келмей ме?", + "conversation.delete": "Пікірталасты өшіру", + "conversation.mark_as_read": "Оқылды деп белгіле", + "conversation.open": "Пікірталасты қарау", + "conversation.with": "{names} атты", + "directory.federated": "Танымал желіден", + "directory.local": "Тек {domain} доменінен", + "directory.new_arrivals": "Жаңадан келгендер", + "directory.recently_active": "Жақында кіргендер", "embed.instructions": "Төмендегі кодты көшіріп алу арқылы жазбаны басқа сайттарға да орналастыра аласыз.", "embed.preview": "Былай көрінетін болады:", "emoji_button.activity": "Белсенділік", @@ -118,7 +136,7 @@ "emoji_button.symbols": "Таңбалар", "emoji_button.travel": "Саяхат", "empty_column.account_timeline": "Жазба жоқ ешқандай!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "Профиль қолжетімді емес", "empty_column.blocks": "Ешкімді бұғаттамағансыз.", "empty_column.community": "Жергілікті желі бос. Сіз бастап жазыңыз!", "empty_column.direct": "Әзірше дым хат жоқ. Өзіңіз жазып көріңіз алдымен.", @@ -134,6 +152,10 @@ "empty_column.mutes": "Әзірше ешқандай үнсізге қойылған қолданушы жоқ.", "empty_column.notifications": "Әзірше ешқандай ескертпе жоқ. Басқалармен араласуды бастаңыз және пікірталастарға қатысыңыз.", "empty_column.public": "Ештеңе жоқ бұл жерде! Өзіңіз бастап жазып көріңіз немесе басқаларға жазылыңыз", + "error.unexpected_crash.explanation": "Кодтағы баг немесе браузердегі қатеден, бұл бет дұрыс ашылмай тұр.", + "error.unexpected_crash.next_steps": "Бетті жаңартып көріңіз. Егер бұл көмектеспесе, Mastodon желісін басқа браузерден немесе мобиль қосымшадан ашып көріңіз.", + "errors.unexpected_crash.copy_stacktrace": "Жиынтықты көшіріп ал клипбордқа", + "errors.unexpected_crash.report_issue": "Мәселені хабарла", "follow_request.authorize": "Авторизация", "follow_request.reject": "Қабылдамау", "getting_started.developers": "Жасаушылар тобы", @@ -158,7 +180,7 @@ "home.column_settings.show_replies": "Жауаптарды көрсету", "intervals.full.days": "{number, plural, one {# күн} other {# күн}}", "intervals.full.hours": "{number, plural, one {# сағат} other {# сағат}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.minutes": "{number, plural, one {# минут} other {# минут}}", "introduction.federation.action": "Келесі", "introduction.federation.federated.headline": "Жаһандық", "introduction.federation.federated.text": "Жаһандық желідегі жазбалар осында көрінетін болады.", @@ -204,14 +226,14 @@ "keyboard_shortcuts.search": "іздеу", "keyboard_shortcuts.start": "бастапқы бағанға бару", "keyboard_shortcuts.toggle_hidden": "жабық мәтінді CW ашу/жабу", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "көрсет/жап", "keyboard_shortcuts.toot": "жаңа жазба бастау", "keyboard_shortcuts.unfocus": "жазба қалдыру алаңынан шығу", "keyboard_shortcuts.up": "тізімде жоғары шығу", "lightbox.close": "Жабу", "lightbox.next": "Келесі", "lightbox.previous": "Алдыңғы", - "lightbox.view_context": "View context", + "lightbox.view_context": "Контексті көрсет", "lists.account.add": "Тізімге қосу", "lists.account.remove": "Тізімнен шығару", "lists.delete": "Тізімді өшіру", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Жаңа тізім аты", "lists.search": "Сіз іздеген адамдар арасында іздеу", "lists.subheading": "Тізімдеріңіз", + "load_pending": "{count, plural, one {# жаңа нәрсе} other {# жаңа нәрсе}}", "loading_indicator.label": "Жүктеу...", "media_gallery.toggle_visible": "Көрінуді қосу", "missing_indicator.label": "Табылмады", @@ -237,7 +260,7 @@ "navigation_bar.favourites": "Таңдаулылар", "navigation_bar.filters": "Үнсіз сөздер", "navigation_bar.follow_requests": "Жазылуға сұранғандар", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Жазылымдар және оқырмандар", "navigation_bar.info": "Сервер туралы", "navigation_bar.keyboard_shortcuts": "Ыстық пернелер", "navigation_bar.lists": "Тізімдер", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Жеке", "navigation_bar.pins": "Жабыстырылғандар", "navigation_bar.preferences": "Басымдықтар", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Жаһандық желі", "navigation_bar.security": "Қауіпсіздік", "notification.favourite": "{name} жазбаңызды таңдаулыға қосты", @@ -277,8 +299,10 @@ "notifications.group": "{count} ескертпе", "poll.closed": "Жабық", "poll.refresh": "Жаңарту", + "poll.total_people": "{count, plural, one {# адам} other {# адам}}", "poll.total_votes": "{count, plural, one {# дауыс} other {# дауыс}}", "poll.vote": "Дауыс беру", + "poll.voted": "Бұл сұраққа жауап бердіңіз", "poll_button.add_poll": "Сауалнама қосу", "poll_button.remove_poll": "Сауалнаманы өшіру", "privacy.change": "Құпиялылықты реттеу", @@ -290,13 +314,14 @@ "privacy.public.short": "Ашық", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Тізімсіз", + "refresh": "Жаңарту", "regeneration_indicator.label": "Жүктеу…", "regeneration_indicator.sublabel": "Жергілікті желі құрылуда!", "relative_time.days": "{number}күн", "relative_time.hours": "{number}сағ", "relative_time.just_now": "жаңа", "relative_time.minutes": "{number}мин", - "relative_time.seconds": "{number}s", + "relative_time.seconds": "{number}с", "reply_indicator.cancel": "Қайтып алу", "report.forward": "Жіберу {target}", "report.forward_hint": "Бұл аккаунт басқа серверден. Аноним шағым жібересіз бе?", @@ -314,7 +339,8 @@ "search_results.accounts": "Адамдар", "search_results.hashtags": "Хэштегтер", "search_results.statuses": "Жазбалар", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.statuses_fts_disabled": "Mastodon серверінде постты толық мәтінмен іздей алмайсыз.", + "search_results.total": "{count, number} {count, plural, one {нәтиже} other {нәтиже}}", "status.admin_account": "@{name} үшін модерация интерфейсін аш", "status.admin_status": "Бұл жазбаны модерация интерфейсінде аш", "status.block": "Бұғаттау @{name}", @@ -352,6 +378,7 @@ "status.show_more": "Толығырақ", "status.show_more_all": "Бәрін толығымен", "status.show_thread": "Желіні көрсет", + "status.uncached_media_warning": "Қолжетімді емес", "status.unmute_conversation": "Пікірталасты үнсіз қылмау", "status.unpin": "Профильден алып тастау", "suggestions.dismiss": "Өткізіп жіберу", @@ -367,14 +394,22 @@ "time_remaining.moments": "Қалған уақыт", "time_remaining.seconds": "{number, plural, one {# секунд} other {# секунд}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} жазған екен", + "trends.trending_now": "Тренд тақырыптар", "ui.beforeunload": "Mastodon желісінен шықсаңыз, нобайыңыз сақталмайды.", "upload_area.title": "Жүктеу үшін сүйреп әкеліңіз", "upload_button.label": "Медиа қосу (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Файл жүктеу лимитінен асып кеттіңіз.", "upload_error.poll": "Сауалнамамен бірге файл жүктеуге болмайды.", "upload_form.description": "Көру қабілеті нашар адамдар үшін сипаттаңыз", - "upload_form.focus": "Превьюді өзгерту", + "upload_form.edit": "Түзету", "upload_form.undo": "Өшіру", + "upload_modal.analyzing_picture": "Суретті анализ жасау…", + "upload_modal.apply": "Қолдану", + "upload_modal.description_placeholder": "Щучинск съезіндегі өрт пе? Вагон-үй, аэромобиль һәм ұшақ фюзеляжы цехінен ғой", + "upload_modal.detect_text": "Суреттен мәтін анықтау", + "upload_modal.edit_media": "Медиафайлды өңдеу", + "upload_modal.hint": "Алдын-ала қарау шеңберін басыңыз немесе сүйреңіз, барлық нобайларда көрінетін фокусты таңдау үшін.", + "upload_modal.preview_label": "Превью ({ratio})", "upload_progress.label": "Жүктеп жатыр...", "video.close": "Видеоны жабу", "video.exit_fullscreen": "Толық экраннан шық", diff --git a/app/javascript/mastodon/locales/kn.json b/app/javascript/mastodon/locales/kn.json new file mode 100644 index 000000000..39ca86a0c --- /dev/null +++ b/app/javascript/mastodon/locales/kn.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.bot": "Bot", + "account.block": "Block @{name}", + "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", + "account.direct": "Direct message @{name}", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Edit profile", + "account.endorse": "Feature on profile", + "account.follow": "Follow", + "account.followers": "Followers", + "account.followers.empty": "No one follows this user yet.", + "account.follows": "Follows", + "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.media": "Media", + "account.mention": "Mention @{name}", + "account.moved_to": "{name} has moved to:", + "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", + "account.never_active": "Never", + "account.posts": "Toots", + "account.posts_with_replies": "Toots and replies", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", + "account.unblock": "Unblock @{name}", + "account.unblock_domain": "Unhide {domain}", + "account.unendorse": "Don't feature on profile", + "account.unfollow": "Unfollow", + "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.direct": "Direct messages", + "column.directory": "Browse profiles", + "column.domain_blocks": "Hidden domains", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.pins": "Pinned toot", + "column.public": "Federated timeline", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", + "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", + "column_header.unpin": "Unpin", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media only", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Remove this 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.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.select.no_options_message": "No suggestions found", + "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "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", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "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.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll.voted": "You voted for this answer", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "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", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 656a36bce..e7c59d68f 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -4,6 +4,7 @@ "account.block": "@{name}을 차단", "account.block_domain": "{domain} 전체를 숨김", "account.blocked": "차단 됨", + "account.cancel_follow_request": "팔로우 요청 취소", "account.direct": "@{name}으로부터의 다이렉트 메시지", "account.domain_blocked": "도메인 숨겨짐", "account.edit_profile": "프로필 편집", @@ -15,6 +16,7 @@ "account.follows.empty": "이 유저는 아직 아무도 팔로우 하고 있지 않습니다.", "account.follows_you": "날 팔로우합니다", "account.hide_reblogs": "@{name}의 부스트를 숨기기", + "account.last_status": "마지막 활동", "account.link_verified_on": "{date}에 이 링크의 소유권이 확인 됨", "account.locked_info": "이 계정의 프라이버시 설정은 잠금으로 설정되어 있습니다. 계정 소유자가 수동으로 팔로어를 승인합니다.", "account.media": "미디어", @@ -23,6 +25,7 @@ "account.mute": "@{name} 뮤트", "account.mute_notifications": "@{name}의 알림을 뮤트", "account.muted": "뮤트 됨", + "account.never_active": "없음", "account.posts": "툿", "account.posts_with_replies": "툿과 답장", "account.report": "@{name} 신고", @@ -35,8 +38,11 @@ "account.unfollow": "팔로우 해제", "account.unmute": "뮤트 해제", "account.unmute_notifications": "@{name}의 알림 뮤트 해제", + "alert.rate_limited.message": "{retry_time, time, medium}에 다시 시도해 주세요.", + "alert.rate_limited.title": "빈도 제한", "alert.unexpected.message": "예측하지 못한 에러가 발생했습니다.", "alert.unexpected.title": "앗!", + "autosuggest_hashtag.per_week": "주간 {count}회", "boost_modal.combo": "{combo}를 누르면 다음부터 이 과정을 건너뛸 수 있습니다", "bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.", "bundle_column_error.retry": "다시 시도", @@ -47,6 +53,7 @@ "column.blocks": "차단 중인 사용자", "column.community": "로컬 타임라인", "column.direct": "다이렉트 메시지", + "column.directory": "프로필 둘러보기", "column.domain_blocks": "숨겨진 도메인", "column.favourites": "즐겨찾기", "column.follow_requests": "팔로우 요청", @@ -92,8 +99,11 @@ "confirmations.delete_list.confirm": "삭제", "confirmations.delete_list.message": "정말로 이 리스트를 삭제하시겠습니까?", "confirmations.domain_block.confirm": "도메인 전체를 숨김", - "confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다. 모든 공개 타임라인과 알림에서 해당 도메인에서 작성된 컨텐츠를 보지 못합니다. 해당 도메인 팔로워와의 관계가 사라집니다.", + "confirmations.domain_block.message": "정말로 {domain} 전체를 차단하시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다. 모든 공개 타임라인과 알림에서 해당 도메인에서 작성된 컨텐츠를 보지 못합니다. 해당 도메인 팔로워와의 관계가 사라집니다.", + "confirmations.logout.confirm": "로그아웃", + "confirmations.logout.message": "정말로 로그아웃 하시겠습니까?", "confirmations.mute.confirm": "뮤트", + "confirmations.mute.explanation": "이 동작은 그의 게시물, 그를 멘션하는 게시물을 숨깁니다, 하지만 여전히 그가 당신의 게시물을 보고 팔로우 할 수 있습니다.", "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?", "confirmations.redraft.confirm": "삭제하고 다시 쓰기", "confirmations.redraft.message": "정말로 이 포스트를 삭제하고 다시 쓰시겠습니까? 해당 포스트에 대한 부스트와 즐겨찾기를 잃게 되고 원본에 대한 답장은 연결 되지 않습니다.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "답글을 달기 위해 현재 작성 중인 메시지가 덮어 씌워집니다. 진행하시겠습니까?", "confirmations.unfollow.confirm": "언팔로우", "confirmations.unfollow.message": "정말로 {name}를 언팔로우하시겠습니까?", + "conversation.delete": "대화 삭제", + "conversation.mark_as_read": "읽은 상태로 표시", + "conversation.open": "대화 보기", + "conversation.with": "{names} 님과", + "directory.federated": "알려진 연합우주로부터", + "directory.local": "{domain}에서만", + "directory.new_arrivals": "새로운 사람들", + "directory.recently_active": "최근 활동", "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.", "embed.preview": "다음과 같이 표시됩니다:", "emoji_button.activity": "활동", @@ -134,10 +152,14 @@ "empty_column.mutes": "아직 아무도 뮤트하지 않았습니다.", "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요.", "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 서버의 유저를 팔로우 해서 채워보세요", + "error.unexpected_crash.explanation": "버그 혹은 브라우저 호환성 문제로 이 페이지를 올바르게 표시할 수 없습니다.", + "error.unexpected_crash.next_steps": "페이지를 새로고침 해보세요. 그래도 해결되지 않는 경우, 다른 브라우저나 네이티브 앱으로도 마스토돈을 이용하실 수 있습니다.", + "errors.unexpected_crash.copy_stacktrace": "에러 내용을 클립보드에 복사", + "errors.unexpected_crash.report_issue": "문제 신고", "follow_request.authorize": "허가", "follow_request.reject": "거부", "getting_started.developers": "개발자", - "getting_started.directory": "프로필 디렉터리", + "getting_started.directory": "프로필 디렉토리", "getting_started.documentation": "문서", "getting_started.heading": "시작", "getting_started.invite": "초대", @@ -221,11 +243,12 @@ "lists.new.title_placeholder": "새 리스트의 이름", "lists.search": "팔로우 중인 사람들 중에서 찾기", "lists.subheading": "당신의 리스트", + "load_pending": "{count}개의 새 항목", "loading_indicator.label": "불러오는 중...", "media_gallery.toggle_visible": "표시 전환", "missing_indicator.label": "찾을 수 없습니다", "missing_indicator.sublabel": "이 리소스를 찾을 수 없었습니다", - "mute_modal.hide_notifications": "이 사용자로부터의 알림을 뮤트하시겠습니까?", + "mute_modal.hide_notifications": "이 사용자로부터의 알림을 숨기시겠습니까?", "navigation_bar.apps": "모바일 앱", "navigation_bar.blocks": "차단한 사용자", "navigation_bar.community_timeline": "로컬 타임라인", @@ -235,7 +258,7 @@ "navigation_bar.domain_blocks": "숨겨진 도메인", "navigation_bar.edit_profile": "프로필 편집", "navigation_bar.favourites": "즐겨찾기", - "navigation_bar.filters": "뮤트", + "navigation_bar.filters": "뮤트된 단어", "navigation_bar.follow_requests": "팔로우 요청", "navigation_bar.follows_and_followers": "팔로우와 팔로워", "navigation_bar.info": "이 서버에 대해서", @@ -246,7 +269,6 @@ "navigation_bar.personal": "개인용", "navigation_bar.pins": "고정된 툿", "navigation_bar.preferences": "사용자 설정", - "navigation_bar.profile_directory": "프로필 디렉토리", "navigation_bar.public_timeline": "연합 타임라인", "navigation_bar.security": "보안", "notification.favourite": "{name}님이 즐겨찾기 했습니다", @@ -277,8 +299,10 @@ "notifications.group": "{count} 개의 알림", "poll.closed": "마감됨", "poll.refresh": "새로고침", - "poll.total_votes": "{count} 명 참여", + "poll.total_people": "{count}명", + "poll.total_votes": "{count} 표", "poll.vote": "투표", + "poll.voted": "이 답변에 투표했습니다", "poll_button.add_poll": "투표 추가", "poll_button.remove_poll": "투표 삭제", "privacy.change": "포스트의 프라이버시 설정을 변경", @@ -290,6 +314,7 @@ "privacy.public.short": "공개", "privacy.unlisted.long": "공개 타임라인에 표시하지 않음", "privacy.unlisted.short": "타임라인에 비표시", + "refresh": "새로고침", "regeneration_indicator.label": "불러오는 중…", "regeneration_indicator.sublabel": "당신의 홈 피드가 준비되는 중입니다!", "relative_time.days": "{number}일 전", @@ -314,6 +339,7 @@ "search_results.accounts": "사람", "search_results.hashtags": "해시태그", "search_results.statuses": "툿", + "search_results.statuses_fts_disabled": "이 마스토돈 서버에선 툿의 내용을 통한 검색이 활성화 되어 있지 않습니다.", "search_results.total": "{count, number}건의 결과", "status.admin_account": "@{name}에 대한 모더레이션 인터페이스 열기", "status.admin_status": "모더레이션 인터페이스에서 이 게시물 열기", @@ -326,7 +352,7 @@ "status.direct": "@{name}에게 다이렉트 메시지", "status.embed": "공유하기", "status.favourite": "즐겨찾기", - "status.filtered": "필터링 됨", + "status.filtered": "필터로 걸러짐", "status.load_more": "더 보기", "status.media_hidden": "미디어 숨겨짐", "status.mention": "답장", @@ -352,6 +378,7 @@ "status.show_more": "더 보기", "status.show_more_all": "모두 펼치기", "status.show_thread": "글타래 보기", + "status.uncached_media_warning": "사용할 수 없음", "status.unmute_conversation": "이 대화의 뮤트 해제하기", "status.unpin": "고정 해제", "suggestions.dismiss": "추천 지우기", @@ -367,14 +394,22 @@ "time_remaining.moments": "남은 시간", "time_remaining.seconds": "{number} 초 남음", "trends.count_by_accounts": "{count} 명의 사람들이 말하고 있습니다", + "trends.trending_now": "지금 유행중", "ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.", "upload_area.title": "드래그 & 드롭으로 업로드", "upload_button.label": "미디어 추가 (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "파일 업로드 제한에 도달했습니다.", "upload_error.poll": "파일 업로드는 투표와 함께 첨부할 수 없습니다.", "upload_form.description": "시각장애인을 위한 설명", - "upload_form.focus": "미리보기 변경", + "upload_form.edit": "편집", "upload_form.undo": "삭제", + "upload_modal.analyzing_picture": "이미지 분석 중…", + "upload_modal.apply": "적용", + "upload_modal.description_placeholder": "다람쥐 헌 쳇바퀴 타고파", + "upload_modal.detect_text": "이미지에서 텍스트 추출", + "upload_modal.edit_media": "미디어 편집", + "upload_modal.hint": "미리보기를 클릭하거나 드래그 해서 포컬 포인트를 맞추세요. 이 점은 썸네일에 항상 보여질 부분을 나타냅니다.", + "upload_modal.preview_label": "미리보기 ({ratio})", "upload_progress.label": "업로드 중...", "video.close": "동영상 닫기", "video.exit_fullscreen": "전체화면 나가기", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index ac58514d4..39ca86a0c 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -4,6 +4,7 @@ "account.block": "Block @{name}", "account.block_domain": "Hide everything from {domain}", "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct message @{name}", "account.domain_blocked": "Domain hidden", "account.edit_profile": "Edit profile", @@ -15,6 +16,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "Follows you", "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Media", @@ -23,10 +25,11 @@ "account.mute": "Mute @{name}", "account.mute_notifications": "Mute notifications from @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", "account.report": "Report @{name}", - "account.requested": "Awaiting approval. Click to cancel follow request", + "account.requested": "Awaiting approval", "account.share": "Share @{name}'s profile", "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Unblock @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.retry": "Try again", @@ -47,6 +53,7 @@ "column.blocks": "Blocked users", "column.community": "Local timeline", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "Favourites", "column.follow_requests": "Follow requests", @@ -64,7 +71,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Embed this status on your website by copying the code below.", "embed.preview": "Here is what it will look like:", "emoji_button.activity": "Activity", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferences", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.security": "Security", "notification.favourite": "{name} favourited your status", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Adjust status privacy", @@ -290,6 +314,7 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Show more", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", "suggestions.dismiss": "Dismiss suggestion", @@ -367,15 +394,23 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Delete", - "upload_progress.label": "Uploading...", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", "video.close": "Close video", "video.exit_fullscreen": "Exit full screen", "video.expand": "Expand video", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 647e23a69..1c0e35501 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -4,6 +4,7 @@ "account.block": "Bloķēt @{name}", "account.block_domain": "Slēpt visu no {domain}", "account.blocked": "Bloķēts", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Privātā ziņa @{name}", "account.domain_blocked": "Domēns ir paslēpts", "account.edit_profile": "Labot profilu", @@ -15,6 +16,7 @@ "account.follows.empty": "Šis lietotājs pagaidām nevienam neseko.", "account.follows_you": "Seko tev", "account.hide_reblogs": "Paslēpt paceltos ierakstus no lietotāja @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Šīs saites piederība ir pārbaudīta {date}", "account.locked_info": "Šī konta privātuma status ir iestatīts slēgts. Īpašnieks izskatīs un izvēlēsies kas viņam drīkst sekot.", "account.media": "Mēdiji", @@ -23,6 +25,7 @@ "account.mute": "Apklusināt @{name}", "account.mute_notifications": "Nerādīt paziņojumus no @{name}", "account.muted": "Apklusināts", + "account.never_active": "Never", "account.posts": "Ieraksti", "account.posts_with_replies": "Ieraksti un atbildes", "account.report": "Ziņot par lietotāju @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Nesekot", "account.unmute": "Noņemt apklusinājumu no lietotāja @{name}", "account.unmute_notifications": "Rādīt paziņojumus no lietotāja @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "Negaidīta kļūda.", "alert.unexpected.title": "Ups!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Nospied {combo} lai izlaistu šo nākamreiz", "bundle_column_error.body": "Kaut kas nogāja greizi ielādējot šo komponenti.", "bundle_column_error.retry": "Mēģini vēlreiz", @@ -47,6 +53,7 @@ "column.blocks": "Bloķētie lietotāji", "column.community": "Lokālā laika līnija", "column.direct": "Privātās ziņas", + "column.directory": "Browse profiles", "column.domain_blocks": "Paslēptie domēni", "column.favourites": "Favorīti", "column.follow_requests": "Sekotāju pieprasījumi", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Vai tiešam vēlies neatgriezeniski dzēst šo sarakstu?", "confirmations.domain_block.confirm": "Paslēpt visu domēnu", "confirmations.domain_block.message": "Vai tu tiešām, tiešam vēlies bloķēt visu domēnu {domain}? Lielākajā daļā gadījumu pietiek ja nobloķē vai apklusini kādu. Tu neredzēsi saturu vai paziņojumus no šī domēna nevienā laika līnijā. Tavi sekotāji no šī domēna tiks noņemti.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Apklusināt", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Vai Tu tiešām velies apklusināt {name}?", "confirmations.redraft.confirm": "Dzēst un pārrakstīt", "confirmations.redraft.message": "Vai tiešām vēlies dzēst un pārrakstīt šo ierakstu? Favorīti un paceltie ieraksti tiks dzēsti, kā arī atbildes tiks atsaistītas no šī ieraksta.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Atbildot tagad tava ziņa ko šobrīd raksti tiks pārrakstīta. Vai tiešām vēlies turpināt?", "confirmations.unfollow.confirm": "Nesekot", "confirmations.unfollow.message": "Vai tiešam vairs nevēlies sekot lietotājam {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Iegul šo ziņojumu savā mājaslapā kopējot kodu zemāk.", "embed.preview": "Tas izskatīsies šādi:", "emoji_button.activity": "Aktivitāte", @@ -134,6 +152,10 @@ "empty_column.mutes": "Tu neesi nevienu apklusinājis.", "empty_column.notifications": "Tev nav paziņojumu. Iesaisties sarunās ar citiem.", "empty_column.public": "Šeit nekā nav, tukšums! Ieraksti kaut ko publiski, vai uzmeklē un seko kādam no citas instances", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Autorizēt", "follow_request.reject": "Noraidīt", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferences", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.security": "Security", "notification.favourite": "{name} favourited your status", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Adjust status privacy", @@ -290,6 +314,7 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Show more", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", "suggestions.dismiss": "Dismiss suggestion", @@ -367,15 +394,23 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Delete", - "upload_progress.label": "Uploading...", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", "video.close": "Close video", "video.exit_fullscreen": "Exit full screen", "video.expand": "Expand video", diff --git a/app/javascript/mastodon/locales/mk.json b/app/javascript/mastodon/locales/mk.json new file mode 100644 index 000000000..5189e8ea8 --- /dev/null +++ b/app/javascript/mastodon/locales/mk.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Додади или одстрани од листа", + "account.badges.bot": "Бот", + "account.block": "Блокирај @{name}", + "account.block_domain": "Сокријај се од {domain}", + "account.blocked": "Блокиран", + "account.cancel_follow_request": "Одкажи барање за следење", + "account.direct": "Директна порана @{name}", + "account.domain_blocked": "Скриен домен", + "account.edit_profile": "Измени профил", + "account.endorse": "Карактеристики на профилот", + "account.follow": "Следи", + "account.followers": "Следбеници", + "account.followers.empty": "Никој не го следи овој корисник сеуште.", + "account.follows": "Следи", + "account.follows.empty": "Корисникот не следи никој сеуште.", + "account.follows_you": "Те следи тебе", + "account.hide_reblogs": "Сокриј буст од @{name}", + "account.last_status": "Последно активен", + "account.link_verified_on": "Сопстевноста на овај линк беше проверен на {date}", + "account.locked_info": "Статусот на приватност на овај корисник е сетиран како заклучен. Корисникот одлучува кој можи да го следи него.", + "account.media": "Медија", + "account.mention": "Спомни @{name}", + "account.moved_to": "{name} се пресели во:", + "account.mute": "Зачути го @{name}", + "account.mute_notifications": "Исклучи известувања од @{name}", + "account.muted": "Зачутено", + "account.never_active": "Никогаш", + "account.posts": "Тутови", + "account.posts_with_replies": "Тутови и реплики", + "account.report": "Пријави @{name}", + "account.requested": "Се чека одобрување. Кликни за да одкажиш барање за следење", + "account.share": "Сподели @{name} профил", + "account.show_reblogs": "Прикажи бустови од @{name}", + "account.unblock": "Одблокирај @{name}", + "account.unblock_domain": "Прикажи {domain}", + "account.unendorse": "Не прикажувај на профил", + "account.unfollow": "Одследи", + "account.unmute": "Зачути го @{name}", + "account.unmute_notifications": "Исклучи известувања од @{name}", + "alert.rate_limited.message": "Обидете се повторно после {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "Неочекувана грешка.", + "alert.unexpected.title": "Упс!", + "autosuggest_hashtag.per_week": "{count} неделно", + "boost_modal.combo": "Кликни {combo} за да го прескокниш ова нареден пат", + "bundle_column_error.body": "Се случи проблем при вчитувањето.", + "bundle_column_error.retry": "Обидете се повторно", + "bundle_column_error.title": "Мрежна грешка", + "bundle_modal_error.close": "Затвори", + "bundle_modal_error.message": "Настана грешка при прикажувањето на оваа веб-страница.", + "bundle_modal_error.retry": "Обидете се повторно", + "column.blocks": "Блокирани корисници", + "column.community": "Локална временска зона", + "column.direct": "Директна порака", + "column.directory": "Види профили", + "column.domain_blocks": "Скриени домеини", + "column.favourites": "Омилени", + "column.follow_requests": "Follow requests", + "column.home": "Дома", + "column.lists": "Листа", + "column.mutes": "Заќутени корисници", + "column.notifications": "Известувања", + "column.pins": "Pinned toot", + "column.public": "Федеративен времеплов", + "column_back_button.label": "Назад", + "column_header.hide_settings": "Скриј подесувања", + "column_header.moveLeft_settings": "Премести колона влево", + "column_header.moveRight_settings": "Премести колона вдесно", + "column_header.pin": "Pin", + "column_header.show_settings": "Прикажи подесувања", + "column_header.unpin": "Unpin", + "column_subheading.settings": "Подесувања", + "community.column_settings.media_only": "Само медиа", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Научи повеќе", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "заклучен", + "compose_form.placeholder": "Што имате на ум?", + "compose_form.poll.add_option": "Додај избор", + "compose_form.poll.duration": "Времетрање на анкета", + "compose_form.poll.option_placeholder": "Избери {number}", + "compose_form.poll.remove_option": "Избриши избор", + "compose_form.publish": "Тутови", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Обележи медиа како сензитивна", + "compose_form.sensitive.marked": "Медиата е обележана како сензитивна", + "compose_form.sensitive.unmarked": "Медиата не е обележана како сензитивна", + "compose_form.spoiler.marked": "Текстот е сокриен зад предупредување", + "compose_form.spoiler.unmarked": "Текстот не е сокриен", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Откажи", + "confirmations.block.block_and_report": "Блокирај и Пријави", + "confirmations.block.confirm": "Блокирај", + "confirmations.block.message": "Сигурни сте дека дека го блокирате {name}?", + "confirmations.delete.confirm": "Избриши", + "confirmations.delete.message": "Сигурни сте дека го бришите статусот?", + "confirmations.delete_list.confirm": "Избриши", + "confirmations.delete_list.message": "Дали сте сигурни дека сакате да го избришете списоков?", + "confirmations.domain_block.confirm": "Сокриј цел домеин", + "confirmations.domain_block.message": "Дали скроз сте сигурни дека ќе блокирате сѐ од {domain}? Во повеќето случаеви неколку таргетирани блокирања или заќутувања се доволни и предложени. Нема да ја видите содржината од тој домеин во никој јавен времеплов или вашите нотификации. Вашите следбеници од тој домеин ќе бидат остранети.", + "confirmations.logout.confirm": "Одјави се", + "confirmations.logout.message": "Дали сте сигурни дека сакате да се одјавите?", + "confirmations.mute.confirm": "Заќути", + "confirmations.mute.explanation": "Ќе сокрие објави од нив и објави кои ги спомнуваат нив, но сеуште ќе им дозволи да ги видат вашите постови и ве следат.", + "confirmations.mute.message": "Дали ќе го заќутите {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Одговори", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Одследи", + "confirmations.unfollow.message": "Сигурни сте дека ќе го отследите {name}?", + "conversation.delete": "Избриши разговор", + "conversation.mark_as_read": "Означете како прочитано", + "conversation.open": "Прегледај разговор", + "conversation.with": "Со {names}", + "directory.federated": "Од познати fediverse", + "directory.local": "Само од {domain}", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Активност", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Храна & Пијалаци", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Природа", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Објекти", + "emoji_button.people": "Луѓе", + "emoji_button.recent": "Најчесто користени", + "emoji_button.search": "Барај...", + "emoji_button.search_results": "Резултати од барање", + "emoji_button.symbols": "Симболи", + "emoji_button.travel": "Патувања и Места", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Недостапен профил", + "empty_column.blocks": "Немате сеуште блокирано корисници.", + "empty_column.community": "Локалниот времеплов е празен. Објавете нешто јавно за да може да почне шоуто!", + "empty_column.direct": "Немате директни пораки. Кога ќе пратите или примите, ќе се појават тука.", + "empty_column.domain_blocks": "Немате сокриени домеини уште.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "јавен времеплов", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Пријавете проблем", + "follow_request.authorize": "Одобри", + "follow_request.reject": "Одбиј", + "getting_started.developers": "Програмери", + "getting_started.directory": "Профил директориум", + "getting_started.documentation": "Документација", + "getting_started.heading": "Започни", + "getting_started.invite": "Покани луѓе", + "getting_started.open_source_notice": "Мастодон е софтвер со отворен код. Можете да придонесувате или пријавувате проблеми во GitHub на {github}.", + "getting_started.security": "Поставки на сметката", + "getting_started.terms": "Услови на користење", + "hashtag.column_header.tag_mode.all": "и {additional}", + "hashtag.column_header.tag_mode.any": "или {additional}", + "hashtag.column_header.tag_mode.none": "без {additional}", + "hashtag.column_settings.select.no_options_message": "Нема предлози", + "hashtag.column_settings.select.placeholder": "Внеси хаштагови…", + "hashtag.column_settings.tag_mode.all": "Сите овие", + "hashtag.column_settings.tag_mode.any": "Било кои", + "hashtag.column_settings.tag_mode.none": "Никои", + "hashtag.column_settings.tag_toggle": "Стави додатни тагови за оваа колона", + "home.column_settings.basic": "Основно", + "home.column_settings.show_reblogs": "Прикажи бустирања", + "home.column_settings.show_replies": "Прикажи одговори", + "intervals.full.days": "{number, plural, one {# ден} other {# дена}}", + "intervals.full.hours": "{number, plural, one {# час} other {# часа}}", + "intervals.full.minutes": "{number, plural, one {# минута} other {# минути}}", + "introduction.federation.action": "Следно", + "introduction.federation.federated.headline": "Федеративно", + "introduction.federation.federated.text": "Јавни објави од други сервери од fediverse ќе се појават во федеративниот времеплов.", + "introduction.federation.home.headline": "Дома", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Локално", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Заврши туторијал!", + "introduction.interactions.favourite.headline": "Омилени", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Бустирај", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Одговори", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Да започнеме!", + "introduction.welcome.headline": "Почетни чекори", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "за да одите назад", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "одговори", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "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", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Уреди профил", + "navigation_bar.favourites": "Омилени", + "navigation_bar.filters": "Замолќени зборови", + "navigation_bar.follow_requests": "Следи покани", + "navigation_bar.follows_and_followers": "Следења и следбеници", + "navigation_bar.info": "За овој сервер", + "navigation_bar.keyboard_shortcuts": "Кратенки", + "navigation_bar.lists": "Листи", + "navigation_bar.logout": "Одјави се", + "navigation_bar.mutes": "Заќутени корисници", + "navigation_bar.personal": "Лично", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Федеративен времеплов", + "navigation_bar.security": "Безбедност", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Резултати од анкета:", + "notifications.column_settings.push": "Пуш нотификации", + "notifications.column_settings.reblog": "Бустови:", + "notifications.column_settings.show": "Прикажи во колона", + "notifications.column_settings.sound": "Свири звуци", + "notifications.filter.all": "Сите", + "notifications.filter.boosts": "Бустови", + "notifications.filter.favourites": "Омилени", + "notifications.filter.follows": "Следења", + "notifications.filter.mentions": "Спомнувања", + "notifications.filter.polls": "Резултати од анкета", + "notifications.group": "{count} нотификации", + "poll.closed": "Затворени", + "poll.refresh": "Освежи", + "poll.total_people": "{count, plural, one {# човек} other {# луѓе}}", + "poll.total_votes": "{count, plural, one {# глас} other {# гласа}}", + "poll.vote": "Гласај", + "poll.voted": "Вие гласавте за овој одговор", + "poll_button.add_poll": "Додадете нова анкета", + "poll_button.remove_poll": "Избришете анкета", + "privacy.change": "Штеловај статус на приватност", + "privacy.direct.long": "Објави само на спомнати корисници", + "privacy.direct.short": "Директно", + "privacy.private.long": "Објави само на следбеници", + "privacy.private.short": "Само следбеници", + "privacy.public.long": "Објави во јавни времплови", + "privacy.public.short": "Јавно", + "privacy.unlisted.long": "Не објавувај на јавни времеплови", + "privacy.unlisted.short": "Необјавено", + "refresh": "Освежи", + "regeneration_indicator.label": "Вчитување…", + "regeneration_indicator.sublabel": "Вашиот новости се подготвуваат!", + "relative_time.days": "{number}д", + "relative_time.hours": "{number}ч", + "relative_time.just_now": "сега", + "relative_time.minutes": "{number}м", + "relative_time.seconds": "{number}с", + "reply_indicator.cancel": "Откажи", + "report.forward": "Проследи до {target}", + "report.forward_hint": "Оваа сметка е од друг сервер. Испрати анонимна копија од пријавата и таму?", + "report.hint": "Пријавата ќе биде испратена до вашиот серверски модератор. Подолу можете да ставите опис зошто ја пријавувате сметката:", + "report.placeholder": "Додатни коментари", + "report.submit": "Испрати", + "report.target": "Пријавување {target}", + "search.placeholder": "Барај", + "search_popout.search_format": "Напреден формат за барање", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "хештег", + "search_popout.tips.status": "состојба", + "search_popout.tips.text": "Прост текст враќа совпаднати имиња, корисници и хештагови", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "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", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Дома", + "tabs_bar.local_timeline": "Локално", + "tabs_bar.notifications": "Нотификации", + "tabs_bar.search": "Барај", + "time_remaining.days": "{number, plural, one {# ден} other {# дена}} {number, plural, one {остана} other {останаа}}", + "time_remaining.hours": "{number, plural, one {# час} other {# часа}} {number, plural, one {остана} other {останаа}}", + "time_remaining.minutes": "{number, plural, one {# минута} other {# минути}} {number, plural, one {остана} other {останаа}}", + "time_remaining.moments": "Уште некои мига", + "time_remaining.seconds": "{number, plural, one {# секунда} other {# секунди}} {number, plural, one {остана} other {останаа}}", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json new file mode 100644 index 000000000..f331590de --- /dev/null +++ b/app/javascript/mastodon/locales/ml.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.bot": "റോബോട്ട്", + "account.block": "@{name} നെ ബ്ലോക്ക് ചെയ്യുക", + "account.block_domain": "{domain} ഇൽ നിന്നുള്ള എല്ലാം മറയ്കുക", + "account.blocked": "തടഞ്ഞു", + "account.cancel_follow_request": "Cancel follow request", + "account.direct": "നേരിട്ട് മെസേജ് അയക്കുക{name}", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "പ്രൊഫൈൽ തിരുത്തുക", + "account.endorse": "Feature on profile", + "account.follow": "പിന്തുടരുക", + "account.followers": "നിങ്ങളെ പിന്തുടരുന്നവർ", + "account.followers.empty": "ഈ ഉപയോക്താവിനെ ആരും ഇതുവരെ പിന്തുടരുന്നില്ല.", + "account.follows": "പിന്തുടരുന്നു", + "account.follows.empty": "ഈ ഉപയോക്താവ് ആരേയും ഇതുവരെ പിന്തുടരുന്നില്ല.", + "account.follows_you": "നിങ്ങളെ പിന്തുടരുന്നു", + "account.hide_reblogs": "@{name} ബൂസ്റ്റ് ചെയ്തവ മറയ്കുക", + "account.last_status": "അവസാനം കണ്ടത്", + "account.link_verified_on": "ഈ ലിങ്കിന്റെ ഉടമസ്തത {date} ഇൽ ഉറപ്പാക്കിയതാണ്", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.media": "മീഡിയ", + "account.mention": "Mention @{name}", + "account.moved_to": "{name} has moved to:", + "account.mute": "നിശ്ശബ്ദമാക്കുക @{name}", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "നിശ്ശബ്ദമാക്കിയിരിക്കുന്നു", + "account.never_active": "ഒരിക്കലും വേണ്ട", + "account.posts": "ടൂട്ടുകൾ", + "account.posts_with_replies": "ടൂട്ടുകളും മറുപടികളും", + "account.report": "റിപ്പോർട്ട് ചെയ്യുക @{name}", + "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", + "account.unblock": "ബ്ലോക്ക് മാറ്റുക @{name}", + "account.unblock_domain": "Unhide {domain}", + "account.unendorse": "Don't feature on profile", + "account.unfollow": "പിന്തുടരുന്നത് നിര്ത്തുക", + "account.unmute": "നിശ്ശബ്ദമാക്കുന്നത് നിർത്തുക @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "അപ്രതീക്ഷിതമായി എന്തോ സംഭവിച്ചു.", + "alert.unexpected.title": "ശ്ശോ!", + "autosuggest_hashtag.per_week": "{count} per week", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "വീണ്ടും ശ്രമിക്കുക", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "അടയ്ക്കുക", + "bundle_modal_error.message": "ഈ വെബ്പേജ് പ്രദർശിപ്പിക്കുമ്പോൾ എന്തോ കുഴപ്പം സംഭവിച്ചു.", + "bundle_modal_error.retry": "വീണ്ടും ശ്രമിക്കുക", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.direct": "Direct messages", + "column.directory": "Browse profiles", + "column.domain_blocks": "Hidden domains", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.pins": "Pinned toot", + "column.public": "Federated timeline", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", + "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", + "column_header.unpin": "Unpin", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media only", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Remove this 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.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.select.no_options_message": "No suggestions found", + "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "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", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "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.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll.voted": "You voted for this answer", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "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", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json new file mode 100644 index 000000000..8af8e3493 --- /dev/null +++ b/app/javascript/mastodon/locales/mr.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "यादीत घाला किंवा यादीतून काढून टाका", + "account.badges.bot": "स्वयंचलित खाते", + "account.block": "@{name} यांना ब्लॉक करा", + "account.block_domain": "{domain} पासून सर्व लपवा", + "account.blocked": "ब्लॉक केले आहे", + "account.cancel_follow_request": "अनुयायी होण्याची विनंती रद्द करा", + "account.direct": "थेट संदेश @{name}", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "प्रोफाइल एडिट करा", + "account.endorse": "Feature on profile", + "account.follow": "अनुयायी व्हा", + "account.followers": "अनुयायी", + "account.followers.empty": "ह्या वापरकर्त्याचा आतापर्यंत कोणी अनुयायी नाही.", + "account.follows": "अनुयायी आहे", + "account.follows.empty": "हा वापरकर्ता अजूनपर्यंत कोणाचा अनुयायी नाही.", + "account.follows_you": "तुमचा अनुयायी आहे", + "account.hide_reblogs": "@{name} पासून सर्व बूस्ट लपवा", + "account.last_status": "शेवटचे सक्रिय", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.media": "दृक्श्राव्य मजकूर", + "account.mention": "@{name} चा उल्लेख करा", + "account.moved_to": "{name} आता आहे:", + "account.mute": "@{name} ला मूक कारा", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", + "account.never_active": "Never", + "account.posts": "Toots", + "account.posts_with_replies": "Toots and replies", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", + "account.show_reblogs": "{name}चे सर्व बुस्ट्स दाखवा", + "account.unblock": "@{name} ला ब्लॉक करा", + "account.unblock_domain": "उघड करा {domain}", + "account.unendorse": "Don't feature on profile", + "account.unfollow": "अनुयायी असणे थांबवा", + "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.title": "अरेरे!", + "autosuggest_hashtag.per_week": "{count} प्रतिसप्ताह", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "हा घटक लोड करतांना काहीतरी चुकले आहे.", + "bundle_column_error.retry": "पुन्हा प्रयत्न करा", + "bundle_column_error.title": "नेटवर्क त्रुटी", + "bundle_modal_error.close": "बंद करा", + "bundle_modal_error.message": "हा घटक लोड करतांना काहीतरी चुकले आहे.", + "bundle_modal_error.retry": "पुन्हा प्रयत्न करा", + "column.blocks": "ब्लॉक केलेले खातेधारक", + "column.community": "Local timeline", + "column.direct": "थेट संदेश", + "column.directory": "Browse profiles", + "column.domain_blocks": "गुप्त डोमेन्स", + "column.favourites": "आवडते", + "column.follow_requests": "अनुचरण विनंत्या", + "column.home": "मुख्यपृष्ठ", + "column.lists": "याद्या", + "column.mutes": "नि: शब्द खातेधारक", + "column.notifications": "सूचना", + "column.pins": "Pinned toot", + "column.public": "Federated timeline", + "column_back_button.label": "मागे", + "column_header.hide_settings": "सेटिंग लपवा", + "column_header.moveLeft_settings": "स्तंभ डावीकडे सरकवा", + "column_header.moveRight_settings": "स्तंभ उजवीकडे सरकवा", + "column_header.pin": "टाचण", + "column_header.show_settings": "सेटिंग्स दाखवा", + "column_header.unpin": "अनपिन करा", + "column_subheading.settings": "सेटिंग्ज", + "community.column_settings.media_only": "केवळ मीडिया", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "अधिक जाणून घ्या", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "आपल्या मनात काय आहे?", + "compose_form.poll.add_option": "नवीन पर्याय", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "निवड {number}", + "compose_form.poll.remove_option": "हा पर्याय काढा", + "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.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "हटवा", + "confirmations.delete.message": "हे स्टेटस तुम्हाला नक्की हटवायचंय?", + "confirmations.delete_list.confirm": "हटवा", + "confirmations.delete_list.message": "ही यादी तुम्हाला नक्की कायमची हटवायचीय?", + "confirmations.domain_block.confirm": "संपूर्ण डोमेन लपवा", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.select.no_options_message": "No suggestions found", + "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "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", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "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.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll.voted": "You voted for this answer", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "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", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index d7c509963..6881d3be3 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -4,6 +4,7 @@ "account.block": "Block @{name}", "account.block_domain": "Hide everything from {domain}", "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct message @{name}", "account.domain_blocked": "Domain hidden", "account.edit_profile": "Edit profile", @@ -15,6 +16,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "Follows you", "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Media", @@ -23,10 +25,11 @@ "account.mute": "Mute @{name}", "account.mute_notifications": "Mute notifications from @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", "account.report": "Report @{name}", - "account.requested": "Awaiting approval. Click to cancel follow request", + "account.requested": "Awaiting approval", "account.share": "Share @{name}'s profile", "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Unblock @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.retry": "Try again", @@ -47,6 +53,7 @@ "column.blocks": "Blocked users", "column.community": "Local timeline", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "Favourites", "column.follow_requests": "Follow requests", @@ -64,7 +71,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Embed this status on your website by copying the code below.", "embed.preview": "Here is what it will look like:", "emoji_button.activity": "Activity", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Pinned toots", "navigation_bar.preferences": "Preferences", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.security": "Security", "notification.favourite": "{name} favourited your status", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Adjust status privacy", @@ -290,6 +314,7 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Show more", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", "suggestions.dismiss": "Dismiss suggestion", @@ -367,15 +394,23 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Delete", - "upload_progress.label": "Uploading...", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", "video.close": "Close video", "video.exit_fullscreen": "Exit full screen", "video.expand": "Expand video", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 4bcab5ba0..74aaad970 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -4,6 +4,7 @@ "account.block": "Blokkeer @{name}", "account.block_domain": "Verberg alles van {domain}", "account.blocked": "Geblokkeerd", + "account.cancel_follow_request": "Volgverzoek annuleren", "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domein verborgen", "account.edit_profile": "Profiel bewerken", @@ -15,6 +16,7 @@ "account.follows.empty": "Deze gebruiker volgt nog niemand.", "account.follows_you": "Volgt jou", "account.hide_reblogs": "Verberg boosts van @{name}", + "account.last_status": "Laatst actief", "account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}", "account.locked_info": "De privacystatus van dit account is op besloten gezet. De eigenaar bepaalt handmatig wie hen kan volgen.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Negeer @{name}", "account.mute_notifications": "Negeer meldingen van @{name}", "account.muted": "Genegeerd", + "account.never_active": "Nooit", "account.posts": "Toots", "account.posts_with_replies": "Toots en reacties", "account.report": "Rapporteer @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Ontvolgen", "account.unmute": "@{name} niet langer negeren", "account.unmute_notifications": "@{name} meldingen niet langer negeren", + "alert.rate_limited.message": "Probeer het nog een keer na {retry_time, time, medium}.", + "alert.rate_limited.title": "Beperkt te gebruiken", "alert.unexpected.message": "Er deed zich een onverwachte fout voor", "alert.unexpected.title": "Oeps!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan", "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.", "bundle_column_error.retry": "Opnieuw proberen", @@ -47,6 +53,7 @@ "column.blocks": "Geblokkeerde gebruikers", "column.community": "Lokale tijdlijn", "column.direct": "Directe berichten", + "column.directory": "Gebruikersgids", "column.domain_blocks": "Genegeerde servers", "column.favourites": "Favorieten", "column.follow_requests": "Volgverzoeken", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Weet je zeker dat je deze lijst definitief wilt verwijderen?", "confirmations.domain_block.confirm": "Verberg alles van deze server", "confirmations.domain_block.message": "Weet je het echt heel erg zeker dat je alles van {domain} wilt negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en beter. Je zult geen toots van deze server op openbare tijdlijnen zien of in jouw meldingen. Jouw volgers van deze server worden verwijderd.", + "confirmations.logout.confirm": "Uitloggen", + "confirmations.logout.message": "Weet je zeker dat je wilt uitloggen?", "confirmations.mute.confirm": "Negeren", + "confirmations.mute.explanation": "Dit verbergt toots van hen en toots waar hen in wordt vermeld, maar hen kan nog steeds jouw toots bekijken en jou volgen.", "confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?", "confirmations.redraft.confirm": "Verwijderen en herschrijven", "confirmations.redraft.message": "Weet je zeker dat je deze toot wilt verwijderen en herschrijven? Je verliest wel de boosts en favorieten, en reacties op de originele toot zitten niet meer aan de nieuwe toot vast.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Door nu te reageren overschrijf je de toot die je op dit moment aan het schrijven bent. Weet je zeker dat je verder wil gaan?", "confirmations.unfollow.confirm": "Ontvolgen", "confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?", + "conversation.delete": "Gesprek verwijderen", + "conversation.mark_as_read": "Als gelezen markeren", + "conversation.open": "Gesprek tonen", + "conversation.with": "Met {names}", + "directory.federated": "Fediverse (wat bekend is)", + "directory.local": "Alleen {domain}", + "directory.new_arrivals": "Nieuwe accounts", + "directory.recently_active": "Onlangs actief", "embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.", "embed.preview": "Zo komt het eruit te zien:", "emoji_button.activity": "Activiteiten", @@ -134,6 +152,10 @@ "empty_column.mutes": "Jij hebt nog geen gebruikers genegeerd.", "empty_column.notifications": "Je hebt nog geen meldingen. Begin met iemand een gesprek.", "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere servers om het te vullen", + "error.unexpected_crash.explanation": "Als gevolg van een bug in onze broncode of als gevolg van een compatibiliteitsprobleem met jouw webbrowser, kan deze pagina niet goed worden weergegeven.", + "error.unexpected_crash.next_steps": "Probeer deze pagina te vernieuwen. Wanneer dit niet helpt is het nog steeds mogelijk om Mastodon in een andere webbrowser of mobiele app te gebruiken.", + "errors.unexpected_crash.copy_stacktrace": "Stacktrace naar klembord kopiëren", + "errors.unexpected_crash.report_issue": "Technisch probleem melden", "follow_request.authorize": "Goedkeuren", "follow_request.reject": "Afkeuren", "getting_started.developers": "Ontwikkelaars", @@ -142,7 +164,7 @@ "getting_started.heading": "Aan de slag", "getting_started.invite": "Mensen uitnodigen", "getting_started.open_source_notice": "Mastodon is vrije software. Je kunt bijdragen of problemen melden op GitHub via {github}.", - "getting_started.security": "Beveiliging", + "getting_started.security": "Accountinstellingen", "getting_started.terms": "Voorwaarden", "hashtag.column_header.tag_mode.all": "en {additional}", "hashtag.column_header.tag_mode.any": "of {additional}", @@ -184,7 +206,7 @@ "keyboard_shortcuts.description": "Omschrijving", "keyboard_shortcuts.direct": "om jouw directe berichten te tonen", "keyboard_shortcuts.down": "om naar beneden door de lijst te bewegen", - "keyboard_shortcuts.enter": "om toot volledig te tonen", + "keyboard_shortcuts.enter": "om uitgebreide toot te tonen", "keyboard_shortcuts.favourite": "om aan jouw favorieten toe te voegen", "keyboard_shortcuts.favourites": "om jouw lijst met favorieten te tonen", "keyboard_shortcuts.federated": "om de globale tijdlijn te tonen", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Naam nieuwe lijst", "lists.search": "Zoek naar mensen die je volgt", "lists.subheading": "Jouw lijsten", + "load_pending": "{count, plural, one {# nieuw item} other {# nieuwe items}}", "loading_indicator.label": "Laden…", "media_gallery.toggle_visible": "Media wel/niet tonen", "missing_indicator.label": "Niet gevonden", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Persoonlijk", "navigation_bar.pins": "Vastgezette toots", "navigation_bar.preferences": "Instellingen", - "navigation_bar.profile_directory": "Gebruikersgids", "navigation_bar.public_timeline": "Globale tijdlijn", "navigation_bar.security": "Beveiliging", "notification.favourite": "{name} voegde jouw toot als favoriet toe", @@ -277,8 +299,10 @@ "notifications.group": "{count} meldingen", "poll.closed": "Gesloten", "poll.refresh": "Vernieuwen", + "poll.total_people": "{count, plural, one {# persoon} other {# mensen}}", "poll.total_votes": "{count, plural, one {# stem} other {# stemmen}}", "poll.vote": "Stemmen", + "poll.voted": "Je hebt hier op gestemd", "poll_button.add_poll": "Poll toevoegen", "poll_button.remove_poll": "Poll verwijderen", "privacy.change": "Zichtbaarheid toot aanpassen", @@ -290,6 +314,7 @@ "privacy.public.short": "Openbaar", "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen", "privacy.unlisted.short": "Minder openbaar", + "refresh": "Vernieuwen", "regeneration_indicator.label": "Aan het laden…", "regeneration_indicator.sublabel": "Jouw tijdlijn wordt aangemaakt!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "Gebruikers", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Het zoeken in toots is op deze Mastodonserver niet ingeschakeld.", "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", "status.admin_account": "Moderatie-omgeving van @{name} openen", "status.admin_status": "Deze toot in de moderatie-omgeving openen", @@ -333,7 +359,7 @@ "status.more": "Meer", "status.mute": "Negeer @{name}", "status.mute_conversation": "Negeer gesprek", - "status.open": "Toot volledig tonen", + "status.open": "Uitgebreide toot tonen", "status.pin": "Aan profielpagina vastmaken", "status.pinned": "Vastgemaakte toot", "status.read_more": "Meer lezen", @@ -352,6 +378,7 @@ "status.show_more": "Meer tonen", "status.show_more_all": "Alles meer tonen", "status.show_thread": "Gesprek tonen", + "status.uncached_media_warning": "Niet beschikbaar", "status.unmute_conversation": "Gesprek niet langer negeren", "status.unpin": "Van profielpagina losmaken", "suggestions.dismiss": "Voorstel verwerpen", @@ -361,20 +388,28 @@ "tabs_bar.local_timeline": "Lokaal", "tabs_bar.notifications": "Meldingen", "tabs_bar.search": "Zoeken", - "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} left", - "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} left", - "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} left", + "time_remaining.days": "{number, plural, one {# dag} other {# dagen}} te gaan", + "time_remaining.hours": "{number, plural, one {# uur} other {# uur}} te gaan", + "time_remaining.minutes": "{number, plural, one {# minuut} other {# minuten}} te gaan", "time_remaining.moments": "Nog enkele ogenblikken resterend", - "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} left", + "time_remaining.seconds": "{number, plural, one {# seconde} other {# seconden}} te gaan", "trends.count_by_accounts": "{count} {rawCount, plural, one {persoon praat} other {mensen praten}} hierover", + "trends.trending_now": "Trends", "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.", - "upload_area.title": "Hierin slepen om te uploaden", - "upload_button.label": "Media toevoegen (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_area.title": "Hiernaar toe slepen om te uploaden", + "upload_button.label": "Media toevoegen ({formats})", "upload_error.limit": "Uploadlimiet van bestand overschreden.", "upload_error.poll": "Het uploaden van bestanden is in polls niet toegestaan.", "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking", - "upload_form.focus": "Voorvertoning aanpassen", + "upload_form.edit": "Bewerken", "upload_form.undo": "Verwijderen", + "upload_modal.analyzing_picture": "Afbeelding analyseren…", + "upload_modal.apply": "Toepassen", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "upload_modal.detect_text": "Tekst in een afbeelding detecteren", + "upload_modal.edit_media": "Media bewerken", + "upload_modal.hint": "Klik of sleep de cirkel in de voorvertoning naar een centraal punt dat op elke thumbnail zichtbaar moet blijven.", + "upload_modal.preview_label": "Voorvertoning ({ratio})", "upload_progress.label": "Uploaden...", "video.close": "Video sluiten", "video.exit_fullscreen": "Volledig scherm sluiten", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json new file mode 100644 index 000000000..039381cf0 --- /dev/null +++ b/app/javascript/mastodon/locales/nn.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Legg til eller ta vekk fra liste", + "account.badges.bot": "Robot", + "account.block": "Blokkér @{name}", + "account.block_domain": "Gøyme alt innhald for domenet {domain}", + "account.blocked": "Blokkert", + "account.cancel_follow_request": "Avslutt føljar-førespurnad", + "account.direct": "Direkte meld @{name}", + "account.domain_blocked": "Domenet er gøymt", + "account.edit_profile": "Rediger profil", + "account.endorse": "Framhev på profilen din", + "account.follow": "Følj", + "account.followers": "Føljare", + "account.followers.empty": "Er ikkje nokon som føljar denne brukaren ennå.", + "account.follows": "Føljingar", + "account.follows.empty": "Denne brukaren foljer ikkje nokon ennå.", + "account.follows_you": "Føljar deg", + "account.hide_reblogs": "Gøym fremhevingar for @{name}", + "account.last_status": "Sist aktiv", + "account.link_verified_on": "Eigerskap for denne linken er sist sjekket den {date}", + "account.locked_info": "Brukarens privat-status er satt til lukka. Eigaren må manuelt døme kvem som kan følje honom.", + "account.media": "Media", + "account.mention": "Nemne @{name}", + "account.moved_to": "{name} har flytta til:", + "account.mute": "Målbind @{name}", + "account.mute_notifications": "Målbind varslingar ifrå @{name}", + "account.muted": "Målbindt", + "account.never_active": "Aldri", + "account.posts": "Tutar", + "account.posts_with_replies": "Tutar og svar", + "account.report": "Rapporter @{name}", + "account.requested": "Venter på samtykke. Klikk for å avbryte føljar-førespurnad", + "account.share": "Del @{name} sin profil", + "account.show_reblogs": "Sjå framhevingar ifrå @{name}", + "account.unblock": "Avblokker @{name}", + "account.unblock_domain": "Vis {domain}", + "account.unendorse": "Ikkje framhev på profil", + "account.unfollow": "Avfølja", + "account.unmute": "Av-demp @{name}", + "account.unmute_notifications": "Av-demp notifikasjoner ifrå @{name}", + "alert.rate_limited.message": "Ver vennlig og prøv igjen {retry_time, time, medium}.", + "alert.rate_limited.title": "Bregrensa rate", + "alert.unexpected.message": "Eit uforventa problem har hendt.", + "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per veke", + "boost_modal.combo": "Du kan trykke {combo} for å hoppe over dette neste gong", + "bundle_column_error.body": "Noko gikk gale mens komponent ble nedlasta.", + "bundle_column_error.retry": "Prøv igjen", + "bundle_column_error.title": "Tenarmaskin feil", + "bundle_modal_error.close": "Lukk", + "bundle_modal_error.message": "Noko gikk gale mens komponent var i ferd med å bli nedlasta.", + "bundle_modal_error.retry": "Prøv igjen", + "column.blocks": "Blokka brukare", + "column.community": "Lokal samtid", + "column.direct": "Direkte meldingar", + "column.directory": "Sjå gjennom profiler", + "column.domain_blocks": "Gøymte domener", + "column.favourites": "Favorittar", + "column.follow_requests": "Føljarførespurnad", + "column.home": "Heim", + "column.lists": "Lister", + "column.mutes": "Målbindte brukare", + "column.notifications": "Varslingar", + "column.pins": "Festa tuter", + "column.public": "Federert samtid", + "column_back_button.label": "Tilbake", + "column_header.hide_settings": "Skjul innstillingar", + "column_header.moveLeft_settings": "Flytt feltet til venstre", + "column_header.moveRight_settings": "Flytt feltet til høgre", + "column_header.pin": "Fest", + "column_header.show_settings": "Vis innstillingar", + "column_header.unpin": "Løys", + "column_subheading.settings": "Innstillingar", + "community.column_settings.media_only": "Kun medie", + "compose_form.direct_message_warning": "Denne tuten vil kun verte synleg for nemnde brukarar.", + "compose_form.direct_message_warning_learn_more": "Lær meir", + "compose_form.hashtag_warning": "Denne tuten vill ikkje bli lista under nokon knagg ettersom den ikkje er opplista. Berre offentlege tutar kan ble søkt på ved emneknagg.", + "compose_form.lock_disclaimer": "Din brukar er ikkje {locked}. Alle kan følje deg for å sjå føljar-modus poster.", + "compose_form.lock_disclaimer.lock": "låst", + "compose_form.placeholder": "Kva har du på hjartet?", + "compose_form.poll.add_option": "Legg til eit punkt", + "compose_form.poll.duration": "Varigheit for spørring", + "compose_form.poll.option_placeholder": "Val {number}", + "compose_form.poll.remove_option": "Ta burt dette valet", + "compose_form.publish": "Tut", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Merk media som sensitivt", + "compose_form.sensitive.marked": "Media er markert som sensitivt", + "compose_form.sensitive.unmarked": "Media er ikkje merka som sensitivt", + "compose_form.spoiler.marked": "Tekst er gøymt bak ein advarsel", + "compose_form.spoiler.unmarked": "Tekst er ikkje gøymt", + "compose_form.spoiler_placeholder": "Skriv varselen din her", + "confirmation_modal.cancel": "Avbrot", + "confirmations.block.block_and_report": "Blokk & rapportér", + "confirmations.block.confirm": "Blokkér", + "confirmations.block.message": "Er du sikker på at du vill blokke {name}?", + "confirmations.delete.confirm": "Slett", + "confirmations.delete.message": "Er du sikker på at du vill slette denne statusen?", + "confirmations.delete_list.confirm": "Slett", + "confirmations.delete_list.message": "Er du sikker på at du vill slette denne listen for alltid?", + "confirmations.domain_block.confirm": "Gøym heile domenet", + "confirmations.domain_block.message": "Er du ordentleg, ordentleg sikker på at du vill blokkere heile {domain}? I dei tilfeller er det bedre med ein målretta blokkering eller demping av individuelle brukare.", + "confirmations.logout.confirm": "Logg ut", + "confirmations.logout.message": "Er du sikker på at du vill logge ut?", + "confirmations.mute.confirm": "Målbind", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", + "confirmations.mute.message": "Er du sikker på at d vill målbinde {name}?", + "confirmations.redraft.confirm": "Slett & gjennopprett", + "confirmations.redraft.message": "Er du sikker på at du vill slette statusen og gjennoprette den? Favoritter og framhevinger vill bli borte, og svar til den originale posten vill bli einstøing.", + "confirmations.reply.confirm": "Svar", + "confirmations.reply.message": "Å svare nå vill overskrive meldingen du er i ferd med å skrive. Er du sikker på at du vill gå fram?", + "confirmations.unfollow.confirm": "Avfølj", + "confirmations.unfollow.message": "Er du sikker på at du vill avfølje {name}?", + "conversation.delete": "Slett samtale", + "conversation.mark_as_read": "Merk som lest", + "conversation.open": "Sjå samtale", + "conversation.with": "Med {names}", + "directory.federated": "Fra kjent fedivers", + "directory.local": "Fra berre {domain} domenet", + "directory.new_arrivals": "Nyankommne", + "directory.recently_active": "Nylig aktiv", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Dette er korleis den vil sjå ut:", + "emoji_button.activity": "Aktivitet", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flagg", + "emoji_button.food": "Mat & drikke", + "emoji_button.label": "Legg til smilefjes", + "emoji_button.nature": "Natur", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objektar", + "emoji_button.people": "Folk", + "emoji_button.recent": "Ofte brukt", + "emoji_button.search": "Søk...", + "emoji_button.search_results": "Søke resultater", + "emoji_button.symbols": "Symbolar", + "emoji_button.travel": "Reise & steder", + "empty_column.account_timeline": "Ikkje nokon tutar her!", + "empty_column.account_unavailable": "Profil ikkje tilgjengelig", + "empty_column.blocks": "Du har ikkje blokkért nokon brukarar ennå.", + "empty_column.community": "Den lokale samtiden er tom. Skriv noko offentleg å få ballen til å rulle!", + "empty_column.direct": "Du har ikkje nokon direkte meldingar ennå. Når du sendar eller får ein, så vill den ende opp her.", + "empty_column.domain_blocks": "Der er ikkje nokon gøymte domener enno.", + "empty_column.favourited_statuses": "Du har ikkje favorisert nokon tutar enno. Når du favoriserer noko, så vill det ende opp her.", + "empty_column.favourites": "Ikkje nokon har favorisert denne tuten enno. Når nokon gjer det, så vill den ende opp her.", + "empty_column.follow_requests": "Du har ikkje nokon føljar førespurnad enno. Når du får ein, så vill den sjåast her.", + "empty_column.hashtag": "Det er ikkje noko i denne emneknaggen her enno.", + "empty_column.home": "Din heime-tidslinja er tom! Dra til {public} eller søk for å starte å møte andre brukare.", + "empty_column.home.public_timeline": "Den offentlege tidslinja", + "empty_column.list": "Det er ikkje noko i denne lista enno. Når medlemmar av denne lista poster statuser, så vill dei sjåast her.", + "empty_column.lists": "Du har ikkje nokon liste enno. Når du lagar ein, så vill den ende up her.", + "empty_column.mutes": "Du har ikkje dempet nokon brukare enno.", + "empty_column.notifications": "Der er ikkje nokon varsler her enno. Kommuniser med andre for å starte samtalen.", + "empty_column.public": "Det er ikkje noko her! Skriv noko offentleg, eller manuelt følje brukare fra andre tenarmaskiner for å fylle det opp", + "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.next_steps": "Prøv å laste på nytt. Om det ikkje hjelper så kan du fortsatt bruke mastodon gjennom ein anna nettlesar eller ein applikasjon.", + "errors.unexpected_crash.copy_stacktrace": "Kopiér stacktrace til klippebord", + "errors.unexpected_crash.report_issue": "Rapportér problem", + "follow_request.authorize": "Autoriser", + "follow_request.reject": "Reject", + "getting_started.developers": "Utviklare", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Dokumentasjon", + "getting_started.heading": "Komme i gong", + "getting_started.invite": "Inviter folk", + "getting_started.open_source_notice": "Mastodon har åpen kilde kode. Du kan hjelpe til med problemar på GitHub gjennom {github}.", + "getting_started.security": "Brukar instillingar", + "getting_started.terms": "Brukarvillkår", + "hashtag.column_header.tag_mode.all": "og {additional}", + "hashtag.column_header.tag_mode.any": "eller {additional}", + "hashtag.column_header.tag_mode.none": "uten {additional}", + "hashtag.column_settings.select.no_options_message": "Ingen forslag funnet", + "hashtag.column_settings.select.placeholder": "Legg til emneknagger…", + "hashtag.column_settings.tag_mode.all": "Alle disse", + "hashtag.column_settings.tag_mode.any": "Kvem som helst av disse", + "hashtag.column_settings.tag_mode.none": "Ikkje nokon av disse", + "hashtag.column_settings.tag_toggle": "Inkludér ekstra emneknagger til denne kolonnen", + "home.column_settings.basic": "Enkel", + "home.column_settings.show_reblogs": "Vis fremhevingar", + "home.column_settings.show_replies": "Vis svar", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Neste", + "introduction.federation.federated.headline": "Federert", + "introduction.federation.federated.text": "Offentlege poster fra tenarmaskiner i fediverset kjem fram i den federerte tidslinja.", + "introduction.federation.home.headline": "Heim", + "introduction.federation.home.text": "Poster frå folk du kjenner kjem i heime tidslinja. Du kan følje kvem som helst ifrå alle tenarmaskiner!", + "introduction.federation.local.headline": "Lokal", + "introduction.federation.local.text": "Offentlege poster frå folk på samme tenarmaskin som deg vill du finne i den lokale tidslinja.", + "introduction.interactions.action": "Fullfør omvisning!", + "introduction.interactions.favourite.headline": "Lik", + "introduction.interactions.favourite.text": "Du kan lagre ein tut til seinere, og la forfatteren vite at ut likte den ved å trykke likar på tuten.", + "introduction.interactions.reblog.headline": "Fremhev", + "introduction.interactions.reblog.text": "Du kan dela folks tutar ved å \"booste\" døm.", + "introduction.interactions.reply.headline": "Svar", + "introduction.interactions.reply.text": "Du kan svare på andre folk sine tuter, om så vill tutane lenkes i hop i ein samtale.", + "introduction.welcome.action": "Ta åt!", + "introduction.welcome.headline": "Første steg", + "introduction.welcome.text": "Velkommen til fediverset! Om ein kort tid vill det være mogleg å skrive til verden og venner gjennom ein stor variasjon av tenarmaskiner. Men denne tenarmaskinen, {domain}, er spesiell - den holder din profil, så ikkje gløym namnet dens.", + "keyboard_shortcuts.back": "for å navigere tilbake", + "keyboard_shortcuts.blocked": "for å åpne lista for blokka brukare", + "keyboard_shortcuts.boost": "for å dele", + "keyboard_shortcuts.column": "for å fokusere på ein status i ein av kollonene", + "keyboard_shortcuts.compose": "for å fokusera på status-skrivaren", + "keyboard_shortcuts.description": "Beskrivelse", + "keyboard_shortcuts.direct": "forå åpne direkte meldingar i ein kollone", + "keyboard_shortcuts.down": "for å bevege seg opp og ned", + "keyboard_shortcuts.enter": "for å åpne status", + "keyboard_shortcuts.favourite": "for å favorisere", + "keyboard_shortcuts.favourites": "for å åpne favoriserings lista", + "keyboard_shortcuts.federated": "for å åpne den fødererte tidslinja", + "keyboard_shortcuts.heading": "Tastatur hurtigtaster", + "keyboard_shortcuts.home": "for åpne heime tidslinja", + "keyboard_shortcuts.hotkey": "Hurtigtast", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "for å åpne den lokale tidslinja", + "keyboard_shortcuts.mention": "for å nemne forfatteren", + "keyboard_shortcuts.muted": "for å åpne lista over dempa brukare", + "keyboard_shortcuts.my_profile": "for å åpne profilen din", + "keyboard_shortcuts.notifications": "for å åpne notifikasjons kolonnen", + "keyboard_shortcuts.pinned": "forå åpne lista for festa statuser", + "keyboard_shortcuts.profile": "for å åpne forfatteren's profil", + "keyboard_shortcuts.reply": "for å svare", + "keyboard_shortcuts.requests": "for å åpne lista for føljar førespurnader", + "keyboard_shortcuts.search": "for å fokus søkje", + "keyboard_shortcuts.start": "for å åpne \"kjem i gong\" kolonna", + "keyboard_shortcuts.toggle_hidden": "for å vise/gøyme tekst bak innholds varsel", + "keyboard_shortcuts.toggle_sensitivity": "for å vise/gøyme media", + "keyboard_shortcuts.toot": "Start ein heilt ny tut", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "for å bevege seg oppover i lista", + "lightbox.close": "Lukk", + "lightbox.next": "Neste", + "lightbox.previous": "Forrige", + "lightbox.view_context": "Sjå kontekst", + "lists.account.add": "Legg til i liste", + "lists.account.remove": "Ta burt fra liste", + "lists.delete": "Slett liste", + "lists.edit": "Rediger liste", + "lists.edit.submit": "Bytt tittel", + "lists.new.create": "Legg til liste", + "lists.new.title_placeholder": "Ny liste tittel", + "lists.search": "Søk gjennom folk du føljar", + "lists.subheading": "Dine lister", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Laster...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Ikkje funne", + "missing_indicator.sublabel": "Denne ressursen ble ikkje funne", + "mute_modal.hide_notifications": "Gøyme notifikasjoner frå denne brukaren?", + "navigation_bar.apps": "Mobil apper", + "navigation_bar.blocks": "Blokka brukare", + "navigation_bar.community_timeline": "Lokal tidslinje", + "navigation_bar.compose": "Lag ein ny status", + "navigation_bar.direct": "Direkte meldingar", + "navigation_bar.discover": "Oppdag", + "navigation_bar.domain_blocks": "Gøymte domener", + "navigation_bar.edit_profile": "Rediger profil", + "navigation_bar.favourites": "Favoritter", + "navigation_bar.filters": "Demp ord", + "navigation_bar.follow_requests": "Føljar førespurnader", + "navigation_bar.follows_and_followers": "Føljer og føljare", + "navigation_bar.info": "Om denne tenarmaskinen", + "navigation_bar.keyboard_shortcuts": "Hurtigtaster", + "navigation_bar.lists": "Lister", + "navigation_bar.logout": "Logg ut", + "navigation_bar.mutes": "Dempa brukare", + "navigation_bar.personal": "Personlig", + "navigation_bar.pins": "Festa tuter", + "navigation_bar.preferences": "Preferanser", + "navigation_bar.public_timeline": "Federert tidslinje", + "navigation_bar.security": "Sikkerheit", + "notification.favourite": "{name} likte din status", + "notification.follow": "{name} fulgte deg", + "notification.mention": "{name} nevnte deg", + "notification.poll": "Ein avstemming du har votert i har endt", + "notification.reblog": "{name} delte statusen din", + "notifications.clear": "Klarer notifikasjoner", + "notifications.clear_confirmation": "Er du sikker på at du vill permanent klarere alle notifikasjonene dine?", + "notifications.column_settings.alert": "Desktop notifikasjoner", + "notifications.column_settings.favourite": "Favoritter:", + "notifications.column_settings.filter_bar.advanced": "Vis alle kategorier", + "notifications.column_settings.filter_bar.category": "Rask filter bar", + "notifications.column_settings.filter_bar.show": "Vis", + "notifications.column_settings.follow": "Nye føljare:", + "notifications.column_settings.mention": "Nemningar:", + "notifications.column_settings.poll": "Avstemming resultater:", + "notifications.column_settings.push": "Push varsler", + "notifications.column_settings.reblog": "Framhevinger:", + "notifications.column_settings.show": "Vis i kolonne", + "notifications.column_settings.sound": "Køyr lyd", + "notifications.filter.all": "Alle", + "notifications.filter.boosts": "Framhevinger", + "notifications.filter.favourites": "Favoritter", + "notifications.filter.follows": "Føljare", + "notifications.filter.mentions": "Nemningar", + "notifications.filter.polls": "Avstemmings resultater", + "notifications.group": "{count} notifikasjoner", + "poll.closed": "Lukka", + "poll.refresh": "Forfriske", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Voter", + "poll.voted": "Du voterte for dette svaret", + "poll_button.add_poll": "Start ein avstemming", + "poll_button.remove_poll": "Slett avstemming", + "privacy.change": "Juster status synligheit", + "privacy.direct.long": "Post berre direkte til nemnte brukare", + "privacy.direct.short": "Direkte", + "privacy.private.long": "Post til berre føljare", + "privacy.private.short": "Berre-føljare", + "privacy.public.long": "Post til offentlege tidslinjar", + "privacy.public.short": "Offentleg", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Uoppført", + "refresh": "Forfrisk", + "regeneration_indicator.label": "Lastar…", + "regeneration_indicator.sublabel": "Din startside driver å blir forberedt!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "no", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Avbrot", + "report.forward": "Send videre til {target}", + "report.forward_hint": "Denne brukaren er frå ein anna tenarmaskin. Send ein anonymisert versjon av rapporten til døm også?", + "report.hint": "Denne rapporten vill bli sendt til din tenarmaskin moderator. Du kan gi ein forklaring til kvifor du rapportérer denne brukaren under:", + "report.placeholder": "Tilleggskommentarer", + "report.submit": "Send inn", + "report.target": "Rapportér {target}", + "search.placeholder": "Søk", + "search_popout.search_format": "Avansert søke format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "emneknagg", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "brukar", + "search_results.accounts": "Folk", + "search_results.hashtags": "Emneknagger", + "search_results.statuses": "Tutar", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Slett", + "status.detailed_status": "Detaljert samtale syn", + "status.direct": "Direkte meld @{name}", + "status.embed": "Bygge inn", + "status.favourite": "Lik", + "status.filtered": "Filter", + "status.load_more": "Last inn meir", + "status.media_hidden": "Media gøymt", + "status.mention": "Nemn @{name}", + "status.more": "Meir", + "status.mute": "Demp @{name}", + "status.mute_conversation": "Demp samtale", + "status.open": "Utvid statusen", + "status.pin": "Fest på profil", + "status.pinned": "Fasta tut", + "status.read_more": "Les meir", + "status.reblog": "Fremhev", + "status.reblog_private": "Fremhev til orginale sjåare", + "status.reblogged_by": "{name} fremheivd", + "status.reblogs.empty": "Ikkje nokon har fremhevd tuten enno. Om nokon gjør det kjem det opp her.", + "status.redraft": "Slett & gjer om", + "status.reply": "Svar", + "status.replyAll": "Svar til tråd", + "status.report": "Rapportér @{name}", + "status.sensitive_warning": "Sensitivt innhald", + "status.share": "Del", + "status.show_less": "Vis mindre", + "status.show_less_all": "Vis mindre for alle", + "status.show_more": "Vis meir", + "status.show_more_all": "Vis meir for alle", + "status.show_thread": "Vis tråd", + "status.uncached_media_warning": "Ikkje tilgjengeleg", + "status.unmute_conversation": "Ikke demp samtale", + "status.unpin": "Angre festing på profil", + "suggestions.dismiss": "Avbrot forslag", + "suggestions.header": "Du e kanskje interessert i…", + "tabs_bar.federated_timeline": "Føderert", + "tabs_bar.home": "Heim", + "tabs_bar.local_timeline": "Lokal", + "tabs_bar.notifications": "Notifikasjoner", + "tabs_bar.search": "Søk", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Kort tid gjennstår", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Kva som trender no", + "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.", + "upload_area.title": "Dra & slipp for å laste opp", + "upload_button.label": "Legg til media ({formats})", + "upload_error.limit": "Fil størrelsen er for stor.", + "upload_error.poll": "Fil opplasting ikkje lov i hop med avstemming.", + "upload_form.description": "Beskriv for synshemmede", + "upload_form.edit": "Redigér", + "upload_form.undo": "Slett", + "upload_modal.analyzing_picture": "Analyserer bilde…", + "upload_modal.apply": "Bruk", + "upload_modal.description_placeholder": "Ein rask brun rev hoppar over den late hunden", + "upload_modal.detect_text": "Finn tekst i bildet", + "upload_modal.edit_media": "Redigér media", + "upload_modal.hint": "Klikk og dra sirkelen på forsyneren for å fokusere synspunktet slik det ska synes i alle minityrbildene.", + "upload_modal.preview_label": "Framsyn ({ratio})", + "upload_progress.label": "Lastar opp...", + "video.close": "Lukk video", + "video.exit_fullscreen": "Lukk fullskjerm", + "video.expand": "Expand video", + "video.fullscreen": "Fullskjerm", + "video.hide": "Gøym video", + "video.mute": "Demp lyd", + "video.pause": "Pause", + "video.play": "Spel", + "video.unmute": "Av-dempe lyd" +} diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 2ba8236e2..5c84c08fe 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -1,28 +1,31 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", + "account.add_or_remove_from_list": "Legg til eller fjern fra lister", "account.badges.bot": "Bot", "account.block": "Blokkér @{name}", "account.block_domain": "Skjul alt fra {domain}", - "account.blocked": "Blocked", + "account.blocked": "Blokkert", + "account.cancel_follow_request": "Avbryt følge forespørsel", "account.direct": "Direct Message @{name}", - "account.domain_blocked": "Domain hidden", + "account.domain_blocked": "Domenet skjult", "account.edit_profile": "Rediger profil", "account.endorse": "Feature on profile", "account.follow": "Følg", "account.followers": "Følgere", - "account.followers.empty": "No one follows this user yet.", + "account.followers.empty": "Ingen følger denne brukeren ennå.", "account.follows": "Følger", - "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows.empty": "Denne brukeren følger ikke noen enda.", "account.follows_you": "Følger deg", "account.hide_reblogs": "Skjul fremhevinger fra @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.last_status": "Sist aktiv", + "account.link_verified_on": "Eierskap av denne lenken ble sjekket {date}", + "account.locked_info": "Denne kontoens personvernstatus er satt til låst. Eieren vurderer manuelt hvem som kan følge dem.", "account.media": "Media", "account.mention": "Nevn @{name}", "account.moved_to": "{name} har flyttet til:", "account.mute": "Demp @{name}", "account.mute_notifications": "Ignorer varsler fra @{name}", "account.muted": "Muted", + "account.never_active": "Aldri", "account.posts": "Innlegg", "account.posts_with_replies": "Toots with replies", "account.report": "Rapportér @{name}", @@ -35,18 +38,22 @@ "account.unfollow": "Avfølg", "account.unmute": "Avdemp @{name}", "account.unmute_notifications": "Vis varsler fra @{name}", - "alert.unexpected.message": "An unexpected error occurred.", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "En uventet feil oppstod.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per uke", "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang", "bundle_column_error.body": "Noe gikk galt mens denne komponenten lastet.", "bundle_column_error.retry": "Prøv igjen", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "Nettverksfeil", "bundle_modal_error.close": "Lukk", "bundle_modal_error.message": "Noe gikk galt da denne komponenten lastet.", "bundle_modal_error.retry": "Prøv igjen", "column.blocks": "Blokkerte brukere", "column.community": "Lokal tidslinje", - "column.direct": "Direct messages", + "column.direct": "Direktemeldinger", + "column.directory": "Bla gjennom profiler", "column.domain_blocks": "Hidden domains", "column.favourites": "Likt", "column.follow_requests": "Følgeforespørsler", @@ -64,20 +71,20 @@ "column_header.show_settings": "Vis innstillinger", "column_header.unpin": "Løsne", "column_subheading.settings": "Innstillinger", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.direct_message_warning_learn_more": "Lær mer", "compose_form.hashtag_warning": "Denne tuten blir ikke listet under noen emneknagger da den er ulistet. Kun offentlige tuter kan søktes etter med emneknagg.", "compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.", "compose_form.lock_disclaimer.lock": "låst", "compose_form.placeholder": "Hva har du på hjertet?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Legg til et valg", + "compose_form.poll.duration": "Avstemningens varighet", + "compose_form.poll.option_placeholder": "Valg {number}", + "compose_form.poll.remove_option": "Fjern dette valget", "compose_form.publish": "Tut", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Merk media som sensitivt", "compose_form.sensitive.marked": "Media is marked as sensitive", "compose_form.sensitive.unmarked": "Media is not marked as sensitive", "compose_form.spoiler.marked": "Text is hidden behind warning", @@ -89,18 +96,29 @@ "confirmations.block.message": "Er du sikker på at du vil blokkere {name}?", "confirmations.delete.confirm": "Slett", "confirmations.delete.message": "Er du sikker på at du vil slette denne statusen?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Slett", "confirmations.delete_list.message": "Er du sikker på at du vil slette denne listen permanent?", "confirmations.domain_block.confirm": "Skjul alt fra domenet", "confirmations.domain_block.message": "Er du sikker på at du vil skjule hele domenet {domain}? I de fleste tilfeller er det bedre med målrettet blokkering eller demping.", + "confirmations.logout.confirm": "Logg ut", + "confirmations.logout.message": "Er du sikker på at du vil logge ut?", "confirmations.mute.confirm": "Demp", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Er du sikker på at du vil dempe {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", - "confirmations.reply.confirm": "Reply", + "confirmations.reply.confirm": "Svar", "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Slutt å følge", "confirmations.unfollow.message": "Er du sikker på at du vil slutte å følge {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "Med {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Kopier koden under for å bygge inn denne statusen på hjemmesiden din.", "embed.preview": "Slik kommer det til å se ut:", "emoji_button.activity": "Aktivitet", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.", "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Autorisér", "follow_request.reject": "Avvis", "getting_started.developers": "Developers", @@ -171,7 +193,7 @@ "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", "introduction.interactions.reblog.headline": "Boost", "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.headline": "Svar", "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", "introduction.welcome.action": "Let's go!", "introduction.welcome.headline": "First steps", @@ -181,7 +203,7 @@ "keyboard_shortcuts.boost": "å fremheve", "keyboard_shortcuts.column": "å fokusere en status i en av kolonnene", "keyboard_shortcuts.compose": "å fokusere komponeringsfeltet", - "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.description": "Beskrivelse", "keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.down": "for å flytte ned i listen", "keyboard_shortcuts.enter": "to open status", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Ny listetittel", "lists.search": "Søk blant personer du følger", "lists.subheading": "Dine lister", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Laster...", "media_gallery.toggle_visible": "Veksle synlighet", "missing_indicator.label": "Ikke funnet", @@ -246,9 +269,8 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Festa tuter", "navigation_bar.preferences": "Preferanser", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Felles tidslinje", - "navigation_bar.security": "Security", + "navigation_bar.security": "Sikkerhet", "notification.favourite": "{name} likte din status", "notification.follow": "{name} fulgte deg", "notification.mention": "{name} nevnte deg", @@ -268,17 +290,19 @@ "notifications.column_settings.reblog": "Fremhevet:", "notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.sound": "Spill lyd", - "notifications.filter.all": "All", + "notifications.filter.all": "Alle", "notifications.filter.boosts": "Boosts", - "notifications.filter.favourites": "Favourites", + "notifications.filter.favourites": "Favoritter", "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.filter.polls": "Poll results", "notifications.group": "{count} notifications", "poll.closed": "Closed", - "poll.refresh": "Refresh", + "poll.refresh": "Oppdater", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Justér synlighet", @@ -290,6 +314,7 @@ "privacy.public.short": "Offentlig", "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer", "privacy.unlisted.short": "Uoppført", + "refresh": "Refresh", "regeneration_indicator.label": "Laster…", "regeneration_indicator.sublabel": "Dine startside forberedes!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -336,7 +362,7 @@ "status.open": "Utvid denne statusen", "status.pin": "Fest på profilen", "status.pinned": "Pinned toot", - "status.read_more": "Read more", + "status.read_more": "Les mer", "status.reblog": "Fremhev", "status.reblog_private": "Boost to original audience", "status.reblogged_by": "Fremhevd av {name}", @@ -352,6 +378,7 @@ "status.show_more": "Vis mer", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Ikke tilgjengelig", "status.unmute_conversation": "Ikke demp samtale", "status.unpin": "Angre festing på profilen", "suggestions.dismiss": "Dismiss suggestion", @@ -360,26 +387,34 @@ "tabs_bar.home": "Hjem", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Varslinger", - "tabs_bar.search": "Search", + "tabs_bar.search": "Søk", "time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.", "upload_area.title": "Dra og slipp for å laste opp", "upload_button.label": "Legg til media", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Beskriv for synshemmede", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Angre", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Bruk", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Laster opp...", "video.close": "Lukk video", "video.exit_fullscreen": "Lukk fullskjerm", "video.expand": "Utvid video", - "video.fullscreen": "Full screen", + "video.fullscreen": "Fullskjerm", "video.hide": "Skjul video", "video.mute": "Skru av lyd", "video.pause": "Pause", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 3178f200d..1ca5f42ed 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -4,6 +4,7 @@ "account.block": "Blocar @{name}", "account.block_domain": "Tot amagar del domeni {domain}", "account.blocked": "Blocat", + "account.cancel_follow_request": "Anullar la demanda de seguiment", "account.direct": "Escriure un MP a @{name}", "account.domain_blocked": "Domeni amagat", "account.edit_profile": "Modificar lo perfil", @@ -15,6 +16,7 @@ "account.follows.empty": "Aqueste utilizaire sèc pas degun pel moment.", "account.follows_you": "Vos sèc", "account.hide_reblogs": "Rescondre los partatges de @{name}", + "account.last_status": "Darrièra activitat", "account.link_verified_on": "La proprietat d’aqueste ligam foguèt verificada lo {date}", "account.locked_info": "L’estatut de privacitat del compte es configurat sus clavat. Lo proprietari causís qual pòt sègre son compte.", "account.media": "Mèdias", @@ -23,6 +25,7 @@ "account.mute": "Rescondre @{name}", "account.mute_notifications": "Rescondre las notificacions de @{name}", "account.muted": "Mes en silenci", + "account.never_active": "Jamai", "account.posts": "Tuts", "account.posts_with_replies": "Tuts e responsas", "account.report": "Senhalar @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Quitar de sègre", "account.unmute": "Quitar de rescondre @{name}", "account.unmute_notifications": "Mostrar las notificacions de @{name}", + "alert.rate_limited.message": "Mercés de tornar ensajar aprèp {retry_time, time, medium}.", + "alert.rate_limited.title": "Taus limitat", "alert.unexpected.message": "Una error s’es producha.", "alert.unexpected.title": "Ops !", + "autosuggest_hashtag.per_week": "{count} per setmana", "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven", "bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.", "bundle_column_error.retry": "Tornar ensajar", @@ -47,6 +53,7 @@ "column.blocks": "Personas blocadas", "column.community": "Flux public local", "column.direct": "Messatges dirèctes", + "column.directory": "Percórrer los perfils", "column.domain_blocks": "Domenis resconduts", "column.favourites": "Favorits", "column.follow_requests": "Demandas d’abonament", @@ -77,7 +84,7 @@ "compose_form.poll.remove_option": "Levar aquesta opcion", "compose_form.publish": "Tut", "compose_form.publish_loud": "{publish} !", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Marcar coma sensible", "compose_form.sensitive.marked": "Lo mèdia es marcat coma sensible", "compose_form.sensitive.unmarked": "Lo mèdia es pas marcat coma sensible", "compose_form.spoiler.marked": "Lo tèxte es rescondut jos l’avertiment", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Volètz vertadièrament suprimir aquesta lista per totjorn ?", "confirmations.domain_block.confirm": "Amagar tot lo domeni", "confirmations.domain_block.message": "Volètz vertadièrament blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.\nVeiretz pas cap de contengut d’aquel domeni dins cap de flux public o dins vòstras notificacions. Vòstres seguidors d’aquel domeni seràn levats.", + "confirmations.logout.confirm": "Desconnexion", + "confirmations.logout.message": "Volètz vertadièrament vos desconnectar ?", "confirmations.mute.confirm": "Rescondre", + "confirmations.mute.explanation": "Aquò lor escondrà las publicacions e mencions, mas aquò lor permetrà encara de veire vòstra publicacions e de vos sègre.", "confirmations.mute.message": "Volètz vertadièrament rescondre {name} ?", "confirmations.redraft.confirm": "Escafar & tornar formular", "confirmations.redraft.message": "Volètz vertadièrament escafar aqueste estatut e lo reformular ? Totes sos partiments e favorits seràn perduts, e sas responsas seràn orfanèlas.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Respondre remplaçarà lo messatge que sètz a escriure. Volètz vertadièrament contunhar ?", "confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?", + "conversation.delete": "Suprimir la conversacion", + "conversation.mark_as_read": "Marcar coma legida", + "conversation.open": "Veire la conversacion", + "conversation.with": "Amb {names}", + "directory.federated": "Del fediverse conegut", + "directory.local": "Solament de {domain}", + "directory.new_arrivals": "Nòus-venguts", + "directory.recently_active": "Actius fa res", "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.", "embed.preview": "Semblarà aquò :", "emoji_button.activity": "Activitats", @@ -134,6 +152,10 @@ "empty_column.mutes": "Encara avètz pas mes en silenci degun.", "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.", "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autres servidors per garnir lo flux public", + "error.unexpected_crash.explanation": "A causa d’una avaria dins nòstre còdi o d’un problèma de compatibilitat de navegador, aquesta pagina se pòt pas afichar corrèctament.", + "error.unexpected_crash.next_steps": "Ensajatz d’actualizar la pagina. S’aquò càmbia pas res, podètz provar d’utilizar Mastodon via un navegador diferent o d’una aplicacion nativa estant.", + "errors.unexpected_crash.copy_stacktrace": "Copiar las traças al quichapapièrs", + "errors.unexpected_crash.report_issue": "Senhalar un problèma", "follow_request.authorize": "Acceptar", "follow_request.reject": "Regetar", "getting_started.developers": "Desvelopaires", @@ -204,14 +226,14 @@ "keyboard_shortcuts.search": "anar a la recèrca", "keyboard_shortcuts.start": "dobrir la colomna « Per començar »", "keyboard_shortcuts.toggle_hidden": "mostrar/amagar lo tèxte dels avertiments", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "per mostrar/rescondre los mèdias", "keyboard_shortcuts.toot": "començar un estatut tot novèl", "keyboard_shortcuts.unfocus": "quitar lo camp tèxte/de recèrca", "keyboard_shortcuts.up": "far montar dins la lista", "lightbox.close": "Tampar", "lightbox.next": "Seguent", "lightbox.previous": "Precedent", - "lightbox.view_context": "View context", + "lightbox.view_context": "Veire lo contèxt", "lists.account.add": "Ajustar a la lista", "lists.account.remove": "Levar de la lista", "lists.delete": "Suprimir la lista", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Títol de la nòva lista", "lists.search": "Cercar demest lo monde que seguètz", "lists.subheading": "Vòstras listas", + "load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}", "loading_indicator.label": "Cargament…", "media_gallery.toggle_visible": "Modificar la visibilitat", "missing_indicator.label": "Pas trobat", @@ -237,7 +260,7 @@ "navigation_bar.favourites": "Favorits", "navigation_bar.filters": "Mots ignorats", "navigation_bar.follow_requests": "Demandas d’abonament", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Abonament e seguidors", "navigation_bar.info": "Tocant aqueste servidor", "navigation_bar.keyboard_shortcuts": "Acorchis clavièr", "navigation_bar.lists": "Listas", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Tuts penjats", "navigation_bar.preferences": "Preferéncias", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Flux public global", "navigation_bar.security": "Seguretat", "notification.favourite": "{name} a ajustat a sos favorits", @@ -277,8 +299,10 @@ "notifications.group": "{count} notificacions", "poll.closed": "Tampat", "poll.refresh": "Actualizar", + "poll.total_people": "{count, plural, one {# persona} other {# personas}}", "poll.total_votes": "{count, plural, one {# vòte} other {# vòtes}}", "poll.vote": "Votar", + "poll.voted": "Avètz votat per aquesta responsa", "poll_button.add_poll": "Ajustar un sondatge", "poll_button.remove_poll": "Levar lo sondatge", "privacy.change": "Ajustar la confidencialitat del messatge", @@ -290,6 +314,7 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Mostrar pas dins los fluxes publics", "privacy.unlisted.short": "Pas-listat", + "refresh": "Actualizar", "regeneration_indicator.label": "Cargament…", "regeneration_indicator.sublabel": "Sèm a preparar vòstre flux d’acuèlh !", "relative_time.days": "fa {number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "Gents", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Tuts", + "search_results.statuses_fts_disabled": "La recèrca de tuts per lor contengut es pas activada sus aqueste servidor Mastodon.", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.admin_account": "Dobrir l’interfàcia de moderacion per @{name}", "status.admin_status": "Dobrir aqueste estatut dins l’interfàcia de moderacion", @@ -352,6 +378,7 @@ "status.show_more": "Desplegar", "status.show_more_all": "Los desplegar totes", "status.show_thread": "Mostrar lo fil", + "status.uncached_media_warning": "Pas disponible", "status.unmute_conversation": "Tornar mostrar la conversacion", "status.unpin": "Tirar del perfil", "suggestions.dismiss": "Regetar la suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments restants", "time_remaining.seconds": "demòra{number, plural, one { # segonda} other {n # segondas}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} ne charra other {people}} ne charran", + "trends.trending_now": "Tendéncia del moment", "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.", "upload_area.title": "Lisatz e depausatz per mandar", "upload_button.label": "Ajustar un mèdia (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Talha maximum pels mandadís subrepassada.", "upload_error.poll": "Lo mandadís de fichièr es pas autorizat pels sondatges.", "upload_form.description": "Descripcion pels mal vesents", - "upload_form.focus": "Modificar l’apercebut", + "upload_form.edit": "Modificar", "upload_form.undo": "Suprimir", + "upload_modal.analyzing_picture": "Analisi de l’imatge…", + "upload_modal.apply": "Aplicar", + "upload_modal.description_placeholder": "Lo dròlle bilingüe manja un yaourt de ròcs exagonals e kiwis verds farà un an mai", + "upload_modal.detect_text": "Detectar lo tèxt de l’imatge", + "upload_modal.edit_media": "Modificar lo mèdia", + "upload_modal.hint": "Clicatz o lissatz lo cercle de l’apercebut per causir lo ponch que serà totjorn visible dins las vinhetas.", + "upload_modal.preview_label": "Apercebut ({ratio})", "upload_progress.label": "Mandadís…", "video.close": "Tampar la vidèo", "video.exit_fullscreen": "Sortir plen ecran", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index d101c21aa..b3513ea46 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -4,6 +4,7 @@ "account.block": "Blokuj @{name}", "account.block_domain": "Blokuj wszystko z {domain}", "account.blocked": "Zablokowany(-a)", + "account.cancel_follow_request": "Zrezygnuj z prośby o możliwość śledzenia", "account.direct": "Wyślij wiadomość bezpośrednią do @{name}", "account.domain_blocked": "Ukryto domenę", "account.edit_profile": "Edytuj profil", @@ -15,6 +16,7 @@ "account.follows.empty": "Ten użytkownik nie śledzi jeszcze nikogo.", "account.follows_you": "Śledzi Cię", "account.hide_reblogs": "Ukryj podbicia od @{name}", + "account.last_status": "Ostatnia aktywność", "account.link_verified_on": "Własność tego odnośnika została potwierdzona {date}", "account.locked_info": "To konto jest prywatne. Właściciel ręcznie wybiera kto może go śledzić.", "account.media": "Zawartość multimedialna", @@ -23,6 +25,7 @@ "account.mute": "Wycisz @{name}", "account.mute_notifications": "Wycisz powiadomienia o @{name}", "account.muted": "Wyciszony", + "account.never_active": "Nigdy", "account.posts": "Wpisy", "account.posts_with_replies": "Wpisy i odpowiedzi", "account.report": "Zgłoś @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Przestań śledzić", "account.unmute": "Cofnij wyciszenie @{name}", "account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}", + "alert.rate_limited.message": "Spróbuj ponownie po {retry_time, time, medium}.", + "alert.rate_limited.title": "Ograniczony czasowo", "alert.unexpected.message": "Wystąpił nieoczekiwany błąd.", "alert.unexpected.title": "O nie!", + "autosuggest_hashtag.per_week": "{count} co tydzień", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", "bundle_column_error.retry": "Spróbuj ponownie", @@ -47,6 +53,7 @@ "column.blocks": "Zablokowani użytkownicy", "column.community": "Lokalna oś czasu", "column.direct": "Wiadomości bezpośrednie", + "column.directory": "Przeglądaj profile", "column.domain_blocks": "Ukryte domeny", "column.favourites": "Ulubione", "column.follow_requests": "Prośby o śledzenie", @@ -97,7 +104,10 @@ "confirmations.delete_list.message": "Czy na pewno chcesz bezpowrotnie usunąć tą listę?", "confirmations.domain_block.confirm": "Ukryj wszysyko z domeny", "confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.", + "confirmations.logout.confirm": "Wyloguj", + "confirmations.logout.message": "Czy na pewno chcesz się wylogować?", "confirmations.mute.confirm": "Wycisz", + "confirmations.mute.explanation": "To schowa ich i wspominające ich posty, ale wciąż pozwoli im widzieć twoje posty i śledzić cię.", "confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?", "confirmations.redraft.confirm": "Usuń i przeredaguj", "confirmations.redraft.message": "Czy na pewno chcesz usunąć i przeredagować ten wpis? Polubienia i podbicia zostaną utracone, a odpowiedzi do oryginalnego wpisu zostaną osierocone.", @@ -105,6 +115,14 @@ "confirmations.reply.message": "W ten sposób utracisz wpis który obecnie tworzysz. Czy na pewno chcesz to zrobić?", "confirmations.unfollow.confirm": "Przestań śledzić", "confirmations.unfollow.message": "Czy na pewno zamierzasz przestać śledzić {name}?", + "conversation.delete": "Usuń rozmowę", + "conversation.mark_as_read": "Oznacz jako przeczytane", + "conversation.open": "Zobacz rozmowę", + "conversation.with": "Z {names}", + "directory.federated": "Ze znanego fediwersum", + "directory.local": "Tylko z {domain}", + "directory.new_arrivals": "Nowości", + "directory.recently_active": "Ostatnio aktywne", "embed.instructions": "Osadź ten wpis na swojej stronie wklejając poniższy kod.", "embed.preview": "Tak będzie to wyglądać:", "emoji_button.activity": "Aktywność", @@ -138,6 +156,10 @@ "empty_column.mutes": "Nie wyciszyłeś(-aś) jeszcze żadnego użytkownika.", "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.", "empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych serwerów, aby to wyświetlić", + "error.unexpected_crash.explanation": "W związku z błędem w naszym kodzie lub braku kompatybilności przeglądarki, ta strona nie może być poprawnie wyświetlona.", + "error.unexpected_crash.next_steps": "Spróbuj odświeżyć stronę. Jeśli to nie pomoże, wciąż jesteś w stanie używać Mastodona przez inną przeglądarkę lub natywną aplikację.", + "errors.unexpected_crash.copy_stacktrace": "Skopiuj ślad stosu do schowka", + "errors.unexpected_crash.report_issue": "Zgłoś problem", "follow_request.authorize": "Autoryzuj", "follow_request.reject": "Odrzuć", "getting_started.developers": "Dla programistów", @@ -225,6 +247,7 @@ "lists.new.title_placeholder": "Wprowadź tytuł listy", "lists.search": "Szukaj wśród osób które śledzisz", "lists.subheading": "Twoje listy", + "load_pending": "{count, plural, one {# nowy przedmiot} other {nowe przedmioty}}", "loading_indicator.label": "Ładowanie…", "media_gallery.toggle_visible": "Przełącz widoczność", "missing_indicator.label": "Nie znaleziono", @@ -251,7 +274,6 @@ "navigation_bar.personal": "Osobiste", "navigation_bar.pins": "Przypięte wpisy", "navigation_bar.preferences": "Preferencje", - "navigation_bar.profile_directory": "Katalog profilów", "navigation_bar.public_timeline": "Globalna oś czasu", "navigation_bar.security": "Bezpieczeństwo", "notification.favourite": "{name} dodał(a) Twój wpis do ulubionych", @@ -282,8 +304,10 @@ "notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}", "poll.closed": "Zamknięte", "poll.refresh": "Odśwież", + "poll.total_people": "{count, plural, one {# osoba} few {# osoby} many {# osób} other {# osób}}", "poll.total_votes": "{count, plural, one {# głos} few {# głosy} many {# głosów} other {# głosów}}", "poll.vote": "Zagłosuj", + "poll.voted": "Zagłosowałeś_aś na tą odpowiedź", "poll_button.add_poll": "Dodaj głosowanie", "poll_button.remove_poll": "Usuń głosowanie", "privacy.change": "Dostosuj widoczność wpisów", @@ -295,6 +319,7 @@ "privacy.public.short": "Publiczny", "privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu", "privacy.unlisted.short": "Niewidoczny", + "refresh": "Odśwież", "regeneration_indicator.label": "Ładuję…", "regeneration_indicator.sublabel": "Twoja oś czasu jest przygotowywana!", "relative_time.days": "{number} dni", @@ -319,6 +344,7 @@ "search_results.accounts": "Ludzie", "search_results.hashtags": "Hashtagi", "search_results.statuses": "Wpisy", + "search_results.statuses_fts_disabled": "Szukanie wpisów przy pomocy ich zawartości nie jest włączone na tym serwerze Mastodona.", "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", "status.admin_account": "Otwórz interfejs moderacyjny dla @{name}", "status.admin_status": "Otwórz ten wpis w interfejsie moderacyjnym", @@ -357,6 +383,7 @@ "status.show_more": "Rozwiń", "status.show_more_all": "Rozwiń wszystkie", "status.show_thread": "Pokaż wątek", + "status.uncached_media_warning": "Niedostępne", "status.unmute_conversation": "Cofnij wyciszenie konwersacji", "status.unpin": "Odepnij z profilu", "suggestions.dismiss": "Odrzuć sugestię", @@ -372,14 +399,22 @@ "time_remaining.moments": "Pozostała chwila", "time_remaining.seconds": "{number, plural, one {Pozostała # sekunda} few {Pozostały # sekundy} many {Pozostało # sekund} other {Pozostało # sekund}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {osoba rozmawia} few {osoby rozmawiają} other {osób rozmawia}} o tym", + "trends.trending_now": "Popularne teraz", "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.", "upload_area.title": "Przeciągnij i upuść aby wysłać", "upload_button.label": "Dodaj zawartość multimedialną (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Przekroczono limit plików do wysłania.", "upload_error.poll": "Dołączanie plików nie dozwolone z głosowaniami.", "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących", - "upload_form.focus": "Dopasuj podgląd", + "upload_form.edit": "Edytuj", "upload_form.undo": "Usuń", + "upload_modal.analyzing_picture": "Analizowanie obrazu…", + "upload_modal.apply": "Zastosuj", + "upload_modal.description_placeholder": "Pchnąć w tę łódź jeża lub ośm skrzyń fig", + "upload_modal.detect_text": "Wykryj tekst ze obrazu", + "upload_modal.edit_media": "Edytuj multimedia", + "upload_modal.hint": "Kliknij lub przeciągnij kółko na podglądzie by wybrać centralny punkt, który zawsze będzie na widoku na miniaturce.", + "upload_modal.preview_label": "Podgląd ({ratio})", "upload_progress.label": "Wysyłanie…", "video.close": "Zamknij film", "video.exit_fullscreen": "Opuść tryb pełnoekranowy", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index dca087af9..7c6a1fd22 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -4,6 +4,7 @@ "account.block": "Bloquear @{name}", "account.block_domain": "Esconder tudo de {domain}", "account.blocked": "Bloqueado", + "account.cancel_follow_request": "Cancelar solicitação para seguir", "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domínio escondido", "account.edit_profile": "Editar perfil", @@ -15,6 +16,7 @@ "account.follows.empty": "Esse usuário não segue ninguém no momento.", "account.follows_you": "Segue você", "account.hide_reblogs": "Esconder compartilhamentos de @{name}", + "account.last_status": "Última atividade", "account.link_verified_on": "A posse desse link foi verificada em {date}", "account.locked_info": "Essa conta está trancada. Se você a seguir sua solicitação será revisada manualmente.", "account.media": "Mídia", @@ -23,6 +25,7 @@ "account.mute": "Silenciar @{name}", "account.mute_notifications": "Silenciar notificações de @{name}", "account.muted": "Silenciado", + "account.never_active": "Nunca", "account.posts": "Toots", "account.posts_with_replies": "Toots e respostas", "account.report": "Denunciar @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}", + "alert.rate_limited.message": "Por favor tente novamente após {retry_time, time, medium}.", + "alert.rate_limited.title": "Limite de tentativas", "alert.unexpected.message": "Um erro inesperado ocorreu.", - "alert.unexpected.title": "Oops!", + "alert.unexpected.title": "Eita!", + "autosuggest_hashtag.per_week": "{count} por semana", "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez", "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.", "bundle_column_error.retry": "Tente novamente", @@ -47,6 +53,7 @@ "column.blocks": "Usuários bloqueados", "column.community": "Local", "column.direct": "Mensagens diretas", + "column.directory": "Explorar perfis", "column.domain_blocks": "Domínios escondidos", "column.favourites": "Favoritos", "column.follow_requests": "Seguidores pendentes", @@ -77,7 +84,7 @@ "compose_form.poll.remove_option": "Remover essa opção", "compose_form.publish": "Publicar", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Marcar mídia como sensível", "compose_form.sensitive.marked": "Mídia está marcada como sensível", "compose_form.sensitive.unmarked": "Mídia não está marcada como sensível", "compose_form.spoiler.marked": "O texto está escondido por um aviso de conteúdo", @@ -89,11 +96,14 @@ "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?", "confirmations.delete.confirm": "Excluir", "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Excluir", "confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?", "confirmations.domain_block.confirm": "Esconder o domínio inteiro", "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado. Você não vai ver conteúdo desse domínio em nenhuma das timelines públicas ou nas suas notificações. Seus seguidores desse domínio serão removidos.", + "confirmations.logout.confirm": "Sair", + "confirmations.logout.message": "Tem certeza que deseja encerrar a sessão?", "confirmations.mute.confirm": "Silenciar", + "confirmations.mute.explanation": "Isto irá esconder postagens e postagens que mencionam, mas ainda vai permitir que eles vejam suas publicações e sigam você.", "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?", "confirmations.redraft.confirm": "Apagar & usar como rascunho", "confirmations.redraft.message": "Você tem certeza que deseja apagar esse status e usá-lo como rascunho? Seus compartilhamentos e favoritos serão perdidos e as respostas ao toot original ficarão desconectadas.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Responder agora vai sobrescrever a mensagem que você está compondo. Você tem certeza que quer continuar?", "confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?", + "conversation.delete": "Excluir conversa", + "conversation.mark_as_read": "Marcar como lida", + "conversation.open": "Ver conversa", + "conversation.with": "Com {names}", + "directory.federated": "De fediverso conhecido", + "directory.local": "De {domain} apenas", + "directory.new_arrivals": "Acabaram de chegar", + "directory.recently_active": "Reverta esta propriedade para seu valor padrão", "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo.", "embed.preview": "Aqui está uma previsão de como ficará:", "emoji_button.activity": "Atividades", @@ -134,6 +152,10 @@ "empty_column.mutes": "Você ainda não silenciou nenhum usuário.", "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar.", "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias", + "error.unexpected_crash.explanation": "Devido a um bug em nosso código ou a um problema de compatibilidade do navegador, esta página não pode ser exibida corretamente.", + "error.unexpected_crash.next_steps": "Tente atualizar a página. Se isso não ajudar, você ainda pode usar Mastodon através de um navegador diferente ou aplicativo nativo.", + "errors.unexpected_crash.copy_stacktrace": "Copiar stacktrace para a área de transferência", + "errors.unexpected_crash.report_issue": "Reportar problema", "follow_request.authorize": "Autorizar", "follow_request.reject": "Rejeitar", "getting_started.developers": "Desenvolvedores", @@ -158,11 +180,11 @@ "home.column_settings.show_replies": "Mostrar as respostas", "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "introduction.federation.action": "Próximo", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Global", "introduction.federation.federated.text": "Posts públicos de outros servidores do fediverso vão aparecer na timeline global.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "Início", "introduction.federation.home.text": "Posts de pessoas que você segue vão aparecer na sua página inicial. Você pode seguir pessoas de qualquer servidor!", "introduction.federation.local.headline": "Local", "introduction.federation.local.text": "Posts públicos de pessoas no mesmo servidor que você vão aparecer na timeline local.", @@ -204,23 +226,24 @@ "keyboard_shortcuts.search": "para focar a pesquisa", "keyboard_shortcuts.start": "para abrir a coluna \"primeiros passos\"", "keyboard_shortcuts.toggle_hidden": "mostrar/esconder o texto com aviso de conteúdo", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/esconder mídia", "keyboard_shortcuts.toot": "para compor um novo toot", "keyboard_shortcuts.unfocus": "para remover o foco da área de composição/pesquisa", "keyboard_shortcuts.up": "para mover para cima na lista", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", - "lightbox.view_context": "View context", + "lightbox.view_context": "Ver contexto", "lists.account.add": "Adicionar a listas", "lists.account.remove": "Remover da lista", - "lists.delete": "Delete list", + "lists.delete": "Excluir lista", "lists.edit": "Editar lista", "lists.edit.submit": "Mudar o título", "lists.new.create": "Adicionar lista", "lists.new.title_placeholder": "Novo título da lista", "lists.search": "Procurar entre as pessoas que você segue", "lists.subheading": "Suas listas", + "load_pending": "{count, plural, one {# novo item} other {# novos items}}", "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", @@ -237,7 +260,7 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palavras silenciadas", "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Seguindo e seguidores", "navigation_bar.info": "Mais informações", "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", "navigation_bar.lists": "Listas", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Pessoal", "navigation_bar.pins": "Postagens fixadas", "navigation_bar.preferences": "Preferências", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Global", "navigation_bar.security": "Segurança", "notification.favourite": "{name} adicionou a sua postagem aos favoritos", @@ -277,8 +299,10 @@ "notifications.group": "{count} notificações", "poll.closed": "Fechada", "poll.refresh": "Atualizar", + "poll.total_people": "{count, plural, one {# pessoa} other {# pessoas}}", "poll.total_votes": "{count, plural, one {# voto} other {# votos}}", "poll.vote": "Votar", + "poll.voted": "Você votou nesta resposta", "poll_button.add_poll": "Adicionar uma enquete", "poll_button.remove_poll": "Remover enquete", "privacy.change": "Ajustar a privacidade da mensagem", @@ -290,6 +314,7 @@ "privacy.public.short": "Pública", "privacy.unlisted.long": "Não publicar em feeds públicos", "privacy.unlisted.short": "Não listada", + "refresh": "Atualizar", "regeneration_indicator.label": "Carregando…", "regeneration_indicator.sublabel": "Sua página inicial está sendo preparada!", "relative_time.days": "{number}d", @@ -314,10 +339,11 @@ "search_results.accounts": "Pessoas", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Pesquisar toots por seu conteúdo não está habilitado neste servidor Mastodon.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "status.admin_account": "Abrir interface de moderação para @{name}", "status.admin_status": "Abrir esse status na interface de moderação", - "status.block": "Block @{name}", + "status.block": "Bloquear @{name}", "status.cancel_reblog_private": "Desfazer compartilhamento", "status.cannot_reblog": "Esta postagem não pode ser compartilhada", "status.copy": "Copiar o link para o status", @@ -352,6 +378,7 @@ "status.show_more": "Mostrar mais", "status.show_more_all": "Mostrar mais para todas as mensagens", "status.show_thread": "Mostrar sequência", + "status.uncached_media_warning": "Não disponível", "status.unmute_conversation": "Desativar silêncio desta conversa", "status.unpin": "Desafixar do perfil", "suggestions.dismiss": "Ignorar a sugestão", @@ -367,14 +394,22 @@ "time_remaining.moments": "Momentos restantes", "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {pessoa} other {pessoas}} falando sobre", + "trends.trending_now": "Em alta no momento", "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.", "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar mídia (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Limite de envio de arquivos excedido.", "upload_error.poll": "Envio de arquivos não é permitido com enquetes.", "upload_form.description": "Descreva a imagem para deficientes visuais", - "upload_form.focus": "Ajustar foco", + "upload_form.edit": "Editar", "upload_form.undo": "Remover", + "upload_modal.analyzing_picture": "Analisando imagem…", + "upload_modal.apply": "Aplicar", + "upload_modal.description_placeholder": "Grave e cabisbaixo, o filho justo zelava pela querida mãe doente", + "upload_modal.detect_text": "Detectar texto da imagem", + "upload_modal.edit_media": "Editar mídia", + "upload_modal.hint": "Clique ou arraste o círculo na visualização para escolher o ponto focal que sempre será visto em todas as miniaturas.", + "upload_modal.preview_label": "Prévia ({ratio})", "upload_progress.label": "Salvando...", "video.close": "Fechar vídeo", "video.exit_fullscreen": "Sair da tela cheia", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt-PT.json index 157090c55..bb8a94516 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -4,6 +4,7 @@ "account.block": "Bloquear @{name}", "account.block_domain": "Esconder tudo do domínio {domain}", "account.blocked": "Bloqueado", + "account.cancel_follow_request": "Cancelar pedido de seguidor", "account.direct": "Mensagem directa @{name}", "account.domain_blocked": "Domínio escondido", "account.edit_profile": "Editar perfil", @@ -15,14 +16,16 @@ "account.follows.empty": "Este utilizador ainda não segue alguém.", "account.follows_you": "É teu seguidor", "account.hide_reblogs": "Esconder partilhas de @{name}", + "account.last_status": "Última actividade", "account.link_verified_on": "A posse deste link foi verificada em {date}", "account.locked_info": "O estatuto de privacidade desta conta é fechado. O dono revê manualmente que a pode seguir.", - "account.media": "Media", + "account.media": "Média", "account.mention": "Mencionar @{name}", "account.moved_to": "{name} mudou a sua conta para:", "account.mute": "Silenciar @{name}", "account.mute_notifications": "Silenciar notificações de @{name}", "account.muted": "Silenciada", + "account.never_active": "Nunca", "account.posts": "Publicações", "account.posts_with_replies": "Publicações e respostas", "account.report": "Denunciar @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", "account.unmute_notifications": "Deixar de silenciar @{name}", + "alert.rate_limited.message": "Volte a tentar depois das {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "Ocorreu um erro inesperado.", "alert.unexpected.title": "Bolas!", + "autosuggest_hashtag.per_week": "{count} por semana", "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.", "bundle_column_error.retry": "Tente de novo", @@ -47,53 +53,57 @@ "column.blocks": "Utilizadores Bloqueados", "column.community": "Cronologia local", "column.direct": "Mensagens directas", + "column.directory": "Procurar perfis", "column.domain_blocks": "Domínios escondidos", "column.favourites": "Favoritos", - "column.follow_requests": "Seguidores Pendentes", + "column.follow_requests": "Seguidores pendentes", "column.home": "Início", "column.lists": "Listas", "column.mutes": "Utilizadores silenciados", "column.notifications": "Notificações", "column.pins": "Publicações fixas", - "column.public": "Cronologia federativa", + "column.public": "Cronologia federada", "column_back_button.label": "Voltar", - "column_header.hide_settings": "Esconder preferências", + "column_header.hide_settings": "Esconder configurações", "column_header.moveLeft_settings": "Mover coluna para a esquerda", "column_header.moveRight_settings": "Mover coluna para a direita", "column_header.pin": "Fixar", - "column_header.show_settings": "Mostrar preferências", + "column_header.show_settings": "Mostrar configurações", "column_header.unpin": "Desafixar", - "column_subheading.settings": "Preferências", - "community.column_settings.media_only": "Somente media", - "compose_form.direct_message_warning": "Esta publicação só será enviada para os utilizadores mencionados.", - "compose_form.direct_message_warning_learn_more": "Aprender mais", - "compose_form.hashtag_warning": "Esta pulbicacção não será listada em nenhuma hashtag por ser não listada. Somente publicações públicas podem ser pesquisadas por hashtag.", + "column_subheading.settings": "Configurações", + "community.column_settings.media_only": "Somente multimédia", + "compose_form.direct_message_warning": "Esta publicação será enviada apenas para os utilizadores mencionados.", + "compose_form.direct_message_warning_learn_more": "Conhecer mais", + "compose_form.hashtag_warning": "Este toot não será listado em nenhuma hashtag por ser não listado. Apenas toots públics podem ser pesquisados por hashtag.", "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.", - "compose_form.lock_disclaimer.lock": "fechada", + "compose_form.lock_disclaimer.lock": "bloqueado", "compose_form.placeholder": "Em que estás a pensar?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", - "compose_form.publish": "Publicar", - "compose_form.publish_loud": "{publicar}!", - "compose_form.sensitive.hide": "Mark media as sensitive", - "compose_form.sensitive.marked": "Media marcado como sensível", - "compose_form.sensitive.unmarked": "Media não está marcado como sensível", + "compose_form.poll.add_option": "Adicionar uma opção", + "compose_form.poll.duration": "Duração da votação", + "compose_form.poll.option_placeholder": "Opção {number}", + "compose_form.poll.remove_option": "Eliminar esta opção", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Marcar multimédia como sensível", + "compose_form.sensitive.marked": "Média marcada como sensível", + "compose_form.sensitive.unmarked": "Média não está marcada como sensível", "compose_form.spoiler.marked": "Texto escondido atrás de aviso", "compose_form.spoiler.unmarked": "O texto não está escondido", "compose_form.spoiler_placeholder": "Escreve o teu aviso aqui", "confirmation_modal.cancel": "Cancelar", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Bloquear e denunciar", "confirmations.block.confirm": "Bloquear", "confirmations.block.message": "De certeza que queres bloquear {name}?", "confirmations.delete.confirm": "Eliminar", "confirmations.delete.message": "De certeza que queres eliminar esta publicação?", - "confirmations.delete_list.confirm": "Apagar", - "confirmations.delete_list.message": "Tens a certeza de que desejas apagar permanentemente esta lista?", + "confirmations.delete_list.confirm": "Eliminar", + "confirmations.delete_list.message": "Tens a certeza de que desejas eliminar permanentemente esta lista?", "confirmations.domain_block.confirm": "Esconder tudo deste domínio", - "confirmations.domain_block.message": "De certeza que queres bloquear completamente o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado. Não irás ver conteúdo daquele domínio em cronologia alguma, nem nas tuas notificações. Os teus seguidores daquele domínio serão removidos.", + "confirmations.domain_block.message": "De certeza que queres bloquear completamente o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é suficiente e é o recomendado. Não irás ver conteúdo daquele domínio em cronologia alguma nem nas tuas notificações. Os teus seguidores daquele domínio serão removidos.", + "confirmations.logout.confirm": "Terminar sessão", + "confirmations.logout.message": "Deseja terminar a sessão?", "confirmations.mute.confirm": "Silenciar", + "confirmations.mute.explanation": "Isto irá esconder publicações deles ou publicações que os mencionem, mas irá permitir que vejam as suas publicações e sejam seus seguidores.", "confirmations.mute.message": "De certeza que queres silenciar {name}?", "confirmations.redraft.confirm": "Apagar & redigir", "confirmations.redraft.message": "Tens a certeza que queres apagar e redigir esta publicação? Os favoritos e as partilhas perder-se-ão e as respostas à publicação original ficarão órfãs.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Responder agora irá reescrever a mensagem que estás a compor actualmente. Tens a certeza que queres continuar?", "confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?", + "conversation.delete": "Eliminar conversa", + "conversation.mark_as_read": "Marcar como lida", + "conversation.open": "Ver conversa", + "conversation.with": "Com {names}", + "directory.federated": "Do fediverso conhecido", + "directory.local": "Apenas de {domain}", + "directory.new_arrivals": "Recém chegados", + "directory.recently_active": "Com actividade recente", "embed.instructions": "Publica esta publicação no teu site copiando o código abaixo.", "embed.preview": "Podes ver aqui como irá ficar:", "emoji_button.activity": "Actividade", @@ -109,23 +127,23 @@ "emoji_button.food": "Comida & Bebida", "emoji_button.label": "Inserir Emoji", "emoji_button.nature": "Natureza", - "emoji_button.not_found": "Não tem emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "Não tem emojis!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objectos", "emoji_button.people": "Pessoas", - "emoji_button.recent": "Regularmente utilizados", - "emoji_button.search": "Procurar...", + "emoji_button.recent": "Utilizados regularmente", + "emoji_button.search": "Pesquisar...", "emoji_button.search_results": "Resultados da pesquisa", "emoji_button.symbols": "Símbolos", "emoji_button.travel": "Viagens & Lugares", - "empty_column.account_timeline": "Sem publicações!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_timeline": "Sem toots por aqui!", + "empty_column.account_unavailable": "Perfil indisponível", "empty_column.blocks": "Ainda não bloqueaste qualquer utilizador.", - "empty_column.community": "Ainda não existe conteúdo local para mostrar!", + "empty_column.community": "A timeline local está vazia. Escreve algo publicamente para começar!", "empty_column.direct": "Ainda não tens qualquer mensagem directa. Quando enviares ou receberes alguma, ela irá aparecer aqui.", "empty_column.domain_blocks": "Ainda não há qualquer domínio escondido.", - "empty_column.favourited_statuses": "Ainda não tens quaisquer publicações favoritas. Quando tiveres alguma, ela irá aparecer aqui.", - "empty_column.favourites": "Ainda ninguém favorizou esta publicação. Quando alguém o fizer, ela irá aparecer aqui.", - "empty_column.follow_requests": "Ainda não tens pedido de seguimento algum. Quando receberes algum, ele irá aparecer aqui.", + "empty_column.favourited_statuses": "Ainda não tens quaisquer toots favoritos. Quando tiveres algum, ele irá aparecer aqui.", + "empty_column.favourites": "Ainda ninguém marcou este toot como favorito. Quando alguém o fizer, ele irá aparecer aqui.", + "empty_column.follow_requests": "Ainda não tens nenhum pedido de seguimento. Quando receberes algum, ele irá aparecer aqui.", "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.", "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", "empty_column.home.public_timeline": "Cronologia pública", @@ -134,14 +152,18 @@ "empty_column.mutes": "Ainda não silenciaste qualquer utilizador.", "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para veres aqui os conteúdos públicos", + "error.unexpected_crash.explanation": "Devido a um erro no nosso código ou a uma compatilidade com o seu navegador, esta página não pôde ser apresentada correctamente.", + "error.unexpected_crash.next_steps": "Tente atualizar a página. Se isso não ajudar, pode usar o Mastodon através de um navegador diferente ou uma aplicação nativa.", + "errors.unexpected_crash.copy_stacktrace": "Copiar a stacktrace para o clipboard", + "errors.unexpected_crash.report_issue": "Reportar problema", "follow_request.authorize": "Autorizar", "follow_request.reject": "Rejeitar", "getting_started.developers": "Responsáveis pelo desenvolvimento", "getting_started.directory": "Directório de perfil", - "getting_started.documentation": "Documentation", + "getting_started.documentation": "Documentação", "getting_started.heading": "Primeiros passos", "getting_started.invite": "Convidar pessoas", - "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.", + "getting_started.open_source_notice": "Mastodon é software de código aberto (open source). Podes contribuir ou reportar problemas no GitHub do projecto: {github}.", "getting_started.security": "Segurança", "getting_started.terms": "Termos de serviço", "hashtag.column_header.tag_mode.all": "e {additional}", @@ -154,28 +176,28 @@ "hashtag.column_settings.tag_mode.none": "Nenhum destes", "hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionais para esta coluna", "home.column_settings.basic": "Básico", - "home.column_settings.show_reblogs": "Mostrar as partilhas", - "home.column_settings.show_replies": "Mostrar as respostas", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "home.column_settings.show_reblogs": "Mostrar boosts", + "home.column_settings.show_replies": "Mostrar respostas", + "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", + "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "introduction.federation.action": "Seguinte", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Federada", "introduction.federation.federated.text": "Publicações públicas de outros servidores do fediverse aparecerão na cronologia federativa.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "Início", "introduction.federation.home.text": "As publicações das pessoas que tu segues aparecerão na tua coluna inicial. Tu podes seguir qualquer pessoa em qualquer servidor!", "introduction.federation.local.headline": "Local", "introduction.federation.local.text": "Publicações públicas de pessoas que tu segues no teu servidor aparecerão na coluna local.", "introduction.interactions.action": "Terminar o tutorial!", "introduction.interactions.favourite.headline": "Favorito", - "introduction.interactions.favourite.text": "Tu podes guardar um toot para depois e deixar o autor saber que gostaste dele, favoritando-o.", - "introduction.interactions.reblog.headline": "Partilhar", + "introduction.interactions.favourite.text": "Podes guardar um toot para depois e deixar o autor saber que gostaste dele, marcando-o como favorito.", + "introduction.interactions.reblog.headline": "Boost", "introduction.interactions.reblog.text": "Podes partilhar os toots de outras pessoas com os teus seguidores partilhando-os.", "introduction.interactions.reply.headline": "Responder", "introduction.interactions.reply.text": "Tu podes responder a toots de outras pessoas e aos teus, o que os irá juntar numa conversa.", "introduction.welcome.action": "Vamos!", "introduction.welcome.headline": "Primeiros passos", - "introduction.welcome.text": "Bem-vindo ao fediverse! Em pouco tempo poderás enviar mensagens e falar com os teus amigos numa grande variedade de servidores. Mas este servidor, {domain}, é especial—ele alberga o teu perfil. Por isso, lembra-te do seu nome.", + "introduction.welcome.text": "Bem-vindo ao fediverso! Em pouco tempo poderás enviar mensagens e falar com os teus amigos numa grande variedade de servidores. Mas este servidor, {domain}, é especial—ele alberga o teu perfil. Por isso, lembra-te do seu nome.", "keyboard_shortcuts.back": "para voltar", "keyboard_shortcuts.blocked": "para abrir a lista de utilizadores bloqueados", "keyboard_shortcuts.boost": "para partilhar", @@ -184,10 +206,10 @@ "keyboard_shortcuts.description": "Descrição", "keyboard_shortcuts.direct": "para abrir a coluna das mensagens directas", "keyboard_shortcuts.down": "para mover para baixo na lista", - "keyboard_shortcuts.enter": "para expandir uma publicação", + "keyboard_shortcuts.enter": "para expandir um estado", "keyboard_shortcuts.favourite": "para adicionar aos favoritos", "keyboard_shortcuts.favourites": "para abrir a lista dos favoritos", - "keyboard_shortcuts.federated": "para abrir a cronologia federativa", + "keyboard_shortcuts.federated": "para abrir a cronologia federada", "keyboard_shortcuts.heading": "Atalhos do teclado", "keyboard_shortcuts.home": "para abrir a cronologia inicial", "keyboard_shortcuts.hotkey": "Atalho", @@ -204,31 +226,32 @@ "keyboard_shortcuts.search": "para focar na pesquisa", "keyboard_shortcuts.start": "para abrir a coluna dos \"primeiros passos\"", "keyboard_shortcuts.toggle_hidden": "para mostrar/esconder texto atrás de CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "para compor um novo post", - "keyboard_shortcuts.unfocus": "para remover o foco da área de publicação/pesquisa", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar média", + "keyboard_shortcuts.toot": "para compor um novo toot", + "keyboard_shortcuts.unfocus": "para remover o foco da área de texto/pesquisa", "keyboard_shortcuts.up": "para mover para cima na lista", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", - "lightbox.view_context": "View context", + "lightbox.view_context": "Ver contexto", "lists.account.add": "Adicionar à lista", "lists.account.remove": "Remover da lista", - "lists.delete": "Delete list", + "lists.delete": "Remover lista", "lists.edit": "Editar lista", "lists.edit.submit": "Mudar o título", "lists.new.create": "Adicionar lista", - "lists.new.title_placeholder": "Novo título da lista", + "lists.new.title_placeholder": "Título da nova lista", "lists.search": "Pesquisa entre as pessoas que segues", "lists.subheading": "As tuas listas", + "load_pending": "{count, plural, one {# novo item} other {# novos itens}}", "loading_indicator.label": "A carregar...", - "media_gallery.toggle_visible": "Esconder/Mostrar", + "media_gallery.toggle_visible": "Mostrar/ocultar", "missing_indicator.label": "Não encontrado", "missing_indicator.sublabel": "Este recurso não foi encontrado", "mute_modal.hide_notifications": "Esconder notificações deste utilizador?", "navigation_bar.apps": "Aplicações móveis", "navigation_bar.blocks": "Utilizadores bloqueados", - "navigation_bar.community_timeline": "Local", + "navigation_bar.community_timeline": "Cronologia local", "navigation_bar.compose": "Escrever novo toot", "navigation_bar.direct": "Mensagens directas", "navigation_bar.discover": "Descobrir", @@ -237,23 +260,22 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palavras silenciadas", "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Seguindo e seguidores", "navigation_bar.info": "Sobre este servidor", "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", "navigation_bar.lists": "Listas", "navigation_bar.logout": "Sair", "navigation_bar.mutes": "Utilizadores silenciados", - "navigation_bar.personal": "Personal", - "navigation_bar.pins": "Posts fixos", + "navigation_bar.personal": "Pessoal", + "navigation_bar.pins": "Toots afixados", "navigation_bar.preferences": "Preferências", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.public_timeline": "Global", + "navigation_bar.public_timeline": "Cronologia federada", "navigation_bar.security": "Segurança", - "notification.favourite": "{name} adicionou o teu post aos favoritos", - "notification.follow": "{name} seguiu-te", + "notification.favourite": "{name} adicionou o teu estado aos favoritos", + "notification.follow": "{name} começou a seguir-te", "notification.mention": "{name} mencionou-te", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} partilhou o teu post", + "notification.poll": "Uma votação em participaste chegou ao fim", + "notification.reblog": "{name} fez boost ao teu o teu estado", "notifications.clear": "Limpar notificações", "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", "notifications.column_settings.alert": "Notificações no computador", @@ -263,24 +285,26 @@ "notifications.column_settings.filter_bar.show": "Mostrar", "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Resultados da votação:", "notifications.column_settings.push": "Notificações Push", - "notifications.column_settings.reblog": "Partilhas:", - "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Mostrar na coluna", "notifications.column_settings.sound": "Reproduzir som", "notifications.filter.all": "Todas", - "notifications.filter.boosts": "Partilhas", - "notifications.filter.favourites": "Favoritas", + "notifications.filter.boosts": "Boosts", + "notifications.filter.favourites": "Favoritos", "notifications.filter.follows": "Seguimento", "notifications.filter.mentions": "Referências", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "Resultados da votação", "notifications.group": "{count} notificações", "poll.closed": "Fechado", "poll.refresh": "Recarregar", + "poll.total_people": "{count, plural, one {# pessoa} other {# pessoas}}", "poll.total_votes": "{contar, plural, um {# vote} outro {# votes}}", "poll.vote": "Votar", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.voted": "Você votou nesta resposta", + "poll_button.add_poll": "Adicionar votação", + "poll_button.remove_poll": "Remover votação", "privacy.change": "Ajustar a privacidade da mensagem", "privacy.direct.long": "Apenas para utilizadores mencionados", "privacy.direct.short": "Directo", @@ -290,6 +314,7 @@ "privacy.public.short": "Público", "privacy.unlisted.long": "Não publicar nos feeds públicos", "privacy.unlisted.short": "Não listar", + "refresh": "Actualizar", "regeneration_indicator.label": "A carregar…", "regeneration_indicator.sublabel": "A tua home está a ser preparada!", "relative_time.days": "{number}d", @@ -300,26 +325,27 @@ "reply_indicator.cancel": "Cancelar", "report.forward": "Reenviar para {target}", "report.forward_hint": "A conta é de outro servidor. Enviar uma cópia anónima do relatório para lá também?", - "report.hint": "O relatório será enviado para os moderadores do teu servidor. Podes fornecer, em baixo, uma explicação do motivo pelo qual estás a relatar esta conta:", + "report.hint": "O relatório será enviado para os moderadores do teu servidor. Podes fornecer, em baixo, uma explicação do motivo pelo qual estás a denunciar esta conta:", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_popout.search_format": "Formato avançado de pesquisa", - "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, favoritaste, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.", + "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, marcaste como favorita, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "estado", "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags", "search_popout.tips.user": "utilizador", "search_results.accounts": "Pessoas", "search_results.hashtags": "Hashtags", - "search_results.statuses": "Publicações", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "A pesquisa de toots pelo seu conteúdo não está disponível neste servidor Mastodon.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "status.admin_account": "Abrir a interface de moderação para @{name}", "status.admin_status": "Abrir esta publicação na interface de moderação", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Não partilhar", - "status.cannot_reblog": "Este post não pode ser partilhado", + "status.block": "Bloquear @{name}", + "status.cancel_reblog_private": "Remover boost", + "status.cannot_reblog": "Não é possível fazer boost a esta publicação", "status.copy": "Copiar o link para a publicação", "status.delete": "Eliminar", "status.detailed_status": "Vista de conversação detalhada", @@ -328,7 +354,7 @@ "status.favourite": "Adicionar aos favoritos", "status.filtered": "Filtrada", "status.load_more": "Carregar mais", - "status.media_hidden": "Media escondida", + "status.media_hidden": "Média escondida", "status.mention": "Mencionar @{name}", "status.more": "Mais", "status.mute": "Silenciar @{name}", @@ -338,48 +364,57 @@ "status.pinned": "Publicação fixa", "status.read_more": "Ler mais", "status.reblog": "Partilhar", - "status.reblog_private": "Partilhar com a audiência original", - "status.reblogged_by": "{name} partilhou", - "status.reblogs.empty": "Ainda ninguém partilhou esta publicação. Quando alguém o fizer, ela irá aparecer aqui.", + "status.reblog_private": "Fazer boost com a audiência original", + "status.reblogged_by": "{name} fez boost", + "status.reblogs.empty": "Ainda ninguém fez boost a este toot. Quando alguém o fizer, ele irá aparecer aqui.", "status.redraft": "Apagar & reescrever", "status.reply": "Responder", "status.replyAll": "Responder à conversa", "status.report": "Denunciar @{name}", "status.sensitive_warning": "Conteúdo sensível", - "status.share": "Compartilhar", + "status.share": "Partilhar", "status.show_less": "Mostrar menos", "status.show_less_all": "Mostrar menos para todas", "status.show_more": "Mostrar mais", "status.show_more_all": "Mostrar mais para todas", "status.show_thread": "Mostrar conversa", + "status.uncached_media_warning": "Não diponível", "status.unmute_conversation": "Deixar de silenciar esta conversa", "status.unpin": "Não fixar no perfil", "suggestions.dismiss": "Dispensar a sugestão", "suggestions.header": "Tu podes estar interessado em…", - "tabs_bar.federated_timeline": "Global", - "tabs_bar.home": "Home", + "tabs_bar.federated_timeline": "Federada", + "tabs_bar.home": "Início", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificações", "tabs_bar.search": "Pesquisar", "time_remaining.days": "{número, plural, um {# day} outro {# days}} faltam", "time_remaining.hours": "{número, plural, um {# hour} outro {# hours}} faltam", "time_remaining.minutes": "{número, plural, um {# minute} outro {# minutes}} faltam", - "time_remaining.moments": "Momentos em falta", + "time_remaining.moments": "Momentos restantes", "time_remaining.seconds": "{número, plural, um {# second} outro {# seconds}} faltam", "trends.count_by_accounts": "{count} {rawCount, plural, uma {person} outra {people}} a falar", - "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.", + "trends.trending_now": "Tendencias actuais", + "ui.beforeunload": "O teu rascunho será perdido se abandonares o Mastodon.", "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar media", "upload_error.limit": "Limite máximo do ficheiro a carregar excedido.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.poll": "Carregamento de ficheiros não é permitido em votações.", "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais", - "upload_form.focus": "Alterar previsualização", + "upload_form.edit": "Editar", "upload_form.undo": "Apagar", + "upload_modal.analyzing_picture": "A analizar imagem…", + "upload_modal.apply": "Aplicar", + "upload_modal.description_placeholder": "Grave e cabisbaixo, o filho justo zelava pela querida mãe doente", + "upload_modal.detect_text": "Detectar texto na imagem", + "upload_modal.edit_media": "Editar média", + "upload_modal.hint": "Clique ou arraste o círculo na pré-visualização para escolher o ponto focal que será sempre visível em todas as miniaturas.", + "upload_modal.preview_label": "Pré-visualizar ({ratio})", "upload_progress.label": "A enviar...", "video.close": "Fechar vídeo", "video.exit_fullscreen": "Sair de full screen", "video.expand": "Expandir vídeo", - "video.fullscreen": "Full screen", + "video.fullscreen": "Ecrã completo", "video.hide": "Esconder vídeo", "video.mute": "Silenciar", "video.pause": "Pausar", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index dcb7a088d..617dbcc0d 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -4,6 +4,7 @@ "account.block": "Blochează @{name}", "account.block_domain": "Ascunde tot de la {domain}", "account.blocked": "Blocat", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Mesaj direct @{name}", "account.domain_blocked": "Domeniu ascuns", "account.edit_profile": "Editează profilul", @@ -15,6 +16,7 @@ "account.follows.empty": "Acest utilizator nu urmărește pe nimeni incă.", "account.follows_you": "Te urmărește", "account.hide_reblogs": "Ascunde redistribuirile de la @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Deținerea acestui link a fost verificată la {date}", "account.locked_info": "Acest profil este privat. Această persoană gestioneaz manual cine o urmărește.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Oprește @{name}", "account.mute_notifications": "Oprește notificările de la @{name}", "account.muted": "Oprit", + "account.never_active": "Never", "account.posts": "Postări", "account.posts_with_replies": "Postări și replici", "account.report": "Raportează @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Nu mai urmări", "account.unmute": "Activează notificările de la @{name}", "account.unmute_notifications": "Activează notificările de la @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "A apărut o eroare neașteptată.", "alert.unexpected.title": "Hopa!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Poți apăsa {combo} pentru a omite asta data viitoare", "bundle_column_error.body": "Ceva nu a funcționat la încărcarea acestui component.", "bundle_column_error.retry": "Încearcă din nou", @@ -47,6 +53,7 @@ "column.blocks": "Utilizatori blocați", "column.community": "Fluxul Local", "column.direct": "Mesaje directe", + "column.directory": "Browse profiles", "column.domain_blocks": "Domenii ascunse", "column.favourites": "Favorite", "column.follow_requests": "Cereri de urmărire", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Ești sigur că vrei să ștergi permanent această listă?", "confirmations.domain_block.confirm": "Ascunde tot domeniul", "confirmations.domain_block.message": "Ești absolut sigur că vrei să blochezi complet {domain}? În cele mai multe cazuri raportarea sau oprirea anumitor lucruri este suficientă și de preferat. Nu vei mai vedea nici un conținut de la acest domeniu in nici un flux public sau în notificările tale. Urmăritorii tăi de la acele domenii vor fi eliminați.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Oprește", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Ești sigur că vrei să oprești {name}?", "confirmations.redraft.confirm": "Șterge și salvează ca ciornă", "confirmations.redraft.message": "Ești sigur că vrei să faci asta? Tot ce ține de această postare, inclusiv răspunsurile vor fi deconectate.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Răspunzând la asta acum, mesajul pe care îl compui în prezent se va șterge. Ești sigur că vrei să continui?", "confirmations.unfollow.confirm": "Nu mai urmări", "confirmations.unfollow.message": "Ești sigur că nu mai vrei să îl urmărești pe {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Inserează această postare pe site-ul tău adăugând codul de mai jos.", "embed.preview": "Cam așa va arăta:", "emoji_button.activity": "Activitate", @@ -134,6 +152,10 @@ "empty_column.mutes": "Nu ai oprit nici un utilizator incă.", "empty_column.notifications": "Nu ai nici o notificare încă. Interacționează cu alții pentru a începe o conversație.", "empty_column.public": "Nu este nimci aici încă! Scrie ceva public, sau urmărește alți utilizatori din alte instanțe pentru a porni fluxul", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Autorizează", "follow_request.reject": "Respinge", "getting_started.developers": "Dezvoltatori", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Titlu pentru noua listă", "lists.search": "Caută printre persoanale pe care le urmărești", "lists.subheading": "Listele tale", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Încărcare...", "media_gallery.toggle_visible": "Comutați vizibilitatea", "missing_indicator.label": "Nu a fost găsit", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Postări fixate", "navigation_bar.preferences": "Preferințe", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Flux global", "navigation_bar.security": "Securitate", "notification.favourite": "{name} a adăugat statusul tău la favorite", @@ -277,8 +299,10 @@ "notifications.group": "{count} notificări", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Cine vede asta", @@ -290,6 +314,7 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Nu afisa in fluxul public", "privacy.unlisted.short": "Nelistat", + "refresh": "Refresh", "regeneration_indicator.label": "Încărcare…", "regeneration_indicator.sublabel": "Fluxul tău este în preparare!", "relative_time.days": "{number}z", @@ -314,6 +339,7 @@ "search_results.accounts": "Oameni", "search_results.hashtags": "Hashtaguri", "search_results.statuses": "Postări", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Arată mai mult", "status.show_more_all": "Arată mai mult pentru toți", "status.show_thread": "Arată topicul", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Repornește conversația", "status.unpin": "Eliberează din profil", "suggestions.dismiss": "Omite sugestia", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} vorbesc", + "trends.trending_now": "Trending now", "ui.beforeunload": "Postarea se va pierde dacă părăsești pagina.", "upload_area.title": "Trage și eliberează pentru a încărca", "upload_button.label": "Adaugă media (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Adaugă o descriere pentru persoanele cu deficiențe de vedere", - "upload_form.focus": "Schimbă previzualizarea", + "upload_form.edit": "Edit", "upload_form.undo": "Șterge", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Se Încarcă...", "video.close": "Închide video", "video.exit_fullscreen": "Închide", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index d720b6272..c8767e535 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -4,17 +4,19 @@ "account.block": "Блокировать", "account.block_domain": "Блокировать все с {domain}", "account.blocked": "Заблокирован(а)", + "account.cancel_follow_request": "Отменить запрос", "account.direct": "Написать @{name}", "account.domain_blocked": "Домен скрыт", "account.edit_profile": "Изменить профиль", "account.endorse": "Рекомендовать в профиле", "account.follow": "Подписаться", "account.followers": "Подписаны", - "account.followers.empty": "Никто не подписан на этого пользователя.", + "account.followers.empty": "На этого пользователя пока никто не подписан.", "account.follows": "Подписки", - "account.follows.empty": "Этот пользователь ни на кого не подписан.", - "account.follows_you": "Подписан(а) на Вас", + "account.follows.empty": "Этот пользователь пока ни на кого не подписался.", + "account.follows_you": "Подписан(а) на вас", "account.hide_reblogs": "Скрыть реблоги от @{name}", + "account.last_status": "Последняя активность", "account.link_verified_on": "Владение этой ссылкой было проверено {date}", "account.locked_info": "Это закрытый аккаунт. Его владелец вручную одобряет подписчиков.", "account.media": "Медиа", @@ -23,20 +25,24 @@ "account.mute": "Заглушить", "account.mute_notifications": "Скрыть уведомления от @{name}", "account.muted": "Приглушён", + "account.never_active": "Никогда", "account.posts": "Посты", - "account.posts_with_replies": "Посты и ответы", + "account.posts_with_replies": "Посты с ответами", "account.report": "Пожаловаться", - "account.requested": "Ожидает подтверждения", + "account.requested": "Ожидает подтверждения. Нажмите для отмены", "account.share": "Поделиться профилем @{name}", "account.show_reblogs": "Показывать продвижения от @{name}", - "account.unblock": "Разблокировать", + "account.unblock": "Разблокировать @{name}", "account.unblock_domain": "Разблокировать {domain}", "account.unendorse": "Не рекомендовать в профиле", "account.unfollow": "Отписаться", - "account.unmute": "Снять глушение", + "account.unmute": "Не скрывать @{name}", "account.unmute_notifications": "Показывать уведомления от @{name}", + "alert.rate_limited.message": "Пожалуйста, повторите через {retry_time, time, medium}.", + "alert.rate_limited.title": "Скорость ограничена", "alert.unexpected.message": "Что-то пошло не так.", "alert.unexpected.title": "Ой!", + "autosuggest_hashtag.per_week": "{count} / неделю", "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз", "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.", "bundle_column_error.retry": "Попробовать снова", @@ -47,12 +53,13 @@ "column.blocks": "Список блокировки", "column.community": "Локальная лента", "column.direct": "Личные сообщения", + "column.directory": "Просмотр профилей", "column.domain_blocks": "Скрытые домены", "column.favourites": "Понравившееся", "column.follow_requests": "Запросы на подписку", "column.home": "Главная", "column.lists": "Списки", - "column.mutes": "Список глушения", + "column.mutes": "Список скрытых пользователей", "column.notifications": "Уведомления", "column.pins": "Закреплённый пост", "column.public": "Глобальная лента", @@ -68,14 +75,14 @@ "compose_form.direct_message_warning": "Этот статус будет виден только упомянутым пользователям.", "compose_form.direct_message_warning_learn_more": "Узнать больше", "compose_form.hashtag_warning": "Этот пост не будет показывается в поиске по хэштегу, т.к. он непубличный. Только публичные посты можно найти в поиске по хэштегу.", - "compose_form.lock_disclaimer": "Ваш аккаунт не {locked}. Любой человек может подписаться на Вас и просматривать посты для подписчиков.", + "compose_form.lock_disclaimer": "Ваша учётная запись не {locked}. Любой пользователь может подписаться на вас и просматривать посты для подписчиков.", "compose_form.lock_disclaimer.lock": "закрыт", - "compose_form.placeholder": "О чем Вы думаете?", - "compose_form.poll.add_option": "Добавить", - "compose_form.poll.duration": "Длительность опроса", + "compose_form.placeholder": "О чем вы думаете?", + "compose_form.poll.add_option": "Добавить вариант", + "compose_form.poll.duration": "Продолжительность опроса", "compose_form.poll.option_placeholder": "Вариант {number}", "compose_form.poll.remove_option": "Удалить этот вариант", - "compose_form.publish": "Трубить", + "compose_form.publish": "Запостить", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive.hide": "Пометить медиафайл как чувствительный", "compose_form.sensitive.marked": "Медиафайлы не отмечены как чувствительные", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Вы действительно хотите навсегда удалить этот список?", "confirmations.domain_block.confirm": "Блокировать весь домен", "confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.", + "confirmations.logout.confirm": "Выйти", + "confirmations.logout.message": "Вы уверены, что хотите выйти?", "confirmations.mute.confirm": "Заглушить", + "confirmations.mute.explanation": "Это скроет посты этого пользователя и те, в которых он упоминается, но при этом он по-прежнему сможет подписаться на вас и смотреть ваши посты.", "confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?", "confirmations.redraft.confirm": "Удалить и исправить", "confirmations.redraft.message": "Вы уверены, что хотите удалить этот статус и превратить в черновик? Вы потеряете все ответы, продвижения и отметки 'нравится' к нему.", @@ -101,10 +111,18 @@ "confirmations.reply.message": "При ответе текст набираемого сообщения будет перезаписан. Продолжить?", "confirmations.unfollow.confirm": "Отписаться", "confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?", - "embed.instructions": "Встройте этот статус на Вашем сайте, скопировав код внизу.", + "conversation.delete": "Удалить беседу", + "conversation.mark_as_read": "Пометить прочитанным", + "conversation.open": "Просмотр беседы", + "conversation.with": "С {names}", + "directory.federated": "Со всей федерации", + "directory.local": "Только из {domain}", + "directory.new_arrivals": "Новички", + "directory.recently_active": "Недавно активные", + "embed.instructions": "Встройте этот пост на свой сайт, скопировав следующий код:", "embed.preview": "Так это будет выглядеть:", "emoji_button.activity": "Занятия", - "emoji_button.custom": "Собственные", + "emoji_button.custom": "Особенные", "emoji_button.flags": "Флаги", "emoji_button.food": "Еда и напитки", "emoji_button.label": "Вставить эмодзи", @@ -116,32 +134,36 @@ "emoji_button.search": "Найти...", "emoji_button.search_results": "Результаты поиска", "emoji_button.symbols": "Символы", - "emoji_button.travel": "Путешествия", - "empty_column.account_timeline": "Статусов нет!", + "emoji_button.travel": "Путешествия и места", + "empty_column.account_timeline": "Здесь нет постов!", "empty_column.account_unavailable": "Профиль недоступен", "empty_column.blocks": "Вы ещё никого не заблокировали.", "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!", - "empty_column.direct": "У Вас пока нет личных сообщений. Когда Вы начнёте их отправлять или получать, они появятся здесь.", + "empty_column.direct": "У вас пока нет личных сообщений. Как только вы отправите или получите одно, оно появится здесь.", "empty_column.domain_blocks": "Скрытых доменов пока нет.", - "empty_column.favourited_statuses": "Вы не добавили ни одного статуса в 'Избранное'. Как только Вы это сделаете, они появятся здесь.", - "empty_column.favourites": "Никто ещё не добавил этот статус в 'Избранное'. Как только кто-то это сделает, они появятся здесь.", + "empty_column.favourited_statuses": "Вы не добавили ни один пост в «Избранное». Как только вы это сделаете, он появится здесь.", + "empty_column.favourites": "Никто ещё не добавил этот пост в «Избранное». Как только кто-то это сделает, это отобразится здесь.", "empty_column.follow_requests": "Вам ещё не приходили запросы на подписку. Все новые запросы будут показаны здесь.", - "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.", - "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", + "empty_column.hashtag": "С этим хэштегом пока ещё ничего не постили.", + "empty_column.home": "Пока вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", "empty_column.home.public_timeline": "публичные ленты", "empty_column.list": "В этом списке пока ничего нет.", - "empty_column.lists": "У Вас ещё нет списков. Все созданные Вами списки будут показаны здесь.", - "empty_column.mutes": "Вы ещё никого не заглушили.", - "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.", - "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.", + "empty_column.lists": "У вас ещё нет списков. Созданные вами списки будут показаны здесь.", + "empty_column.mutes": "Вы ещё никого не скрывали.", + "empty_column.notifications": "У вас пока нет уведомлений. Взаимодействуйте с другими, чтобы завести разговор.", + "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту", + "error.unexpected_crash.explanation": "Из-за несовместимого браузера или ошибки в нашем коде, эта страница не может быть корректно отображена.", + "error.unexpected_crash.next_steps": "Попробуйте обновить страницу. Если проблема не исчезает, используйте Mastodon из-под другого браузера или приложения.", + "errors.unexpected_crash.copy_stacktrace": "Копировать стектрейс в буфер обмена", + "errors.unexpected_crash.report_issue": "Сообщить о проблеме", "follow_request.authorize": "Авторизовать", "follow_request.reject": "Отказать", - "getting_started.developers": "Для разработчиков", + "getting_started.developers": "Разработчикам", "getting_started.directory": "Каталог профилей", "getting_started.documentation": "Документация", "getting_started.heading": "Добро пожаловать", "getting_started.invite": "Пригласить людей", - "getting_started.open_source_notice": "Mastodon - сервис с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}.", + "getting_started.open_source_notice": "Mastodon — сервис с открытым исходным кодом. Вы можете внести вклад или сообщить о проблемах на GitHub: {github}.", "getting_started.security": "Безопасность", "getting_started.terms": "Условия использования", "hashtag.column_header.tag_mode.all": "и {additional}", @@ -152,30 +174,30 @@ "hashtag.column_settings.tag_mode.all": "Все из списка", "hashtag.column_settings.tag_mode.any": "Любой из списка", "hashtag.column_settings.tag_mode.none": "Ни один из списка", - "hashtag.column_settings.tag_toggle": "Включая дополнительные хэштеге из этой колонки", + "hashtag.column_settings.tag_toggle": "Включить дополнительные теги для этой колонки", "home.column_settings.basic": "Основные", "home.column_settings.show_reblogs": "Показывать продвижения", "home.column_settings.show_replies": "Показывать ответы", - "intervals.full.days": "{number, plural, one {# день} few {# дня} many {# дней} other {# дней}}", - "intervals.full.hours": "{number, plural, one {# час} few {# часа} many {# часов} other {# часов}}", - "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} many {# минут} other {# минут}}", + "intervals.full.days": "{number, plural, one {# день} few {# дня} other {# дней}}", + "intervals.full.hours": "{number, plural, one {# час} few {# часа} other {# часов}}", + "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} other {# минут}}", "introduction.federation.action": "Далее", "introduction.federation.federated.headline": "Глобальная лента", "introduction.federation.federated.text": "Публичные статусы с других серверов федеративной сети расположатся в глобальной ленте.", "introduction.federation.home.headline": "Домашняя лента", - "introduction.federation.home.text": "Статусы от тех, на кого вы подписаны, появятся в вашей домашней ленте. Вы можете подписаться на кого угодно с любого сервера!", + "introduction.federation.home.text": "Посты людей, на которых вы подписаны, будут отображаться в домашней ленте. Подписываться можно на кого угодно, независимо от узла.", "introduction.federation.local.headline": "Локальная лента", "introduction.federation.local.text": "Публичные статусы от людей с того же сервера, что и вы, будут отображены в локальной ленте.", "introduction.interactions.action": "Завершить обучение", - "introduction.interactions.favourite.headline": "Отметки \"нравится\"", + "introduction.interactions.favourite.headline": "Отметки «нравится»", "introduction.interactions.favourite.text": "Вы можете отметить статус, чтобы вернуться к нему позже и дать знать автору, что запись вам понравилась, поставив отметку \"нравится\".", "introduction.interactions.reblog.headline": "Продвижения", - "introduction.interactions.reblog.text": "Вы можете делиться статусами других людей, продвигая их в своём аккаунте.", + "introduction.interactions.reblog.text": "Вы можете делиться статусами других людей, продвигая их в своей учётной записи.", "introduction.interactions.reply.headline": "Ответы", "introduction.interactions.reply.text": "Вы можете отвечать свои и чужие посты, образуя цепочки сообщений (обсуждения).", "introduction.welcome.action": "Поехали!", "introduction.welcome.headline": "Первые шаги", - "introduction.welcome.text": "Добро пожаловать в федеративную сеть! Уже через мгновение вы сможете отправлять сообщения и общаться со своими друзьями на любом сервере. Но этот сервер — {domain} — особенный: на нём располагается ваш профиль. Запомните его название.", + "introduction.welcome.text": "Добро пожаловать в Федиверс! Уже через мгновение вы сможете отправлять сообщения и общаться со своими друзьями с любых узлов. Но этот узел — {domain} — особенный: на нём располагается ваш профиль, так что не забудьте его название.", "keyboard_shortcuts.back": "перейти назад", "keyboard_shortcuts.blocked": "чтобы открыть список заблокированных", "keyboard_shortcuts.boost": "продвинуть пост", @@ -219,8 +241,9 @@ "lists.edit.submit": "Изменить название", "lists.new.create": "Новый список", "lists.new.title_placeholder": "Заголовок списка", - "lists.search": "Искать из ваших подписок", + "lists.search": "Искать среди подписок", "lists.subheading": "Ваши списки", + "load_pending": "{count, plural, one {# новый элемент} few {# новых элемента} other {# новых элементов}}", "loading_indicator.label": "Загрузка...", "media_gallery.toggle_visible": "Показать/скрыть", "missing_indicator.label": "Не найдено", @@ -242,18 +265,17 @@ "navigation_bar.keyboard_shortcuts": "Сочетания клавиш", "navigation_bar.lists": "Списки", "navigation_bar.logout": "Выйти", - "navigation_bar.mutes": "Список глушения", + "navigation_bar.mutes": "Список скрытых пользователей", "navigation_bar.personal": "Личное", "navigation_bar.pins": "Закреплённые посты", - "navigation_bar.preferences": "Опции", - "navigation_bar.profile_directory": "Каталог профилей", + "navigation_bar.preferences": "Настройки", "navigation_bar.public_timeline": "Глобальная лента", "navigation_bar.security": "Безопасность", - "notification.favourite": "{name} понравился Ваш статус", - "notification.follow": "{name} подписался(-лась) на Вас", - "notification.mention": "{name} упомянул(а) Вас", + "notification.favourite": "{name} добавил(а) ваш статус в избранное", + "notification.follow": "{name} подписался (-лась) на вас", + "notification.mention": "{name} упомянул(а) вас", "notification.poll": "Опрос, в котором вы приняли участие, завершился", - "notification.reblog": "{name} продвинул(а) Ваш статус", + "notification.reblog": "{name} продвинул(а) ваш статус", "notifications.clear": "Очистить уведомления", "notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?", "notifications.column_settings.alert": "Десктопные уведомления", @@ -277,8 +299,10 @@ "notifications.group": "{count} уведомл.", "poll.closed": "Завершён", "poll.refresh": "Обновить", + "poll.total_people": "{count, plural, one {# человек} few {# человека} many {# человек} other {# человек}}", "poll.total_votes": "{count, plural, one {# голос} few {# голоса} many {# голосов} other {# голосов}}", "poll.vote": "Голосовать", + "poll.voted": "Вы проголосовали за этот вариант", "poll_button.add_poll": "Добавить опрос", "poll_button.remove_poll": "Удалить опрос", "privacy.change": "Изменить видимость статуса", @@ -290,8 +314,9 @@ "privacy.public.short": "Публичный", "privacy.unlisted.long": "Не показывать в лентах", "privacy.unlisted.short": "Скрытый", + "refresh": "Обновить", "regeneration_indicator.label": "Загрузка…", - "regeneration_indicator.sublabel": "Ваша домашняя лента готовится!", + "regeneration_indicator.sublabel": "Один момент, мы подготавливаем вашу ленту!", "relative_time.days": "{number}д", "relative_time.hours": "{number}ч", "relative_time.just_now": "только что", @@ -299,14 +324,14 @@ "relative_time.seconds": "{number}с", "reply_indicator.cancel": "Отмена", "report.forward": "Переслать в {target}", - "report.forward_hint": "Этот аккаунт расположен на другом сервере. Отправить туда анонимную копию Вашей жалобы?", - "report.hint": "Жалоба будет отправлена модераторам Вашего сервера. Вы также можете указать подробную причину жалобы ниже:", + "report.forward_hint": "Этот аккаунт расположен на другом узле. Отправить туда анонимную копию вашей жалобы?", + "report.hint": "Жалоба будет отправлена модераторам вашего узла. Вы также можете указать подробную причину жалобы ниже:", "report.placeholder": "Комментарий", "report.submit": "Отправить", "report.target": "Жалоба на {target}", "search.placeholder": "Поиск", "search_popout.search_format": "Продвинутый формат поиска", - "search_popout.tips.full_text": "Возвращает посты, которые Вы написали, отметили как 'избранное', продвинули или в которых были упомянуты, а также содержащие юзернейм, имя и хэштеги.", + "search_popout.tips.full_text": "Поиск по простому тексту отобразит посты, которые вы написали, добавили в избранное, продвинули или в которых были упомянуты, а также подходящие имена пользователей и хэштеги.", "search_popout.tips.hashtag": "хэштег", "search_popout.tips.status": "статус", "search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги", @@ -314,6 +339,7 @@ "search_results.accounts": "Люди", "search_results.hashtags": "Хэштеги", "search_results.statuses": "Посты", + "search_results.statuses_fts_disabled": "Поиск постов по их контенту не поддерживается на этом сервере Mastodon.", "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", "status.admin_account": "Открыть интерфейс модератора для @{name}", "status.admin_status": "Открыть этот статус в интерфейсе модератора", @@ -352,6 +378,7 @@ "status.show_more": "Развернуть", "status.show_more_all": "Развернуть для всех", "status.show_thread": "Показать обсуждение", + "status.uncached_media_warning": "Недоступно", "status.unmute_conversation": "Снять глушение с обсуждения", "status.unpin": "Открепить от профиля", "suggestions.dismiss": "Удалить предложение", @@ -366,15 +393,23 @@ "time_remaining.minutes": "{number, plural, one {осталась # минута} few {осталось # минуты} many {осталось # минут} other {осталось # минут}}", "time_remaining.moments": "остались считанные мгновения", "time_remaining.seconds": "{number, plural, one {осталась # секунду} few {осталось # секунды} many {осталось # секунд} other {осталось # секунд}}", - "trends.count_by_accounts": "Популярно у {count} {rawCount, plural, one {человека} few {человек} many {человек} other {человек}}", + "trends.count_by_accounts": "{count} {rawCount, plural, one {человек говорит} few {человека говорят} other {человек говорят}} про это", + "trends.trending_now": "Самое актуальное", "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.", "upload_area.title": "Перетащите сюда, чтобы загрузить", "upload_button.label": "Добавить медиаконтент", "upload_error.limit": "Достигнут лимит загруженных файлов.", "upload_error.poll": "К опросам нельзя прикреплять файлы.", - "upload_form.description": "Описать для людей с нарушениями зрения", - "upload_form.focus": "Обрезать", + "upload_form.description": "Добавьте описание для людей с нарушениями зрения:", + "upload_form.edit": "Изменить", "upload_form.undo": "Отменить", + "upload_modal.analyzing_picture": "Обработка изображения…", + "upload_modal.apply": "Применить", + "upload_modal.description_placeholder": "На дворе трава, на траве дрова", + "upload_modal.detect_text": "Найти текст на картинке", + "upload_modal.edit_media": "Изменение медиа", + "upload_modal.hint": "Нажмите и перетащите круг в предпросмотре в точку фокуса, которая всегда будет видна на эскизах.", + "upload_modal.preview_label": "Предпросмотр ({ratio})", "upload_progress.label": "Загрузка...", "video.close": "Закрыть видео", "video.exit_fullscreen": "Покинуть полноэкранный режим", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 56edf5469..0766e703f 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -1,10 +1,11 @@ { - "account.add_or_remove_from_list": "Pridaj, alebo odstráň zo zoznamov", + "account.add_or_remove_from_list": "Pridaj do, alebo odober zo zoznamov", "account.badges.bot": "Bot", "account.block": "Blokuj @{name}", "account.block_domain": "Ukry všetko z {domain}", "account.blocked": "Blokovaný/á", - "account.direct": "Súkromná správa pre @{name}", + "account.cancel_follow_request": "Zruš požiadanie o sledovanie", + "account.direct": "Priama správa pre @{name}", "account.domain_blocked": "Doména ukrytá", "account.edit_profile": "Uprav profil", "account.endorse": "Zobrazuj na profile", @@ -15,15 +16,17 @@ "account.follows.empty": "Tento užívateľ ešte nikoho nenásleduje.", "account.follows_you": "Následuje ťa", "account.hide_reblogs": "Skry vyzdvihnutia od @{name}", + "account.last_status": "Naposledy aktívny", "account.link_verified_on": "Vlastníctvo tohto odkazu bolo skontrolované {date}", "account.locked_info": "Stav súkromia pre tento účet je nastavený na zamknutý. Jeho vlastník sám prehodnocuje, kto ho môže sledovať.", "account.media": "Médiá", "account.mention": "Spomeň @{name}", "account.moved_to": "{name} sa presunul/a na:", - "account.mute": "Ignorovať @{name}", + "account.mute": "Nevšímaj si @{name}", "account.mute_notifications": "Stĺm oboznámenia od @{name}", "account.muted": "Utíšený/á", - "account.posts": "Príspevky", + "account.never_active": "Nikdy", + "account.posts": "Príspevkov", "account.posts_with_replies": "Príspevky aj s odpoveďami", "account.report": "Nahlás @{name}", "account.requested": "Čaká na schválenie. Klikni pre zrušenie žiadosti", @@ -35,8 +38,11 @@ "account.unfollow": "Prestaň následovať", "account.unmute": "Prestaň ignorovať @{name}", "account.unmute_notifications": "Zruš stĺmenie oboznámení od @{name}", + "alert.rate_limited.message": "Prosím, skús to znova za {retry_time, time, medium}.", + "alert.rate_limited.title": "Tempo obmedzené", "alert.unexpected.message": "Vyskytla sa nečakaná chyba.", - "alert.unexpected.title": "Oops!", + "alert.unexpected.title": "Ups!", + "autosuggest_hashtag.per_week": "{count} týždenne", "boost_modal.combo": "Nabudúce môžeš kliknúť {combo} pre preskočenie", "bundle_column_error.body": "Pri načítaní tohto prvku nastala nejaká chyba.", "bundle_column_error.retry": "Skús to znova", @@ -46,7 +52,8 @@ "bundle_modal_error.retry": "Skúsiť znova", "column.blocks": "Blokovaní užívatelia", "column.community": "Miestna časová os", - "column.direct": "Súkromné správy", + "column.direct": "Priame správy", + "column.directory": "Prehľadávaj profily", "column.domain_blocks": "Skryté domény", "column.favourites": "Obľúbené", "column.follow_requests": "Žiadosti o sledovanie", @@ -62,11 +69,11 @@ "column_header.moveRight_settings": "Presuň stĺpec doprava", "column_header.pin": "Pripni", "column_header.show_settings": "Ukáž nastavenia", - "column_header.unpin": "Odopnúť", + "column_header.unpin": "Odopni", "column_subheading.settings": "Nastavenia", "community.column_settings.media_only": "Iba médiá", - "compose_form.direct_message_warning": "Tento príspevok bude videný výhradne iba spomenutými užívateľmi. Ber ale na vedomie že správci tvojej a všetkých iných zahrnutých instancií majú možnosť skontrolovať túto správu.", - "compose_form.direct_message_warning_learn_more": "Zistiť viac", + "compose_form.direct_message_warning": "Tento príspevok bude boslaný iba spomenutým užívateľom.", + "compose_form.direct_message_warning_learn_more": "Zisti viac", "compose_form.hashtag_warning": "Tento toot nebude zobrazený pod žiadným haštagom lebo nieje listovaný. Iba verejné tooty môžu byť nájdené podľa haštagu.", "compose_form.lock_disclaimer": "Tvoj účet nie je {locked}. Ktokoľvek ťa môže nasledovať a vidieť tvoje správy pre sledujúcich.", "compose_form.lock_disclaimer.lock": "zamknutý", @@ -77,14 +84,14 @@ "compose_form.poll.remove_option": "Odstráň túto voľbu", "compose_form.publish": "Pošli", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Označ médiá ako chúlostivé", "compose_form.sensitive.marked": "Médiálny obsah je označený ako chúlostivý", "compose_form.sensitive.unmarked": "Médiálny obsah nieje označený ako chúlostivý", "compose_form.spoiler.marked": "Text je ukrytý za varovaním", "compose_form.spoiler.unmarked": "Text nieje ukrytý", "compose_form.spoiler_placeholder": "Sem napíš tvoje varovanie", "confirmation_modal.cancel": "Zruš", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Zablokuj a nahlás", "confirmations.block.confirm": "Blokuj", "confirmations.block.message": "Si si istý/á, že chceš blokovať {name}?", "confirmations.delete.confirm": "Vymaž", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Si si istý/á, že chceš natrvalo vymazať tento zoznam?", "confirmations.domain_block.confirm": "Skry celú doménu", "confirmations.domain_block.message": "Si si naozaj istý/á, že chceš blokovať celú doménu {domain}? Vo väčšine prípadov stačí blokovať alebo ignorovať pár konkrétnych užívateľov, čo sa doporučuje. Neuvidíš obsah z tejto domény v žiadnej verejnej časovej osi, ani v oznámeniach. Tvoji následovníci pochádzajúci z tejto domény budú odstránení.", + "confirmations.logout.confirm": "Odhlás sa", + "confirmations.logout.message": "Si si istý/á, že sa chceš odhlásiť?", "confirmations.mute.confirm": "Ignoruj", + "confirmations.mute.explanation": "Toto nastavenie pred tebou skryje ich príspevky, alebo príspevky od iných v ktorých sú spomenutí, ale umožní im vidieť tvoje príspevky, aj ťa následovať.", "confirmations.mute.message": "Naozaj chceš ignorovať {name}?", "confirmations.redraft.confirm": "Vyčisti a prepíš", "confirmations.redraft.message": "Si si istý/á, že chceš premazať a prepísať tento príspevok? Jeho nadobudnuté vyzdvihnutia a obľúbenia, ale i odpovede na pôvodný príspevok budú odlúčené.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Odpovedaním akurát teraz prepíšeš správu, ktorú máš práve rozpísanú. Si si istý/á, že chceš pokračovať?", "confirmations.unfollow.confirm": "Nesleduj", "confirmations.unfollow.message": "Naozaj chceš prestať sledovať {name}?", + "conversation.delete": "Vymaž konverzáciu", + "conversation.mark_as_read": "Označ za prečítané", + "conversation.open": "Ukáž konverzáciu", + "conversation.with": "S {names}", + "directory.federated": "Zo známého fedivesmíru", + "directory.local": "Iba z {domain}", + "directory.new_arrivals": "Nové príchody", + "directory.recently_active": "Nedávno aktívne", "embed.instructions": "Umiestni kód uvedený nižšie pre pridanie tohto statusu na tvoju web stránku.", "embed.preview": "Tu je ako to bude vyzerať:", "emoji_button.activity": "Aktivita", @@ -121,7 +139,7 @@ "empty_column.account_unavailable": "Profil nedostupný", "empty_column.blocks": "Ešte si nikoho nezablokoval/a.", "empty_column.community": "Lokálna časová os je prázdna. Napíšte niečo, aby sa to tu začalo hýbať!", - "empty_column.direct": "Ešte nemáš žiadne súkromné správy. Keď nejakú pošleš, alebo dostaneš, ukáže sa tu.", + "empty_column.direct": "Ešte nemáš žiadne priame správy. Keď nejakú pošleš, alebo dostaneš, ukáže sa tu.", "empty_column.domain_blocks": "Žiadne domény ešte niesú skryté.", "empty_column.favourited_statuses": "Nemáš obľúbené ešte žiadne príspevky. Keď si nejaký obľúbiš, bude zobrazený práve tu.", "empty_column.favourites": "Tento toot si ešte nikto neobľúbil. Ten kto si ho obľúbi, bude zobrazený tu.", @@ -134,13 +152,17 @@ "empty_column.mutes": "Ešte si nestĺmil žiadných užívateľov.", "empty_column.notifications": "Ešte nemáš žiadne oznámenia. Začni komunikovať s ostatnými, aby diskusia mohla začať.", "empty_column.public": "Ešte tu nič nie je. Napíš niečo verejne, alebo začni sledovať užívateľov z iných serverov, aby tu niečo pribudlo", + "error.unexpected_crash.explanation": "Kvôli chybe v našom kóde, alebo problému s kompatibilitou prehliadača, túto stránku nebolo možné zobraziť správne.", + "error.unexpected_crash.next_steps": "Skús obnoviť stránku. Ak to nepomôže, pravdepodobne budeš stále môcť používať Mastodon cez iný prehliadač, alebo natívnu aplikáciu.", + "errors.unexpected_crash.copy_stacktrace": "Skopíruj stacktrace do schránky", + "errors.unexpected_crash.report_issue": "Nahlás problém", "follow_request.authorize": "Povoľ prístup", "follow_request.reject": "Odmietni", "getting_started.developers": "Vývojári", - "getting_started.directory": "Databáza profilov", + "getting_started.directory": "Zoznam profilov", "getting_started.documentation": "Dokumentácia", "getting_started.heading": "Začni tu", - "getting_started.invite": "Pozvať ľudí", + "getting_started.invite": "Pozvi ľudí", "getting_started.open_source_notice": "Mastodon je softvér s otvoreným kódom. Nahlásiť chyby, alebo prispievať môžeš na GitHube v {github}.", "getting_started.security": "Zabezpečenie", "getting_started.terms": "Podmienky prevozu", @@ -156,9 +178,9 @@ "home.column_settings.basic": "Základné", "home.column_settings.show_reblogs": "Zobraziť povýšené", "home.column_settings.show_replies": "Ukázať odpovede", - "intervals.full.days": "{number, plural, one {# deň} few {# dní} many {# dní} other {# dni}}", - "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodiny}}", - "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minúty}}", + "intervals.full.days": "{number, plural, one {# deň} few {# dní} many {# dní} other {# dní}}", + "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodín}}", + "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minút}}", "introduction.federation.action": "Ďalej", "introduction.federation.federated.headline": "Federovaná", "introduction.federation.federated.text": "Verejné príspevky z ostatných serverov vo fediverse budú zobrazené vo federovanej časovej osi.", @@ -182,7 +204,7 @@ "keyboard_shortcuts.column": "zameraj sa na príspevok v jednom zo stĺpcov", "keyboard_shortcuts.compose": "zameraj sa na písaciu plochu", "keyboard_shortcuts.description": "Popis", - "keyboard_shortcuts.direct": "otvor panel súkromných správ", + "keyboard_shortcuts.direct": "pre otvorenie panelu priamých správ", "keyboard_shortcuts.down": "posunúť sa dole v zozname", "keyboard_shortcuts.enter": "otvoriť správu", "keyboard_shortcuts.favourite": "pridaj do obľúbených", @@ -204,14 +226,14 @@ "keyboard_shortcuts.search": "zameraj sa na vyhľadávanie", "keyboard_shortcuts.start": "otvor panel ''začíname''", "keyboard_shortcuts.toggle_hidden": "ukáž/skry text za CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "pre zobrazenie/skrytie médií", "keyboard_shortcuts.toot": "začni úplne nový príspevok", "keyboard_shortcuts.unfocus": "nesústreď sa na písaciu plochu, alebo hľadanie", "keyboard_shortcuts.up": "posuň sa vyššie v zozname", "lightbox.close": "Zatvor", "lightbox.next": "Ďalšie", "lightbox.previous": "Predchádzajúci", - "lightbox.view_context": "View context", + "lightbox.view_context": "Ukáž kontext", "lists.account.add": "Pridaj do zoznamu", "lists.account.remove": "Odober zo zoznamu", "lists.delete": "Vymaž list", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Názov nového zoznamu", "lists.search": "Vyhľadávaj medzi užívateľmi, ktorých sleduješ", "lists.subheading": "Tvoje zoznamy", + "load_pending": "{count, plural, one {# nová položka} other {# nových položiek}}", "loading_indicator.label": "Načítam...", "media_gallery.toggle_visible": "Zapni/Vypni viditeľnosť", "missing_indicator.label": "Nenájdené", @@ -230,14 +253,14 @@ "navigation_bar.blocks": "Blokovaní užívatelia", "navigation_bar.community_timeline": "Miestna časová os", "navigation_bar.compose": "Napíš nový príspevok", - "navigation_bar.direct": "Súkromné správy", + "navigation_bar.direct": "Priame správy", "navigation_bar.discover": "Objavuj", "navigation_bar.domain_blocks": "Skryté domény", "navigation_bar.edit_profile": "Uprav profil", "navigation_bar.favourites": "Obľúbené", "navigation_bar.filters": "Filtrované slová", "navigation_bar.follow_requests": "Žiadosti o sledovanie", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Sledovania a následovatelia", "navigation_bar.info": "O tomto serveri", "navigation_bar.keyboard_shortcuts": "Klávesové skratky", "navigation_bar.lists": "Zoznamy", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Osobné", "navigation_bar.pins": "Pripnuté príspevky", "navigation_bar.preferences": "Voľby", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federovaná časová os", "navigation_bar.security": "Zabezbečenie", "notification.favourite": "{name} si obľúbil/a tvoj príspevok", @@ -254,7 +276,7 @@ "notification.mention": "{name} ťa spomenul/a", "notification.poll": "Anketa v ktorej si hlasoval/a sa skončila", "notification.reblog": "{name} zdieľal/a tvoj príspevok", - "notifications.clear": "Vyčistiť zoznam oboznámení", + "notifications.clear": "Vyčisti oboznámenia", "notifications.clear_confirmation": "Naozaj chceš nenávratne prečistiť všetky tvoje oboznámenia?", "notifications.column_settings.alert": "Oboznámenia na ploche", "notifications.column_settings.favourite": "Obľúbené:", @@ -276,9 +298,11 @@ "notifications.filter.polls": "Výsledky ankiet", "notifications.group": "{count} oboznámení", "poll.closed": "Uzatvorená", - "poll.refresh": "Aktualizuj", - "poll.total_votes": "{count, plural, one {# hlas} few {# hlasov} many {# hlasov} other {# hlasy}}", + "poll.refresh": "Občerstvi", + "poll.total_people": "{count, plural, one {# človek} few {# ľudia} other {# ľudí}}", + "poll.total_votes": "{count, plural, one {# hlas} few {# hlasov} many {# hlasov} other {# hlasov}}", "poll.vote": "Hlasuj", + "poll.voted": "Hlasoval/a si za túto voľbu", "poll_button.add_poll": "Pridaj anketu", "poll_button.remove_poll": "Odstráň anketu", "privacy.change": "Uprav súkromie príspevku", @@ -290,13 +314,14 @@ "privacy.public.short": "Verejné", "privacy.unlisted.long": "Neposielaj do verejných časových osí", "privacy.unlisted.short": "Verejne, ale nezobraziť v osi", + "refresh": "Občerstvi", "regeneration_indicator.label": "Načítava sa…", "regeneration_indicator.sublabel": "Vaša domovská nástenka sa pripravuje!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", + "relative_time.days": "{number}dní", + "relative_time.hours": "{number}hod", "relative_time.just_now": "teraz", - "relative_time.minutes": "{number}m", - "relative_time.seconds": "{number}s", + "relative_time.minutes": "{number}min", + "relative_time.seconds": "{number}sek", "reply_indicator.cancel": "Zrušiť", "report.forward": "Posuň ku {target}", "report.forward_hint": "Tento účet je z iného serveru. Chceš poslať anonymnú kópiu hlásenia aj tam?", @@ -308,12 +333,13 @@ "search_popout.search_format": "Pokročilé vyhľadávanie", "search_popout.tips.full_text": "Vráti jednoduchý textový výpis príspevkov ktoré si napísal/a, ktoré si obľúbil/a, povýšil/a, alebo aj tých, v ktorých si bol/a spomenutý/á, a potom všetky zadaniu odpovedajúce prezívky, mená a haštagy.", "search_popout.tips.hashtag": "haštag", - "search_popout.tips.status": "status", + "search_popout.tips.status": "príspevok", "search_popout.tips.text": "Vráti jednoduchý textový výpis zhodujúcich sa mien, prezývok a haštagov", "search_popout.tips.user": "užívateľ", "search_results.accounts": "Ľudia", "search_results.hashtags": "Haštagy", "search_results.statuses": "Príspevky", + "search_results.statuses_fts_disabled": "Vyhľadávanie v obsahu príspevkov nieje na tomto Mastodon serveri povolené.", "search_results.total": "{count, number} {count, plural, one {výsledok} many {výsledkov} other {výsledky}}", "status.admin_account": "Otvor moderovacie rozhranie užívateľa @{name}", "status.admin_status": "Otvor tento príspevok v moderovacom rozhraní", @@ -323,7 +349,7 @@ "status.copy": "Skopíruj odkaz na príspevok", "status.delete": "Zmazať", "status.detailed_status": "Podrobný náhľad celej konverzácie", - "status.direct": "Súkromná správa @{name}", + "status.direct": "Priama správa pre @{name}", "status.embed": "Vložiť", "status.favourite": "Páči sa mi", "status.filtered": "Filtrované", @@ -352,6 +378,7 @@ "status.show_more": "Ukáž viac", "status.show_more_all": "Všetkým ukáž viac", "status.show_thread": "Ukáž diskusné vlákno", + "status.uncached_media_warning": "Nedostupný/é", "status.unmute_conversation": "Prestaň ignorovať konverzáciu", "status.unpin": "Odopni z profilu", "suggestions.dismiss": "Zavrhni návrh", @@ -361,20 +388,28 @@ "tabs_bar.local_timeline": "Miestna", "tabs_bar.notifications": "Oboznámenia", "tabs_bar.search": "Hľadaj", - "time_remaining.days": "Ostáva {number, plural, one {# deň} few {# dní} many {# dni} other {# dni}}", + "time_remaining.days": "Ostáva {number, plural, one {# deň} few {# dní} many {# dní} other {# dní}}", "time_remaining.hours": "Ostáva {number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodiny}}", "time_remaining.minutes": "Ostáva {number, plural, one {# minúta} few {# minút} many {# minút} other {# minúty}}", "time_remaining.moments": "Ostáva už iba chviľka", - "time_remaining.seconds": "Ostáva {number, plural, one {# sekunda} few {# sekúnd} many {# sekúnd} other {# sekundy}}", + "time_remaining.seconds": "Ostáva {number, plural, one {# sekunda} few {# sekúnd} many {# sekúnd} other {# sekúnd}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {človek vraví} other {ľudia vravia}}", + "trends.trending_now": "Teraz populárne", "ui.beforeunload": "Čo máš rozpísané sa stratí, ak opustíš Mastodon.", "upload_area.title": "Pretiahni a pusť pre nahratie", "upload_button.label": "Pridaj médiálny súbor (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Limit pre nahrávanie súborov bol prekročený.", "upload_error.poll": "Nahrávanie súborov pri anketách nieje možné.", "upload_form.description": "Opis pre slabo vidiacich", - "upload_form.focus": "Pozmeň náhľad", + "upload_form.edit": "Uprav", "upload_form.undo": "Vymaž", + "upload_modal.analyzing_picture": "Analyzujem obrázok…", + "upload_modal.apply": "Použi", + "upload_modal.description_placeholder": "Rýchla hnedá líška skáče ponad lenivého psa", + "upload_modal.detect_text": "Rozpoznaj text z obrázka", + "upload_modal.edit_media": "Uprav médiá", + "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.preview_label": "Náhľad ({ratio})", "upload_progress.label": "Nahráva sa...", "video.close": "Zavri video", "video.exit_fullscreen": "Vypni zobrazenie na celú obrazovku", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 736d7da36..ecac0d9c7 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -4,6 +4,7 @@ "account.block": "Blokiraj @{name}", "account.block_domain": "Skrij vse iz {domain}", "account.blocked": "Blokirano", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Neposredno sporočilo @{name}", "account.domain_blocked": "Skrita domena", "account.edit_profile": "Uredi profil", @@ -13,31 +14,36 @@ "account.followers.empty": "Nihče ne sledi temu uporabniku.", "account.follows": "Sledi", "account.follows.empty": "Ta uporabnik še ne sledi nikomur.", - "account.follows_you": "Ti sledi", - "account.hide_reblogs": "Skrij sunke od @{name}", + "account.follows_you": "Sledi tebi", + "account.hide_reblogs": "Skrij spodbude od @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Lastništvo te povezave je bilo preverjeno {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.locked_info": "Stanje zasebnosti računa je nastavljeno na zaklenjeno. Lastnik ročno pregleda, kdo ga lahko spremlja.", "account.media": "Mediji", "account.mention": "Omeni @{name}", "account.moved_to": "{name} se je premaknil na:", "account.mute": "Utišaj @{name}", "account.mute_notifications": "Utišaj obvestila od @{name}", "account.muted": "Utišan", + "account.never_active": "Never", "account.posts": "Tuti", "account.posts_with_replies": "Tuti in odgovori", "account.report": "Prijavi @{name}", "account.requested": "Čakanje na odobritev. Kliknite, da prekličete prošnjo za sledenje", "account.share": "Delite profil osebe @{name}", - "account.show_reblogs": "Pokaži delitve osebe @{name}", + "account.show_reblogs": "Pokaži spodbude osebe @{name}", "account.unblock": "Odblokiraj @{name}", "account.unblock_domain": "Razkrij {domain}", - "account.unendorse": "Don't feature on profile", + "account.unendorse": "Ne vključi v profil", "account.unfollow": "Prenehaj slediti", "account.unmute": "Odtišaj @{name}", "account.unmute_notifications": "Vklopi obvestila od @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "Zgodila se je nepričakovana napaka.", "alert.unexpected.title": "Uups!", - "boost_modal.combo": "Če želite naslednjič preskočiti to, lahko pritisnete {combo}", + "autosuggest_hashtag.per_week": "{count} per week", + "boost_modal.combo": "Če želite preskočiti to, lahko pritisnete {combo}", "bundle_column_error.body": "Med nalaganjem te komponente je prišlo do napake.", "bundle_column_error.retry": "Poskusi ponovno", "bundle_column_error.title": "Napaka omrežja", @@ -47,6 +53,7 @@ "column.blocks": "Blokirani uporabniki", "column.community": "Lokalna časovnica", "column.direct": "Neposredna sporočila", + "column.directory": "Browse profiles", "column.domain_blocks": "Skrite domene", "column.favourites": "Priljubljene", "column.follow_requests": "Sledi prošnjam", @@ -67,49 +74,60 @@ "community.column_settings.media_only": "Samo mediji", "compose_form.direct_message_warning": "Ta tut bo viden le vsem omenjenim uporabnikom.", "compose_form.direct_message_warning_learn_more": "Nauči se več", - "compose_form.hashtag_warning": "Ta tut ne bo naveden pod nobenim hashtagom, ker ni dodan hashtag. Samo javne tute lahko iščete pod hashtagom.", + "compose_form.hashtag_warning": "Ta tut ne bo naveden pod nobenim ključnikom, ker ni javen. Samo javne tute lahko iščete s ključniki.", "compose_form.lock_disclaimer": "Vaš račun ni {locked}. Vsakdo vam lahko sledi in si ogleda objave, ki so namenjene samo sledilcem.", "compose_form.lock_disclaimer.lock": "zaklenjen", "compose_form.placeholder": "O čem razmišljaš?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Dodaj izbiro", + "compose_form.poll.duration": "Trajanje ankete", + "compose_form.poll.option_placeholder": "Izbira {number}", + "compose_form.poll.remove_option": "Odstrani to izbiro", "compose_form.publish": "Tutni", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Označi medij kot občutljiv", "compose_form.sensitive.marked": "Medij je označen kot občutljiv", "compose_form.sensitive.unmarked": "Medij ni označen kot občutljiv", "compose_form.spoiler.marked": "Besedilo je skrito za opozorilom", "compose_form.spoiler.unmarked": "Besedilo ni skrito", - "compose_form.spoiler_placeholder": "Napišite opozorilo tukaj", + "compose_form.spoiler_placeholder": "Tukaj napišite opozorilo", "confirmation_modal.cancel": "Prekliči", - "confirmations.block.block_and_report": "Block & Report", - "confirmations.block.confirm": "Block", + "confirmations.block.block_and_report": "Blokiraj in Prijavi", + "confirmations.block.confirm": "Blokiraj", "confirmations.block.message": "Ali ste prepričani, da želite blokirati {name}?", - "confirmations.delete.confirm": "Delete", + "confirmations.delete.confirm": "Izbriši", "confirmations.delete.message": "Ali ste prepričani, da želite izbrisati to stanje?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Izbriši", "confirmations.delete_list.message": "Ali ste prepričani, da želite trajno izbrisati ta seznam?", "confirmations.domain_block.confirm": "Skrij celotno domeno", - "confirmations.domain_block.message": "Ali ste res, res prepričani, da želite blokirati celotno {domain}? V večini primerov je nekaj ciljnih blokiranj ali utišanj dovolj in boljše.", + "confirmations.domain_block.message": "Ali ste res, res prepričani, da želite blokirati celotno {domain}? V večini primerov je nekaj ciljnih blokiranj ali utišanj dovolj in boljše. Vsebino iz te domene ne boste videli v javnih časovnicah ali obvestilih. Vaši sledilci iz te domene bodo odstranjeni.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Utišanje", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Ali ste prepričani, da želite utišati {name}?", "confirmations.redraft.confirm": "Izbriši in preoblikuj", - "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", - "confirmations.reply.confirm": "Reply", + "confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati ta status in ga preoblikovati? Vzljubi in spodbude bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.", + "confirmations.reply.confirm": "Odgovori", "confirmations.reply.message": "Odgovarjanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?", "confirmations.unfollow.confirm": "Prenehaj slediti", "confirmations.unfollow.message": "Ali ste prepričani, da ne želite več slediti {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Vstavi ta status na svojo spletno stran tako, da kopirate spodnjo kodo.", - "embed.preview": "Tukaj je, kako bo izgledalo:", + "embed.preview": "Tako bo izgledalo:", "emoji_button.activity": "Dejavnost", "emoji_button.custom": "Po meri", "emoji_button.flags": "Zastave", "emoji_button.food": "Hrana in Pijača", - "emoji_button.label": "Vstavi emojija", + "emoji_button.label": "Vstavi emotikon", "emoji_button.nature": "Narava", - "emoji_button.not_found": "Ni emojijev!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "Ni emotikonov!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Predmeti", "emoji_button.people": "Ljudje", "emoji_button.recent": "Pogosto uporabljeni", @@ -117,231 +135,239 @@ "emoji_button.search_results": "Rezultati iskanja", "emoji_button.symbols": "Simboli", "emoji_button.travel": "Potovanja in Kraji", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_timeline": "Tukaj ni tutov!", + "empty_column.account_unavailable": "Profil ni na voljo", "empty_column.blocks": "Niste še blokirali nobenega uporabnika.", "empty_column.community": "Lokalna časovnica je prazna. Napišite nekaj javnega, da se bo žoga zakotalila!", - "empty_column.direct": "Nimate še nobenih neposrednih sporočil. Ko ga pošljete ali prejmete, se prikaže tukaj.", + "empty_column.direct": "Nimate še nobenih neposrednih sporočil. Ko ga boste poslali ali prejeli, se bo prikazal tukaj.", "empty_column.domain_blocks": "Še vedno ni skritih domen.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", - "empty_column.hashtag": "V tem hashtagu še ni nič.", + "empty_column.favourited_statuses": "Nimate priljubljenih tutov. Ko boste vzljubili kakšnega, se bo prikazal tukaj.", + "empty_column.favourites": "Nihče še ni vzljubil tega tuta. Ko ga bo nekdo, se bo pojavil tukaj.", + "empty_column.follow_requests": "Nimate prošenj za sledenje. Ko boste prejeli kakšno, se bo prikazala tukaj.", + "empty_column.hashtag": "V tem ključniku še ni nič.", "empty_column.home": "Vaša domača časovnica je prazna! Obiščite {public} ali uporabite iskanje, da se boste srečali druge uporabnike.", "empty_column.home.public_timeline": "javna časovnica", "empty_column.list": "Na tem seznamu ni ničesar. Ko bodo člani tega seznama objavili nove statuse, se bodo pojavili tukaj.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", - "empty_column.notifications": "Nimate še nobenih obvestil. Poveži se z drugimi, da začnete pogovor.", - "empty_column.public": "Tukaj ni ničesar! Da ga napolnite, napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih vozlišč", - "follow_request.authorize": "Odobri", + "empty_column.lists": "Nimate seznamov. Ko ga boste ustvarili, se bo prikazal tukaj.", + "empty_column.mutes": "Niste utišali še nobenega uporabnika.", + "empty_column.notifications": "Nimate še nobenih obvestil. Povežite se z drugimi, da začnete pogovor.", + "empty_column.public": "Tukaj ni ničesar! Da ga napolnite, napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih strežnikov", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", + "follow_request.authorize": "Overi", "follow_request.reject": "Zavrni", - "getting_started.developers": "Developers", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", - "getting_started.heading": "Prvi koraki", - "getting_started.invite": "Invite people", - "getting_started.open_source_notice": "Mastodon je odprtokodna programska oprema. V GitHubu na {github} lahko prispevate ali poročate o napakah.", - "getting_started.security": "Security", - "getting_started.terms": "Terms of service", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", - "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "getting_started.developers": "Razvijalci", + "getting_started.directory": "Imenik profilov", + "getting_started.documentation": "Dokumentacija", + "getting_started.heading": "Kako začeti", + "getting_started.invite": "Povabite osebe", + "getting_started.open_source_notice": "Mastodon je odprtokodna programska oprema. Na GitHubu na {github} lahko prispevate ali poročate o napakah.", + "getting_started.security": "Varnost", + "getting_started.terms": "Pogoji uporabe", + "hashtag.column_header.tag_mode.all": "in {additional}", + "hashtag.column_header.tag_mode.any": "ali {additional}", + "hashtag.column_header.tag_mode.none": "brez {additional}", + "hashtag.column_settings.select.no_options_message": "Ni najdenih predlogov", + "hashtag.column_settings.select.placeholder": "Vpiši ključnik…", + "hashtag.column_settings.tag_mode.all": "Vse od naštetega", + "hashtag.column_settings.tag_mode.any": "Karkoli od naštetega", + "hashtag.column_settings.tag_mode.none": "Nič od naštetega", + "hashtag.column_settings.tag_toggle": "Za ta stolpec vključi dodatne oznake", "home.column_settings.basic": "Osnovno", - "home.column_settings.show_reblogs": "Pokaži sunke", + "home.column_settings.show_reblogs": "Pokaži spodbude", "home.column_settings.show_replies": "Pokaži odgovore", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", - "introduction.federation.action": "Next", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "keyboard_shortcuts.back": "za krmarjenje nazaj", - "keyboard_shortcuts.blocked": "to open blocked users list", - "keyboard_shortcuts.boost": "suniti", - "keyboard_shortcuts.column": "osredotočiti status v enega od stolpcev", - "keyboard_shortcuts.compose": "osredotočiti na sestavljanje besedila", + "intervals.full.days": "{number, plural, one {# dan} two {# dni} few {# dni} other {# dni}}", + "intervals.full.hours": "{number, plural, one {# ura} two {# uri} few {# ure} other {# ur}}", + "intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}", + "introduction.federation.action": "Naprej", + "introduction.federation.federated.headline": "Združeno", + "introduction.federation.federated.text": "Javne objave iz drugih strežnikov fediverse-a bodo prikazane v združeni časovnici.", + "introduction.federation.home.headline": "Domov", + "introduction.federation.home.text": "Objave oseb, ki jim sledite, bodo prikazane v vaši domači časovnici. Lahko sledite vsakomur na katerem koli strežniku!", + "introduction.federation.local.headline": "Lokalno", + "introduction.federation.local.text": "Javne objave ljudi na istem strežniku, se bodo prikazale na lokalni časovnici.", + "introduction.interactions.action": "Zaključi vadnico!", + "introduction.interactions.favourite.headline": "Vzljubi", + "introduction.interactions.favourite.text": "Tut lahko shranite za pozneje in ga vzljubite ter s tem pokažete avtorju, da vam je ta tut priljubljen.", + "introduction.interactions.reblog.headline": "Spodbudi", + "introduction.interactions.reblog.text": "Tute drugih ljudi lahko delite z vašimi sledilci, tako da spodbudite tute.", + "introduction.interactions.reply.headline": "Odgovori", + "introduction.interactions.reply.text": "Lahko odgovarjate na tuje in vaše tute, kar bo odgovore povezalo v pogovor.", + "introduction.welcome.action": "Gremo!", + "introduction.welcome.headline": "Prvi koraki", + "introduction.welcome.text": "Dobrodošli v fediverse-u! Čez nekaj trenutkov boste lahko oddajali sporočila in se pogovarjali s prijatelji prek različnih strežnikov. Vendar je ta strežnik {domain} poseben - gosti vaš profil, zato si zapomnite njegovo ime.", + "keyboard_shortcuts.back": "pojdi nazaj", + "keyboard_shortcuts.blocked": "odpri seznam blokiranih uporabnikov", + "keyboard_shortcuts.boost": "spodbudi", + "keyboard_shortcuts.column": "fokusiraj na status v enemu od stolpcev", + "keyboard_shortcuts.compose": "fokusiraj na območje za sestavljanje besedila", "keyboard_shortcuts.description": "Opis", - "keyboard_shortcuts.direct": "to open direct messages column", - "keyboard_shortcuts.down": "premakniti navzdol po seznamu", - "keyboard_shortcuts.enter": "odpreti status", - "keyboard_shortcuts.favourite": "to favourite", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.direct": "odpri stolpec za neposredna sporočila", + "keyboard_shortcuts.down": "premakni se navzdol po seznamu", + "keyboard_shortcuts.enter": "odpri status", + "keyboard_shortcuts.favourite": "vzljubi", + "keyboard_shortcuts.favourites": "odpri seznam priljubljenih", + "keyboard_shortcuts.federated": "odpri združeno časovnico", "keyboard_shortcuts.heading": "Tipkovne bližnjice", - "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.home": "odpri domačo časovnico", "keyboard_shortcuts.hotkey": "Hitra tipka", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "to focus search", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "da začnete povsem nov tut", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", - "keyboard_shortcuts.up": "to move up in the list", - "lightbox.close": "Close", - "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", - "lists.edit": "Edit list", - "lists.edit.submit": "Change title", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", - "loading_indicator.label": "Loading...", - "media_gallery.toggle_visible": "Toggle visibility", - "missing_indicator.label": "Not found", - "missing_indicator.sublabel": "This resource could not be found", - "mute_modal.hide_notifications": "Hide notifications from this user?", - "navigation_bar.apps": "Mobile apps", - "navigation_bar.blocks": "Blocked users", - "navigation_bar.community_timeline": "Local timeline", - "navigation_bar.compose": "Compose new toot", - "navigation_bar.direct": "Direct messages", - "navigation_bar.discover": "Discover", - "navigation_bar.domain_blocks": "Hidden domains", - "navigation_bar.edit_profile": "Edit profile", - "navigation_bar.favourites": "Favourites", - "navigation_bar.filters": "Muted words", - "navigation_bar.follow_requests": "Follow requests", - "navigation_bar.follows_and_followers": "Follows and followers", - "navigation_bar.info": "O tem vozlišču", - "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", - "navigation_bar.lists": "Lists", - "navigation_bar.logout": "Logout", - "navigation_bar.mutes": "Muted users", - "navigation_bar.personal": "Personal", + "keyboard_shortcuts.legend": "pokaži to legendo", + "keyboard_shortcuts.local": "odpri lokalno časovnico", + "keyboard_shortcuts.mention": "omeni avtorja", + "keyboard_shortcuts.muted": "odpri seznam utišanih uporabnikov", + "keyboard_shortcuts.my_profile": "odpri svoj profil", + "keyboard_shortcuts.notifications": "odpri stolpec z obvestili", + "keyboard_shortcuts.pinned": "odpri seznam pripetih tutov", + "keyboard_shortcuts.profile": "odpri avtorjev profil", + "keyboard_shortcuts.reply": "odgovori", + "keyboard_shortcuts.requests": "odpri seznam s prošnjami za sledenje", + "keyboard_shortcuts.search": "fokusiraj na iskanje", + "keyboard_shortcuts.start": "odpri stolpec \"začni\"", + "keyboard_shortcuts.toggle_hidden": "prikaži/skrij besedilo za CW", + "keyboard_shortcuts.toggle_sensitivity": "prikaži/skrij medije", + "keyboard_shortcuts.toot": "začni povsem nov tut", + "keyboard_shortcuts.unfocus": "odfokusiraj območje za sestavljanje besedila/iskanje", + "keyboard_shortcuts.up": "premakni se navzgor po seznamu", + "lightbox.close": "Zapri", + "lightbox.next": "Naslednji", + "lightbox.previous": "Prejšnji", + "lightbox.view_context": "Poglej kontekst", + "lists.account.add": "Dodaj na seznam", + "lists.account.remove": "Odstrani s seznama", + "lists.delete": "Izbriši seznam", + "lists.edit": "Uredi seznam", + "lists.edit.submit": "Spremeni naslov", + "lists.new.create": "Dodaj seznam", + "lists.new.title_placeholder": "Nov naslov seznama", + "lists.search": "Išči med ljudmi, katerim sledite", + "lists.subheading": "Vaši seznami", + "load_pending": "{count, plural, one {# nov element} other {# novih elementov}}", + "loading_indicator.label": "Nalaganje...", + "media_gallery.toggle_visible": "Preklopi vidljivost", + "missing_indicator.label": "Ni najdeno", + "missing_indicator.sublabel": "Tega vira ni bilo mogoče najti", + "mute_modal.hide_notifications": "Skrij obvestila tega uporabnika?", + "navigation_bar.apps": "Mobilne aplikacije", + "navigation_bar.blocks": "Blokirani uporabniki", + "navigation_bar.community_timeline": "Lokalna časovnica", + "navigation_bar.compose": "Sestavi nov tut", + "navigation_bar.direct": "Neposredna sporočila", + "navigation_bar.discover": "Odkrijte", + "navigation_bar.domain_blocks": "Skrite domene", + "navigation_bar.edit_profile": "Uredi profil", + "navigation_bar.favourites": "Priljubljeni", + "navigation_bar.filters": "Utišane besede", + "navigation_bar.follow_requests": "Prošnje za sledenje", + "navigation_bar.follows_and_followers": "Sledenja in sledilci", + "navigation_bar.info": "O tem strežniku", + "navigation_bar.keyboard_shortcuts": "Hitre tipke", + "navigation_bar.lists": "Seznami", + "navigation_bar.logout": "Odjava", + "navigation_bar.mutes": "Utišani uporabniki", + "navigation_bar.personal": "Osebno", "navigation_bar.pins": "Pripeti tuti", - "navigation_bar.preferences": "Preferences", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.public_timeline": "Federated timeline", - "navigation_bar.security": "Security", - "notification.favourite": "{name} favourited your status", - "notification.follow": "{name} followed you", - "notification.mention": "{name} mentioned you", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} boosted your status", - "notifications.clear": "Clear notifications", - "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", - "notifications.column_settings.alert": "Desktop notifications", - "notifications.column_settings.favourite": "Favourites:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", - "notifications.column_settings.follow": "New followers:", - "notifications.column_settings.mention": "Mentions:", - "notifications.column_settings.poll": "Poll results:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Show in column", - "notifications.column_settings.sound": "Play sound", - "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.group": "{count} notifications", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", - "privacy.change": "Adjust status privacy", - "privacy.direct.long": "Post to mentioned users only", - "privacy.direct.short": "Direct", - "privacy.private.long": "Post to followers only", - "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", - "privacy.public.short": "Public", - "privacy.unlisted.long": "Do not show in public timelines", - "privacy.unlisted.short": "Unlisted", - "regeneration_indicator.label": "Loading…", - "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "navigation_bar.preferences": "Nastavitve", + "navigation_bar.public_timeline": "Združena časovnica", + "navigation_bar.security": "Varnost", + "notification.favourite": "{name} je vzljubil/a vaš status", + "notification.follow": "{name} vam sledi", + "notification.mention": "{name} vas je omenil/a", + "notification.poll": "Glasovanje, v katerem ste sodelovali, se je končalo", + "notification.reblog": "{name} je spodbudil/a vaš status", + "notifications.clear": "Počisti obvestila", + "notifications.clear_confirmation": "Ali ste prepričani, da želite trajno izbrisati vsa vaša obvestila?", + "notifications.column_settings.alert": "Namizna obvestila", + "notifications.column_settings.favourite": "Priljubljeni:", + "notifications.column_settings.filter_bar.advanced": "Prikaži vse kategorije", + "notifications.column_settings.filter_bar.category": "Vrstica za hitro filtriranje", + "notifications.column_settings.filter_bar.show": "Pokaži", + "notifications.column_settings.follow": "Novi sledilci:", + "notifications.column_settings.mention": "Omembe:", + "notifications.column_settings.poll": "Rezultati glasovanja:", + "notifications.column_settings.push": "Potisna obvestila", + "notifications.column_settings.reblog": "Spodbude:", + "notifications.column_settings.show": "Prikaži v stolpcu", + "notifications.column_settings.sound": "Predvajaj zvok", + "notifications.filter.all": "Vse", + "notifications.filter.boosts": "Spodbude", + "notifications.filter.favourites": "Priljubljeni", + "notifications.filter.follows": "Sledi", + "notifications.filter.mentions": "Omembe", + "notifications.filter.polls": "Rezultati glasovanj", + "notifications.group": "{count} obvestil", + "poll.closed": "Zaprto", + "poll.refresh": "Osveži", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural,one {# glas} other {# glasov}}", + "poll.vote": "Glasuj", + "poll.voted": "You voted for this answer", + "poll_button.add_poll": "Dodaj anketo", + "poll_button.remove_poll": "Odstrani anketo", + "privacy.change": "Prilagodi zasebnost statusa", + "privacy.direct.long": "Objavi samo omenjenim uporabnikom", + "privacy.direct.short": "Neposredno", + "privacy.private.long": "Objavi samo sledilcem", + "privacy.private.short": "Samo sledilci", + "privacy.public.long": "Objavi na javne časovnice", + "privacy.public.short": "Javno", + "privacy.unlisted.long": "Ne objavi na javne časovnice", + "privacy.unlisted.short": "Ni prikazano", + "refresh": "Refresh", + "regeneration_indicator.label": "Nalaganje…", + "regeneration_indicator.sublabel": "Vaš domači vir se pripravlja!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "zdaj", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", - "reply_indicator.cancel": "Cancel", - "report.forward": "Forward to {target}", - "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", - "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", - "report.placeholder": "Additional comments", - "report.submit": "Submit", - "report.target": "Report {target}", - "search.placeholder": "Search", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", - "search_results.accounts": "People", - "search_results.hashtags": "Hashtags", + "reply_indicator.cancel": "Prekliči", + "report.forward": "Posreduj do {target}", + "report.forward_hint": "Račun je iz drugega strežnika. Pošljem anonimno kopijo poročila tudi na drugi strežnik?", + "report.hint": "Poročilo bo poslano moderatorjem vašega vozlišča. Spodaj lahko navedete, zakaj prijavljate ta račun:", + "report.placeholder": "Dodatni komentarji", + "report.submit": "Pošlji", + "report.target": "Prijavi {target}", + "search.placeholder": "Iskanje", + "search_popout.search_format": "Napredna oblika iskanja", + "search_popout.tips.full_text": "Enostavno besedilo vrne statuse, ki ste jih napisali, vzljubili, spodbudili ali ste bili v njih omenjeni, kot tudi ujemajoča se uporabniška imena, prikazna imena in ključnike.", + "search_popout.tips.hashtag": "ključnik", + "search_popout.tips.status": "stanje", + "search_popout.tips.text": "Enostavno besedilo vrne ujemajoča se prikazna imena, uporabniška imena in ključnike", + "search_popout.tips.user": "uporabnik", + "search_results.accounts": "Ljudje", + "search_results.hashtags": "Ključniki", "search_results.statuses": "Tuti", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Unboost", - "status.cannot_reblog": "This post cannot be boosted", - "status.copy": "Copy link to status", - "status.delete": "Delete", - "status.detailed_status": "Detailed conversation view", - "status.direct": "Direct message @{name}", - "status.embed": "Embed", - "status.favourite": "Favourite", - "status.filtered": "Filtered", - "status.load_more": "Load more", - "status.media_hidden": "Media hidden", - "status.mention": "Mention @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", - "status.mute_conversation": "Mute conversation", - "status.open": "Expand this status", - "status.pin": "Pin on profile", + "search_results.statuses_fts_disabled": "Iskanje tutov po njihovi vsebini ni omogočeno na tem strežniku Mastodon.", + "search_results.total": "{count, number} {count, plural, one {rezultat} other {rezultatov}}", + "status.admin_account": "Odpri vmesnik za moderiranje za @{name}", + "status.admin_status": "Odpri status v vmesniku za moderiranje", + "status.block": "Blokiraj @{name}", + "status.cancel_reblog_private": "Prekini spodbudo", + "status.cannot_reblog": "Te objave ni mogoče spodbuditi", + "status.copy": "Kopiraj povezavo do statusa", + "status.delete": "Izbriši", + "status.detailed_status": "Podroben pogled pogovora", + "status.direct": "Neposredno sporočilo @{name}", + "status.embed": "Vgradi", + "status.favourite": "Priljubljen", + "status.filtered": "Filtrirano", + "status.load_more": "Naloži več", + "status.media_hidden": "Mediji so skriti", + "status.mention": "Omeni @{name}", + "status.more": "Več", + "status.mute": "Utišaj @{name}", + "status.mute_conversation": "Utišaj pogovor", + "status.open": "Razširi ta status", + "status.pin": "Pripni na profil", "status.pinned": "Pripeti tut", - "status.read_more": "Read more", - "status.reblog": "Suni", - "status.reblog_private": "Suni v prvotno občinstvo", - "status.reblogged_by": "{name} sunjen", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", - "status.redraft": "Delete & re-draft", + "status.read_more": "Preberi več", + "status.reblog": "Spodbudi", + "status.reblog_private": "Spodbudi izvirnemu občinstvu", + "status.reblogged_by": "{name} spodbuja", + "status.reblogs.empty": "Nihče še ni spodbudil tega tuta. Ko se bo to zgodilo, se bodo pojavili tukaj.", + "status.redraft": "Izbriši in preoblikuj", "status.reply": "Odgovori", "status.replyAll": "Odgovori na objavo", "status.report": "Prijavi @{name}", @@ -351,30 +377,39 @@ "status.show_less_all": "Prikaži manj za vse", "status.show_more": "Prikaži več", "status.show_more_all": "Prikaži več za vse", - "status.show_thread": "Show thread", + "status.show_thread": "Prikaži objavo", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Odtišaj pogovor", "status.unpin": "Odpni iz profila", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Zavrni predlog", + "suggestions.header": "Morda bi vas zanimalo…", "tabs_bar.federated_timeline": "Združeno", "tabs_bar.home": "Domov", "tabs_bar.local_timeline": "Lokalno", "tabs_bar.notifications": "Obvestila", - "tabs_bar.search": "Poišči", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "tabs_bar.search": "Iskanje", + "time_remaining.days": "{number, plural, one {# dan} other {# dni}} je ostalo", + "time_remaining.hours": "{number, plural, one {# ura} other {# ur}} je ostalo", + "time_remaining.minutes": "{number, plural, one {# minuta} other {# minut}} je ostalo", + "time_remaining.moments": "Preostali trenutki", + "time_remaining.seconds": "{number, plural, one {# sekunda} other {# sekund}} je ostalo", + "trends.count_by_accounts": "{count} {rawCount, plural, one {oseba} other {ljudi}} govori", + "trends.trending_now": "Trending now", "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.", - "upload_area.title": "Povlecite in spustite za pošiljanje", - "upload_button.label": "Dodaj medij", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_area.title": "Za pošiljanje povlecite in spustite", + "upload_button.label": "Dodaj medije ({formats})", + "upload_error.limit": "Omejitev prenosa datoteke je presežena.", + "upload_error.poll": "Prenos datoteke z anketami ni dovoljen.", "upload_form.description": "Opišite za slabovidne", - "upload_form.focus": "Obreži", + "upload_form.edit": "Edit", "upload_form.undo": "Izbriši", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Pošiljanje...", "video.close": "Zapri video", "video.exit_fullscreen": "Izhod iz celozaslonskega načina", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 13ce4e978..f58466d87 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -4,6 +4,7 @@ "account.block": "Blloko @{name}", "account.block_domain": "Fshih gjithçka prej {domain}", "account.blocked": "E bllokuar", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Mesazh i drejtpërdrejt për @{name}", "account.domain_blocked": "Përkatësi e fshehur", "account.edit_profile": "Përpunoni profilin", @@ -15,6 +16,7 @@ "account.follows.empty": "Ky përdorues ende s’ndjek njeri.", "account.follows_you": "Ju ndjek", "account.hide_reblogs": "Fshih përforcime nga @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Pronësia e kësaj lidhjeje qe kontrolluar më {date}", "account.locked_info": "Gjendja e privatësisë së kësaj llogarie është caktuar si e kyçur. I zoti merr dorazi në shqyrtim cilët mund ta ndjekin.", "account.media": "Media", @@ -23,6 +25,7 @@ "account.mute": "Heshtoni @{name}", "account.mute_notifications": "Heshtoji njoftimet prej @{name}", "account.muted": "Heshtuar", + "account.never_active": "Never", "account.posts": "Mesazhe", "account.posts_with_replies": "Mesazhe dhe përgjigje", "account.report": "Raportojeni @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Resht së ndjekuri", "account.unmute": "Ktheji zërin @{name}", "account.unmute_notifications": "Hiqua ndalimin e shfaqjes njoftimeve nga @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "Ndodhi një gabim të papritur.", "alert.unexpected.title": "Hëm!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Mund të shtypni {combo}, që të anashkalohet kjo herës tjetër", "bundle_column_error.body": "Diç shkoi ters teksa ngarkohej ky përbërës.", "bundle_column_error.retry": "Riprovoni", @@ -47,6 +53,7 @@ "column.blocks": "Përdorues të bllokuar", "column.community": "Rrjedhë kohore vendore", "column.direct": "Mesazhe të drejtpërdrejta", + "column.directory": "Browse profiles", "column.domain_blocks": "Përkatësi të fshehura", "column.favourites": "Të parapëlqyer", "column.follow_requests": "Kërkesa për ndjekje", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Jeni i sigurt që doni të fshihet përgjithmonë kjo listë?", "confirmations.domain_block.confirm": "Fshih krejt përkatësinë", "confirmations.domain_block.message": "Jeni i sigurt, shumë i sigurt se doni të bllokohet krejt {domain}? Në shumicën e rasteve, ndoca bllokime ose heshtime me synim të caktuar janë të mjaftueshme dhe të parapëlqyera. S’keni për të parë lëndë nga kjo përkatësi në ndonjë rrjedhë kohore publike, apo te njoftimet tuaja. Ndjekësit tuaj prej asaj përkatësie do të hiqen.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Heshtoje", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Jeni i sigurt se doni të heshtohet {name}?", "confirmations.redraft.confirm": "Fshijeni & rihartojeni", "confirmations.redraft.message": "Jeni i sigurt se doni të fshihet kjo gjendje dhe të rihartohet? Parapëlqimet dhe boosts do të humbin, ndërsa përgjigjet te postimi origjinal do të bëhen jetime.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Përgjigja tani do të shkaktojë mbishkrimin e mesazhit që po hartoni. Jeni i sigurt se doni të vazhdohet më tej?", "confirmations.unfollow.confirm": "Resht së ndjekuri", "confirmations.unfollow.message": "Jeni i sigurt se doni të mos ndiqet më {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Trupëzojeni këtë gjendje në sajtin tuaj duke kopjuar kodin më poshtë.", "embed.preview": "Ja si do të duket:", "emoji_button.activity": "Veprimtari", @@ -134,6 +152,10 @@ "empty_column.mutes": "S’keni heshtuar ende ndonjë përdorues.", "empty_column.notifications": "Ende s’keni ndonjë njoftim. Ndërveproni me të tjerët që të nisë biseda.", "empty_column.public": "S’ka gjë këtu! Shkruani diçka publikisht, ose ndiqni dorazi përdorues prej instancash të tjera, që ta mbushni këtë zonë", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Autorizoje", "follow_request.reject": "Hidhe tej", "getting_started.developers": "Zhvillues", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Titull liste të re", "lists.search": "Kërkoni mes personash që ndiqni", "lists.subheading": "Listat tuaja", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Po ngarkohet…", "media_gallery.toggle_visible": "Ndërroni dukshmërinë", "missing_indicator.label": "S’u gjet", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personale", "navigation_bar.pins": "Mesazhe të fiksuar", "navigation_bar.preferences": "Parapëlqime", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Rrjedhë kohore të federuarish", "navigation_bar.security": "Siguri", "notification.favourite": "{name} parapëlqeu gjendjen tuaj", @@ -277,8 +299,10 @@ "notifications.group": "{count}s njoftime", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Rregulloni privatësi gjendje", @@ -290,6 +314,7 @@ "privacy.public.short": "Publike", "privacy.unlisted.long": "Mos e postoni në rrjedha publike kohore", "privacy.unlisted.short": "Jo në lista", + "refresh": "Refresh", "regeneration_indicator.label": "Po ngarkohet…", "regeneration_indicator.sublabel": "Prurja juaj vetjake po përgatiteet!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "Persona", "search_results.hashtags": "Hashtagë", "search_results.statuses": "Mesazhe", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, një {result} {results} të tjera}", "status.admin_account": "Hap ndërfaqe moderimi për @{name}", "status.admin_status": "Hape këtë gjendje te ndërfaqja e moderimit", @@ -352,6 +378,7 @@ "status.show_more": "Shfaq më tepër", "status.show_more_all": "Shfaq më tepër për të tërë", "status.show_thread": "Shfaq rrjedhën", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Ktheji zërin bisedës", "status.unpin": "Shfiksoje nga profili", "suggestions.dismiss": "Mos e merr parasysh sugjerimin", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, një {person} {people} të tjerë} po flasin", + "trends.trending_now": "Trending now", "ui.beforeunload": "Skica juaj do të humbë nëse dilni nga Mastodon-i.", "upload_area.title": "Merreni & vëreni që të ngarkohet", "upload_button.label": "Shtoni media (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "U tejkalua kufi ngarkimi kartelash.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Përshkruajeni për persona me probleme shikimi", - "upload_form.focus": "Ndryshoni parapamjen", + "upload_form.edit": "Edit", "upload_form.undo": "Fshije", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Po ngarkohet…", "video.close": "Mbylle videon", "video.exit_fullscreen": "Dil nga mënyra Sa Krejt Ekrani", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 8f8ca7c30..59b8459ae 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -4,6 +4,7 @@ "account.block": "Blokiraj korisnika @{name}", "account.block_domain": "Sakrij sve sa domena {domain}", "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domain hidden", "account.edit_profile": "Izmeni profil", @@ -15,6 +16,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "Prati Vas", "account.hide_reblogs": "Sakrij podrške koje daje korisnika @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Mediji", @@ -23,6 +25,7 @@ "account.mute": "Ućutkaj korisnika @{name}", "account.mute_notifications": "Isključi obaveštenja od korisnika @{name}", "account.muted": "Muted", + "account.never_active": "Never", "account.posts": "Statusa", "account.posts_with_replies": "Toots with replies", "account.report": "Prijavi @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Otprati", "account.unmute": "Ukloni ućutkavanje korisniku @{name}", "account.unmute_notifications": "Uključi nazad obaveštenja od korisnika @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Možete pritisnuti {combo} da preskočite ovo sledeći put", "bundle_column_error.body": "Nešto je pošlo po zlu prilikom učitavanja ove komponente.", "bundle_column_error.retry": "Pokušajte ponovo", @@ -47,6 +53,7 @@ "column.blocks": "Blokirani korisnici", "column.community": "Lokalna lajna", "column.direct": "Direct messages", + "column.directory": "Browse profiles", "column.domain_blocks": "Hidden domains", "column.favourites": "Omiljeni", "column.follow_requests": "Zahtevi za praćenje", @@ -64,7 +71,7 @@ "column_header.show_settings": "Prikaži postavke", "column_header.unpin": "Otkači", "column_subheading.settings": "Postavke", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Da li ste sigurni da želite da bespovratno obrišete ovu listu?", "confirmations.domain_block.confirm": "Sakrij ceo domen", "confirmations.domain_block.message": "Da li ste stvarno, stvarno sigurno da želite da blokirate ceo domen {domain}? U većini slučajeva, par dobrih blokiranja ili ućutkavanja su dovoljna i preporučljiva.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Ućutkaj", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Da li stvarno želite da ućutkate korisnika {name}?", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Otprati", "confirmations.unfollow.message": "Da li ste sigurni da želite da otpratite korisnika {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Ugradi ovaj status na Vaš veb sajt kopiranjem koda ispod.", "embed.preview": "Ovako će da izgleda:", "emoji_button.activity": "Aktivnost", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "Trenutno nemate obaveštenja. Družite se malo da započnete razgovore.", "empty_column.public": "Ovde nema ničega! Napišite nešto javno, ili nađite korisnike sa drugih instanci koje ćete zapratiti da popunite ovu prazninu", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Odobri", "follow_request.reject": "Odbij", "getting_started.developers": "Developers", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Naslov nove liste", "lists.search": "Pretraži među ljudima koje pratite", "lists.subheading": "Vaše liste", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Uključi/isključi vidljivost", "missing_indicator.label": "Nije pronađeno", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Prikačeni tutovi", "navigation_bar.preferences": "Podešavanja", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federisana lajna", "navigation_bar.security": "Security", "notification.favourite": "{name} je stavio Vaš status kao omiljeni", @@ -277,8 +299,10 @@ "notifications.group": "{count} notifications", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Podesi status privatnosti", @@ -290,6 +314,7 @@ "privacy.public.short": "Javno", "privacy.unlisted.long": "Ne objavljuj na javnim lajnama", "privacy.unlisted.short": "Neizlistano", + "refresh": "Refresh", "regeneration_indicator.label": "Loading…", "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Prikaži više", "status.show_more_all": "Show more for all", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Uključi prepisku", "status.unpin": "Otkači sa profila", "suggestions.dismiss": "Dismiss suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", "ui.beforeunload": "Ako napustite Mastodont, izgubićete napisani nacrt.", "upload_area.title": "Prevucite ovde da otpremite", "upload_button.label": "Dodaj multimediju", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Opiši za slabovide osobe", - "upload_form.focus": "Crop", + "upload_form.edit": "Edit", "upload_form.undo": "Opozovi", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Otpremam...", "video.close": "Zatvori video", "video.exit_fullscreen": "Napusti ceo ekran", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index 8ef18a774..d222e774d 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -4,6 +4,7 @@ "account.block": "Блокирај @{name}", "account.block_domain": "Сакриј све са домена {domain}", "account.blocked": "Блокиран", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "Директна порука @{name}", "account.domain_blocked": "Домен сакривен", "account.edit_profile": "Измени профил", @@ -15,6 +16,7 @@ "account.follows.empty": "Корисник тренутно не прати никога.", "account.follows_you": "Прати Вас", "account.hide_reblogs": "Сакриј подршке које даје корисника @{name}", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "Медији", @@ -23,6 +25,7 @@ "account.mute": "Ућуткај корисника @{name}", "account.mute_notifications": "Искључи обавештења од корисника @{name}", "account.muted": "Ућуткан", + "account.never_active": "Never", "account.posts": "Трубе", "account.posts_with_replies": "Трубе и одговори", "account.report": "Пријави @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "Отпрати", "account.unmute": "Уклони ућуткавање кориснику @{name}", "account.unmute_notifications": "Укључи назад обавештења од корисника @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "Појавила се неочекивана грешка.", "alert.unexpected.title": "Упс!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "Можете притиснути {combo} да прескочите ово следећи пут", "bundle_column_error.body": "Нешто је пошло по злу приликом учитавања ове компоненте.", "bundle_column_error.retry": "Покушајте поново", @@ -47,6 +53,7 @@ "column.blocks": "Блокирани корисници", "column.community": "Локална временска линија", "column.direct": "Директне поруке", + "column.directory": "Browse profiles", "column.domain_blocks": "Скривени домени", "column.favourites": "Омиљене", "column.follow_requests": "Захтеви за праћење", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Да ли сте сигурни да желите да бесповратно обришете ову листу?", "confirmations.domain_block.confirm": "Сакриј цео домен", "confirmations.domain_block.message": "Да ли сте заиста сигурни да желите да блокирате цео домен {domain}? У већини случајева, неколико добро промишљених блокирања или ућуткавања су довољна и препоручљива.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "Ућуткај", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "Да ли стварно желите да ућуткате корисника {name}?", "confirmations.redraft.confirm": "Избриши и преправи", "confirmations.redraft.message": "Да ли сте сигурни да желите да избришете овај статус и да га преправите? Сва стављања у омиљене трубе, као и подршке ће бити изгубљене, а одговори на оригинални пост ће бити поништени.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "Отпрати", "confirmations.unfollow.message": "Да ли сте сигурни да желите да отпратите корисника {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "Угради овај статус на Ваш веб сајт копирањем кода испод.", "embed.preview": "Овако ће да изгледа:", "emoji_button.activity": "Активност", @@ -134,6 +152,10 @@ "empty_column.mutes": "Још увек немате ућутканих корисника.", "empty_column.notifications": "Тренутно немате обавештења. Дружите се мало да започнете разговор.", "empty_column.public": "Овде нема ничега! Напишите нешто јавно, или нађите кориснике са других инстанци које ћете запратити да попуните ову празнину", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Одобри", "follow_request.reject": "Одбиј", "getting_started.developers": "Програмери", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Наслов нове листе", "lists.search": "Претражи међу људима које пратите", "lists.subheading": "Ваше листе", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Учитавам...", "media_gallery.toggle_visible": "Укључи/искључи видљивост", "missing_indicator.label": "Није пронађено", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Прикачене трубе", "navigation_bar.preferences": "Подешавања", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Здружена временска линија", "navigation_bar.security": "Безбедност", "notification.favourite": "{name} је ставио/ла Ваш статус као омиљени", @@ -277,8 +299,10 @@ "notifications.group": "{count} обавештења", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "Подеси статус приватности", @@ -290,6 +314,7 @@ "privacy.public.short": "Јавно", "privacy.unlisted.long": "Не објављуј на јавним временским линијама", "privacy.unlisted.short": "Неизлистано", + "refresh": "Refresh", "regeneration_indicator.label": "Учитавање…", "regeneration_indicator.sublabel": "Ваша почетна страница се припрема!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "Људи", "search_results.hashtags": "Тарабе", "search_results.statuses": "Трубе", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {резултат} few {резултата} other {резултата}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "Прикажи више", "status.show_more_all": "Прикажи више за све", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "Укључи преписку", "status.unpin": "Откачи са профила", "suggestions.dismiss": "Dismiss suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {човек} other {људи}} прича", + "trends.trending_now": "Trending now", "ui.beforeunload": "Ако напустите Мастодонт, изгубићете написани нацрт.", "upload_area.title": "Превуците овде да отпремите", "upload_button.label": "Додај мултимедију (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Опишите за особе са оштећеним видом", - "upload_form.focus": "Подесите", + "upload_form.edit": "Edit", "upload_form.undo": "Обриши", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "Отпремам...", "video.close": "Затвори видео", "video.exit_fullscreen": "Напусти цео екран", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index ab12be885..9b12f337b 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -1,217 +1,239 @@ { - "account.add_or_remove_from_list": "Lägg till eller ta bort från listor", + "account.add_or_remove_from_list": "Lägg till i eller ta bort från listor", "account.badges.bot": "Robot", "account.block": "Blockera @{name}", "account.block_domain": "Dölj allt från {domain}", "account.blocked": "Blockerad", - "account.direct": "Direktmeddelande @{name}", + "account.cancel_follow_request": "Avbryt följarförfrågan", + "account.direct": "Skicka ett direktmeddelande till @{name}", "account.domain_blocked": "Domän dold", "account.edit_profile": "Redigera profil", - "account.endorse": "Feature on profile", + "account.endorse": "Visa på profil", "account.follow": "Följ", "account.followers": "Följare", - "account.followers.empty": "Ingen följer denna användaren än.", + "account.followers.empty": "Ingen följer denna användare än.", "account.follows": "Följer", - "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows.empty": "Denna användare följer inte någon än.", "account.follows_you": "Följer dig", "account.hide_reblogs": "Dölj knuffar från @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.last_status": "Senast aktiv", + "account.link_verified_on": "Ägarskap för detta konto kontrollerades den {date}", + "account.locked_info": "Detta konto har låst integritetsstatus. Ägaren väljer manuellt vem som kan följa.", "account.media": "Media", "account.mention": "Nämna @{name}", "account.moved_to": "{name} har flyttat till:", "account.mute": "Tysta @{name}", "account.mute_notifications": "Stäng av notifieringar från @{name}", - "account.muted": "Nertystad", - "account.posts": "Inlägg", - "account.posts_with_replies": "Toots och svar", + "account.muted": "Tystad", + "account.never_active": "Aldrig", + "account.posts": "Tutningar", + "account.posts_with_replies": "Tutningar och svar", "account.report": "Rapportera @{name}", - "account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan", - "account.share": "Dela @{name}'s profil", + "account.requested": "Inväntar godkännande. Klicka för att avbryta följarförfrågan", + "account.share": "Dela @{name}s profil", "account.show_reblogs": "Visa knuffar från @{name}", "account.unblock": "Avblockera @{name}", - "account.unblock_domain": "Ta fram {domain}", - "account.unendorse": "Don't feature on profile", - "account.unfollow": "Sluta följa", - "account.unmute": "Ta bort tystad @{name}", - "account.unmute_notifications": "Återaktivera notifikationer från @{name}", + "account.unblock_domain": "Sluta dölja {domain}", + "account.unendorse": "Visa inte på profil", + "account.unfollow": "Sluta följ", + "account.unmute": "Sluta tysta @{name}", + "account.unmute_notifications": "Återaktivera notifieringar från @{name}", + "alert.rate_limited.message": "Vänligen försök igen efter {retry_time, time, medium}.", + "alert.rate_limited.title": "Mängd begränsad", "alert.unexpected.message": "Ett oväntat fel uppstod.", - "alert.unexpected.title": "Whups!", - "boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång", - "bundle_column_error.body": "Något gick fel när du laddade denna komponent.", + "alert.unexpected.title": "Hoppsan!", + "autosuggest_hashtag.per_week": "{count} per vecka", + "boost_modal.combo": "Du kan trycka {combo} för att slippa detta nästa gång", + "bundle_column_error.body": "Något gick fel medan denna komponent laddades.", "bundle_column_error.retry": "Försök igen", "bundle_column_error.title": "Nätverksfel", "bundle_modal_error.close": "Stäng", - "bundle_modal_error.message": "Något gick fel när du laddade denna komponent.", + "bundle_modal_error.message": "Något gick fel när denna komponent laddades.", "bundle_modal_error.retry": "Försök igen", "column.blocks": "Blockerade användare", "column.community": "Lokal tidslinje", - "column.direct": "Direktmeddelande", + "column.direct": "Direktmeddelanden", + "column.directory": "Bläddra bland profiler", "column.domain_blocks": "Dolda domäner", "column.favourites": "Favoriter", - "column.follow_requests": "Följ förfrågningar", + "column.follow_requests": "Följarförfrågningar", "column.home": "Hem", "column.lists": "Listor", "column.mutes": "Tystade användare", - "column.notifications": "Meddelanden", + "column.notifications": "Aviseringar", "column.pins": "Nålade toots", "column.public": "Förenad tidslinje", "column_back_button.label": "Tillbaka", "column_header.hide_settings": "Dölj inställningar", - "column_header.moveLeft_settings": "Flytta kolumnen till vänster", - "column_header.moveRight_settings": "Flytta kolumnen till höger", + "column_header.moveLeft_settings": "Flytta kolumnen åt vänster", + "column_header.moveRight_settings": "Flytta kolumnen åt höger", "column_header.pin": "Fäst", "column_header.show_settings": "Visa inställningar", "column_header.unpin": "Ångra fäst", "column_subheading.settings": "Inställningar", - "community.column_settings.media_only": "Enbart media", - "compose_form.direct_message_warning": "Denna toot kommer endast att skickas nämnda nämnda användare.", - "compose_form.direct_message_warning_learn_more": "Visa mer", - "compose_form.hashtag_warning": "Denna toot kommer inte att listas under någon hashtag eftersom den är onoterad. Endast offentliga toots kan sökas med hashtag.", - "compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vemsomhelst kan följa dig och även se dina inlägg skrivna för endast dina följare.", + "community.column_settings.media_only": "Endast media", + "compose_form.direct_message_warning": "Denna tutning kommer endast skickas till de nämnda användarna.", + "compose_form.direct_message_warning_learn_more": "Lär dig mer", + "compose_form.hashtag_warning": "Denna toot kommer inte att visas under någon hashtag eftersom den är onoterad. Endast offentliga toots kan sökas med hashtag.", + "compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vem som helst kan följa dig för att se dina inlägg som endast är för följare.", "compose_form.lock_disclaimer.lock": "låst", "compose_form.placeholder": "Vad funderar du på?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", - "compose_form.publish": "Toot", + "compose_form.poll.add_option": "Lägg till ett val", + "compose_form.poll.duration": "Varaktighet för omröstning", + "compose_form.poll.option_placeholder": "Val {number}", + "compose_form.poll.remove_option": "Ta bort detta val", + "compose_form.publish": "Tut", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Markera media som känsligt", "compose_form.sensitive.marked": "Media har markerats som känsligt", - "compose_form.sensitive.unmarked": "Media har inte markerats som känsligt", - "compose_form.spoiler.marked": "Texten har dolts bakom en varning", + "compose_form.sensitive.unmarked": "Media är inte markerat som känsligt", + "compose_form.spoiler.marked": "Texten är dold bakom en varning", "compose_form.spoiler.unmarked": "Texten är inte dold", "compose_form.spoiler_placeholder": "Skriv din varning här", "confirmation_modal.cancel": "Ångra", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Blockera & rapportera", "confirmations.block.confirm": "Blockera", - "confirmations.block.message": "Är du säker att du vill blockera {name}?", + "confirmations.block.message": "Är du säker på att du vill blockera {name}?", "confirmations.delete.confirm": "Ta bort", - "confirmations.delete.message": "Är du säker att du vill ta bort denna status?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete.message": "Är du säker på att du vill ta bort denna status?", + "confirmations.delete_list.confirm": "Ta bort", "confirmations.delete_list.message": "Är du säker på att du vill radera denna lista permanent?", - "confirmations.domain_block.confirm": "Blockera hela domänen", - "confirmations.domain_block.message": "Är du verkligen säker på att du vill blockera hela {domain}? I de flesta fall är några riktade blockeringar eller nedtystade konton tillräckligt och att föredra. Du kommer sluta se innehåll från {domain}-domänen i den allmänna tidslinjen och i dina egna notifieringar. Du kommer även sluta följa alla eventuella följare du har från {domain}.", + "confirmations.domain_block.confirm": "Dölj hela domänen", + "confirmations.domain_block.message": "Är du verkligen, verkligen säker på att du vill blockera hela {domain}? I de flesta fall är några riktade blockeringar eller nedtystade konton tillräckligt och att föredra. Du kommer inte se innehåll från den domänen i den allmänna tidslinjen eller i dina notifieringar. Dina följare från den domänen komer att tas bort.", + "confirmations.logout.confirm": "Logga ut", + "confirmations.logout.message": "Är du säker på att du vill logga ut?", "confirmations.mute.confirm": "Tysta", - "confirmations.mute.message": "Är du säker du vill tysta ner {name}?", - "confirmations.redraft.confirm": "Radera och gör om", - "confirmations.redraft.message": "Är du säker på att du vill radera meddelandet och göra om det? Du kommer förlora alla svar, knuffar och favoriter som hänvisar till meddelandet.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "confirmations.unfollow.confirm": "Sluta följa", - "confirmations.unfollow.message": "Är du säker på att du vill sluta följa {name}?", - "embed.instructions": "Bädda in den här statusen på din webbplats genom att kopiera koden nedan.", - "embed.preview": "Här ser du hur det kommer att se ut:", + "confirmations.mute.explanation": "Detta kommer att dölja poster från dem och poster som nämner dem, men fortfarande tillåta dem att se dina poster och följa dig.", + "confirmations.mute.message": "Är du säker på att du vill tysta {name}?", + "confirmations.redraft.confirm": "Radera & gör om", + "confirmations.redraft.message": "Är du säker på att du vill radera detta meddelande och göra om det? Du kommer förlora alla favoriter, knuffar och svar till det ursprungliga meddelandet.", + "confirmations.reply.confirm": "Svara", + "confirmations.reply.message": "Om du svarar nu kommer det att ersätta meddelandet du håller på att skapa. Är du säker på att du vill fortsätta?", + "confirmations.unfollow.confirm": "Avfölj", + "confirmations.unfollow.message": "Är du säker på att du vill avfölja {name}?", + "conversation.delete": "Radera konversation", + "conversation.mark_as_read": "Markera som läst", + "conversation.open": "Se konversation", + "conversation.with": "Med {names}", + "directory.federated": "Från känt servernätverk", + "directory.local": "Endast från {domain}", + "directory.new_arrivals": "Nyanlända", + "directory.recently_active": "Nyligen aktiva", + "embed.instructions": "Lägg in denna status på din webbplats genom att kopiera koden nedan.", + "embed.preview": "Så här kommer det att se ut:", "emoji_button.activity": "Aktivitet", - "emoji_button.custom": "Specialgjord", + "emoji_button.custom": "Anpassad", "emoji_button.flags": "Flaggor", - "emoji_button.food": "Mat & Dryck", + "emoji_button.food": "Mat & dryck", "emoji_button.label": "Lägg till emoji", "emoji_button.nature": "Natur", "emoji_button.not_found": "Inga emojos!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objekt", - "emoji_button.people": "Människor", + "emoji_button.people": "Personer", "emoji_button.recent": "Ofta använda", "emoji_button.search": "Sök...", "emoji_button.search_results": "Sökresultat", "emoji_button.symbols": "Symboler", - "emoji_button.travel": "Resor & Platser", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.blocks": "You haven't blocked any users yet.", - "empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att få bollen att rulla!", - "empty_column.direct": "Du har inga direktmeddelanden än. När du skickar eller tar emot kommer den att dyka upp här.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "emoji_button.travel": "Resor & platser", + "empty_column.account_timeline": "Inga inlägg här!", + "empty_column.account_unavailable": "Profilen ej tillgänglig", + "empty_column.blocks": "Du har ännu ej blockerat några användare.", + "empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att sätta bollen i rullning!", + "empty_column.direct": "Du har inga direktmeddelanden än. När du skickar eller tar emot ett kommer det att visas här.", + "empty_column.domain_blocks": "Det finns ännu inga dolda domäner.", + "empty_column.favourited_statuses": "Du har inga favoritmarkerade toots än. När du favoritmarkerar en kommer den visas här.", + "empty_column.favourites": "Ingen har favoritmarkerat den här tooten än. När någon gör det kommer den visas här.", + "empty_column.follow_requests": "Du har inga följarförfrågningar än. När du får en kommer den visas här.", "empty_column.hashtag": "Det finns inget i denna hashtag ännu.", "empty_column.home": "Din hemma-tidslinje är tom! Besök {public} eller använd sökning för att komma igång och träffa andra användare.", "empty_column.home.public_timeline": "den publika tidslinjen", "empty_column.list": "Det finns inget i denna lista än. När medlemmar i denna lista lägger till nya statusar kommer de att visas här.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", + "empty_column.lists": "Du har inga listor än. När skapar en kommer den dyka upp här.", + "empty_column.mutes": "Du har ännu inte tystat några användare.", "empty_column.notifications": "Du har inga meddelanden än. Interagera med andra för att starta konversationen.", "empty_column.public": "Det finns inget här! Skriv något offentligt, eller följ manuellt användarna från andra instanser för att fylla på det", + "error.unexpected_crash.explanation": "På grund av en bugg i vår kod eller kompatiblitetsproblem i webbläsaren kan den här sidan inte visas korrekt.", + "error.unexpected_crash.next_steps": "Prova att ladda om sidan. Om det inte hjälper kan du försöka använda Mastodon med en annan webbläsare eller app.", + "errors.unexpected_crash.copy_stacktrace": "Kopiera stacktrace till urklipp", + "errors.unexpected_crash.report_issue": "Rapportera problem", "follow_request.authorize": "Godkänn", "follow_request.reject": "Avvisa", "getting_started.developers": "Utvecklare", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", + "getting_started.directory": "Profilkatalog", + "getting_started.documentation": "Dokumentation", "getting_started.heading": "Kom igång", "getting_started.invite": "Skicka inbjudningar", "getting_started.open_source_notice": "Mastodon är programvara med öppen källkod. Du kan bidra eller rapportera problem via GitHub på {github}.", "getting_started.security": "Säkerhet", "getting_started.terms": "Användarvillkor", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_header.tag_mode.all": "och {additional}", + "hashtag.column_header.tag_mode.any": "eller {additional}", + "hashtag.column_header.tag_mode.none": "utan {additional}", + "hashtag.column_settings.select.no_options_message": "Inga förslag hittades", + "hashtag.column_settings.select.placeholder": "Ange hashtags …", + "hashtag.column_settings.tag_mode.all": "Alla dessa", + "hashtag.column_settings.tag_mode.any": "Någon av dessa", "hashtag.column_settings.tag_mode.none": "Ingen av dessa", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", "home.column_settings.basic": "Grundläggande", "home.column_settings.show_reblogs": "Visa knuffar", "home.column_settings.show_replies": "Visa svar", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "{number, plural, one {# dag} other {# dagar}}", + "intervals.full.hours": "{hours, plural, one {# timme} other {# timmar}}", + "intervals.full.minutes": "{minutes, plural, one {1 minut} other {# minuter}}", "introduction.federation.action": "Nästa", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "keyboard_shortcuts.back": "att navigera tillbaka", - "keyboard_shortcuts.blocked": "to open blocked users list", - "keyboard_shortcuts.boost": "att knuffa", - "keyboard_shortcuts.column": "att fokusera en status i en av kolumnerna", - "keyboard_shortcuts.compose": "att fokusera komponera text fältet", - "keyboard_shortcuts.description": "Description", - "keyboard_shortcuts.direct": "to open direct messages column", - "keyboard_shortcuts.down": "att flytta ner i listan", - "keyboard_shortcuts.enter": "to open status", - "keyboard_shortcuts.favourite": "att favorisera", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", - "keyboard_shortcuts.heading": "Keyboard Shortcuts", - "keyboard_shortcuts.home": "to open home timeline", - "keyboard_shortcuts.hotkey": "Snabbvalstangent", - "keyboard_shortcuts.legend": "att visa denna översikt", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "att nämna författaren", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "att svara", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "att fokusera sökfältet", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "att visa/gömma text bakom CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "att börja en helt ny toot", - "keyboard_shortcuts.unfocus": "att avfokusera komponera text fält / sökfält", - "keyboard_shortcuts.up": "att flytta upp i listan", + "introduction.federation.federated.headline": "Federerad", + "introduction.federation.federated.text": "Publika inlägg från andra servrar i servernätverket visas i den förenade tidslinjen.", + "introduction.federation.home.headline": "Hem", + "introduction.federation.home.text": "Inlägg från personer du följer kommer att visas i din hemtidlinje. Du kan följa vem du vill på vald server!", + "introduction.federation.local.headline": "Lokal", + "introduction.federation.local.text": "Publika inlägg från personer på samma server som du kommer att visas på den lokala tidslinjen.", + "introduction.interactions.action": "Slutför introduktionsguide!", + "introduction.interactions.favourite.headline": "Favorit", + "introduction.interactions.favourite.text": "Du kan spara en tut till senare, och visa författaren att du gillade den genom att göra den till favorit.", + "introduction.interactions.reblog.headline": "Knuffa", + "introduction.interactions.reblog.text": "Du kan dela andra personers tutar med dina följare genom att knuffa dem.", + "introduction.interactions.reply.headline": "Svara", + "introduction.interactions.reply.text": "Du kan besvara andra personers och dina egna tutar, vilket kommer att koppla ihop dem i en konversation.", + "introduction.welcome.action": "Sätt igång!", + "introduction.welcome.headline": "Första stegen", + "introduction.welcome.text": "Välkommen till fediverse! Om några ögonblick kommer du kunna sända ut meddelanden och prata med dina vänner över en mängd servrar. Men den här servern, {domain}, är speciell — den är hem åt din profil, så kom ihåg vad den heter.", + "keyboard_shortcuts.back": "för att gå bakåt", + "keyboard_shortcuts.blocked": "för att öppna listan över blockerade användare", + "keyboard_shortcuts.boost": "för att knuffa", + "keyboard_shortcuts.column": "för att fokusera en status i en av kolumnerna", + "keyboard_shortcuts.compose": "för att fokusera skrivfältet", + "keyboard_shortcuts.description": "Beskrivning", + "keyboard_shortcuts.direct": "för att öppna Direktmeddelanden", + "keyboard_shortcuts.down": "för att flytta nedåt i listan", + "keyboard_shortcuts.enter": "för att öppna en status", + "keyboard_shortcuts.favourite": "för att sätta som favorit", + "keyboard_shortcuts.favourites": "för att öppna Favoriter", + "keyboard_shortcuts.federated": "för att öppna Förenad tidslinje", + "keyboard_shortcuts.heading": "Tangentbordsgenvägar", + "keyboard_shortcuts.home": "för att öppna Hem-tidslinjen", + "keyboard_shortcuts.hotkey": "Kommando", + "keyboard_shortcuts.legend": "för att visa denna översikt", + "keyboard_shortcuts.local": "för att öppna Lokal tidslinje", + "keyboard_shortcuts.mention": "för att nämna skaparen", + "keyboard_shortcuts.muted": "för att öppna listan över tystade användare", + "keyboard_shortcuts.my_profile": "för att öppna din profil", + "keyboard_shortcuts.notifications": "för att öppna Meddelanden", + "keyboard_shortcuts.pinned": "för att öppna Nålade toots", + "keyboard_shortcuts.profile": "för att öppna skaparens profil", + "keyboard_shortcuts.reply": "för att svara", + "keyboard_shortcuts.requests": "för att öppna Följförfrågningar", + "keyboard_shortcuts.search": "för att fokusera sökfältet", + "keyboard_shortcuts.start": "för att öppna \"Kom igång\"-kolumnen", + "keyboard_shortcuts.toggle_hidden": "för att visa/gömma text bakom CW", + "keyboard_shortcuts.toggle_sensitivity": "för att visa/gömma media", + "keyboard_shortcuts.toot": "för att påbörja en helt ny toot", + "keyboard_shortcuts.unfocus": "för att avfokusera skrivfält/sökfält", + "keyboard_shortcuts.up": "för att flytta uppåt i listan", "lightbox.close": "Stäng", "lightbox.next": "Nästa", "lightbox.previous": "Tidigare", - "lightbox.view_context": "View context", + "lightbox.view_context": "Visa kontext", "lists.account.add": "Lägg till i lista", "lists.account.remove": "Ta bort från lista", "lists.delete": "Radera lista", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Ny listrubrik", "lists.search": "Sök bland personer du följer", "lists.subheading": "Dina listor", + "load_pending": "{count, plural, other {# objekt}}", "loading_indicator.label": "Laddar...", "media_gallery.toggle_visible": "Växla synlighet", "missing_indicator.label": "Hittades inte", @@ -229,58 +252,59 @@ "navigation_bar.apps": "Mobilappar", "navigation_bar.blocks": "Blockerade användare", "navigation_bar.community_timeline": "Lokal tidslinje", - "navigation_bar.compose": "Compose new toot", + "navigation_bar.compose": "Författa ny toot", "navigation_bar.direct": "Direktmeddelanden", "navigation_bar.discover": "Upptäck", "navigation_bar.domain_blocks": "Dolda domäner", "navigation_bar.edit_profile": "Redigera profil", "navigation_bar.favourites": "Favoriter", - "navigation_bar.filters": "Muted words", + "navigation_bar.filters": "Tystade ord", "navigation_bar.follow_requests": "Följförfrågningar", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Följer och följare", "navigation_bar.info": "Om denna instans", - "navigation_bar.keyboard_shortcuts": "Tangentbordsgenvägar", + "navigation_bar.keyboard_shortcuts": "Kortkommandon", "navigation_bar.lists": "Listor", "navigation_bar.logout": "Logga ut", "navigation_bar.mutes": "Tystade användare", - "navigation_bar.personal": "Personal", + "navigation_bar.personal": "Personligt", "navigation_bar.pins": "Nålade inlägg (toots)", "navigation_bar.preferences": "Inställningar", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Förenad tidslinje", "navigation_bar.security": "Säkerhet", "notification.favourite": "{name} favoriserade din status", "notification.follow": "{name} följer dig", "notification.mention": "{name} nämnde dig", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "En omröstning du röstat i har avslutats", "notification.reblog": "{name} knuffade din status", "notifications.clear": "Rensa meddelanden", "notifications.clear_confirmation": "Är du säker på att du vill radera alla dina meddelanden permanent?", "notifications.column_settings.alert": "Skrivbordsmeddelanden", "notifications.column_settings.favourite": "Favoriter:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Visa alla kategorier", + "notifications.column_settings.filter_bar.category": "Snabbfilter", + "notifications.column_settings.filter_bar.show": "Visa", "notifications.column_settings.follow": "Nya följare:", "notifications.column_settings.mention": "Omnämningar:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Omröstningsresultat:", "notifications.column_settings.push": "Push meddelanden", "notifications.column_settings.reblog": "Knuffar:", "notifications.column_settings.show": "Visa i kolumnen", "notifications.column_settings.sound": "Spela upp ljud", - "notifications.filter.all": "All", - "notifications.filter.boosts": "Boosts", + "notifications.filter.all": "Alla", + "notifications.filter.boosts": "Knuffar", "notifications.filter.favourites": "Favoriter", - "notifications.filter.follows": "Follows", - "notifications.filter.mentions": "Mentions", - "notifications.filter.polls": "Poll results", + "notifications.filter.follows": "Följer", + "notifications.filter.mentions": "Omnämningar", + "notifications.filter.polls": "Omröstningsresultat", "notifications.group": "{count} aviseringar", - "poll.closed": "Closed", + "poll.closed": "Stängd", "poll.refresh": "Ladda om", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.total_people": "{persons, plural, one {# person} other {# personer}}", + "poll.total_votes": "{count, plural, one {1 röst} other {# röster}}", "poll.vote": "Rösta", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.voted": "Du röstade för detta svar", + "poll_button.add_poll": "Lägg till en omröstning", + "poll_button.remove_poll": "Ta bort omröstning", "privacy.change": "Justera sekretess", "privacy.direct.long": "Skicka endast till nämnda användare", "privacy.direct.short": "Direkt", @@ -290,13 +314,14 @@ "privacy.public.short": "Publik", "privacy.unlisted.long": "Skicka inte till publik tidslinje", "privacy.unlisted.short": "Olistad", + "refresh": "Läs om", "regeneration_indicator.label": "Laddar…", "regeneration_indicator.sublabel": "Ditt hemmaflöde förbereds!", "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", + "relative_time.hours": "{number}tim", "relative_time.just_now": "nu", - "relative_time.minutes": "{number}m", - "relative_time.seconds": "{number}s", + "relative_time.minutes": "{number}min", + "relative_time.seconds": "{number}sek", "reply_indicator.cancel": "Ångra", "report.forward": "Vidarebefordra till {target}", "report.forward_hint": "Kontot är från en annan server. Skicka även en anonymiserad kopia av anmälan dit?", @@ -312,21 +337,22 @@ "search_popout.tips.text": "Enkel text returnerar matchande visningsnamn, användarnamn och hashtags", "search_popout.tips.user": "användare", "search_results.accounts": "Människor", - "search_results.hashtags": "Hashtags", + "search_results.hashtags": "Hashtaggar", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Att söka toots med deras innehåll är inte möjligt på denna Mastodon-server.", "search_results.total": "{count, number} {count, plural, ett {result} andra {results}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", + "status.admin_account": "Öppet modereringsgränssnitt för @{name}", + "status.admin_status": "Öppna denna status i modereringsgränssnittet", + "status.block": "Blockera @{name}", "status.cancel_reblog_private": "Ta bort knuff", "status.cannot_reblog": "Detta inlägg kan inte knuffas", - "status.copy": "Copy link to status", + "status.copy": "Kopiera länk till status", "status.delete": "Ta bort", - "status.detailed_status": "Detailed conversation view", + "status.detailed_status": "Detaljerad samtalsvy", "status.direct": "Direktmeddela @{name}", "status.embed": "Bädda in", "status.favourite": "Favorit", - "status.filtered": "Filtered", + "status.filtered": "Filtrerat", "status.load_more": "Ladda fler", "status.media_hidden": "Media dold", "status.mention": "Omnämn @{name}", @@ -337,10 +363,10 @@ "status.pin": "Fäst i profil", "status.pinned": "Fäst toot", "status.read_more": "Läs mer", - "status.reblog": "Knuff", + "status.reblog": "Knuffa", "status.reblog_private": "Knuffa till de ursprungliga åhörarna", "status.reblogged_by": "{name} knuffade", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", + "status.reblogs.empty": "Ingen har favoriserat den här tutningen än. När någon gör det kommer den att synas här.", "status.redraft": "Radera & gör om", "status.reply": "Svara", "status.replyAll": "Svara på tråden", @@ -352,29 +378,38 @@ "status.show_more": "Visa mer", "status.show_more_all": "Visa mer för alla", "status.show_thread": "Visa tråd", + "status.uncached_media_warning": "Ej tillgängligt", "status.unmute_conversation": "Öppna konversation", "status.unpin": "Ångra fäst i profil", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Avfärda förslag", + "suggestions.header": "Du kanske är intresserad av…", "tabs_bar.federated_timeline": "Förenad", "tabs_bar.home": "Hem", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Meddelanden", "tabs_bar.search": "Sök", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "time_remaining.days": "{number, plural, one {# dag} other {# dagar}} kvar", + "time_remaining.hours": "{hours, plural, one {# timme} other {# timmar}} kvar", + "time_remaining.minutes": "{minutes, plural, one {1 minut} other {# minuter}} kvar", + "time_remaining.moments": "Återstående tillfällen", + "time_remaining.seconds": "{hours, plural, one {# sekund} other {# sekunder}} kvar", "trends.count_by_accounts": "{count} {rawCount, plural, en {person} andra {people}} pratar", + "trends.trending_now": "Trendar nu", "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar Mastodon.", "upload_area.title": "Dra & släpp för att ladda upp", "upload_button.label": "Lägg till media", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.limit": "Filöverföringsgränsen överskriden.", + "upload_error.poll": "Filuppladdning tillåts inte med omröstningar.", "upload_form.description": "Beskriv för synskadade", - "upload_form.focus": "Beskär", + "upload_form.edit": "Redigera", "upload_form.undo": "Ta bort", + "upload_modal.analyzing_picture": "Analyserar bild…", + "upload_modal.apply": "Verkställ", + "upload_modal.description_placeholder": "En snabb brun räv hoppar över den lata hunden", + "upload_modal.detect_text": "Upptäck bildens text", + "upload_modal.edit_media": "Redigera meida", + "upload_modal.hint": "Klicka eller dra cirkeln på förhandstitten för att välja den fokusering som alltid kommer synas på alla miniatyrer.", + "upload_modal.preview_label": "Förhandstitt ({ratio})", "upload_progress.label": "Laddar upp...", "video.close": "Stäng video", "video.exit_fullscreen": "Stäng helskärm", @@ -382,7 +417,7 @@ "video.fullscreen": "Helskärm", "video.hide": "Dölj video", "video.mute": "Stäng av ljud", - "video.pause": "Pause", + "video.pause": "Pausa", "video.play": "Spela upp", "video.unmute": "Spela upp ljud" } diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index 637ca884a..9be141523 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -1,33 +1,36 @@ { "account.add_or_remove_from_list": "பட்டியல்களில் இருந்து சேர் அல்லது நீக்குக", "account.badges.bot": "பாட்", - "account.block": "Block @{name}", - "account.block_domain": "எல்லாவற்றையும் மறைக்க {domain}", - "account.blocked": "தடைமுட்டுகள்", + "account.block": "@{name} -ஐத் தடு", + "account.block_domain": "{domain} யில் இருந்து வரும் எல்லாவற்றையும் மறை", + "account.blocked": "முடக்கப்பட்டது", + "account.cancel_follow_request": "பின்தொடரும் முடிவைக் கைவிடவும்", "account.direct": "நேரடி செய்தி @{name}", - "account.domain_blocked": "டொமைன் மறைக்கப்பட்டது", - "account.edit_profile": "சுயவிவரத்தைத் திருத்தவும்", - "account.endorse": "சுயவிவரத்தில் அம்சம்", - "account.follow": "பின்பற்று", + "account.domain_blocked": "மறைக்கப்பட்டத் தளங்கள்", + "account.edit_profile": "சுயவிவரத்தை மாற்று", + "account.endorse": "சுயவிவரத்தில் வெளிப்படுத்து", + "account.follow": "பின்தொடர்", "account.followers": "பின்பற்றுபவர்கள்", "account.followers.empty": "இதுவரை யாரும் இந்த பயனரைப் பின்தொடரவில்லை.", - "account.follows": "பின்பற்று", + "account.follows": "பின்தொடர்", "account.follows.empty": "இந்த பயனர் இதுவரை யாரையும் பின்தொடரவில்லை.", - "account.follows_you": "நீ பின் தொடர்கிறாய்", + "account.follows_you": "உங்களைப் பின்தொடர்கிறார்", "account.hide_reblogs": "இருந்து ஊக்கியாக மறை @{name}", + "account.last_status": "கடைசி செயல்பாடு", "account.link_verified_on": "இந்த இணைப்பை உரிமையாளர் சரிபார்க்கப்பட்டது {date}", - "account.locked_info": "இந்தக் கணக்கு தனியுரிமை நிலை பூட்டப்பட்டுள்ளது. அவர்களைப் பின்தொடர்பவர் யார் என்பதை உரிமையாளர் கைமுறையாக மதிப்பாய்வு செய்கிறார்.", - "account.media": "Media", + "account.locked_info": "இந்த கணக்கின் தனியுரிமை நிலை பூட்டப்படவுள்ளது. உரிமையாளர் தன்னை யார் பின்தொடரலாம் என்பதை தானே முடிவு செய்வார்.", + "account.media": "ஊடகங்கள்", "account.mention": "குறிப்பிடு @{name}", "account.moved_to": "{name} நகர்த்தப்பட்டது:", "account.mute": "ஊமையான @{name}", "account.mute_notifications": "அறிவிப்புகளை முடக்கு @{name}", "account.muted": "முடக்கியது", - "account.posts": "Toots", + "account.never_active": "எப்போதுமில்லை", + "account.posts": "டூட்டுகள்", "account.posts_with_replies": "Toots மற்றும் பதில்கள்", - "account.report": "Report @{name}", + "account.report": "@{name} -ஐப் புகாரளி", "account.requested": "ஒப்புதலுக்காக காத்திருக்கிறது. கோரிக்கையை ரத்துசெய்ய கிளிக் செய்க", - "account.share": "பங்கிடு @{name}'s மனித முகத்தின்", + "account.share": "@{name} உடைய விவரத்தை பகிர்", "account.show_reblogs": "காட்டு boosts இருந்து @{name}", "account.unblock": "விடுவி @{name}", "account.unblock_domain": "காண்பி {domain}", @@ -35,28 +38,32 @@ "account.unfollow": "பின்தொடராட்", "account.unmute": "தடுப்புநீக்கு @{name}", "account.unmute_notifications": "அறிவிப்புகளை அகற்றவும் @{name}", - "alert.unexpected.message": "எதிர் பாராத பிழை ஏற்பட்டு விட்டது.", + "alert.rate_limited.message": "{retry_time, time, medium} மணிக்குப் பிறகு மீண்டும் முயற்சிக்கவும்.", + "alert.rate_limited.title": "விகித அளவுக்கு உட்படுத்தப்பட்டது", + "alert.unexpected.message": "எதிர்பாராத பிழை ஏற்பட்டுவிட்டது.", "alert.unexpected.title": "அச்சச்சோ!", - "boost_modal.combo": "நீங்கள் அழுத்தவும் {combo} அடுத்த முறை தவிர்க்கவும்", - "bundle_column_error.body": "இந்த கூறுகளை ஏற்றும்போது ஏதோ தவறு ஏற்பட்டது.", + "autosuggest_hashtag.per_week": "வாரத்திற்கு {count}", + "boost_modal.combo": "நீங்கள் இதை அடுத்தமுறை தவிர்க்க {combo} வை அழுத்தவும்", + "bundle_column_error.body": "இந்த பகுதி கூறை ஏற்றம் செய்யும் பொது ஏதோ தவறு ஏற்பட்டுள்ளது.", "bundle_column_error.retry": "மீண்டும் முயற்சி செய்", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "பிணையப் பிழை", "bundle_modal_error.close": "நெருக்கமாக", "bundle_modal_error.message": "இந்த கூறுகளை ஏற்றும்போது ஏதோ தவறு ஏற்பட்டது.", "bundle_modal_error.retry": "மீண்டும் முயற்சி செய்", "column.blocks": "தடுக்கப்பட்ட பயனர்கள்", "column.community": "உள்ளூர் காலக்கெடு", "column.direct": "நேரடி செய்திகள்", + "column.directory": "சுயவிவரங்களை உலாவு", "column.domain_blocks": "மறைந்த களங்கள்", "column.favourites": "விருப்பத்துக்குகந்த", "column.follow_requests": "கோரிக்கைகளை பின்பற்றவும்", - "column.home": "Home", + "column.home": "முதற்பக்கம்", "column.lists": "குதிரை வீர்ர்கள்", "column.mutes": "முடக்கப்பட்ட பயனர்கள்", - "column.notifications": "Notifications", + "column.notifications": "அறிவிப்புகள்", "column.pins": "Pinned toot", "column.public": "கூட்டாட்சி காலக்கெடு", - "column_back_button.label": "ஆதரி", + "column_back_button.label": "முந்தைய பக்கம்", "column_header.hide_settings": "அமைப்புகளை மறை", "column_header.moveLeft_settings": "நெடுவரிசையை இடதுபுறமாக நகர்த்தவும்", "column_header.moveRight_settings": "நெடுவரிசை வலது புறமாக நகர்த்து", @@ -75,25 +82,28 @@ "compose_form.poll.duration": "வாக்கெடுப்பு காலம்", "compose_form.poll.option_placeholder": "தேர்ந்தெடுப்ப {number}", "compose_form.poll.remove_option": "இந்த விருப்பத்தை அகற்றவும்", - "compose_form.publish": "Toot", + "compose_form.publish": "டூட் செய்க", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "அனைவருக்கும் ஏற்ற ஊடகம் இல்லை எனக் குறியிடுக", "compose_form.sensitive.marked": "ஊடகம் உணர்திறன் என குறிக்கப்பட்டுள்ளது", "compose_form.sensitive.unmarked": "ஊடகம் உணர்திறன் என குறிக்கப்படவில்லை", "compose_form.spoiler.marked": "எச்சரிக்கை பின்னால் உரை மறைக்கப்பட்டுள்ளது", "compose_form.spoiler.unmarked": "உரை மறைக்கப்படவில்லை", "compose_form.spoiler_placeholder": "இங்கே உங்கள் எச்சரிக்கையை எழுதுங்கள்", "confirmation_modal.cancel": "எதிராணை", - "confirmations.block.block_and_report": "Block & Report", - "confirmations.block.confirm": "Block", + "confirmations.block.block_and_report": "தடுத்துப் புகாரளி", + "confirmations.block.confirm": "தடு", "confirmations.block.message": "நீங்கள் நிச்சயமாக தடைசெய்ய விரும்புகிறீர்களா {name}?", - "confirmations.delete.confirm": "Delete", + "confirmations.delete.confirm": "அழி", "confirmations.delete.message": "இந்த நிலையை நிச்சயமாக நீக்க விரும்புகிறீர்களா?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "அழி", "confirmations.delete_list.message": "இந்த பட்டியலில் நிரந்தரமாக நீக்க விரும்புகிறீர்களா?", "confirmations.domain_block.confirm": "முழு டொமைனை மறை", "confirmations.domain_block.message": "நீங்கள் உண்மையில், நிச்சயமாக நீங்கள் முழு தடுக்க வேண்டும் நிச்சயமாக {domain}? பெரும்பாலான சந்தர்ப்பங்களில் ஒரு சில இலக்குகள் அல்லது மியூட்கள் போதுமானவை மற்றும் சிறந்தவை. எந்த பொது நேரத்திலும் அல்லது உங்கள் அறிவிப்புகளிலும் அந்தக் களத்திலிருந்து உள்ளடக்கத்தை நீங்கள் பார்க்க மாட்டீர்கள். அந்த களத்தில் இருந்து உங்கள் ஆதரவாளர்கள் அகற்றப்படுவார்கள்.", + "confirmations.logout.confirm": "வெளியேறு", + "confirmations.logout.message": "கண்டிப்பாக வெளியேறத்தான் வேண்டுமா?", "confirmations.mute.confirm": "ஊமையான", + "confirmations.mute.explanation": "இது அவர்களின் பதிவுகளையும், அவர்களைக் குறிப்பிடும் பதிவுகளையும் மறைத்துவிடும். ஆனால் அவர்கள் உங்கள் பதிவுகளைப் பார்க்கவும், உங்களைப் பின் தொடரவும் அனுமதிக்கப்படுவார்கள்.", "confirmations.mute.message": "நிச்சயமாக நீங்கள் முடக்க விரும்புகிறீர்களா {name}?", "confirmations.redraft.confirm": "நீக்கு & redraft", "confirmations.redraft.message": "நிச்சயமாக இந்த நிலையை நீக்கி, அதை மறுபடியும் உருவாக்க வேண்டுமா? பிடித்தவை மற்றும் ஊக்கங்கள் இழக்கப்படும், மற்றும் அசல் இடுகையில் பதில்கள் அனாதையான இருக்கும்.", @@ -101,21 +111,29 @@ "confirmations.reply.message": "இப்போது பதில், தற்போது நீங்கள் உருவாக்கும் செய்தி மேலெழுதப்படும். நீங்கள் தொடர விரும்புகிறீர்களா?", "confirmations.unfollow.confirm": "பின்தொடராட்", "confirmations.unfollow.message": "நிச்சயமாக நீங்கள் பின்தொடர விரும்புகிறீர்களா {name}?", + "conversation.delete": "உரையாடலை அழிக்கவும்", + "conversation.mark_as_read": "படிக்கபட்டதாகக் குறி", + "conversation.open": "உரையாடலைக் காண்க", + "conversation.with": "{names} உடன்", + "directory.federated": "தெரிந்த ஃபெடிவெர்சிலிருந்து", + "directory.local": "{domain} இல் இருந்து மட்டும்", + "directory.new_arrivals": "புதிய வரவு", + "directory.recently_active": "தற்பொழுது இயாக்கதிலிருப்பவர்கள்", "embed.instructions": "கீழே உள்ள குறியீட்டை நகலெடுப்பதன் மூலம் உங்கள் இணையதளத்தில் இந்த நிலையை உட்பொதிக்கவும்.", "embed.preview": "இது போன்ற தோற்றத்தை இங்கு காணலாம்:", "emoji_button.activity": "நடவடிக்கை", "emoji_button.custom": "வழக்கம்", "emoji_button.flags": "கொடி", "emoji_button.food": "உணவு மற்றும் பானம்", - "emoji_button.label": "Insert emoji", + "emoji_button.label": "குறுப்படங்களை உள்ளிடு", "emoji_button.nature": "இயற்கை", "emoji_button.not_found": "எமோஜோஸ் இல்லை! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "மறுப்ப கூறு", - "emoji_button.people": "People", + "emoji_button.people": "மக்கள்", "emoji_button.recent": "அடிக்கடி பயன்படுத்தப்படும்", "emoji_button.search": "தேடல்...", "emoji_button.search_results": "தேடல் முடிவுகள்", - "emoji_button.symbols": "Symbols", + "emoji_button.symbols": "குறியீடுகள்", "emoji_button.travel": "சுற்றுலா மற்றும் இடங்கள்", "empty_column.account_timeline": "இல்லை toots இங்கே!", "empty_column.account_unavailable": "சுயவிவரம் கிடைக்கவில்லை", @@ -134,11 +152,15 @@ "empty_column.mutes": "நீங்கள் இதுவரை எந்த பயனர்களையும் முடக்கியிருக்கவில்லை.", "empty_column.notifications": "உங்களிடம் எந்த அறிவிப்புகளும் இல்லை. உரையாடலைத் தொடங்க பிறருடன் தொடர்புகொள்ளவும்.", "empty_column.public": "இங்கே எதுவும் இல்லை! பகிரங்கமாக ஒன்றை எழுதவும் அல்லது மற்ற நிகழ்வுகளிலிருந்து பயனர்களை அதை நிரப்புவதற்கு கைமுறையாக பின்பற்றவும்", + "error.unexpected_crash.explanation": "மென்பொருள் பழுதுனாலோ அல்லது உங்கள் இணை உலாவியின் பொருந்தாதன்மையினாலோ இந்தப் பக்கத்தை சரியாகக் காண்பிக்க முடியவில்லை.", + "error.unexpected_crash.next_steps": "பக்கத்தை புதுப்பித்துப் பார்க்கவும். வேலை செய்யவில்லையெனில், வேறு ஒரு உலாவியில் இருந்தோ அல்லது உங்கள் கருவிக்கு பொருத்தமான வேறு செயலியில் இருந்தோ மச்டோடனைப் பயன்படுத்தவும்.", + "errors.unexpected_crash.copy_stacktrace": "பழுசெய்தியை பிடிப்புப் பலகைக்கு நகல் எடு", + "errors.unexpected_crash.report_issue": "புகாரளி", "follow_request.authorize": "அதிகாரமளி", "follow_request.reject": "விலக்கு", "getting_started.developers": "உருவாக்குநர்கள்", "getting_started.directory": "சுயவிவர அடைவு", - "getting_started.documentation": "Documentation", + "getting_started.documentation": "ஆவணங்கள்", "getting_started.heading": "தொடங்குதல்", "getting_started.invite": "நபர்களை அழைக்கவும்", "getting_started.open_source_notice": "Mastodon திறந்த மூல மென்பொருள். GitHub இல் நீங்கள் பங்களிக்கவோ அல்லது புகார் அளிக்கவோ முடியும் {github}.", @@ -160,11 +182,11 @@ "intervals.full.hours": "{number, plural, one {# hour} மற்ற {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} மற்ற {# minutes}}", "introduction.federation.action": "அடுத்த", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "கூட்டமைந்த", "introduction.federation.federated.text": "கூட்டமைப்பின் பிற சேவையகங்களிலிருந்து பொது பதிவுகள் கூட்டப்பட்ட காலக்கெடுவில் தோன்றும்.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "முகப்பு", "introduction.federation.home.text": "நீங்கள் பின்பற்றும் நபர்களின் இடுகைகள் உங்கள் வீட்டு ஊட்டத்தில் தோன்றும். நீங்கள் எந்த சர்வரில் யாரையும் பின்பற்ற முடியும்!", - "introduction.federation.local.headline": "Local", + "introduction.federation.local.headline": "அருகாமை", "introduction.federation.local.text": "உள்ளூர் சேவையகத்தில் தோன்றும் அதே சர்வரில் உள்ளவர்களின் பொது இடுகைகள்.", "introduction.interactions.action": "பயிற்சி முடிக்க!", "introduction.interactions.favourite.headline": "விருப்பத்துக்குகந்த", @@ -176,15 +198,15 @@ "introduction.welcome.action": "போகலாம்!", "introduction.welcome.headline": "முதல் படிகள்", "introduction.welcome.text": "கூட்டாளிக்கு வருக! ஒரு சில நிமிடங்களில், பலவிதமான சேவையகங்களில் செய்திகளை உரையாட மற்றும் உங்கள் நண்பர்களிடம் பேச முடியும். ஆனால் இந்த சர்வர், {domain}, சிறப்பு - இது உங்கள் சுயவிவரத்தை வழங்குகிறது, எனவே அதன் பெயரை நினைவில் கொள்ளுங்கள்.", - "keyboard_shortcuts.back": "மீண்டும் செல்லவும்", + "keyboard_shortcuts.back": "பின் செல்வதற்கு", "keyboard_shortcuts.blocked": "தடுக்கப்பட்ட பயனர்களின் பட்டியலைத் திறக்க", "keyboard_shortcuts.boost": "அதிகரிக்கும்", "keyboard_shortcuts.column": "நெடுவரிசைகளில் ஒன்றில் நிலைக்கு கவனம் செலுத்த வேண்டும்", "keyboard_shortcuts.compose": "தொகு உரைப்பகுதியை கவனத்தில் கொள்ளவும்", - "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.description": "விவரம்", "keyboard_shortcuts.direct": "நேரடி செய்திகள் பத்தி திறக்க", "keyboard_shortcuts.down": "பட்டியலில் கீழே நகர்த்த", - "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.enter": "பதிவைத்திறக்க", "keyboard_shortcuts.favourite": "பிடித்தது", "keyboard_shortcuts.favourites": "பிடித்தவை பட்டியலை திறக்க", "keyboard_shortcuts.federated": "ஒருங்கிணைந்த நேரத்தை திறக்க", @@ -204,23 +226,24 @@ "keyboard_shortcuts.search": "தேடல் கவனம் செலுத்த", "keyboard_shortcuts.start": "'தொடங்குவதற்கு' நெடுவரிசை திறக்க", "keyboard_shortcuts.toggle_hidden": "CW க்கு பின்னால் உரையை மறைக்க / மறைக்க", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "படிமங்களைக் காட்ட/மறைக்க", "keyboard_shortcuts.toot": "தொடங்க ஒரு புதிய toot", "keyboard_shortcuts.unfocus": "உரை பகுதியை / தேடலை கவனம் செலுத்த வேண்டும்", "keyboard_shortcuts.up": "பட்டியலில் மேலே செல்ல", "lightbox.close": "நெருக்கமாக", "lightbox.next": "அடுத்த", "lightbox.previous": "சென்ற", - "lightbox.view_context": "View context", + "lightbox.view_context": "சூழலைக் பார்", "lists.account.add": "பட்டியலில் சேர்", "lists.account.remove": "பட்டியலில் இருந்து அகற்று", - "lists.delete": "Delete list", + "lists.delete": "பட்டியலை நீக்கு", "lists.edit": "பட்டியலை திருத்து", "lists.edit.submit": "தலைப்பு மாற்றவும்", "lists.new.create": "பட்டியலில் சேர்", "lists.new.title_placeholder": "புதிய பட்டியல் தலைப்பு", "lists.search": "நீங்கள் பின்தொடரும் நபர்கள் மத்தியில் தேடுதல்", "lists.subheading": "உங்கள் பட்டியல்கள்", + "load_pending": "{count, plural,one {# புதியது}other {# புதியவை}}", "loading_indicator.label": "ஏற்றுதல்...", "media_gallery.toggle_visible": "நிலைமாற்று தெரியும்", "missing_indicator.label": "கிடைக்கவில்லை", @@ -237,20 +260,19 @@ "navigation_bar.favourites": "விருப்பத்துக்குகந்த", "navigation_bar.filters": "முடக்கப்பட்ட வார்த்தைகள்", "navigation_bar.follow_requests": "கோரிக்கைகளை பின்பற்றவும்", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "பின்பற்றல்கள் மற்றும் பின்பற்றுபவர்கள்", "navigation_bar.info": "இந்த நிகழ்வு பற்றி", "navigation_bar.keyboard_shortcuts": "சுருக்குவிசைகள்", "navigation_bar.lists": "குதிரை வீர்ர்கள்", "navigation_bar.logout": "விடு பதிகை", "navigation_bar.mutes": "முடக்கப்பட்ட பயனர்கள்", - "navigation_bar.personal": "Personal", + "navigation_bar.personal": "தனிப்பட்டவை", "navigation_bar.pins": "பொருத்தப்பட்டன toots", "navigation_bar.preferences": "விருப்பங்கள்", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "கூட்டாட்சி காலக்கெடு", "navigation_bar.security": "பத்திரம்", "notification.favourite": "{name} ஆர்வம் கொண்டவர், உங்கள் நிலை", - "notification.follow": "{name} நீங்கள் தொடர்ந்து வந்தீர்கள்", + "notification.follow": "{name} உங்களைப் பின்தொடர்கிறார்", "notification.mention": "{name} நீங்கள் குறிப்பிட்டுள்ளீர்கள்", "notification.poll": "நீங்கள் வாக்களித்த வாக்கெடுப்பு முடிவடைந்தது", "notification.reblog": "{name} உங்கள் நிலை அதிகரித்தது", @@ -264,7 +286,7 @@ "notifications.column_settings.follow": "புதிய பின்பற்றுபவர்கள்:", "notifications.column_settings.mention": "குறிப்பிடுகிறது:", "notifications.column_settings.poll": "கருத்துக்கணிப்பு முடிவுகள்:", - "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.push": "தள் அறிவிப்புகள்", "notifications.column_settings.reblog": "மதிப்பை உயர்த்து:", "notifications.column_settings.show": "பத்தியில் காண்பி", "notifications.column_settings.sound": "ஒலி விளையாட", @@ -274,11 +296,13 @@ "notifications.filter.follows": "பின்பற்று", "notifications.filter.mentions": "குறிப்பிடுகிறார்", "notifications.filter.polls": "கருத்துக்கணிப்பு முடிவுகள்", - "notifications.group": "{count} notifications", + "notifications.group": "{count} அறிவிப்புகள்", "poll.closed": "மூடிய", "poll.refresh": "பத்துயிர்ப்ப?ட்டு", + "poll.total_people": "{count, plural, one {# நபர்} other {# நபர்கள்}}", "poll.total_votes": "{count, plural, one {# vote} மற்ற {# votes}}", "poll.vote": "வாக்களி", + "poll.voted": "உங்கள் தேர்வு", "poll_button.add_poll": "வாக்கெடுப்பைச் சேர்க்கவும்", "poll_button.remove_poll": "வாக்கெடுப்பை அகற்று", "privacy.change": "நிலை தனியுரிமை", @@ -287,41 +311,43 @@ "privacy.private.long": "பின்தொடர்பவர்களுக்கு மட்டுமே இடுகை", "privacy.private.short": "பின்பற்றுபவர்கள் மட்டும்", "privacy.public.long": "பொது நேரங்களுக்கான இடுகை", - "privacy.public.short": "Public", + "privacy.public.short": "பொது", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "பட்டியலிடப்படாத", + "refresh": "புதுப்பி", "regeneration_indicator.label": "சுமையேற்றம்…", "regeneration_indicator.sublabel": "உங்கள் வீட்டு ஊட்டம் தயார் செய்யப்படுகிறது!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", + "relative_time.days": "{number}நா", + "relative_time.hours": "{number}ம", "relative_time.just_now": "இப்பொழுது", - "relative_time.minutes": "{number}m", - "relative_time.seconds": "{number}s", + "relative_time.minutes": "{number}நி", + "relative_time.seconds": "{number}வி", "reply_indicator.cancel": "எதிராணை", "report.forward": "முன்னோக்கி {target}", "report.forward_hint": "கணக்கு மற்றொரு சேவையகத்திலிருந்து வருகிறது. அறிக்கையின் அநாமதேய பிரதி ஒன்றை அனுப்பவும்.?", "report.hint": "அறிக்கை உங்கள் மாதிரியாக மாற்றியமைக்கப்படும். கீழே உள்ள கணக்கை நீங்கள் ஏன் புகாரளிக்கிறீர்கள் என்பதற்கான விளக்கத்தை வழங்கலாம்:", "report.placeholder": "கூடுதல் கருத்துரைகள்", - "report.submit": "Submit", + "report.submit": "சமர்ப்பி", "report.target": "Report {target}", "search.placeholder": "தேடு", "search_popout.search_format": "மேம்பட்ட தேடல் வடிவம்", "search_popout.tips.full_text": "எளிமையான உரை நீங்கள் எழுதப்பட்ட, புகழ், அதிகரித்தது, அல்லது குறிப்பிட்டுள்ள, அதே போல் பயனர் பெயர்கள், காட்சி பெயர்கள், மற்றும் ஹேஸ்டேகைகளை கொண்டுள்ளது என்று நிலைகளை கொடுக்கிறது.", "search_popout.tips.hashtag": "ஹேஸ்டேக்", - "search_popout.tips.status": "status", + "search_popout.tips.status": "நிலைமை", "search_popout.tips.text": "எளிய உரை காட்சி பெயர்கள், பயனர்பெயர்கள் மற்றும் ஹாஷ்டேட்களுடன் பொருந்துகிறது", - "search_popout.tips.user": "user", - "search_results.accounts": "People", + "search_popout.tips.user": "பயனர்", + "search_results.accounts": "மக்கள்", "search_results.hashtags": "ஹாஷ்டேக்குகளைச்", - "search_results.statuses": "Toots", + "search_results.statuses": "டூட்டுகள்", + "search_results.statuses_fts_disabled": "டூட்டுகளின் வார்த்தைகளைக்கொண்டு தேடுவது இந்த மச்டோடன் வழங்கியில் இயல்விக்கப்படவில்லை.", "search_results.total": "{count, number} {count, plural, one {result} மற்ற {results}}", "status.admin_account": "மிதமான இடைமுகத்தை திறக்க @{name}", "status.admin_status": "மிதமான இடைமுகத்தில் இந்த நிலையை திறக்கவும்", - "status.block": "Block @{name}", + "status.block": "@{name} -ஐத் தடு", "status.cancel_reblog_private": "இல்லை பூஸ்ட்", "status.cannot_reblog": "இந்த இடுகை அதிகரிக்க முடியாது", "status.copy": "நிலைக்கு இணைப்பை நகலெடு", - "status.delete": "Delete", + "status.delete": "நீக்கு", "status.detailed_status": "விரிவான உரையாடல் காட்சி", "status.direct": "நேரடி செய்தி @{name}", "status.embed": "கிடத்து", @@ -344,7 +370,7 @@ "status.redraft": "நீக்கு மற்றும் மீண்டும் வரைவு", "status.reply": "பதில்", "status.replyAll": "நூலுக்கு பதிலளிக்கவும்", - "status.report": "Report @{name}", + "status.report": "@{name} மீது புகாரளி", "status.sensitive_warning": "உணர்திறன் உள்ளடக்கம்", "status.share": "பங்கிடு", "status.show_less": "குறைவாகக் காண்பி", @@ -352,14 +378,15 @@ "status.show_more": "மேலும் காட்ட", "status.show_more_all": "அனைவருக்கும் மேலும் காட்டு", "status.show_thread": "நூல் காட்டு", + "status.uncached_media_warning": "கிடைக்கவில்லை", "status.unmute_conversation": "ஊமையாக உரையாடல் இல்லை", "status.unpin": "சுயவிவரத்திலிருந்து நீக்கவும்", "suggestions.dismiss": "பரிந்துரை விலக்க", "suggestions.header": "நீங்கள் ஆர்வமாக இருக்கலாம் …", - "tabs_bar.federated_timeline": "Federated", - "tabs_bar.home": "Home", - "tabs_bar.local_timeline": "Local", - "tabs_bar.notifications": "Notifications", + "tabs_bar.federated_timeline": "கூட்டமைந்த", + "tabs_bar.home": "முகப்பு", + "tabs_bar.local_timeline": "உள்ளூர்", + "tabs_bar.notifications": "அறிவிப்புகள்", "tabs_bar.search": "தேடு", "time_remaining.days": "{number, plural, one {# day} மற்ற {# days}} left", "time_remaining.hours": "{number, plural, one {# hour} மற்ற {# hours}} left", @@ -367,22 +394,30 @@ "time_remaining.moments": "தருணங்கள் மீதமுள்ளன", "time_remaining.seconds": "{number, plural, one {# second} மற்ற {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} மற்ற {people}} உரையாடு", + "trends.trending_now": "இப்போது செல்திசையில் இருப்பவை", "ui.beforeunload": "நீங்கள் வெளியே சென்றால் உங்கள் வரைவு இழக்கப்படும் மஸ்தோடோன்.", "upload_area.title": "பதிவேற்ற & இழுக்கவும்", "upload_button.label": "மீடியாவைச் சேர்க்கவும் (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "கோப்பு பதிவேற்ற வரம்பு மீறப்பட்டது.", "upload_error.poll": "கோப்பு பதிவேற்றம் அனுமதிக்கப்படவில்லை.", "upload_form.description": "பார்வையற்ற விவரிக்கவும்", - "upload_form.focus": "மாற்றம் முன்னோட்டம்", - "upload_form.undo": "Delete", + "upload_form.edit": "தொகு", + "upload_form.undo": "நீக்கு", + "upload_modal.analyzing_picture": "படம் ஆராயப்படுகிறது…", + "upload_modal.apply": "உபயோகி", + "upload_modal.description_placeholder": "பொருள் விளக்கம்", + "upload_modal.detect_text": "படத்தில் இருக்கும் எழுத்தை கண்டறி", + "upload_modal.edit_media": "படத்தைத் தொகு", + "upload_modal.hint": "எல்லா வில்லைப்பட்த்திலும் தெரியவேண்டிய, படத்தின் முக்கிய குவியப்புள்ளிக்கு, வட்டத்தை சொடுக்கி இழுத்துச்செல்லவும்.", + "upload_modal.preview_label": "முன்னோட்டம் ({ratio})", "upload_progress.label": "ஏற்றுகிறது ...", "video.close": "வீடியோவை மூடு", "video.exit_fullscreen": "முழு திரையில் இருந்து வெளியேறவும்", "video.expand": "வீடியோவை விரிவாக்கு", - "video.fullscreen": "Full screen", + "video.fullscreen": "முழுத்திரை", "video.hide": "வீடியோவை மறை", "video.mute": "ஒலி முடக்கவும்", - "video.pause": "Pause", + "video.pause": "இடைநிறுத்து", "video.play": "விளையாடு", "video.unmute": "ஒலி மெளனமாக இல்லை" } diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index 269ea45c3..3cfbc5786 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -4,6 +4,7 @@ "account.block": "@{name} ను బ్లాక్ చేయి", "account.block_domain": "{domain} నుంచి అన్నీ దాచిపెట్టు", "account.blocked": "బ్లాక్ అయినవి", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "@{name}కు నేరుగా సందేశం పంపు", "account.domain_blocked": "డొమైన్ దాచిపెట్టబడినది", "account.edit_profile": "ప్రొఫైల్ని సవరించండి", @@ -15,6 +16,7 @@ "account.follows.empty": "ఈ వినియోగదారి ఇంకా ఎవరినీ అనుసరించడంలేదు.", "account.follows_you": "మిమ్మల్ని అనుసరిస్తున్నారు", "account.hide_reblogs": "@{name} నుంచి బూస్ట్ లను దాచిపెట్టు", + "account.last_status": "Last active", "account.link_verified_on": "ఈ లంకె యొక్క యాజమాన్యం {date}న పరీక్షించబడింది", "account.locked_info": "ఈ ఖాతా యొక్క గోప్యత స్థితి లాక్ చేయబడి వుంది. ఈ ఖాతాను ఎవరు అనుసరించవచ్చో యజమానే నిర్ణయం తీసుకుంటారు.", "account.media": "మీడియా", @@ -23,6 +25,7 @@ "account.mute": "@{name}ను మ్యూట్ చెయ్యి", "account.mute_notifications": "@{name}నుంచి ప్రకటనలను మ్యూట్ చెయ్యి", "account.muted": "మ్యూట్ అయినవి", + "account.never_active": "Never", "account.posts": "టూట్లు", "account.posts_with_replies": "టూట్లు మరియు ప్రత్యుత్తరములు", "account.report": "@{name}పై ఫిర్యాదుచేయు", @@ -35,8 +38,11 @@ "account.unfollow": "అనుసరించవద్దు", "account.unmute": "@{name}పై మ్యూట్ ని తొలగించు", "account.unmute_notifications": "@{name} నుంచి ప్రకటనలపై మ్యూట్ ని తొలగించు", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "అనుకోని తప్పు జరిగినది.", "alert.unexpected.title": "అయ్యో!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "మీరు తదుపరిసారి దీనిని దాటవేయడానికి {combo} నొక్కవచ్చు", "bundle_column_error.body": "ఈ భాగం లోడ్ అవుతున్నప్పుడు ఏదో తప్పు జరిగింది.", "bundle_column_error.retry": "మళ్ళీ ప్రయత్నించండి", @@ -47,6 +53,7 @@ "column.blocks": "బ్లాక్ చేయబడిన వినియోగదారులు", "column.community": "స్థానిక కాలక్రమం", "column.direct": "ప్రత్యక్ష సందేశాలు", + "column.directory": "Browse profiles", "column.domain_blocks": "దాచిన డొమైన్లు", "column.favourites": "ఇష్టపడినవి", "column.follow_requests": "అనుసరించడానికి అభ్యర్ధనలు", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "మీరు ఖచ్చితంగా ఈ జాబితాను శాశ్వతంగా తొలగించాలనుకుంటున్నారా?", "confirmations.domain_block.confirm": "మొత్తం డొమైన్ను దాచు", "confirmations.domain_block.message": "మీరు నిజంగా నిజంగా మొత్తం {domain} ని బ్లాక్ చేయాలనుకుంటున్నారా? చాలా సందర్భాలలో కొన్ని లక్ష్యంగా ఉన్న బ్లాక్స్ లేదా మ్యూట్స్ సరిపోతాయి మరియు ఉత్తమమైనవి. మీరు ఆ డొమైన్ నుండి కంటెంట్ను ఏ ప్రజా కాలక్రమాలలో లేదా మీ నోటిఫికేషన్లలో చూడలేరు. ఆ డొమైన్ నుండి మీ అనుచరులు తీసివేయబడతారు.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "మ్యూట్ చేయి", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "{name}ను మీరు ఖచ్చితంగా మ్యూట్ చేయాలనుకుంటున్నారా?", "confirmations.redraft.confirm": "తొలగించు & తిరగరాయు", "confirmations.redraft.message": "మీరు ఖచ్చితంగా ఈ స్టేటస్ ని తొలగించి తిరగరాయాలనుకుంటున్నారా? ఈ స్టేటస్ యొక్క బూస్ట్ లు మరియు ఇష్టాలు పోతాయి,మరియు ప్రత్యుత్తరాలు అనాధలు అయిపోతాయి.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "ఇప్పుడే ప్రత్యుత్తరం ఇస్తే మీరు ప్రస్తుతం వ్రాస్తున్న సందేశం తిరగరాయబడుతుంది. మీరు ఖచ్చితంగా కొనసాగించాలనుకుంటున్నారా?", "confirmations.unfollow.confirm": "అనుసరించవద్దు", "confirmations.unfollow.message": "{name}ను మీరు ఖచ్చితంగా అనుసరించవద్దనుకుంటున్నారా?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "దిగువ కోడ్ను కాపీ చేయడం ద్వారా మీ వెబ్సైట్లో ఈ స్టేటస్ ని పొందుపరచండి.", "embed.preview": "అది ఈ క్రింది విధంగా కనిపిస్తుంది:", "emoji_button.activity": "కార్యకలాపాలు", @@ -134,6 +152,10 @@ "empty_column.mutes": "మీరు ఇంకా ఏ వినియోగదారులనూ మ్యూట్ చేయలేదు.", "empty_column.notifications": "మీకు ఇంకా ఏ నోటిఫికేషన్లు లేవు. సంభాషణను ప్రారంభించడానికి ఇతరులతో ప్రతిస్పందించండి.", "empty_column.public": "ఇక్కడ ఏమీ లేదు! దీన్ని నింపడానికి బహిరంగంగా ఏదైనా వ్రాయండి, లేదా ఇతర సేవికల నుండి వినియోగదారులను అనుసరించండి", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "అనుమతించు", "follow_request.reject": "తిరస్కరించు", "getting_started.developers": "డెవలపర్లు", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "కొత్త జాబితా శీర్షిక", "lists.search": "మీరు అనుసరించే వ్యక్తులలో శోధించండి", "lists.subheading": "మీ జాబితాలు", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "లోడ్ అవుతోంది...", "media_gallery.toggle_visible": "దృశ్యమానతను టోగుల్ చేయండి", "missing_indicator.label": "దొరకలేదు", @@ -246,7 +269,6 @@ "navigation_bar.personal": "వ్యక్తిగతం", "navigation_bar.pins": "అతికించిన టూట్లు", "navigation_bar.preferences": "ప్రాధాన్యతలు", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "సమాఖ్య కాలక్రమం", "navigation_bar.security": "భద్రత", "notification.favourite": "{name} మీ స్టేటస్ ను ఇష్టపడ్డారు", @@ -277,8 +299,10 @@ "notifications.group": "{count} ప్రకటనలు", "poll.closed": "మూసివేయబడినవి", "poll.refresh": "నవీకరించు", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "ఎన్నుకోండి", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "ఒక ఎన్నికను చేర్చు", "poll_button.remove_poll": "ఎన్నికను తొలగించు", "privacy.change": "స్టేటస్ గోప్యతను సర్దుబాటు చేయండి", @@ -290,6 +314,7 @@ "privacy.public.short": "ప్రజా", "privacy.unlisted.long": "ప్రజా కాలక్రమాలలో చూపించవద్దు", "privacy.unlisted.short": "జాబితా చేయబడనిది", + "refresh": "Refresh", "regeneration_indicator.label": "లోడ్ అవుతోంది…", "regeneration_indicator.sublabel": "మీ హోమ్ ఫీడ్ సిద్ధమవుతోంది!", "relative_time.days": "{number}d", @@ -314,6 +339,7 @@ "search_results.accounts": "వ్యక్తులు", "search_results.hashtags": "హాష్ ట్యాగ్లు", "search_results.statuses": "టూట్లు", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "@{name} కొరకు సమన్వయ వినిమయసీమను తెరువు", "status.admin_status": "సమన్వయ వినిమయసీమలో ఈ స్టేటస్ ను తెరవండి", @@ -352,6 +378,7 @@ "status.show_more": "ఇంకా చూపించు", "status.show_more_all": "అన్నిటికీ ఇంకా చూపించు", "status.show_thread": "గొలుసును చూపించు", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "సంభాషణను అన్మ్యూట్ చేయి", "status.unpin": "ప్రొఫైల్ నుండి పీకివేయు", "suggestions.dismiss": "సూచనను రద్దు చేయి", @@ -367,14 +394,22 @@ "time_remaining.moments": "కొన్ని క్షణాలు మాత్రమే మిగిలి ఉన్నాయి", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} మాట్లాడుతున్నారు", + "trends.trending_now": "Trending now", "ui.beforeunload": "మీరు మాస్టొడొన్ను వదిలివేస్తే మీ డ్రాఫ్ట్లు పోతాయి.", "upload_area.title": "అప్లోడ్ చేయడానికి డ్రాగ్ & డ్రాప్ చేయండి", "upload_button.label": "మీడియాను జోడించండి (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "దృష్టి లోపమున్న వారి కోసం వివరించండి", - "upload_form.focus": "ప్రివ్యూను మార్చు", + "upload_form.edit": "Edit", "upload_form.undo": "తొలగించు", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "అప్లోడ్ అవుతోంది...", "video.close": "వీడియోని మూసివేయి", "video.exit_fullscreen": "పూర్తి స్క్రీన్ నుండి నిష్క్రమించు", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index bd042c1d1..7db168338 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -1,9 +1,10 @@ { "account.add_or_remove_from_list": "เพิ่มหรือเอาออกจากรายการ", "account.badges.bot": "บอต", - "account.block": "ปิดกั้น @{name}", + "account.block": "บล็อค @{name}", "account.block_domain": "ซ่อนทุกอย่างจาก {domain}", - "account.blocked": "ปิดกั้นอยู่", + "account.blocked": "ถูกบล็อค", + "account.cancel_follow_request": "ยกเลิกคำขอติดตาม", "account.direct": "ส่งข้อความโดยตรงถึง @{name}", "account.domain_blocked": "ซ่อนโดเมนอยู่", "account.edit_profile": "แก้ไขโปรไฟล์", @@ -11,18 +12,20 @@ "account.follow": "ติดตาม", "account.followers": "ผู้ติดตาม", "account.followers.empty": "ยังไม่มีใครติดตามผู้ใช้นี้", - "account.follows": "ติดตาม", + "account.follows": "การติดตาม", "account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร", "account.follows_you": "ติดตามคุณ", "account.hide_reblogs": "ซ่อนการดันจาก @{name}", + "account.last_status": "ใช้งานล่าสุด", "account.link_verified_on": "ตรวจสอบความเป็นเจ้าของของลิงก์นี้เมื่อ {date}", - "account.locked_info": "บัญชีนี้ถูกล็อคไว้ เจ้าของจะต้องรับรองการติดตามของคุณด้วย", + "account.locked_info": "มีการตั้งสถานะความเป็นส่วนตัวของบัญชีนี้เป็นล็อคอยู่ เจ้าของตรวจทานผู้ที่สามารถติดตามเขาด้วยตนเอง", "account.media": "สื่อ", "account.mention": "กล่าวถึง @{name}", "account.moved_to": "{name} ได้ย้ายไปยัง:", "account.mute": "ปิดเสียง @{name}", "account.mute_notifications": "ปิดเสียงการแจ้งเตือนจาก @{name}", "account.muted": "ปิดเสียงอยู่", + "account.never_active": "ไม่เลย", "account.posts": "โพสต์", "account.posts_with_replies": "โพสต์และการตอบกลับ", "account.report": "รายงาน @{name}", @@ -35,9 +38,12 @@ "account.unfollow": "เลิกติดตาม", "account.unmute": "เลิกปิดเสียง @{name}", "account.unmute_notifications": "เลิกปิดเสียงการแจ้งเตือนจาก @{name}", + "alert.rate_limited.message": "โปรดลองใหม่หลังจาก {retry_time, time, medium}", + "alert.rate_limited.title": "เข้าใช้งานบ่อยเกินไป", "alert.unexpected.message": "เกิดข้อผิดพลาดที่ไม่คาดคิด", "alert.unexpected.title": "อุปส์!", - "boost_modal.combo": "กด {combo} เพื่อข้าม", + "autosuggest_hashtag.per_week": "{count} ต่อสัปดาห์", + "boost_modal.combo": "คุณสามารถกด {combo} เพื่อข้ามสิ่งนี้ในครั้งถัดไป", "bundle_column_error.body": "มีบางอย่างผิดพลาดขณะโหลดส่วนประกอบนี้", "bundle_column_error.retry": "ลองอีกครั้ง", "bundle_column_error.title": "ข้อผิดพลาดเครือข่าย", @@ -47,6 +53,7 @@ "column.blocks": "ผู้ใช้ที่ปิดกั้นอยู่", "column.community": "เส้นเวลาในเว็บ", "column.direct": "ข้อความโดยตรง", + "column.directory": "เรียกดูโปรไฟล์", "column.domain_blocks": "โดเมนที่ซ่อนอยู่", "column.favourites": "รายการโปรด", "column.follow_requests": "คำขอติดตาม", @@ -65,23 +72,23 @@ "column_header.unpin": "ถอนหมุด", "column_subheading.settings": "การตั้งค่า", "community.column_settings.media_only": "สื่อเท่านั้น", - "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", + "compose_form.direct_message_warning": "จะส่งโพสต์นี้ไปยังผู้ใช้ที่กล่าวถึงเท่านั้น", "compose_form.direct_message_warning_learn_more": "เรียนรู้เพิ่มเติม", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", - "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "compose_form.lock_disclaimer.lock": "locked", - "compose_form.placeholder": "What is on your mind?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.hashtag_warning": "จะไม่แสดงรายการโพสต์นี้ภายใต้แฮชแท็กใด ๆ เนื่องจากไม่อยู่ในรายการ เฉพาะโพสต์สาธารณะเท่านั้นที่สามารถค้นหาโดยแฮชแท็ก", + "compose_form.lock_disclaimer": "บัญชีของคุณไม่ได้ {locked} ใครก็ตามสามารถติดตามคุณเพื่อดูโพสต์สำหรับผู้ติดตามเท่านั้นของคุณ", + "compose_form.lock_disclaimer.lock": "ล็อคอยู่", + "compose_form.placeholder": "คุณกำลังคิดอะไรอยู่?", + "compose_form.poll.add_option": "เพิ่มทางเลือก", + "compose_form.poll.duration": "ระยะเวลาโพล", + "compose_form.poll.option_placeholder": "ทางเลือก {number}", + "compose_form.poll.remove_option": "เอาทางเลือกนี้ออก", "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.spoiler.marked": "Text is hidden behind warning", - "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.sensitive.hide": "ทำเครื่องหมายสื่อว่าละเอียดอ่อน", + "compose_form.sensitive.marked": "มีการทำเครื่องหมายสื่อว่าละเอียดอ่อน", + "compose_form.sensitive.unmarked": "ไม่มีการทำเครื่องหมายสื่อว่าละเอียดอ่อน", + "compose_form.spoiler.marked": "มีการซ่อนข้อความอยู่หลังคำเตือน", + "compose_form.spoiler.unmarked": "ไม่มีการซ่อนข้อความ", "compose_form.spoiler_placeholder": "เขียนคำเตือนของคุณที่นี่", "confirmation_modal.cancel": "ยกเลิก", "confirmations.block.block_and_report": "ปิดกั้นแล้วรายงาน", @@ -91,25 +98,36 @@ "confirmations.delete.message": "คุณแน่ใจหรือไม่ว่าต้องการลบสถานะนี้?", "confirmations.delete_list.confirm": "ลบ", "confirmations.delete_list.message": "คุณแน่ใจหรือไม่ว่าต้องการลบรายการนี้อย่างถาวร?", - "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.confirm": "ซ่อนทั้งโดเมน", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", + "confirmations.logout.confirm": "ออกจากระบบ", + "confirmations.logout.message": "คุณแน่ใจหรือไม่ว่าต้องการออกจากระบบ?", "confirmations.mute.confirm": "ปิดเสียง", + "confirmations.mute.explanation": "การกระทำนี้จะซ่อนโพสต์ของเขาและโพสต์ที่มีการกล่าวถึงเขา แต่ยังอนุญาตให้เขาเห็นโพสต์ของคุณและติดตามคุณได้", "confirmations.mute.message": "คุณแน่ใจหรือไม่ว่าต้องการปิดเสียง {name}?", "confirmations.redraft.confirm": "ลบแล้วร่างใหม่", - "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", + "confirmations.redraft.message": "คุณแน่ใจหรือไม่ว่าต้องการลบสถานะนี้แล้วร่างใหม่? รายการโปรดและการดันจะหายไป และการตอบกลับโพสต์ดั้งเดิมจะไม่มีความเกี่ยวพัน", "confirmations.reply.confirm": "ตอบกลับ", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.reply.message": "การตอบกลับตอนนี้จะเขียนทับข้อความที่คุณกำลังเขียน คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?", "confirmations.unfollow.confirm": "เลิกติดตาม", "confirmations.unfollow.message": "คุณแน่ใจหรือไม่ว่าต้องการเลิกติดตาม {name}?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", + "conversation.delete": "ลบการสนทนา", + "conversation.mark_as_read": "ทำเครื่องหมายว่าอ่านแล้ว", + "conversation.open": "ดูการสนทนา", + "conversation.with": "กับ {names}", + "directory.federated": "จากเฟดิเวิร์สที่รู้จัก", + "directory.local": "จาก {domain} เท่านั้น", + "directory.new_arrivals": "มาใหม่", + "directory.recently_active": "ใช้งานล่าสุด", + "embed.instructions": "ฝังสถานะนี้ในเว็บไซต์ของคุณโดยคัดลอกโค้ดด้านล่าง", + "embed.preview": "นี่คือลักษณะที่จะปรากฏ:", "emoji_button.activity": "กิจกรรม", "emoji_button.custom": "กำหนดเอง", "emoji_button.flags": "ธง", "emoji_button.food": "อาหารและเครื่องดื่ม", "emoji_button.label": "แทรกอีโมจิ", "emoji_button.nature": "ธรรมชาติ", - "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "ไม่มีอีโมโจ!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "วัตถุ", "emoji_button.people": "ผู้คน", "emoji_button.recent": "ที่ใช้บ่อย", @@ -118,22 +136,26 @@ "emoji_button.symbols": "สัญลักษณ์", "emoji_button.travel": "การเดินทางและสถานที่", "empty_column.account_timeline": "ไม่มีโพสต์ที่นี่!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.blocks": "You haven't blocked any users yet.", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.account_unavailable": "ไม่มีโปรไฟล์", + "empty_column.blocks": "คุณยังไม่ได้ปิดกั้นผู้ใช้ใด ๆ", + "empty_column.community": "เส้นเวลาในเว็บว่างเปล่า เขียนบางอย่างเป็นสาธารณะเพื่อเริ่มต้น!", + "empty_column.direct": "คุณยังไม่มีข้อความโดยตรงใด ๆ เมื่อคุณส่งหรือรับข้อความ ข้อความจะปรากฏที่นี่", "empty_column.domain_blocks": "ยังไม่มีโดเมนที่ซ่อนอยู่", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", - "empty_column.hashtag": "There is nothing in this hashtag yet.", - "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.favourited_statuses": "คุณยังไม่มีโพสต์ที่ชื่นชอบใด ๆ เมื่อคุณชื่นชอบโพสต์ โพสต์จะปรากฏที่นี่", + "empty_column.favourites": "ยังไม่มีใครชื่นชอบโพสต์นี้ เมื่อใครสักคนชื่นชอบ เขาจะปรากฏที่นี่", + "empty_column.follow_requests": "คุณยังไม่มีคำขอติดตามใด ๆ เมื่อคุณได้รับคำขอ คำขอจะปรากฏที่นี่", + "empty_column.hashtag": "ยังไม่มีสิ่งใดในแฮชแท็กนี้", + "empty_column.home": "เส้นเวลาหน้าแรกของคุณว่างเปล่า! เยี่ยมชม {public} หรือใช้การค้นหาเพื่อเริ่มต้นใช้งานและพบปะผู้ใช้อื่น ๆ", "empty_column.home.public_timeline": "เส้นเวลาสาธารณะ", - "empty_column.list": "There is nothing in this list yet.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", - "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", + "empty_column.list": "ยังไม่มีสิ่งใดในรายการนี้ เมื่อสมาชิกของรายการนี้โพสต์สถานะใหม่ สถานะจะปรากฏที่นี่", + "empty_column.lists": "คุณยังไม่มีรายการใด ๆ เมื่อคุณสร้างรายการ รายการจะปรากฏที่นี่", + "empty_column.mutes": "คุณยังไม่ได้ปิดเสียงผู้ใช้ใด ๆ", + "empty_column.notifications": "คุณยังไม่มีการแจ้งเตือนใด ๆ โต้ตอบกับผู้อื่นเพื่อเริ่มการสนทนา", + "empty_column.public": "ไม่มีสิ่งใดที่นี่! เขียนบางอย่างเป็นสาธารณะ หรือติดตามผู้ใช้จากเซิร์ฟเวอร์อื่น ๆ ด้วยตนเองเพื่อเติมให้เต็ม", + "error.unexpected_crash.explanation": "หน้านี้ไม่สามารถแสดงผลได้อย่างถูกต้อง เนื่องจากบั๊กในโค้ดของเราหรือปัญหาความเข้ากับได้กับเบราเซอร์", + "error.unexpected_crash.next_steps": "ลองรีเฟรชหน้านี้ หากไม่สามารถช่วยได้ คุณอาจจะยังคงใช้ Mastodon ผ่านเบราเซอร์อื่นหรือผ่านแอปได้", + "errors.unexpected_crash.copy_stacktrace": "คัดลอกการติดตามสแตกไปยังคลิปบอร์ด", + "errors.unexpected_crash.report_issue": "รายงานปัญหา", "follow_request.authorize": "อนุญาต", "follow_request.reject": "ปฏิเสธ", "getting_started.developers": "นักพัฒนา", @@ -141,7 +163,7 @@ "getting_started.documentation": "เอกสารประกอบ", "getting_started.heading": "เริ่มต้นใช้งาน", "getting_started.invite": "เชิญผู้คน", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.open_source_notice": "Mastodon เป็นซอฟต์แวร์เปิดต้นฉบับ คุณสามารถมีส่วนร่วมหรือรายงานปัญหาที่ GitHub ที่ {github}", "getting_started.security": "ความปลอดภัย", "getting_started.terms": "เงื่อนไขการให้บริการ", "hashtag.column_header.tag_mode.all": "และ {additional}", @@ -149,69 +171,69 @@ "hashtag.column_header.tag_mode.none": "โดยไม่มี {additional}", "hashtag.column_settings.select.no_options_message": "ไม่พบข้อเสนอแนะ", "hashtag.column_settings.select.placeholder": "ป้อนแฮชแท็ก…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", - "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "hashtag.column_settings.tag_mode.all": "ทั้งหมดนี้", + "hashtag.column_settings.tag_mode.any": "ใดก็ตามนี้", + "hashtag.column_settings.tag_mode.none": "ไม่ใช่ทั้งหมดนี้", + "hashtag.column_settings.tag_toggle": "รวมแท็กเพิ่มเติมสำหรับคอลัมน์นี้", "home.column_settings.basic": "พื้นฐาน", "home.column_settings.show_reblogs": "แสดงการดัน", "home.column_settings.show_replies": "แสดงการตอบกลับ", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "{number, plural, other {# วัน}}", + "intervals.full.hours": "{number, plural, other {# ชั่วโมง}}", + "intervals.full.minutes": "{number, plural, other {# นาที}}", "introduction.federation.action": "ถัดไป", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "keyboard_shortcuts.back": "to navigate back", - "keyboard_shortcuts.blocked": "to open blocked users list", - "keyboard_shortcuts.boost": "to boost", - "keyboard_shortcuts.column": "to focus a status in one of the columns", - "keyboard_shortcuts.compose": "to focus the compose textarea", + "introduction.federation.federated.headline": "ที่ติดต่อกับภายนอก", + "introduction.federation.federated.text": "โพสต์สาธารณะจากเซิร์ฟเวอร์อื่น ๆ ของเฟดิเวิร์สจะปรากฏในเส้นเวลาที่ติดต่อกับภายนอก", + "introduction.federation.home.headline": "หน้าแรก", + "introduction.federation.home.text": "โพสต์จากผู้คนที่คุณติดตามจะปรากฏในฟีดหน้าแรกของคุณ คุณสามารถติดตามใครก็ตามในเซิร์ฟเวอร์ใดก็ตาม!", + "introduction.federation.local.headline": "ในเว็บ", + "introduction.federation.local.text": "โพสต์สาธารณะจากผู้คนในเซิร์ฟเวอร์เดียวกันกับคุณจะปรากฏในเส้นเวลาในเว็บ", + "introduction.interactions.action": "เสร็จสิ้นบทช่วยสอน!", + "introduction.interactions.favourite.headline": "ชื่นชอบ", + "introduction.interactions.favourite.text": "คุณสามารถบันทึกโพสต์เพื่ออ่านทีหลัง และแจ้งให้ผู้เขียนโพสต์ทราบว่าคุณชอบโพสต์นั้นโดยการชื่นชอบโพสต์", + "introduction.interactions.reblog.headline": "ดัน", + "introduction.interactions.reblog.text": "คุณสามารถแชร์โพสต์ของผู้อื่นให้กับผู้ติดตามของคุณได้โดยการดันโพสต์", + "introduction.interactions.reply.headline": "ตอบกลับ", + "introduction.interactions.reply.text": "คุณสามารถตอบกลับโพสต์ของผู้อื่นและโพสต์ของคุณได้ ซึ่งจะถูกรวมเข้าด้วยกันเป็นบทสนทนา", + "introduction.welcome.action": "ไปกันเลย!", + "introduction.welcome.headline": "ขั้นตอนแรก", + "introduction.welcome.text": "ยินดีต้อนรับสู่เฟดิเวิร์ส! ในอีกสักครู่คุณจะได้เผยแพร่ข้อความและคุยกับเพื่อนของคุณในหลากหลายเซิร์ฟเวอร์ แต่เซิร์ฟเวอร์ {domain} นี้มีความพิเศษ เพราะเป็นที่ที่โปรไฟล์ของคุณตั้งอยู่ จำชื่อไว้ด้วยนะ", + "keyboard_shortcuts.back": "เพื่อนำทางย้อนกลับ", + "keyboard_shortcuts.blocked": "เพื่อเปิดรายการผู้ใช้ที่ปิดกั้นอยู่", + "keyboard_shortcuts.boost": "เพื่อดัน", + "keyboard_shortcuts.column": "เพื่อโฟกัสสถานะในหนึ่งในคอลัมน์", + "keyboard_shortcuts.compose": "เพื่อโฟกัสพื้นที่เขียนข้อความ", "keyboard_shortcuts.description": "คำอธิบาย", - "keyboard_shortcuts.direct": "to open direct messages column", - "keyboard_shortcuts.down": "to move down in the list", - "keyboard_shortcuts.enter": "to open status", - "keyboard_shortcuts.favourite": "to favourite", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", - "keyboard_shortcuts.heading": "Keyboard Shortcuts", - "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.direct": "เพื่อเปิดคอลัมน์ข้อความโดยตรง", + "keyboard_shortcuts.down": "เพื่อย้ายลงในรายการ", + "keyboard_shortcuts.enter": "เพื่อเปิดสถานะ", + "keyboard_shortcuts.favourite": "เพื่อชื่นชอบ", + "keyboard_shortcuts.favourites": "เพื่อเปิดรายการโปรด", + "keyboard_shortcuts.federated": "เพื่อเปิดเส้นเวลาที่ติดต่อกับภายนอก", + "keyboard_shortcuts.heading": "แป้นพิมพ์ลัด", + "keyboard_shortcuts.home": "เพื่อเปิดเส้นเวลาหน้าแรก", "keyboard_shortcuts.hotkey": "ปุ่มลัด", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.local": "to open local timeline", - "keyboard_shortcuts.mention": "to mention author", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.reply": "to reply", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "to focus search", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "to start a brand new toot", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", - "keyboard_shortcuts.up": "to move up in the list", + "keyboard_shortcuts.legend": "เพื่อแสดงคำอธิบายนี้", + "keyboard_shortcuts.local": "เพื่อเปิดเส้นเวลาในเว็บ", + "keyboard_shortcuts.mention": "เพื่อกล่าวถึงผู้สร้าง", + "keyboard_shortcuts.muted": "เพื่อเปิดรายการผู้ใช้ที่ปิดเสียงอยู่", + "keyboard_shortcuts.my_profile": "เพื่อเปิดโปรไฟล์ของคุณ", + "keyboard_shortcuts.notifications": "เพื่อเปิดคอลัมน์การแจ้งเตือน", + "keyboard_shortcuts.pinned": "เพื่อเปิดรายการโพสต์ที่ปักหมุด", + "keyboard_shortcuts.profile": "เพื่อเปิดโปรไฟล์ของผู้สร้าง", + "keyboard_shortcuts.reply": "เพื่อตอบกลับ", + "keyboard_shortcuts.requests": "เพื่อเปิดรายการคำขอติดตาม", + "keyboard_shortcuts.search": "เพื่อโฟกัสการค้นหา", + "keyboard_shortcuts.start": "เพื่อเปิดคอลัมน์ \"เริ่มต้นใช้งาน\"", + "keyboard_shortcuts.toggle_hidden": "เพื่อแสดง/ซ่อนข้อความที่อยู่หลังคำเตือนเนื้อหา", + "keyboard_shortcuts.toggle_sensitivity": "เพื่อแสดง/ซ่อนสื่อ", + "keyboard_shortcuts.toot": "เพื่อเริ่มโพสต์ใหม่", + "keyboard_shortcuts.unfocus": "เพื่อเลิกโฟกัสพื้นที่เขียนข้อความ/การค้นหา", + "keyboard_shortcuts.up": "เพื่อย้ายขึ้นในรายการ", "lightbox.close": "ปิด", "lightbox.next": "ถัดไป", "lightbox.previous": "ก่อนหน้า", - "lightbox.view_context": "View context", + "lightbox.view_context": "ดูบริบท", "lists.account.add": "เพิ่มไปยังรายการ", "lists.account.remove": "เอาออกจากรายการ", "lists.delete": "ลบรายการ", @@ -221,12 +243,13 @@ "lists.new.title_placeholder": "ชื่อเรื่องรายการใหม่", "lists.search": "ค้นหาในหมู่ผู้คนที่คุณติดตาม", "lists.subheading": "รายการของคุณ", + "load_pending": "{count, plural, other {# รายการใหม่}}", "loading_indicator.label": "กำลังโหลด...", "media_gallery.toggle_visible": "เปิด/ปิดการมองเห็น", "missing_indicator.label": "ไม่พบ", "missing_indicator.sublabel": "ไม่พบทรัพยากรนี้", "mute_modal.hide_notifications": "ซ่อนการแจ้งเตือนจากผู้ใช้นี้?", - "navigation_bar.apps": "แอปสำหรับมือถือ", + "navigation_bar.apps": "แอปมือถือ", "navigation_bar.blocks": "ผู้ใช้ที่ปิดกั้นอยู่", "navigation_bar.community_timeline": "เส้นเวลาในเว็บ", "navigation_bar.compose": "เขียนโพสต์ใหม่", @@ -237,7 +260,7 @@ "navigation_bar.favourites": "รายการโปรด", "navigation_bar.filters": "คำที่ปิดเสียงอยู่", "navigation_bar.follow_requests": "คำขอติดตาม", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "การติดตามและผู้ติดตาม", "navigation_bar.info": "เกี่ยวกับเซิร์ฟเวอร์นี้", "navigation_bar.keyboard_shortcuts": "ปุ่มลัด", "navigation_bar.lists": "รายการ", @@ -246,24 +269,23 @@ "navigation_bar.personal": "ส่วนบุคคล", "navigation_bar.pins": "โพสต์ที่ปักหมุด", "navigation_bar.preferences": "การกำหนดลักษณะ", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "เส้นเวลาที่ติดต่อกับภายนอก", "navigation_bar.security": "ความปลอดภัย", "notification.favourite": "{name} ได้ชื่นชอบสถานะของคุณ", "notification.follow": "{name} ได้ติดตามคุณ", "notification.mention": "{name} ได้กล่าวถึงคุณ", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "โพลที่คุณได้ลงคะแนนได้สิ้นสุดแล้ว", "notification.reblog": "{name} ได้ดันสถานะของคุณ", "notifications.clear": "ล้างการแจ้งเตือน", "notifications.clear_confirmation": "คุณแน่ใจหรือไม่ว่าต้องการล้างการแจ้งเตือนทั้งหมดของคุณอย่างถาวร?", - "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.alert": "การแจ้งเตือนบนเดสก์ท็อป", "notifications.column_settings.favourite": "รายการโปรด:", "notifications.column_settings.filter_bar.advanced": "แสดงหมวดหมู่ทั้งหมด", "notifications.column_settings.filter_bar.category": "แถบตัวกรองด่วน", "notifications.column_settings.filter_bar.show": "แสดง", "notifications.column_settings.follow": "ผู้ติดตามใหม่:", "notifications.column_settings.mention": "การกล่าวถึง:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "ผลลัพธ์โพล:", "notifications.column_settings.push": "การแจ้งเตือนแบบผลัก", "notifications.column_settings.reblog": "การดัน:", "notifications.column_settings.show": "แสดงในคอลัมน์", @@ -273,15 +295,17 @@ "notifications.filter.favourites": "รายการโปรด", "notifications.filter.follows": "การติดตาม", "notifications.filter.mentions": "การกล่าวถึง", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "ผลลัพธ์โพล", "notifications.group": "{count} การแจ้งเตือน", "poll.closed": "ปิดแล้ว", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", - "privacy.change": "Adjust status privacy", + "poll.refresh": "รีเฟรช", + "poll.total_people": "{count, plural, other {# คน}}", + "poll.total_votes": "{count, plural, other {# การลงคะแนน}}", + "poll.vote": "ลงคะแนน", + "poll.voted": "คุณได้ลงคะแนนให้กับคำตอบนี้", + "poll_button.add_poll": "เพิ่มโพล", + "poll_button.remove_poll": "เอาโพลออก", + "privacy.change": "ปรับเปลี่ยนความเป็นส่วนตัวของสถานะ", "privacy.direct.long": "โพสต์ไปยังผู้ใช้ที่กล่าวถึงเท่านั้น", "privacy.direct.short": "โดยตรง", "privacy.private.long": "โพสต์ไปยังผู้ติดตามเท่านั้น", @@ -290,8 +314,9 @@ "privacy.public.short": "สาธารณะ", "privacy.unlisted.long": "ไม่โพสต์ไปยังเส้นเวลาสาธารณะ", "privacy.unlisted.short": "ไม่อยู่ในรายการ", + "refresh": "รีเฟรช", "regeneration_indicator.label": "กำลังโหลด…", - "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "regeneration_indicator.sublabel": "กำลังเตรียมฟีดหน้าแรกของคุณ!", "relative_time.days": "{number} วัน", "relative_time.hours": "{number} ชั่วโมง", "relative_time.just_now": "ตอนนี้", @@ -299,22 +324,23 @@ "relative_time.seconds": "{number} วินาที", "reply_indicator.cancel": "ยกเลิก", "report.forward": "ส่งต่อไปยัง {target}", - "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", - "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "report.forward_hint": "บัญชีมาจากเซิร์ฟเวอร์อื่น ส่งสำเนาของรายงานที่ไม่ระบุตัวตนไปที่นั่นด้วย?", + "report.hint": "จะส่งรายงานไปยังผู้ควบคุมเซิร์ฟเวอร์ของคุณ คุณสามารถให้คำอธิบายเหตุผลที่คุณรายงานบัญชีนี้ด้านล่าง:", "report.placeholder": "ความคิดเห็นเพิ่มเติม", "report.submit": "ส่ง", "report.target": "กำลังรายงาน {target}", "search.placeholder": "ค้นหา", "search_popout.search_format": "รูปแบบการค้นหาขั้นสูง", - "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.full_text": "ข้อความแบบง่ายส่งคืนสถานะที่คุณได้เขียน ชื่นชอบ ดัน หรือได้รับการกล่าวถึง ตลอดจนชื่อผู้ใช้, ชื่อที่แสดง และแฮชแท็กที่ตรงกัน", "search_popout.tips.hashtag": "แฮชแท็ก", "search_popout.tips.status": "สถานะ", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.text": "ข้อความแบบง่ายส่งคืนชื่อที่แสดง, ชื่อผู้ใช้ และแฮชแท็กที่ตรงกัน", "search_popout.tips.user": "ผู้ใช้", "search_results.accounts": "ผู้คน", "search_results.hashtags": "แฮชแท็ก", "search_results.statuses": "โพสต์", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.statuses_fts_disabled": "การค้นหาเนื้อหาในโพสต์ถูกปิดบนเซิร์ฟเวอร์ Mastodon นี้", + "search_results.total": "{count, number} {count, plural, other {ผลลัพธ์}}", "status.admin_account": "เปิดส่วนติดต่อการควบคุมสำหรับ @{name}", "status.admin_status": "เปิดสถานะนี้ในส่วนติดต่อการควบคุม", "status.block": "ปิดกั้น @{name}", @@ -324,7 +350,7 @@ "status.delete": "ลบ", "status.detailed_status": "มุมมองการสนทนาโดยละเอียด", "status.direct": "ส่งข้อความโดยตรงถึง @{name}", - "status.embed": "Embed", + "status.embed": "ฝัง", "status.favourite": "ชื่นชอบ", "status.filtered": "กรองอยู่", "status.load_more": "โหลดเพิ่มเติม", @@ -340,7 +366,7 @@ "status.reblog": "ดัน", "status.reblog_private": "ดันไปยังผู้ชมดั้งเดิม", "status.reblogged_by": "{name} ได้ดัน", - "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", + "status.reblogs.empty": "ยังไม่มีใครดันโพสต์นี้ เมื่อใครสักคนดัน เขาจะปรากฏที่นี่", "status.redraft": "ลบแล้วร่างใหม่", "status.reply": "ตอบกลับ", "status.replyAll": "ตอบกลับกระทู้", @@ -352,6 +378,7 @@ "status.show_more": "แสดงเพิ่มเติม", "status.show_more_all": "แสดงเพิ่มเติมทั้งหมด", "status.show_thread": "แสดงกระทู้", + "status.uncached_media_warning": "ไม่สามารถดูได้", "status.unmute_conversation": "เลิกปิดเสียงการสนทนา", "status.unpin": "ถอนหมุดจากโปรไฟล์", "suggestions.dismiss": "ยกเลิกข้อเสนอแนะ", @@ -361,20 +388,28 @@ "tabs_bar.local_timeline": "ในเว็บ", "tabs_bar.notifications": "การแจ้งเตือน", "tabs_bar.search": "ค้นหา", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "time_remaining.days": "เหลืออีก {number, plural, other {# วัน}}", + "time_remaining.hours": "เหลืออีก {number, plural, other {# ชั่วโมง}}", + "time_remaining.minutes": "เหลืออีก {number, plural, other {# นาที}}", + "time_remaining.moments": "ช่วงเวลาที่เหลือ", + "time_remaining.seconds": "เหลืออีก {number, plural, other {# วินาที}}", + "trends.count_by_accounts": "{count} {rawCount, plural, other {คน}}กำลังคุย", + "trends.trending_now": "กำลังนิยม", "ui.beforeunload": "แบบร่างของคุณจะหายไปหากคุณออกจาก Mastodon", "upload_area.title": "ลากแล้วปล่อยเพื่ออัปโหลด", "upload_button.label": "เพิ่มสื่อ (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "เกินขีดจำกัดการอัปโหลดไฟล์", - "upload_error.poll": "File upload not allowed with polls.", - "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "ตัวอย่างการเปลี่ยนแปลง", + "upload_error.poll": "ไม่อนุญาตให้อัปโหลดไฟล์กับการลงคะแนน", + "upload_form.description": "อธิบายสำหรับผู้บกพร่องทางการมองเห็น", + "upload_form.edit": "แก้ไข", "upload_form.undo": "ลบ", + "upload_modal.analyzing_picture": "กำลังวิเคราะห์รูปภาพ…", + "upload_modal.apply": "นำไปใช้", + "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.preview_label": "ตัวอย่าง ({ratio})", "upload_progress.label": "กำลังอัปโหลด...", "video.close": "ปิดวิดีโอ", "video.exit_fullscreen": "ออกจากเต็มหน้าจอ", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index ec4657b9b..89efafd1d 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -1,43 +1,49 @@ { "account.add_or_remove_from_list": "Listelere ekle veya kaldır", "account.badges.bot": "Bot", - "account.block": "Engelle @{name}", + "account.block": "@{name} adlı kişiyi engelle", "account.block_domain": "{domain} alanından her şeyi gizle", "account.blocked": "Engellenmiş", + "account.cancel_follow_request": "Takip isteğini iptal et", "account.direct": "Mesaj gönder : @{name}", "account.domain_blocked": "Alan adı gizlendi", "account.edit_profile": "Profili düzenle", "account.endorse": "Profildeki özellik", "account.follow": "Takip et", - "account.followers": "Takipçiler", + "account.followers": "Takipçi", "account.followers.empty": "Henüz kimse bu kullanıcıyı takip etmiyor.", "account.follows": "Takip ettikleri", "account.follows.empty": "Bu kullanıcı henüz kimseyi takip etmiyor.", "account.follows_you": "Seni takip ediyor", - "account.hide_reblogs": "@{name} kişisinden boost'ları gizle", + "account.hide_reblogs": "@{name} kişisinin yinelemelerini gizle", + "account.last_status": "Son aktivite", "account.link_verified_on": "Bu bağlantının mülkiyeti {date} tarihinde kontrol edildi", - "account.locked_info": "Bu hesabın gizlilik durumu kilitli olarak ayarlanmış. Sahibi, onu kimin takip edebileceğini elle inceler.", + "account.locked_info": "Bu hesabın gizlilik durumu kilitli olarak ayarlanmış. Sahibi, onu kimin takip edebileceğini elle inceliyor.", "account.media": "Medya", "account.mention": "@{name} kullanıcısından bahset", "account.moved_to": "{name} şuraya taşındı:", - "account.mute": "@{name} kullanıcısını sessize al", - "account.mute_notifications": "@{name} kullanıcısının bildirimlerini kapat", - "account.muted": "Sesi kısık", - "account.posts": "Gönderiler", - "account.posts_with_replies": "Gönderiler ve yanıtlar", - "account.report": "@{name} kullanıcısını bildir", - "account.requested": "Onay bekliyor. Takip isteğini iptal etmek için tıklayın", + "account.mute": "@{name} adlı kişiyi sessize al", + "account.mute_notifications": "@{name} adlı kişinin bildirimlerini kapat", + "account.muted": "Susturuldu", + "account.never_active": "Asla", + "account.posts": "Toot", + "account.posts_with_replies": "Tootlar ve cevaplar", + "account.report": "@{name} adlı kişiyi bildir", + "account.requested": "Onay Bekleniyor. Takip isteğini iptal etmek için tıklayın", "account.share": "@{name} kullanıcısının profilini paylaş", - "account.show_reblogs": "@{name} kullanıcısından boostları göster", - "account.unblock": "Engeli kaldır @{name}", + "account.show_reblogs": "@{name} kullanıcısının yinelemelerini göster", + "account.unblock": "@{name} adlı kişinin engelini kaldır", "account.unblock_domain": "{domain} göster", "account.unendorse": "Profilde özellik yok", - "account.unfollow": "Takipten vazgeç", - "account.unmute": "Sesi aç : @{name}", - "account.unmute_notifications": "@{name} kullanıcısından bildirimleri aç", + "account.unfollow": "Takipi bırak", + "account.unmute": "@{name} adlı kişinin sesini aç", + "account.unmute_notifications": "@{name} adlı kişinin bildirimlerini aç", + "alert.rate_limited.message": "Lütfen sonra tekrar deneyin {retry_time, time, medium}.", + "alert.rate_limited.title": "Oran sınırlıdır", "alert.unexpected.message": "Beklenmedik bir hata oluştu.", "alert.unexpected.title": "Hay aksi!", - "boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz", + "autosuggest_hashtag.per_week": "Haftada {count}", + "boost_modal.combo": "Bir daha ki sefere {combo} tuşuna basabilirsiniz", "bundle_column_error.body": "Bu bileşen yüklenirken bir şeyler ters gitti.", "bundle_column_error.retry": "Tekrar deneyin", "bundle_column_error.title": "Ağ hatası", @@ -47,6 +53,7 @@ "column.blocks": "Engellenen kullanıcılar", "column.community": "Yerel zaman tüneli", "column.direct": "Doğrudan mesajlar", + "column.directory": "Profillere göz at", "column.domain_blocks": "Gizli alan adları", "column.favourites": "Favoriler", "column.follow_requests": "Takip istekleri", @@ -54,7 +61,7 @@ "column.lists": "Listeler", "column.mutes": "Susturulmuş kullanıcılar", "column.notifications": "Bildirimler", - "column.pins": "Sabitlenmiş gönderi", + "column.pins": "Sabitlenmiş tootlar", "column.public": "Federe zaman tüneli", "column_back_button.label": "Geri", "column_header.hide_settings": "Ayarları gizle", @@ -65,9 +72,9 @@ "column_header.unpin": "Sabitlemeyi kaldır", "column_subheading.settings": "Ayarlar", "community.column_settings.media_only": "Sadece medya", - "compose_form.direct_message_warning": "Bu gönderi sadece belirtilen kullanıcılara gönderilecektir.", + "compose_form.direct_message_warning": "Bu toot sadece belirtilen kullanıcılara gönderilecektir.", "compose_form.direct_message_warning_learn_more": "Daha fazla bilgi edinin", - "compose_form.hashtag_warning": "Bu paylaşım liste dışı olduğu için hiç bir hashtag'de yer almayacak. Sadece herkese açık gönderiler hashtaglerde bulunabilir.", + "compose_form.hashtag_warning": "Bu toot liste dışı olduğu için hiç bir etikette yer almayacak. Sadece herkese açık tootlar etiketlerde bulunabilir.", "compose_form.lock_disclaimer": "Hesabınız {locked} değil. Sadece takipçilerle paylaştığınız gönderileri görebilmek için sizi herhangi bir kullanıcı takip edebilir.", "compose_form.lock_disclaimer.lock": "kilitli", "compose_form.placeholder": "Aklınızdan ne geçiyor?", @@ -75,9 +82,9 @@ "compose_form.poll.duration": "Anket süresi", "compose_form.poll.option_placeholder": "Seçim {number}", "compose_form.poll.remove_option": "Bu seçimi kaldır", - "compose_form.publish": "Gönder", + "compose_form.publish": "Tootla", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Medyayı hassas olarak işaretle", "compose_form.sensitive.marked": "Medya hassas olarak işaretlendi", "compose_form.sensitive.unmarked": "Medya hassas olarak işaretlenmemiş", "compose_form.spoiler.marked": "Metin uyarının arkasına gizlenir", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "Bu listeyi kalıcı olarak silmek istediğinize emin misiniz?", "confirmations.domain_block.confirm": "Alan adının tamamını gizle", "confirmations.domain_block.message": "tüm {domain} alan adını engellemek istediğinizden emin misiniz? Genellikle birkaç hedefli engel ve susturma işi görür ve tercih edilir.", + "confirmations.logout.confirm": "Çıkış Yap", + "confirmations.logout.message": "Çıkış yapmak istediğinize emin misiniz?", "confirmations.mute.confirm": "Sessize al", + "confirmations.mute.explanation": "Bu onlardan gelen ve onlardan bahseden gönderileri gizleyecek, fakat yine de onların gönderilerinizi görmelerine ve sizi takip etmelerine izin verecektir.", "confirmations.mute.message": "{name} kullanıcısını sessize almak istiyor musunuz?", "confirmations.redraft.confirm": "Sil ve yeniden tasarla", "confirmations.redraft.message": "Bu durumu silip tekrar taslaklaştırmak istediğinizden emin misiniz? Tüm cevapları, boostları ve favorileri kaybedeceksiniz.", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Şimdi yanıtlarken o an oluşturduğunuz mesajın üzerine yazılır. Devam etmek istediğinize emin misiniz?", "confirmations.unfollow.confirm": "Takibi kaldır", "confirmations.unfollow.message": "{name}'yi takipten çıkarmak istediğinizden emin misiniz?", + "conversation.delete": "Konuşmayı sil", + "conversation.mark_as_read": "Okunmuş olarak işaretle", + "conversation.open": "Konuşmayı görüntüle", + "conversation.with": "{names} ile", + "directory.federated": "Bilinen fediverse'lerden", + "directory.local": "Yalnızca {domain} adresinden", + "directory.new_arrivals": "Yeni gelenler", + "directory.recently_active": "Son zamanlarda aktif", "embed.instructions": "Aşağıdaki kodu kopyalayarak bu durumu sitenize gömün.", "embed.preview": "İşte nasıl görüneceği:", "emoji_button.activity": "Aktivite", @@ -117,14 +135,14 @@ "emoji_button.search_results": "Arama sonuçları", "emoji_button.symbols": "Semboller", "emoji_button.travel": "Seyahat ve Yerler", - "empty_column.account_timeline": "Burada hiç gönderi yok!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_timeline": "Burada hiç toot yok!", + "empty_column.account_unavailable": "Profil kullanılamıyor", "empty_column.blocks": "Henüz bir kullanıcıyı engellemediniz.", "empty_column.community": "Yerel zaman çizelgesi boş. Daha fazla eğlence için herkese açık bir gönderi paylaşın!", "empty_column.direct": "Henüz doğrudan mesajınız yok. Bir tane gönderdiğinizde veya aldığınızda burada görünecektir.", "empty_column.domain_blocks": "Henüz hiçbir gizli alan adı yok.", - "empty_column.favourited_statuses": "Hiç favori gönderiminiz yok. Bir tane olursa burada görünecek.", - "empty_column.favourites": "Kimse bu gönderiyi favorilerine eklememiş. Biri eklerse burada görünecek.", + "empty_column.favourited_statuses": "Hiç favori tootunuz yok. Bir tane olduğunda burada görünecek.", + "empty_column.favourites": "Kimse bu tootu favorilerine eklememiş. Biri eklediğinde burada görünecek.", "empty_column.follow_requests": "Hiç takip isteğiniz yok. Bir tane aldığınızda burada görünecek.", "empty_column.hashtag": "Henüz bu hashtag’e sahip hiçbir gönderi yok.", "empty_column.home": "Henüz kimseyi takip etmiyorsunuz. {public} ziyaret edebilir veya arama kısmını kullanarak diğer kullanıcılarla iletişime geçebilirsiniz.", @@ -134,6 +152,10 @@ "empty_column.mutes": "Henüz hiçbir kullanıcıyı sessize almadınız.", "empty_column.notifications": "Henüz hiçbir bildiriminiz yok. Diğer insanlarla sobhet edebilmek için etkileşime geçebilirsiniz.", "empty_column.public": "Burada hiçbir şey yok! Herkese açık bir şeyler yazın veya burayı doldurmak için diğer sunuculardaki kullanıcıları takip edin", + "error.unexpected_crash.explanation": "Bizim kodumuzdaki bir hatadan ya da tarayıcı uyumluluk sorunundan dolayı, bu sayfa düzgün görüntülenemedi.", + "error.unexpected_crash.next_steps": "Sayfayı yenilemeyi deneyin. Eğer bu yardımcı olmazsa, Mastodon'u farklı bir tarayıcı ya da yerel uygulama üzerinden kullanabilirsiniz.", + "errors.unexpected_crash.copy_stacktrace": "Yığın izlemeyi (stacktrace) panoya kopyala", + "errors.unexpected_crash.report_issue": "Sorun bildir", "follow_request.authorize": "Yetkilendir", "follow_request.reject": "Reddet", "getting_started.developers": "Geliştiriciler", @@ -156,23 +178,23 @@ "home.column_settings.basic": "Temel", "home.column_settings.show_reblogs": "Boost edilenleri göster", "home.column_settings.show_replies": "Cevapları göster", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.days": "{number, plural, one {# gün} other {# gün}}", + "intervals.full.hours": "{number, plural, one {# saat} other {# saat}}", + "intervals.full.minutes": "{number, plural, one {# dakika} other {# dakika}}", "introduction.federation.action": "İleri", "introduction.federation.federated.headline": "Birleşik", "introduction.federation.federated.text": "Diğer dosya sunucularından gelen genel gönderiler, birleşik zaman çizelgesinde görünecektir.", "introduction.federation.home.headline": "Ana sayfa", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.home.text": "Takip ettiğiniz kişilerin yayınları ana sayfada gösterilecek. Herhangi bir sunucudaki herkesi takip edebilirsiniz!", "introduction.federation.local.headline": "Yerel", "introduction.federation.local.text": "Aynı sunucudaki kişilerin gönderileri yerel zaman tünelinde gözükecektir.", "introduction.interactions.action": "Öğreticiyi bitirin!", "introduction.interactions.favourite.headline": "Favori", - "introduction.interactions.favourite.text": "Bir gönderiyi favorilerinize alarak sonrası için saklayabilirsiniz ve yazara gönderiyi beğendiğinizi söyleyebilirsiniz.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "Başkalarının gönderilerini boostlayarak kendi takipçilerinizle paylaşabillirsiniz.", + "introduction.interactions.favourite.text": "Bir tootu favorilerinize alarak sonrası için saklayabilirsiniz ve yazara tootu beğendiğinizi söyleyebilirsiniz.", + "introduction.interactions.reblog.headline": "Yinele", + "introduction.interactions.reblog.text": "Başkalarının tootlarını yineleyerek onları kendi takipçilerinizle paylaşabillirsiniz.", "introduction.interactions.reply.headline": "Yanıt", - "introduction.interactions.reply.text": "Başkalarının gönderilerini ve kendi gönderilerinizi yanıtlayabilirsiniz. Bir konuşmada zincirli bir şekilde olacaklardır.", + "introduction.interactions.reply.text": "Başkalarının ve kendinizin tootlarına cevap verebilirsiniz. Bu, onları bir konuşmada zincirli bir şekilde gösterecektir.", "introduction.welcome.action": "Hadi gidelim!", "introduction.welcome.headline": "İlk adımlar", "introduction.welcome.text": "Krallığa hoş geldiniz! Az sonra, geniş bir sunucu yelpazesinde mesaj gönderip arkadaşlarınızla konuşabileceksiniz. Ama bu sunucu, {domain}, özel (profilinizi barındırır, bu yüzden adresini hatırlayın).", @@ -197,21 +219,21 @@ "keyboard_shortcuts.muted": "susturulmuş kullanıcı listesini açmak için", "keyboard_shortcuts.my_profile": "profilinizi açmak için", "keyboard_shortcuts.notifications": "bildirimler sütununu açmak için", - "keyboard_shortcuts.pinned": "sabitlenmiş gönderiler listesini açmak için", + "keyboard_shortcuts.pinned": "sabitlenmiş tootların listesini açmak için", "keyboard_shortcuts.profile": "yazarın profilini açmak için", "keyboard_shortcuts.reply": "cevaplamak için", "keyboard_shortcuts.requests": "takip istekleri listesini açmak için", "keyboard_shortcuts.search": "aramaya odaklanmak için", "keyboard_shortcuts.start": "\"başlayın\" sütununu açmak için", "keyboard_shortcuts.toggle_hidden": "CW'den önceki yazıyı göstermek/gizlemek için", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "yeni bir gönderiye başlamak için", + "keyboard_shortcuts.toggle_sensitivity": "medyayı göstermek/gizlemek için", + "keyboard_shortcuts.toot": "yeni bir toot başlatmak için", "keyboard_shortcuts.unfocus": "aramada bir gönderiye odaklanmamak için", "keyboard_shortcuts.up": "listede yukarıya çıkmak için", "lightbox.close": "Kapat", "lightbox.next": "Sonraki", "lightbox.previous": "Önceli", - "lightbox.view_context": "View context", + "lightbox.view_context": "İçeriği göster", "lists.account.add": "Listeye ekle", "lists.account.remove": "Listeden kaldır", "lists.delete": "Listeyi sil", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "Yeni liste başlığı", "lists.search": "Takip ettiğiniz kişiler arasından arayın", "lists.subheading": "Listeleriniz", + "load_pending": "{count, plural, one {# yeni öğe} other {# yeni öğe}}", "loading_indicator.label": "Yükleniyor...", "media_gallery.toggle_visible": "Görünürlüğü değiştir", "missing_indicator.label": "Bulunamadı", @@ -229,7 +252,7 @@ "navigation_bar.apps": "Mobil uygulamalar", "navigation_bar.blocks": "Engellenen kullanıcılar", "navigation_bar.community_timeline": "Yerel zaman tüneli", - "navigation_bar.compose": "Yeni bir gönderi yazın", + "navigation_bar.compose": "Yeni toot oluştur", "navigation_bar.direct": "Direkt Mesajlar", "navigation_bar.discover": "Keşfet", "navigation_bar.domain_blocks": "Gizli alan adları", @@ -237,21 +260,20 @@ "navigation_bar.favourites": "Favoriler", "navigation_bar.filters": "Susturulmuş kelimeler", "navigation_bar.follow_requests": "Takip istekleri", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Takip edilenler ve takipçiler", "navigation_bar.info": "Genişletilmiş bilgi", "navigation_bar.keyboard_shortcuts": "Klavye kısayolları", "navigation_bar.lists": "Listeler", "navigation_bar.logout": "Çıkış", "navigation_bar.mutes": "Sessize alınmış kullanıcılar", "navigation_bar.personal": "Kişisel", - "navigation_bar.pins": "Sabitlenmiş gönderiler", + "navigation_bar.pins": "Sabitlenmiş tootlar", "navigation_bar.preferences": "Tercihler", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Federe zaman tüneli", "navigation_bar.security": "Güvenlik", "notification.favourite": "{name} senin durumunu favorilere ekledi", "notification.follow": "{name} seni takip ediyor", - "notification.mention": "{name} mentioned you", + "notification.mention": "{name} senden bahsetti", "notification.poll": "Oy verdiğiniz bir anket bitti", "notification.reblog": "{name} senin durumunu boost etti", "notifications.clear": "Bildirimleri temizle", @@ -277,8 +299,10 @@ "notifications.group": "{count} bildirim", "poll.closed": "Kapandı", "poll.refresh": "Yenile", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.total_people": "{count, plural, one {# kişi} other {# kişi}}", + "poll.total_votes": "{count, plural, one {# oy} other {# oy}}", "poll.vote": "Oy ver", + "poll.voted": "Bu cevap için oy kullandınız", "poll_button.add_poll": "Bir anket ekleyin", "poll_button.remove_poll": "Anket kaldır", "privacy.change": "Gönderi gizliliğini ayarla", @@ -290,6 +314,7 @@ "privacy.public.short": "Herkese açık", "privacy.unlisted.long": "Herkese açık zaman tüneline gönderme", "privacy.unlisted.short": "Listelenmemiş", + "refresh": "Yenile", "regeneration_indicator.label": "Yükleniyor…", "regeneration_indicator.sublabel": "Ev akışınız hazırlanıyor!", "relative_time.days": "{number}g", @@ -306,14 +331,15 @@ "report.target": "Raporlama", "search.placeholder": "Ara", "search_popout.search_format": "Gelişmiş arama formatı", - "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", - "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.full_text": "Basit metin yazdığınız, tercih ettiğiniz, yinelediğiniz veya bunlardan bahsettiğiniz durumların yanı sıra kullanıcı adlarını, görünen adları ve hashtag'leri eşleştiren durumları döndürür.", + "search_popout.tips.hashtag": "etiketler", "search_popout.tips.status": "durum", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.text": "Basit metin, eşleşen görünen adları, kullanıcı adlarını ve hashtag'leri döndürür", "search_popout.tips.user": "kullanıcı", "search_results.accounts": "İnsanlar", "search_results.hashtags": "Hashtagler", - "search_results.statuses": "Gönderiler", + "search_results.statuses": "Tootlar", + "search_results.statuses_fts_disabled": "Bu Mastodon sunucusunda toot içeriğine göre arama etkin değil.", "search_results.total": "{count, number} {count, plural, one {sonuç} other {sonuçlar}}", "status.admin_account": "@{name} için denetim arayüzünü açın", "status.admin_status": "Denetim arayüzünde bu durumu açın", @@ -335,12 +361,12 @@ "status.mute_conversation": "Yazışmayı sustur", "status.open": "Bu gönderiyi genişlet", "status.pin": "Profile sabitle", - "status.pinned": "Sabitlenmiş gönderi", + "status.pinned": "Sabitlenmiş toot", "status.read_more": "Daha dazla oku", "status.reblog": "Boostla", - "status.reblog_private": "Boost to original audience", + "status.reblog_private": "Orjinal kitleye yinele", "status.reblogged_by": "{name} boost etti", - "status.reblogs.empty": "Kimse bu gönderiyi boostlamadı. Biri yaptığında burada gözükecek.", + "status.reblogs.empty": "Henüz kimse bu tootu yinelemedi. Biri yaptığında burada görünecek.", "status.redraft": "Sil & tekrar taslakla", "status.reply": "Cevapla", "status.replyAll": "Konuşmayı cevapla", @@ -352,7 +378,8 @@ "status.show_more": "Daha fazla göster", "status.show_more_all": "Hepsi için daha fazla göster", "status.show_thread": "Başlığı göster", - "status.unmute_conversation": "Unmute conversation", + "status.uncached_media_warning": "Mevcut değil", + "status.unmute_conversation": "Sohbeti aç", "status.unpin": "Profilden sabitlemeyi kaldır", "suggestions.dismiss": "Öneriyi görmezden gel", "suggestions.header": "Şuna ilgi duyuyor olabilirsiniz…", @@ -361,20 +388,28 @@ "tabs_bar.local_timeline": "Yerel", "tabs_bar.notifications": "Bildirimler", "tabs_bar.search": "Ara", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "time_remaining.days": "{number, plural, one {# gün} other {# gün}} kaldı", + "time_remaining.hours": "{number, plural, one {# saat} other {# saat}} kaldı", + "time_remaining.minutes": "{number, plural, one {# dakika} other {# dakika}} kaldı", + "time_remaining.moments": "Sadece birkaç dakika kaldı", + "time_remaining.seconds": "{number, plural, one {# saniye} other {# saniye}} kaldı", + "trends.count_by_accounts": "{count} {rawCount, plural, one {kişi} other {kişi}} konuşuyor", + "trends.trending_now": "Şu an popüler", "ui.beforeunload": "Mastodon'dan ayrılırsanız taslağınız kaybolacak.", "upload_area.title": "Karşıya yükleme için sürükle bırak yapınız", "upload_button.label": "Görsel ekle", "upload_error.limit": "Dosya yükleme sınırı aşıldı.", "upload_error.poll": "Anketlerde dosya yüklemesine izin verilmez.", - "upload_form.description": "Describe for the visually impaired", - "upload_form.focus": "Kırp", + "upload_form.description": "Görme engelliler için açıklama", + "upload_form.edit": "Düzenle", "upload_form.undo": "Geri al", + "upload_modal.analyzing_picture": "Resmi analiz ediyor…", + "upload_modal.apply": "Uygula", + "upload_modal.description_placeholder": "Pijamalı hasta yağız şoföre çabucak güvendi", + "upload_modal.detect_text": "Resimdeki metni algıla", + "upload_modal.edit_media": "Medyayı düzenle", + "upload_modal.hint": "Her zaman tüm küçük resimlerde görüntülenecek odak noktasını seçmek için ön izlemedeki daireyi tıklayın veya sürükleyin.", + "upload_modal.preview_label": "Ön izleme ({ratio})", "upload_progress.label": "Yükleniyor...", "video.close": "Videoyu kapat", "video.exit_fullscreen": "Tam ekrandan çık", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 124b9fb07..c3b3e45b9 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -1,52 +1,59 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", + "account.add_or_remove_from_list": "Додати або видалити зі списків", "account.badges.bot": "Бот", "account.block": "Заблокувати @{name}", "account.block_domain": "Заглушити {domain}", "account.blocked": "Заблоковані", + "account.cancel_follow_request": "Скасувати запит на підписку", "account.direct": "Пряме повідомлення @{name}", "account.domain_blocked": "Домен приховано", "account.edit_profile": "Редагувати профіль", - "account.endorse": "Feature on profile", + "account.endorse": "Публікувати у профілі", "account.follow": "Підписатися", "account.followers": "Підписники", - "account.followers.empty": "No one follows this user yet.", + "account.followers.empty": "Ніхто ще не підписався на цього користувача.", "account.follows": "Підписки", - "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows.empty": "Цей користувач ще ні на кого не підписався.", "account.follows_you": "Підписаний(-а) на Вас", "account.hide_reblogs": "Сховати передмухи від @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.last_status": "Крайня активність", + "account.link_verified_on": "Права власності на це посилання були перевірені {date}", + "account.locked_info": "Статус конфіденційності цього облікового запису встановлено у заблокований. Власник вручну переглядає, хто може за ним стежити.", "account.media": "Медіа", "account.mention": "Згадати @{name}", "account.moved_to": "{name} переїхав на:", "account.mute": "Заглушити @{name}", "account.mute_notifications": "Не показувати сповіщення від @{name}", "account.muted": "Заглушений", + "account.never_active": "Ніколи", "account.posts": "Дмухи", "account.posts_with_replies": "Дмухи й відповіді", "account.report": "Поскаржитися на @{name}", "account.requested": "Очікує підтвердження. Натисніть щоб відмінити запит", "account.share": "Поширити профіль @{name}", "account.show_reblogs": "Показати передмухи від @{name}", - "account.unblock": "Розблокувати", + "account.unblock": "Розблокувати @{name}", "account.unblock_domain": "Розблокувати {domain}", - "account.unendorse": "Don't feature on profile", + "account.unendorse": "Не публікувати у профілі", "account.unfollow": "Відписатися", - "account.unmute": "Зняти глушення @{name}", + "account.unmute": "Зняти глушення з @{name}", "account.unmute_notifications": "Показувати сповіщення від @{name}", + "alert.rate_limited.message": "Спробуйте ще раз через {retry_time, time, medium}.", + "alert.rate_limited.title": "Швидкість обмежена", "alert.unexpected.message": "Трапилась неочікувана помилка.", "alert.unexpected.title": "Ой!", + "autosuggest_hashtag.per_week": "{count} в тиждень", "boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу", - "bundle_column_error.body": "Щось пішло не так при завантаженні компоненту.", - "bundle_column_error.retry": "Спробуйте ще", + "bundle_column_error.body": "Щось пішло не так під час завантаження компоненту.", + "bundle_column_error.retry": "Спробуйте ще раз", "bundle_column_error.title": "Помилка мережі", "bundle_modal_error.close": "Закрити", - "bundle_modal_error.message": "Щось пішло не так при завантаженні компоненту.", - "bundle_modal_error.retry": "Спробувати ще", + "bundle_modal_error.message": "Щось пішло не так під час завантаження компоненту.", + "bundle_modal_error.retry": "Спробувати ще раз", "column.blocks": "Заблоковані користувачі", "column.community": "Локальна стрічка", "column.direct": "Прямі повідомлення", + "column.directory": "Переглянути профілі", "column.domain_blocks": "Приховані домени", "column.favourites": "Вподобане", "column.follow_requests": "Запити на підписку", @@ -58,7 +65,7 @@ "column.public": "Глобальна стрічка", "column_back_button.label": "Назад", "column_header.hide_settings": "Приховати налаштування", - "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveLeft_settings": "Змістити колонку вліво", "column_header.moveRight_settings": "Змістити колонку вправо", "column_header.pin": "Закріпити", "column_header.show_settings": "Показати налаштування", @@ -66,25 +73,25 @@ "column_subheading.settings": "Налаштування", "community.column_settings.media_only": "Тільки медіа", "compose_form.direct_message_warning": "Цей дмух буде видимий тільки згаданим користувачам.", - "compose_form.direct_message_warning_learn_more": "Дізнатись більше", - "compose_form.hashtag_warning": "Цей дмух не буде відображений у жодній стрічці хештеґу, так як він прихований. Тільки публічні дмухи можуть бути знайдені за хештеґом.", + "compose_form.direct_message_warning_learn_more": "Дізнатися більше", + "compose_form.hashtag_warning": "Цей дмух не буде відображений у жодній стрічці хештеґу, оскільки він прихований. Тільки публічні дмухи можуть бути знайдені за хештеґом.", "compose_form.lock_disclaimer": "Ваш акаунт не {locked}. Кожен може підписатися на Вас та бачити Ваші приватні пости.", "compose_form.lock_disclaimer.lock": "приватний", "compose_form.placeholder": "Що у Вас на думці?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Додати варіант", + "compose_form.poll.duration": "Тривалість опитування", + "compose_form.poll.option_placeholder": "Варіант {number}", + "compose_form.poll.remove_option": "Видалити цей варіант", "compose_form.publish": "Дмухнути", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", - "compose_form.sensitive.marked": "Медіа відмічене <b>несприйнятливим</b>", - "compose_form.sensitive.unmarked": "Медіа відмічене сприйнятливим", - "compose_form.spoiler.marked": "Текст приховано за попередженням", + "compose_form.sensitive.hide": "Позначити медіа як дражливе", + "compose_form.sensitive.marked": "Медіа відмічене як дражливе", + "compose_form.sensitive.unmarked": "Медіа не відмічене як дражливе", + "compose_form.spoiler.marked": "Текст приховано під попередженням", "compose_form.spoiler.unmarked": "Текст видимий", - "compose_form.spoiler_placeholder": "Попередження щодо прихованого тексту", + "compose_form.spoiler_placeholder": "Напишіть своє попередження тут", "confirmation_modal.cancel": "Відмінити", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Заблокувати та поскаржитися", "confirmations.block.confirm": "Заблокувати", "confirmations.block.message": "Ви впевнені, що хочете заблокувати {name}?", "confirmations.delete.confirm": "Видалити", @@ -93,15 +100,26 @@ "confirmations.delete_list.message": "Ви впевнені, що хочете видалити цей список назавжди?", "confirmations.domain_block.confirm": "Сховати весь домен", "confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів. Ви не зможете бачити контент з цього домену у будь-яких стрічках або ваших сповіщеннях. Ваші підписники з цього домену будуть відписані від вас.", + "confirmations.logout.confirm": "Вийти", + "confirmations.logout.message": "Ви впевнені, що хочете вийти?", "confirmations.mute.confirm": "Заглушити", + "confirmations.mute.explanation": "Це приховає пости від них і пости зі згадками про них, проте вони все одно матимуть змогу бачити ваші пости і підписуватися на вас.", "confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?", - "confirmations.redraft.confirm": "Видалити і перестворити", + "confirmations.redraft.confirm": "Видалити та перестворити", "confirmations.redraft.message": "Ви впевнені, що хочете видалити допис і перестворити його? Ви втратите всі відповіді, передмухи та вподобайки допису.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.reply.confirm": "Відповісти", + "confirmations.reply.message": "Поточна відповідь перезапише повідомлення, яке ви зараз пишете. Ви впевнені, що хочете продовжити?", "confirmations.unfollow.confirm": "Відписатися", "confirmations.unfollow.message": "Ви впевнені, що хочете відписатися від {name}?", - "embed.instructions": "Інтегруйте цей статус на вашому вебсайті, скопіювавши код нижче.", + "conversation.delete": "Видалити цю бесіду", + "conversation.mark_as_read": "Позначити як прочитане", + "conversation.open": "Переглянути бесіду", + "conversation.with": "З {names}", + "directory.federated": "З відомого федесвіту", + "directory.local": "Тільки з домену {domain}", + "directory.new_arrivals": "Нові надходження", + "directory.recently_active": "Активні нещодавно", + "embed.instructions": "Вбудуйте цей статус до вашого вебсайту, скопіювавши код нижче.", "embed.preview": "Ось як він виглядатиме:", "emoji_button.activity": "Заняття", "emoji_button.custom": "Особливі", @@ -113,123 +131,128 @@ "emoji_button.objects": "Предмети", "emoji_button.people": "Люди", "emoji_button.recent": "Часто використовувані", - "emoji_button.search": "Знайти...", + "emoji_button.search": "Шукати...", "emoji_button.search_results": "Результати пошуку", "emoji_button.symbols": "Символи", "emoji_button.travel": "Подорожі", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.account_timeline": "Тут дмухалок немає!", + "empty_column.account_unavailable": "Профіль недоступний", + "empty_column.blocks": "Ви ще не заблокували жодного користувача.", "empty_column.community": "Локальна стрічка пуста. Напишіть щось, щоб розігріти народ!", "empty_column.direct": "У вас ще немає прямих повідомлень. Коли ви відправите чи отримаєте якесь, воно з'явиться тут.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.domain_blocks": "Тут поки немає прихованих доменів.", + "empty_column.favourited_statuses": "У вас ще немає вподобаних дмухів. Коли ви щось вподобаєте, воно з'явиться тут.", + "empty_column.favourites": "Ніхто ще не вподобав цього дмуху. Коли хтось це зробить, вони з'являться тут.", + "empty_column.follow_requests": "У вас ще немає запитів на підписку. Коли ви їх отримаєте, вони з'являться тут.", "empty_column.hashtag": "Дописів з цим хештегом поки не існує.", "empty_column.home": "Ви поки ні на кого не підписані. Погортайте {public}, або скористуйтесь пошуком, щоб освоїтися та познайомитися з іншими користувачами.", "empty_column.home.public_timeline": "публічні стрічки", "empty_column.list": "Немає нічого в цьому списку. Коли його учасники дмухнуть нові статуси, вони з'являться тут.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", + "empty_column.lists": "У вас ще немає списків. Коли ви їх створите, вони з'являться тут.", + "empty_column.mutes": "Ви ще не заглушили жодного користувача.", "empty_column.notifications": "У вас ще немає сповіщень. Переписуйтесь з іншими користувачами, щоб почати розмову.", "empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших інстанцій, щоб заповнити стрічку", + "error.unexpected_crash.explanation": "Ця сторінка не може бути коректно відображена через баґ у нашому коді або через проблему сумісності браузера.", + "error.unexpected_crash.next_steps": "Спробуйте перезавантажити сторінку. Якщо це не допоможе, ви все ще зможете використовувати Mastodon через інший браузер або рідний додаток.", + "errors.unexpected_crash.copy_stacktrace": "Скопіювати трасування стека у буфер обміну", + "errors.unexpected_crash.report_issue": "Повідомити про проблему", "follow_request.authorize": "Авторизувати", "follow_request.reject": "Відмовити", "getting_started.developers": "Розробникам", - "getting_started.directory": "Profile directory", + "getting_started.directory": "Каталог профілів", "getting_started.documentation": "Документація", "getting_started.heading": "Ласкаво просимо", "getting_started.invite": "Запросіть людей", - "getting_started.open_source_notice": "Mastodon - програма з відкритим вихідним кодом. Ви можете допомогти проекту, або повідомити про проблеми на GitHub за адресою {github}.", + "getting_started.open_source_notice": "Mastodon — програма з відкритим сирцевим кодом. Ви можете допомогти проекту, або повідомити про проблеми на GitHub за адресою {github}.", "getting_started.security": "Безпека", "getting_started.terms": "Умови використання", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", - "hashtag.column_settings.tag_mode.any": "Any of these", - "hashtag.column_settings.tag_mode.none": "None of these", - "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "hashtag.column_header.tag_mode.all": "та {additional}", + "hashtag.column_header.tag_mode.any": "або {additional}", + "hashtag.column_header.tag_mode.none": "без {additional}", + "hashtag.column_settings.select.no_options_message": "Не знайдено пропозицій", + "hashtag.column_settings.select.placeholder": "Введіть хештеґи…", + "hashtag.column_settings.tag_mode.all": "Усі ці", + "hashtag.column_settings.tag_mode.any": "Який-небудь з цих", + "hashtag.column_settings.tag_mode.none": "Жоден з цих", + "hashtag.column_settings.tag_toggle": "Додайте додаткові теґи до цього стовпчика", "home.column_settings.basic": "Основні", "home.column_settings.show_reblogs": "Показувати передмухи", "home.column_settings.show_replies": "Показувати відповіді", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", - "introduction.federation.action": "Next", - "introduction.federation.federated.headline": "Federated", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", - "introduction.interactions.action": "Finish toot-orial!", - "introduction.interactions.favourite.headline": "Favourite", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "intervals.full.days": "{number, plural, one {# день} few {# дні} other {# днів}}", + "intervals.full.hours": "{number, plural, one {# година} few {# години} other {# годин}}", + "intervals.full.minutes": "{number, plural, one {# хвилина} few {# хвилини} other {# хвилин}}", + "introduction.federation.action": "Далі", + "introduction.federation.federated.headline": "Глобальна", + "introduction.federation.federated.text": "Публічні пости з інших серверів федіверсу будуть з'являтися у глобальній стрічці.", + "introduction.federation.home.headline": "Головна", + "introduction.federation.home.text": "Пости від людей, за якими ви слідкуєте, з'являться у Вашій домашній стрічці. Ви можете слідкувати за кожним на будь-якому сервері!", + "introduction.federation.local.headline": "Локальна", + "introduction.federation.local.text": "Публічні пости від людей на сервері, на якому Ви знаходитесь, будуть з'являтися у локальній стрічці.", + "introduction.interactions.action": "Завершити вступ!", + "introduction.interactions.favourite.headline": "Улюблене", + "introduction.interactions.favourite.text": "Ви можете зберегти дмух на потім і повідомити автора, що він вам сподобався, додавши його в улюблене.", + "introduction.interactions.reblog.headline": "Передмухнути", + "introduction.interactions.reblog.text": "Ви можете ділитися дмухами інших людей зі своїми підписниками, передмухуючи їх.", + "introduction.interactions.reply.headline": "Відповісти", + "introduction.interactions.reply.text": "Ви можете відповідати на дмухи інших людей та власні, створюючи ланцюжки розмов.", + "introduction.welcome.action": "Поїхали!", + "introduction.welcome.headline": "Перші кроки", + "introduction.welcome.text": "Вітаємо у федіверсі! Невдовзі ви зможете поширювати повідомлення та спілкуватися зі своїми друзями на розмаїтті серверів. Але цей сервер, {domain}, є особливим — на ньому розміщений ваш профіль, тож запам'ятайте його назву.", "keyboard_shortcuts.back": "переходити назад", - "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.blocked": "відкрити список заблокованих користувачів", "keyboard_shortcuts.boost": "передмухувати", "keyboard_shortcuts.column": "фокусуватися на одній з колонок", "keyboard_shortcuts.compose": "фокусуватися на полі введення", "keyboard_shortcuts.description": "Опис", - "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.direct": "відкрити колонку прямих повідомлень", "keyboard_shortcuts.down": "рухатися вниз стрічкою", "keyboard_shortcuts.enter": "відкрити статус", "keyboard_shortcuts.favourite": "вподобати", - "keyboard_shortcuts.favourites": "to open favourites list", - "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.favourites": "відкрити список улюбленого", + "keyboard_shortcuts.federated": "відкрити глобальну стрічку", "keyboard_shortcuts.heading": "Гарячі клавіші", - "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.home": "відкрити домашню стрічку", "keyboard_shortcuts.hotkey": "Гаряча клавіша", "keyboard_shortcuts.legend": "показати підказку", - "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.local": "відкрити локальну стрічку", "keyboard_shortcuts.mention": "згадати автора", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.muted": "відкрити список заглушених користувачів", + "keyboard_shortcuts.my_profile": "відкрити ваш профіль", + "keyboard_shortcuts.notifications": "відкрити колонку сповіщень", + "keyboard_shortcuts.pinned": "відкрити список закріплених дмухів", "keyboard_shortcuts.profile": "відкрити профіль автора", "keyboard_shortcuts.reply": "відповісти", - "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.requests": "відкрити список бажаючих підписатися", "keyboard_shortcuts.search": "сфокусуватися на пошуку", - "keyboard_shortcuts.start": "to open \"get started\" column", - "keyboard_shortcuts.toggle_hidden": "показати/приховати прихований текст", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.start": "відкрити колонку \"Початок\"", + "keyboard_shortcuts.toggle_hidden": "показати/приховати текст під попередженням", + "keyboard_shortcuts.toggle_sensitivity": "показати/приховати медіа", "keyboard_shortcuts.toot": "почати писати новий дмух", "keyboard_shortcuts.unfocus": "розфокусуватися з нового допису чи пошуку", "keyboard_shortcuts.up": "рухатися вверх списком", "lightbox.close": "Закрити", "lightbox.next": "Далі", "lightbox.previous": "Назад", - "lightbox.view_context": "View context", + "lightbox.view_context": "Переглянути контекст", "lists.account.add": "Додати до списку", "lists.account.remove": "Видалити зі списку", "lists.delete": "Видалити список", "lists.edit": "Редагувати список", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Змінити назву", "lists.new.create": "Додати список", "lists.new.title_placeholder": "Нова назва списку", "lists.search": "Шукати серед людей, на яких ви підписані", "lists.subheading": "Ваші списки", + "load_pending": "{count, plural, one {# новий елемент} other {# нових елементів}}", "loading_indicator.label": "Завантаження...", "media_gallery.toggle_visible": "Показати/приховати", "missing_indicator.label": "Не знайдено", "missing_indicator.sublabel": "Ресурс не знайдений", "mute_modal.hide_notifications": "Приховати сповіщення від користувача?", - "navigation_bar.apps": "Mobile apps", + "navigation_bar.apps": "Мобільні додатки", "navigation_bar.blocks": "Заблоковані користувачі", "navigation_bar.community_timeline": "Локальна стрічка", - "navigation_bar.compose": "Compose new toot", + "navigation_bar.compose": "Написати новий дмух", "navigation_bar.direct": "Прямі повідомлення", "navigation_bar.discover": "Знайти", "navigation_bar.domain_blocks": "Приховані домени", @@ -237,61 +260,63 @@ "navigation_bar.favourites": "Вподобане", "navigation_bar.filters": "Приховані слова", "navigation_bar.follow_requests": "Запити на підписку", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Підписки і підписники", "navigation_bar.info": "Про сайт", - "navigation_bar.keyboard_shortcuts": "Гарячі клавіши", + "navigation_bar.keyboard_shortcuts": "Гарячі клавіші", "navigation_bar.lists": "Списки", "navigation_bar.logout": "Вийти", "navigation_bar.mutes": "Заглушені користувачі", "navigation_bar.personal": "Особисте", "navigation_bar.pins": "Закріплені дмухи", "navigation_bar.preferences": "Налаштування", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "Глобальна стрічка", "navigation_bar.security": "Безпека", "notification.favourite": "{name} вподобав(-ла) ваш допис", "notification.follow": "{name} підписався(-лась) на Вас", "notification.mention": "{name} згадав(-ла) Вас", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "Опитування, у якому ви голосували, закінчилося", "notification.reblog": "{name} передмухнув(-ла) Ваш допис", "notifications.clear": "Очистити сповіщення", "notifications.clear_confirmation": "Ви впевнені, що хочете назавжди видалити всі сповіщеня?", "notifications.column_settings.alert": "Сповіщення на комп'ютері", "notifications.column_settings.favourite": "Вподобане:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Показати всі категорії", + "notifications.column_settings.filter_bar.category": "Панель швидкого фільтру", + "notifications.column_settings.filter_bar.show": "Показати", "notifications.column_settings.follow": "Нові підписники:", "notifications.column_settings.mention": "Згадки:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Результати опитування:", "notifications.column_settings.push": "Push-сповіщення", "notifications.column_settings.reblog": "Передмухи:", "notifications.column_settings.show": "Показати в колонці", "notifications.column_settings.sound": "Відтворювати звуки", - "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.all": "Усі", + "notifications.filter.boosts": "Передмухи", + "notifications.filter.favourites": "Улюблені", + "notifications.filter.follows": "Підписки", + "notifications.filter.mentions": "Згадки", + "notifications.filter.polls": "Результати опитування", "notifications.group": "{count} сповіщень", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.closed": "Закрито", + "poll.refresh": "Оновити", + "poll.total_people": "{count, plural, one {# особа} other {# осіб}}", + "poll.total_votes": "{count, plural, one {# голос} few {# голоси} many {# голосів} other {# голосів}}", + "poll.vote": "Проголосувати", + "poll.voted": "Ви голосували за цю відповідь", + "poll_button.add_poll": "Додати опитування", + "poll_button.remove_poll": "Видалити опитування", "privacy.change": "Змінити видимість допису", "privacy.direct.long": "Показати тільки згаданим користувачам", - "privacy.direct.short": "Направлений", + "privacy.direct.short": "Особисто", "privacy.private.long": "Показати тільки підписникам", "privacy.private.short": "Тільки для підписників", "privacy.public.long": "Показувати у публічних стрічках", - "privacy.public.short": "Публічний", + "privacy.public.short": "Публічно", "privacy.unlisted.long": "Не показувати у публічних стрічках", "privacy.unlisted.short": "Прихований", + "refresh": "Оновити", "regeneration_indicator.label": "Завантаження…", - "regeneration_indicator.sublabel": "Ваша домашня стрічка готова!", + "regeneration_indicator.sublabel": "Ваша домашня стрічка готується!", "relative_time.days": "{number}д", "relative_time.hours": "{number}г", "relative_time.just_now": "щойно", @@ -303,81 +328,91 @@ "report.hint": "Скаргу буде відправлено модераторам Вашого сайту. Ви можете надати їм пояснення, чому ви скаржитесь на аккаунт нижче:", "report.placeholder": "Додаткові коментарі", "report.submit": "Відправити", - "report.target": "Скаржимося на", + "report.target": "Скаржимося на {target}", "search.placeholder": "Пошук", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", - "search_results.accounts": "People", - "search_results.hashtags": "Hashtags", - "search_results.statuses": "Toots", + "search_popout.search_format": "Розширений формат пошуку", + "search_popout.tips.full_text": "Пошук за текстом знаходить статуси, які ви написали, вподобали, передмухнули, або в яких вас згадували. Також він знаходить імена користувачів, реальні імена та хештеґи.", + "search_popout.tips.hashtag": "хештеґ", + "search_popout.tips.status": "статус", + "search_popout.tips.text": "Пошук за текстом знаходить імена користувачів, реальні імена та хештеґи", + "search_popout.tips.user": "користувач", + "search_results.accounts": "Люди", + "search_results.hashtags": "Хештеґи", + "search_results.statuses": "Дмухів", + "search_results.statuses_fts_disabled": "Пошук дмухів за вмістом недоступний на цьому сервері Mastodon.", "search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Unboost", + "status.admin_account": "Відкрити інтерфейс модерації для @{name}", + "status.admin_status": "Відкрити цей статус в інтерфейсі модерації", + "status.block": "Заблокувати @{name}", + "status.cancel_reblog_private": "Відмінити передмухання", "status.cannot_reblog": "Цей допис не може бути передмухнутий", - "status.copy": "Copy link to status", + "status.copy": "Копіювати посилання до статусу", "status.delete": "Видалити", - "status.detailed_status": "Detailed conversation view", - "status.direct": "Direct message @{name}", - "status.embed": "Embed", + "status.detailed_status": "Детальний вигляд бесіди", + "status.direct": "Пряме повідомлення до @{name}", + "status.embed": "Вбудувати", "status.favourite": "Подобається", - "status.filtered": "Filtered", + "status.filtered": "Відфільтровано", "status.load_more": "Завантажити більше", "status.media_hidden": "Медіаконтент приховано", - "status.mention": "Згадати", - "status.more": "More", - "status.mute": "Mute @{name}", + "status.mention": "Згадати @{name}", + "status.more": "Більше", + "status.mute": "Заглушити @{name}", "status.mute_conversation": "Заглушити діалог", "status.open": "Розгорнути допис", - "status.pin": "Pin on profile", - "status.pinned": "Pinned toot", - "status.read_more": "Read more", + "status.pin": "Закріпити у профілі", + "status.pinned": "Закріплений дмух", + "status.read_more": "Дізнатися більше", "status.reblog": "Передмухнути", - "status.reblog_private": "Boost to original audience", + "status.reblog_private": "Передмухнути для початкової аудиторії", "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", + "status.reblogs.empty": "Ніхто ще не передмухнув цього дмуху. Коли якісь користувачі це зроблять, вони будуть відображені тут.", + "status.redraft": "Видалити та перестворити", "status.reply": "Відповісти", - "status.replyAll": "Відповісти на тред", - "status.report": "Поскаржитися", - "status.sensitive_warning": "Непристойний зміст", - "status.share": "Share", + "status.replyAll": "Відповісти на ланцюжок", + "status.report": "Поскаржитися на @{name}", + "status.sensitive_warning": "Дражливий зміст", + "status.share": "Поділитися", "status.show_less": "Згорнути", - "status.show_less_all": "Show less for all", + "status.show_less_all": "Показувати менше для всіх", "status.show_more": "Розгорнути", - "status.show_more_all": "Show more for all", - "status.show_thread": "Show thread", + "status.show_more_all": "Показувати більше для всіх", + "status.show_thread": "Показати ланцюжок", + "status.uncached_media_warning": "Недоступно", "status.unmute_conversation": "Зняти глушення з діалогу", - "status.unpin": "Unpin from profile", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "status.unpin": "Відкріпити від профілю", + "suggestions.dismiss": "Відхилити пропозицію", + "suggestions.header": "Вас може зацікавити…", "tabs_bar.federated_timeline": "Глобальна", "tabs_bar.home": "Головна", "tabs_bar.local_timeline": "Локальна", "tabs_bar.notifications": "Сповіщення", "tabs_bar.search": "Пошук", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "time_remaining.days": "{number, plural, one {# день} few {# дні} other {# днів}}", + "time_remaining.hours": "{number, plural, one {# година} few {# години} other {# годин}}", + "time_remaining.minutes": "{number, plural, one {# хвилина} few {# хвилини} other {# хвилин}}", + "time_remaining.moments": "Залишилось секунд", + "time_remaining.seconds": "{number, plural, one {# секунда} few {# секунди} other {# секунд}}", + "trends.count_by_accounts": "{count} {rawCount, plural, one {людина} few {людини} many {людей} other {людей}} обговорюють це", + "trends.trending_now": "Актуальні", "ui.beforeunload": "Вашу чернетку буде втрачено, якщо ви покинете Mastodon.", "upload_area.title": "Перетягніть сюди, щоб завантажити", - "upload_button.label": "Додати медіаконтент", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_button.label": "Додати медіаконтент ({formats})", + "upload_error.limit": "Ліміт завантаження файлів перевищено.", + "upload_error.poll": "Не можна завантажувати файли до опитувань.", "upload_form.description": "Опишіть для людей з вадами зору", - "upload_form.focus": "Обрізати", + "upload_form.edit": "Змінити", "upload_form.undo": "Видалити", + "upload_modal.analyzing_picture": "Аналізуємо малюнок…", + "upload_modal.apply": "Застосувати", + "upload_modal.description_placeholder": "Щурячий бугай із їжаком-харцизом в'ючись підписали ґешефт у єнах", + "upload_modal.detect_text": "Виявити текст на малюнку", + "upload_modal.edit_media": "Редагувати медіа", + "upload_modal.hint": "Клацніть або перетягніть коло на превью, щоб обрати точку, яку буде завжди видно на мініатюрах.", + "upload_modal.preview_label": "Переглянути ({ratio})", "upload_progress.label": "Завантаження...", "video.close": "Закрити відео", - "video.exit_fullscreen": "Вийти з повного екрану", + "video.exit_fullscreen": "Вийти з повноекранного режиму", "video.expand": "Розширити відео", "video.fullscreen": "На весь екран", "video.hide": "Приховати відео", diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json new file mode 100644 index 000000000..39ca86a0c --- /dev/null +++ b/app/javascript/mastodon/locales/ur.json @@ -0,0 +1,423 @@ +{ + "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.bot": "Bot", + "account.block": "Block @{name}", + "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", + "account.direct": "Direct message @{name}", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Edit profile", + "account.endorse": "Feature on profile", + "account.follow": "Follow", + "account.followers": "Followers", + "account.followers.empty": "No one follows this user yet.", + "account.follows": "Follows", + "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.media": "Media", + "account.mention": "Mention @{name}", + "account.moved_to": "{name} has moved to:", + "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", + "account.never_active": "Never", + "account.posts": "Toots", + "account.posts_with_replies": "Toots and replies", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", + "account.unblock": "Unblock @{name}", + "account.unblock_domain": "Unhide {domain}", + "account.unendorse": "Don't feature on profile", + "account.unfollow": "Unfollow", + "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.direct": "Direct messages", + "column.directory": "Browse profiles", + "column.domain_blocks": "Hidden domains", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.pins": "Pinned toot", + "column.public": "Federated timeline", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", + "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", + "column_header.unpin": "Unpin", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media only", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Remove this 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.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.select.no_options_message": "No suggestions found", + "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "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", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "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.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll.voted": "You voted for this answer", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "refresh": "Refresh", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "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", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading…", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/whitelist_pt.json b/app/javascript/mastodon/locales/whitelist_br.json index 0d4f101c7..0d4f101c7 100644 --- a/app/javascript/mastodon/locales/whitelist_pt.json +++ b/app/javascript/mastodon/locales/whitelist_br.json diff --git a/app/javascript/mastodon/locales/whitelist_es-AR.json b/app/javascript/mastodon/locales/whitelist_es-AR.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_es-AR.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_et.json b/app/javascript/mastodon/locales/whitelist_et.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_et.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_ga.json b/app/javascript/mastodon/locales/whitelist_ga.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_ga.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_kn.json b/app/javascript/mastodon/locales/whitelist_kn.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_kn.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_mk.json b/app/javascript/mastodon/locales/whitelist_mk.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_mk.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_ml.json b/app/javascript/mastodon/locales/whitelist_ml.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_ml.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_mr.json b/app/javascript/mastodon/locales/whitelist_mr.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_mr.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_nn.json b/app/javascript/mastodon/locales/whitelist_nn.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_nn.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_pt-PT.json b/app/javascript/mastodon/locales/whitelist_pt-PT.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_pt-PT.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_ur.json b/app/javascript/mastodon/locales/whitelist_ur.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_ur.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 785419eae..550810680 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -4,6 +4,7 @@ "account.block": "屏蔽 @{name}", "account.block_domain": "隐藏来自 {domain} 的内容", "account.blocked": "已屏蔽", + "account.cancel_follow_request": "取消关注请求", "account.direct": "发送私信给 @{name}", "account.domain_blocked": "网站已屏蔽", "account.edit_profile": "修改个人资料", @@ -12,9 +13,10 @@ "account.followers": "关注者", "account.followers.empty": "目前无人关注此用户。", "account.follows": "正在关注", - "account.follows.empty": "此用户目前没有关注任何人。", + "account.follows.empty": "此用户目前尚未关注任何人。", "account.follows_you": "关注了你", "account.hide_reblogs": "隐藏来自 @{name} 的转嘟", + "account.last_status": "最近活动", "account.link_verified_on": "此链接的所有权已在 {date} 检查", "account.locked_info": "此账户已锁嘟。账户的主人会手动审核关注者。", "account.media": "媒体", @@ -23,20 +25,24 @@ "account.mute": "隐藏 @{name}", "account.mute_notifications": "隐藏来自 @{name} 的通知", "account.muted": "已隐藏", + "account.never_active": "从未活跃", "account.posts": "嘟文", "account.posts_with_replies": "嘟文和回复", "account.report": "举报 @{name}", "account.requested": "正在等待对方同意。点击以取消发送关注请求", "account.share": "分享 @{name} 的个人资料", "account.show_reblogs": "显示来自 @{name} 的转嘟", - "account.unblock": "不再屏蔽 @{name}", + "account.unblock": "解除屏蔽 @{name}", "account.unblock_domain": "不再隐藏来自 {domain} 的内容", "account.unendorse": "不在个人资料中推荐此用户", "account.unfollow": "取消关注", "account.unmute": "不再隐藏 @{name}", "account.unmute_notifications": "不再隐藏来自 @{name} 的通知", + "alert.rate_limited.message": "请在 {retry_time, time, medium} 后重试。", + "alert.rate_limited.title": "频率受限", "alert.unexpected.message": "发生了意外错误。", "alert.unexpected.title": "哎呀!", + "autosuggest_hashtag.per_week": "每星期 {count} 条", "boost_modal.combo": "下次按住 {combo} 即可跳过此提示", "bundle_column_error.body": "载入这个组件时发生了错误。", "bundle_column_error.retry": "重试", @@ -47,8 +53,9 @@ "column.blocks": "已屏蔽的用户", "column.community": "本站时间轴", "column.direct": "私信", + "column.directory": "浏览用户资料", "column.domain_blocks": "已屏蔽的网站", - "column.favourites": "收藏过的嘟文", + "column.favourites": "喜欢", "column.follow_requests": "关注请求", "column.home": "主页", "column.lists": "列表", @@ -92,15 +99,26 @@ "confirmations.delete_list.confirm": "删除", "confirmations.delete_list.message": "你确定要永久删除这个列表吗?", "confirmations.domain_block.confirm": "隐藏整个网站的内容", - "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户应该就能满足你的需要了。来自该网站的内容将不再出现在你的公共时间轴或通知列表里。来自该网站的关注者将会被移除。", + "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就已经足够了。来自该网站的内容将不再出现在你的任何公共时间轴或通知列表里。来自该网站的关注者将会被移除。", + "confirmations.logout.confirm": "登出", + "confirmations.logout.message": "您确定要登出吗?", "confirmations.mute.confirm": "隐藏", + "confirmations.mute.explanation": "这将隐藏他们的嘟文以及提到他们的嘟文,但他们仍可以看到你的嘟文并关注你。", "confirmations.mute.message": "你确定要隐藏 {name} 吗?", "confirmations.redraft.confirm": "删除并重新编辑", - "confirmations.redraft.message": "你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和收藏都会被清除,回复将会被孤立。", + "confirmations.redraft.message": "你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和喜欢都会被清除,回复将会失去关联。", "confirmations.reply.confirm": "回复", - "confirmations.reply.message": "回复此消息会覆盖掉当前正在编辑的消息。确定继续吗?", + "confirmations.reply.message": "回复此消息将会覆盖当前正在编辑的信息。确定继续吗?", "confirmations.unfollow.confirm": "取消关注", "confirmations.unfollow.message": "你确定要取消关注 {name} 吗?", + "conversation.delete": "删除对话", + "conversation.mark_as_read": "标记为已读", + "conversation.open": "查看对话", + "conversation.with": "与 {names}", + "directory.federated": "来自联邦宇宙的已知部分", + "directory.local": "仅来自 {domain}", + "directory.new_arrivals": "新来者", + "directory.recently_active": "最近活跃", "embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。", "embed.preview": "它会像这样显示出来:", "emoji_button.activity": "活动", @@ -120,28 +138,32 @@ "empty_column.account_timeline": "这里没有嘟文!", "empty_column.account_unavailable": "个人资料不可用", "empty_column.blocks": "你目前没有屏蔽任何用户。", - "empty_column.community": "本站时间轴暂时没有内容,快嘟几个来抢头香啊!", + "empty_column.community": "本站时间轴暂时没有内容,快写点什么让它动起来吧!", "empty_column.direct": "你还没有使用过私信。当你发出或者收到私信时,它会在这里显示。", "empty_column.domain_blocks": "目前没有被隐藏的站点。", - "empty_column.favourited_statuses": "你还没有收藏过任何嘟文。收藏过的嘟文会显示在这里。", - "empty_column.favourites": "没人收藏过这条嘟文。假如有人收藏了,就会显示在这里。", + "empty_column.favourited_statuses": "你还没有喜欢过任何嘟文。喜欢过的嘟文会显示在这里。", + "empty_column.favourites": "没有人喜欢过这条嘟文。如果有人喜欢了,就会显示在这里。", "empty_column.follow_requests": "你没有收到新的关注请求。收到了之后就会显示在这里。", "empty_column.hashtag": "这个话题标签下暂时没有内容。", - "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", + "empty_column.home": "你还没有关注任何用户。快看看{public},向其他人问个好吧。", "empty_column.home.public_timeline": "公共时间轴", "empty_column.list": "这个列表中暂时没有内容。列表中用户所发送的的新嘟文将会在这里显示。", - "empty_column.lists": "你没有创建过列表。你创建的列表会在这里显示。", + "empty_column.lists": "你还没有创建过列表。你创建的列表会在这里显示。", "empty_column.mutes": "你没有隐藏任何用户。", - "empty_column.notifications": "你还没有收到过任何通知,快向其他用户搭讪吧。", + "empty_column.notifications": "你还没有收到过任何通知,快和其他用户互动吧。", "empty_column.public": "这里什么都没有!写一些公开的嘟文,或者关注其他服务器的用户后,这里就会有嘟文出现了", + "error.unexpected_crash.explanation": "此页面无法正确现实,这可能是因为我们的代码中有错误,也可能是因为浏览器兼容问题。", + "error.unexpected_crash.next_steps": "刷新一下页面试试。如果没用,您可以换个浏览器或者用本地应用。", + "errors.unexpected_crash.copy_stacktrace": "把堆栈跟踪信息复制到剪贴板", + "errors.unexpected_crash.report_issue": "报告问题", "follow_request.authorize": "同意", "follow_request.reject": "拒绝", "getting_started.developers": "开发", - "getting_started.directory": "用户资料目录", + "getting_started.directory": "用户目录", "getting_started.documentation": "文档", "getting_started.heading": "开始使用", "getting_started.invite": "邀请用户", - "getting_started.open_source_notice": "Mastodon 是一个开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。", + "getting_started.open_source_notice": "Mastodon 是开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。", "getting_started.security": "帐户安全", "getting_started.terms": "使用条款", "hashtag.column_header.tag_mode.all": "以及 {additional}", @@ -161,21 +183,21 @@ "intervals.full.minutes": "{number} 分钟", "introduction.federation.action": "下一步", "introduction.federation.federated.headline": "跨站", - "introduction.federation.federated.text": "其他跨站服务器的公共动态会显示在跨站时间线中。", + "introduction.federation.federated.text": "联邦宇宙中其他服务器的公开嘟文会显示在跨站时间轴中。", "introduction.federation.home.headline": "主页", - "introduction.federation.home.text": "你所关注的用户的动态会显示在主页里。你可以关注任何服务器上的任何人!", + "introduction.federation.home.text": "你所关注的用户的动态会显示在主页里。你可以关注任何服务器上的任何人!", "introduction.federation.local.headline": "本站", "introduction.federation.local.text": "你所关注的用户的动态会显示在主页里,你可以关注任何服务器上的任何人。", "introduction.interactions.action": "教程结束!", - "introduction.interactions.favourite.headline": "收藏", - "introduction.interactions.favourite.text": "你可以保存嘟文以便以后阅读。或者通过收藏功能告诉作者你点了个赞。", + "introduction.interactions.favourite.headline": "喜欢", + "introduction.interactions.favourite.text": "通过喜欢一条嘟文,你可以保存它以便以后阅读,并且告诉作者你喜欢它。", "introduction.interactions.reblog.headline": "转嘟", "introduction.interactions.reblog.text": "通过转嘟,你可以向你的关注者分享其他人的嘟文。", "introduction.interactions.reply.headline": "回复", - "introduction.interactions.reply.text": "你可以向其他人回复,这些回复会像对话一样串在一起。", + "introduction.interactions.reply.text": "你可以回复其他嘟文,这些回复会像对话一样关联在一起。", "introduction.welcome.action": "让我们开始吧!", "introduction.welcome.headline": "首先", - "introduction.welcome.text": "欢迎来到联邦!稍后,您将可以广播消息冰河您的朋友交流,这些消息将穿越于联邦中的各式服务器。但是这台服务器,{domain},是特殊的——它保存了你的个人资料,所以请记住它的名字。", + "introduction.welcome.text": "欢迎来到联邦宇宙!很快,您就可以发布信息并和您的朋友交流,这些消息将发送到联邦中的各个服务器中。但是这台服务器,{domain},是特殊的——它保存了你的个人资料,所以请记住它的名字。", "keyboard_shortcuts.back": "返回上一页", "keyboard_shortcuts.blocked": "打开被屏蔽用户列表", "keyboard_shortcuts.boost": "转嘟", @@ -185,18 +207,18 @@ "keyboard_shortcuts.direct": "打开私信栏", "keyboard_shortcuts.down": "在列表中让光标下移", "keyboard_shortcuts.enter": "展开嘟文", - "keyboard_shortcuts.favourite": "收藏嘟文", - "keyboard_shortcuts.favourites": "打开收藏列表", - "keyboard_shortcuts.federated": "打开跨站时间线", + "keyboard_shortcuts.favourite": "喜欢嘟文", + "keyboard_shortcuts.favourites": "打开喜欢的嘟文列表", + "keyboard_shortcuts.federated": "打开跨站时间轴", "keyboard_shortcuts.heading": "快捷键列表", - "keyboard_shortcuts.home": "打开主页时间线", + "keyboard_shortcuts.home": "打开主页时间轴", "keyboard_shortcuts.hotkey": "快捷键", "keyboard_shortcuts.legend": "显示此列表", - "keyboard_shortcuts.local": "打开本站时间线", + "keyboard_shortcuts.local": "打开本站时间轴", "keyboard_shortcuts.mention": "提及嘟文作者", - "keyboard_shortcuts.muted": "打开屏蔽用户列表", + "keyboard_shortcuts.muted": "打开隐藏用户列表", "keyboard_shortcuts.my_profile": "打开你的个人资料", - "keyboard_shortcuts.notifications": "打卡通知栏", + "keyboard_shortcuts.notifications": "打开通知栏", "keyboard_shortcuts.pinned": "打开置顶嘟文列表", "keyboard_shortcuts.profile": "打开作者的个人资料", "keyboard_shortcuts.reply": "回复嘟文", @@ -213,7 +235,7 @@ "lightbox.previous": "上一个", "lightbox.view_context": "查看上下文", "lists.account.add": "添加到列表", - "lists.account.remove": "从列表中删除", + "lists.account.remove": "从列表中移除", "lists.delete": "删除列表", "lists.edit": "编辑列表", "lists.edit.submit": "更改标题", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "新列表的标题", "lists.search": "搜索你关注的人", "lists.subheading": "你的列表", + "load_pending": "{count} 项", "loading_indicator.label": "加载中……", "media_gallery.toggle_visible": "切换显示/隐藏", "missing_indicator.label": "找不到内容", @@ -234,30 +257,29 @@ "navigation_bar.discover": "发现", "navigation_bar.domain_blocks": "已屏蔽的网站", "navigation_bar.edit_profile": "修改个人资料", - "navigation_bar.favourites": "收藏的内容", - "navigation_bar.filters": "被隐藏的词", + "navigation_bar.favourites": "喜欢", + "navigation_bar.filters": "屏蔽关键词", "navigation_bar.follow_requests": "关注请求", - "navigation_bar.follows_and_followers": "正在关注以及关注者", + "navigation_bar.follows_and_followers": "关注管理", "navigation_bar.info": "关于本站", "navigation_bar.keyboard_shortcuts": "快捷键列表", "navigation_bar.lists": "列表", - "navigation_bar.logout": "注销", + "navigation_bar.logout": "登出", "navigation_bar.mutes": "已隐藏的用户", "navigation_bar.personal": "个人", "navigation_bar.pins": "置顶嘟文", "navigation_bar.preferences": "首选项", - "navigation_bar.profile_directory": "用户资料目录", "navigation_bar.public_timeline": "跨站公共时间轴", "navigation_bar.security": "安全", - "notification.favourite": "{name} 收藏了你的嘟文", + "notification.favourite": "{name} 喜欢了你的嘟文", "notification.follow": "{name} 开始关注你", - "notification.mention": "{name} 提及你", + "notification.mention": "{name} 提及了你", "notification.poll": "你参与的一个投票已经结束", "notification.reblog": "{name} 转嘟了你的嘟文", "notifications.clear": "清空通知列表", "notifications.clear_confirmation": "你确定要永久清空通知列表吗?", "notifications.column_settings.alert": "桌面通知", - "notifications.column_settings.favourite": "当你的嘟文被收藏时:", + "notifications.column_settings.favourite": "当你的嘟文被喜欢时:", "notifications.column_settings.filter_bar.advanced": "显示所有类别", "notifications.column_settings.filter_bar.category": "快速过滤栏", "notifications.column_settings.filter_bar.show": "显示", @@ -270,15 +292,17 @@ "notifications.column_settings.sound": "播放音效", "notifications.filter.all": "全部", "notifications.filter.boosts": "转嘟", - "notifications.filter.favourites": "收藏", + "notifications.filter.favourites": "喜欢", "notifications.filter.follows": "关注", "notifications.filter.mentions": "提及", "notifications.filter.polls": "投票结果", "notifications.group": "{count} 条通知", "poll.closed": "已关闭", "poll.refresh": "刷新", + "poll.total_people": "{count, plural, one {# 人} other {# 人}}", "poll.total_votes": "{count} 票", "poll.vote": "投票", + "poll.voted": "您已经对这个答案投过票了", "poll_button.add_poll": "发起投票", "poll_button.remove_poll": "移除投票", "privacy.change": "设置嘟文可见范围", @@ -290,6 +314,7 @@ "privacy.public.short": "公开", "privacy.unlisted.long": "所有人可见,但不会出现在公共时间轴上", "privacy.unlisted.short": "不公开", + "refresh": "刷新", "regeneration_indicator.label": "加载中……", "regeneration_indicator.sublabel": "你的主页时间轴正在准备中!", "relative_time.days": "{number}天", @@ -300,32 +325,33 @@ "reply_indicator.cancel": "取消", "report.forward": "发送举报至 {target}", "report.forward_hint": "这名用户来自另一个服务器。是否要向那个服务器发送一条匿名的举报?", - "report.hint": "举报将会发送给你所在服务器的监察员。你可以在下面填写举报这个用户的理由:", - "report.placeholder": "附言", + "report.hint": "举报将会发送给你所在服务器的监察员。你可以在下面填写举报该用户的理由:", + "report.placeholder": "备注", "report.submit": "提交", "report.target": "举报 {target}", "search.placeholder": "搜索", "search_popout.search_format": "高级搜索格式", - "search_popout.tips.full_text": "输入其他内容将会返回所有你撰写、收藏、转嘟过或提及到你的嘟文,同时也会在用户名、昵称和话题标签中进行搜索。", + "search_popout.tips.full_text": "输入关键词检索所有你发送、喜欢、转嘟过或提及到你的嘟文,以及其他用户公开的用户名、昵称和话题标签。", "search_popout.tips.hashtag": "话题标签", "search_popout.tips.status": "嘟文", - "search_popout.tips.text": "输入其他内容将会返回昵称、用户名和话题标签", + "search_popout.tips.text": "输入关键词检索昵称、用户名和话题标签", "search_popout.tips.user": "用户", "search_results.accounts": "用户", "search_results.hashtags": "话题标签", "search_results.statuses": "嘟文", + "search_results.statuses_fts_disabled": "此Mastodon服务器未启用嘟文内容搜索。", "search_results.total": "共 {count, number} 个结果", "status.admin_account": "打开 @{name} 的管理界面", "status.admin_status": "打开这条嘟文的管理界面", "status.block": "屏蔽 @{name}", "status.cancel_reblog_private": "取消转嘟", - "status.cannot_reblog": "无法转嘟这条嘟文", - "status.copy": "复制链接到嘟文中", + "status.cannot_reblog": "这条嘟文不允许被转嘟", + "status.copy": "复制嘟文链接", "status.delete": "删除", "status.detailed_status": "对话详情", "status.direct": "发送私信给 @{name}", "status.embed": "嵌入", - "status.favourite": "收藏", + "status.favourite": "喜欢", "status.filtered": "已过滤", "status.load_more": "加载更多", "status.media_hidden": "隐藏媒体内容", @@ -338,9 +364,9 @@ "status.pinned": "置顶嘟文", "status.read_more": "阅读全文", "status.reblog": "转嘟", - "status.reblog_private": "转嘟给原有关注者", + "status.reblog_private": "转嘟(可见者不变)", "status.reblogged_by": "{name} 转嘟了", - "status.reblogs.empty": "无人转嘟此条。如果有人转嘟了,就会显示在这里。", + "status.reblogs.empty": "没有人转嘟过此条嘟文。如果有人转嘟了,就会显示在这里。", "status.redraft": "删除并重新编辑", "status.reply": "回复", "status.replyAll": "回复所有人", @@ -352,6 +378,7 @@ "status.show_more": "显示内容", "status.show_more_all": "显示所有内容", "status.show_thread": "显示全部对话", + "status.uncached_media_warning": "不可用", "status.unmute_conversation": "不再隐藏此对话", "status.unpin": "在个人资料页面取消置顶", "suggestions.dismiss": "关闭建议", @@ -367,15 +394,23 @@ "time_remaining.moments": "即将结束", "time_remaining.seconds": "剩余 {number, plural, one {# 秒} other {# 秒}}", "trends.count_by_accounts": "{count} 人正在讨论", - "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。", + "trends.trending_now": "现在流行", + "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会丢失。", "upload_area.title": "将文件拖放到此处开始上传", "upload_button.label": "上传媒体文件 (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.limit": "超过文件上传限制。", + "upload_error.limit": "文件大小超过限制。", "upload_error.poll": "投票中不允许上传文件。", "upload_form.description": "为视觉障碍人士添加文字说明", - "upload_form.focus": "剪裁", + "upload_form.edit": "编辑", "upload_form.undo": "删除", - "upload_progress.label": "上传中…", + "upload_modal.analyzing_picture": "分析图片…", + "upload_modal.apply": "应用", + "upload_modal.description_placeholder": "天地玄黄 宇宙洪荒 日月盈仄 辰宿列张", + "upload_modal.detect_text": "从图片中检测文本", + "upload_modal.edit_media": "编辑媒体", + "upload_modal.hint": "在预览图上点击或拖动圆圈,以选择缩略图的焦点。", + "upload_modal.preview_label": "预览 ({ratio})", + "upload_progress.label": "上传中……", "video.close": "关闭视频", "video.exit_fullscreen": "退出全屏", "video.expand": "展开视频", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 2cfc11703..3ca9f0e2e 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -4,6 +4,7 @@ "account.block": "封鎖 @{name}", "account.block_domain": "隱藏來自 {domain} 的一切文章", "account.blocked": "封鎖", + "account.cancel_follow_request": "Cancel follow request", "account.direct": "私訊 @{name}", "account.domain_blocked": "服務站被隱藏", "account.edit_profile": "修改個人資料", @@ -15,6 +16,7 @@ "account.follows.empty": "This user doesn't follow anyone yet.", "account.follows_you": "關注你", "account.hide_reblogs": "隱藏 @{name} 的轉推", + "account.last_status": "Last active", "account.link_verified_on": "Ownership of this link was checked on {date}", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.media": "媒體", @@ -23,6 +25,7 @@ "account.mute": "將 @{name} 靜音", "account.mute_notifications": "將來自 @{name} 的通知靜音", "account.muted": "靜音", + "account.never_active": "Never", "account.posts": "文章", "account.posts_with_replies": "包含回覆的文章", "account.report": "舉報 @{name}", @@ -35,8 +38,11 @@ "account.unfollow": "取消關注", "account.unmute": "取消 @{name} 的靜音", "account.unmute_notifications": "取消來自 @{name} 通知的靜音", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", "alert.unexpected.message": "發生不可預期的錯誤。", "alert.unexpected.title": "噢!", + "autosuggest_hashtag.per_week": "{count} per week", "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},", "bundle_column_error.body": "加載本組件出錯。", "bundle_column_error.retry": "重試", @@ -47,6 +53,7 @@ "column.blocks": "封鎖用戶", "column.community": "本站時間軸", "column.direct": "個人訊息", + "column.directory": "Browse profiles", "column.domain_blocks": "隱藏的服務站", "column.favourites": "最愛的文章", "column.follow_requests": "關注請求", @@ -93,7 +100,10 @@ "confirmations.delete_list.message": "你確定要永久刪除這列表嗎?", "confirmations.domain_block.confirm": "隱藏整個網站", "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。你從此將不會再看到該站的內容和通知。來自該站的關注者亦會被移除。", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.mute.confirm": "靜音", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "你確定要將{name}靜音嗎?", "confirmations.redraft.confirm": "刪除並編輯", "confirmations.redraft.message": "你確定要刪除並重新編輯嗎?所有相關的回覆、轉推與最愛都會被刪除。", @@ -101,6 +111,14 @@ "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.unfollow.confirm": "取消關注", "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?", + "conversation.delete": "Delete conversation", + "conversation.mark_as_read": "Mark as read", + "conversation.open": "View conversation", + "conversation.with": "With {names}", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", "embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。", "embed.preview": "看上去會是這樣:", "emoji_button.activity": "活動", @@ -134,6 +152,10 @@ "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。", "empty_column.public": "跨站時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "批准", "follow_request.reject": "拒絕", "getting_started.developers": "開發者", @@ -221,6 +243,7 @@ "lists.new.title_placeholder": "新列表標題", "lists.search": "從你關注的用戶中搜索", "lists.subheading": "列表", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "載入中...", "media_gallery.toggle_visible": "打開或關上", "missing_indicator.label": "找不到內容", @@ -246,7 +269,6 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "置頂文章", "navigation_bar.preferences": "偏好設定", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "跨站時間軸", "navigation_bar.security": "安全", "notification.favourite": "{name} 收藏了你的文章", @@ -277,8 +299,10 @@ "notifications.group": "{count} 條通知", "poll.closed": "Closed", "poll.refresh": "Refresh", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", "poll.vote": "Vote", + "poll.voted": "You voted for this answer", "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "privacy.change": "調整私隱設定", @@ -290,6 +314,7 @@ "privacy.public.short": "公共", "privacy.unlisted.long": "公開,但不在公共時間軸顯示", "privacy.unlisted.short": "公開", + "refresh": "Refresh", "regeneration_indicator.label": "載入中……", "regeneration_indicator.sublabel": "你的主頁時間軸正在準備中!", "relative_time.days": "{number}日", @@ -314,6 +339,7 @@ "search_results.accounts": "使用者", "search_results.hashtags": "標籤", "search_results.statuses": "文章", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} 項結果", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -352,6 +378,7 @@ "status.show_more": "顯示更多", "status.show_more_all": "顯示更多這類文章", "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", "status.unmute_conversation": "解禁對話", "status.unpin": "解除置頂", "suggestions.dismiss": "Dismiss suggestion", @@ -367,14 +394,22 @@ "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "trends.count_by_accounts": "{count} 位用戶在討論", + "trends.trending_now": "Trending now", "ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。", "upload_area.title": "將檔案拖放至此上載", "upload_button.label": "上載媒體檔案", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "為視覺障礙人士添加文字說明", - "upload_form.focus": "裁切", + "upload_form.edit": "Edit", "upload_form.undo": "刪除", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "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.preview_label": "Preview ({ratio})", "upload_progress.label": "上載中……", "video.close": "關閉影片", "video.exit_fullscreen": "退出全熒幕", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 5715ef01a..136573381 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -2,27 +2,30 @@ "account.add_or_remove_from_list": "從名單中新增或移除", "account.badges.bot": "機器人", "account.block": "封鎖 @{name}", - "account.block_domain": "隱藏來自 {domain} 的所有嘟文", + "account.block_domain": "隱藏來自 {domain} 的所有內容", "account.blocked": "已封鎖", + "account.cancel_follow_request": "取消關注請求", "account.direct": "傳私訊給 @{name}", "account.domain_blocked": "已隱藏網域", "account.edit_profile": "編輯個人資料", "account.endorse": "在個人資料推薦對方", "account.follow": "關注", "account.followers": "關注者", - "account.followers.empty": "還沒有人關注這位使用者。", + "account.followers.empty": "尚沒有人關注這位使用者。", "account.follows": "正在關注", - "account.follows.empty": "這個使用者尚未關注任何使用者。", + "account.follows.empty": "這位使用者尚未關注任何使用者。", "account.follows_you": "關注了你", "account.hide_reblogs": "隱藏來自 @{name} 的轉推", - "account.link_verified_on": "此連結的所有權已在 {date} 檢查", - "account.locked_info": "此帳號的隱私狀態被設為鎖定,擁有者將手動審核可關注此帳號的人。", + "account.last_status": "上次活躍", + "account.link_verified_on": "已在 {date} 檢查此連結的擁有者權限", + "account.locked_info": "這隻帳戶的隱私狀態被設成鎖定。該擁有者會手動審核能關注這隻帳號的人。", "account.media": "媒體", "account.mention": "提及 @{name}", "account.moved_to": "{name} 已遷移至:", "account.mute": "靜音 @{name}", "account.mute_notifications": "靜音來自 @{name} 的通知", "account.muted": "已靜音", + "account.never_active": "永不", "account.posts": "嘟文", "account.posts_with_replies": "嘟文與回覆", "account.report": "檢舉 @{name}", @@ -33,22 +36,26 @@ "account.unblock_domain": "取消隱藏 {domain}", "account.unendorse": "不再於個人資料頁面推薦對方", "account.unfollow": "取消關注", - "account.unmute": "不再靜音 @{name}", - "account.unmute_notifications": "不再靜音來自 @{name} 的通知", + "account.unmute": "取消靜音 @{name}", + "account.unmute_notifications": "重新接收來自 @{name} 的通知", + "alert.rate_limited.message": "請在 {retry_time, time, medium} 過後重試", + "alert.rate_limited.title": "已限速", "alert.unexpected.message": "發生了非預期的錯誤。", "alert.unexpected.title": "哎呀!", + "autosuggest_hashtag.per_week": "{count} / 週", "boost_modal.combo": "下次您可以按 {combo} 跳過", - "bundle_column_error.body": "載入此組件時發生錯誤。", + "bundle_column_error.body": "載入此元件時發生錯誤。", "bundle_column_error.retry": "重試", "bundle_column_error.title": "網路錯誤", "bundle_modal_error.close": "關閉", - "bundle_modal_error.message": "載入此組件時發生錯誤。", + "bundle_modal_error.message": "載入此元件時發生錯誤。", "bundle_modal_error.retry": "重試", "column.blocks": "封鎖的使用者", - "column.community": "本地時間軸", + "column.community": "本機時間軸", "column.direct": "私訊", + "column.directory": "瀏覽個人資料", "column.domain_blocks": "隱藏的網域", - "column.favourites": "最愛", + "column.favourites": "收藏", "column.follow_requests": "關注請求", "column.home": "主頁", "column.lists": "名單", @@ -64,44 +71,55 @@ "column_header.show_settings": "顯示設定", "column_header.unpin": "取消釘選", "column_subheading.settings": "設定", - "community.column_settings.media_only": "僅媒體", - "compose_form.direct_message_warning": "這條嘟文只有被提及的使用者才能看到。", + "community.column_settings.media_only": "只有媒體", + "compose_form.direct_message_warning": "這條嘟文只有被提及的使用者才看得到。", "compose_form.direct_message_warning_learn_more": "了解更多", - "compose_form.hashtag_warning": "因這則嘟文設成「不公開」,因此它不會列在任何「#」標籤下。只有公開嘟文才能用「#」標籤找到。", - "compose_form.lock_disclaimer": "您的帳戶尚未{locked}。任何人都能關注您並看到您設定成僅關注者能看的嘟文。", + "compose_form.hashtag_warning": "由於這則嘟文被設定成「不公開」,所以它將不會被列在任何主題標籤下。只有公開的嘟文才能藉主題標籤找到。", + "compose_form.lock_disclaimer": "您的帳戶尚未{locked}。任何人都能關注您並看到您設定成只有關注者能看的嘟文。", "compose_form.lock_disclaimer.lock": "上鎖", "compose_form.placeholder": "您正在想些什麼?", "compose_form.poll.add_option": "新增選擇", "compose_form.poll.duration": "投票期限", "compose_form.poll.option_placeholder": "第 {number} 個選擇", "compose_form.poll.remove_option": "移除此選擇", - "compose_form.publish": "嘟掉", + "compose_form.publish": "嘟出去", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "標記媒體為敏感內容", "compose_form.sensitive.marked": "此媒體被標記為敏感內容", - "compose_form.sensitive.unmarked": "此媒體未被標記為敏感內容", - "compose_form.spoiler.marked": "正文已隱藏在警告之後", + "compose_form.sensitive.unmarked": "此媒體未標記為敏感內容", + "compose_form.spoiler.marked": "正文已隱藏到警告之後", "compose_form.spoiler.unmarked": "正文未被隱藏", "compose_form.spoiler_placeholder": "請在此處寫入警告訊息", "confirmation_modal.cancel": "取消", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "封鎖並檢舉", "confirmations.block.confirm": "封鎖", - "confirmations.block.message": "你確定要封鎖 {name} ?", + "confirmations.block.message": "確定封鎖 {name} ?", "confirmations.delete.confirm": "刪除", "confirmations.delete.message": "你確定要刪除這條嘟文?", "confirmations.delete_list.confirm": "刪除", - "confirmations.delete_list.message": "確定要永久刪除此名單?", + "confirmations.delete_list.message": "確定永久刪除此名單?", "confirmations.domain_block.confirm": "隱藏整個網域", - "confirmations.domain_block.message": "確定封鎖整個 {domain} 嗎?多數情況下,封鎖或靜音幾個特定使用者應該就能滿足你的需求了。您將不能在任何公開時間軸或通知中看到來自該網域的內容。來自該網域的關注者將被移除。", + "confirmations.domain_block.message": "真的非常確定封鎖整個 {domain} 嗎?大部分情況下,你只需要封鎖或靜音少數特定的人就能滿足需求了。你將不能在任何公開的時間軸及通知中看到那個網域的內容。你來自該網域的關注者也會被移除。", + "confirmations.logout.confirm": "登出", + "confirmations.logout.message": "確定要登出嗎?", "confirmations.mute.confirm": "靜音", + "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.message": "確定靜音 {name} ?", "confirmations.redraft.confirm": "刪除並重新編輯", - "confirmations.redraft.message": "你確定要刪除這條嘟文並重新編輯它嗎?這麼做將失去轉嘟和最愛,而對原始嘟文的回覆將被孤立。", + "confirmations.redraft.message": "確定刪掉這則嘟文並重新編輯嗎?將會失去這則嘟文的轉嘟及收藏,且回覆這則的嘟文將會變成獨立的嘟文。", "confirmations.reply.confirm": "回覆", "confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?", "confirmations.unfollow.confirm": "取消關注", "confirmations.unfollow.message": "真的要取消關注 {name} 嗎?", - "embed.instructions": "要嵌入此嘟文,請將以下代碼貼進你的網站。", + "conversation.delete": "刪除對話", + "conversation.mark_as_read": "標為已讀", + "conversation.open": "檢視對話", + "conversation.with": "與 {names}", + "directory.federated": "來自已知聯邦宇宙", + "directory.local": "僅來自 {domain}", + "directory.new_arrivals": "新貨", + "directory.recently_active": "最近活躍", + "embed.instructions": "要嵌入此嘟文,請將以下程式碼貼進你的網站。", "embed.preview": "他會顯示成這樣:", "emoji_button.activity": "活動", "emoji_button.custom": "自訂", @@ -109,7 +127,7 @@ "emoji_button.food": "飲食", "emoji_button.label": "插入表情符號", "emoji_button.nature": "大自然", - "emoji_button.not_found": "就沒這表情符號吼!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "啊就沒這表情符號吼!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "物件", "emoji_button.people": "使用者", "emoji_button.recent": "最常使用", @@ -118,22 +136,26 @@ "emoji_button.symbols": "符號", "emoji_button.travel": "旅遊與地點", "empty_column.account_timeline": "這裡還沒有嘟文!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_unavailable": "無法取得個人資料", "empty_column.blocks": "你還沒有封鎖任何使用者。", "empty_column.community": "本地時間軸是空的。快公開嘟些文搶頭香啊!", "empty_column.direct": "您還沒有任何私訊。當您私訊別人或收到私訊時,它將於此顯示。", "empty_column.domain_blocks": "尚未隱藏任何網域。", - "empty_column.favourited_statuses": "你還沒有將任何嘟文標為最愛。最愛的嘟文將顯示於此。", - "empty_column.favourites": "還沒有人將此嘟文標為最愛。如果有人標成最愛,則會顯示在這裡。", - "empty_column.follow_requests": "您尚未收到任何關注請求。收到時會顯示於此。", - "empty_column.hashtag": "這個「#」標籤下什麼都沒有。", + "empty_column.favourited_statuses": "你還沒收藏任何嘟文。這裡將會顯示你收藏的嘟文。", + "empty_column.favourites": "還沒有人收藏這則嘟文。這裡將會顯示被收藏的嘟文。", + "empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。", + "empty_column.hashtag": "這個主題標籤下什麼也沒有。", "empty_column.home": "您的首頁時間軸是空的!前往 {public} 或使用搜尋功能來認識其他人。", "empty_column.home.public_timeline": "公開時間軸", - "empty_column.list": "此份名單還沒有東西。當此名單的成員嘟出了新的嘟文時,它們就會出現在這裡。", - "empty_column.lists": "你還沒有建立任何名單。你建立的名單將會顯示在這裡。", - "empty_column.mutes": "你還沒有靜音任何使用者。", + "empty_column.list": "這份名單還沒有東西。當此名單的成員嘟出了新的嘟文時,它們就會顯示於此。", + "empty_column.lists": "你還沒有建立任何名單。這裡將會顯示你所建立的名單。", + "empty_column.mutes": "你尚未靜音任何使用者。", "empty_column.notifications": "您尚未收到任何通知,和別人互動開啟對話吧。", "empty_column.public": "這裡什麼都沒有!嘗試寫些公開的嘟文,或著自己關注其他伺服器的使用者後就會有嘟文出現了", + "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.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.", + "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", + "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "授權", "follow_request.reject": "拒絕", "getting_started.developers": "開發者", @@ -146,72 +168,72 @@ "getting_started.terms": "服務條款", "hashtag.column_header.tag_mode.all": "以及{additional}", "hashtag.column_header.tag_mode.any": "或是{additional}", - "hashtag.column_header.tag_mode.none": "而不用{additional}", + "hashtag.column_header.tag_mode.none": "而無需{additional}", "hashtag.column_settings.select.no_options_message": "找不到建議", - "hashtag.column_settings.select.placeholder": "輸入「#」標籤…", + "hashtag.column_settings.select.placeholder": "輸入主題標籤…", "hashtag.column_settings.tag_mode.all": "全部", "hashtag.column_settings.tag_mode.any": "任一", - "hashtag.column_settings.tag_mode.none": "全都不要", - "hashtag.column_settings.tag_toggle": "對此欄位加入額外標籤", + "hashtag.column_settings.tag_mode.none": "全不", + "hashtag.column_settings.tag_toggle": "將額外標籤加入到這個欄位", "home.column_settings.basic": "基本", - "home.column_settings.show_reblogs": "顯示轉推", + "home.column_settings.show_reblogs": "顯示轉嘟", "home.column_settings.show_replies": "顯示回覆", "intervals.full.days": "{number, plural, one {# 天} other {# 天}}", "intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}", "intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}", "introduction.federation.action": "下一步", - "introduction.federation.federated.headline": "聯邦", - "introduction.federation.federated.text": "來自聯邦網路中其他伺服器的公開嘟文將會在聯邦網路時間軸中顯示。", + "introduction.federation.federated.headline": "站台聯盟", + "introduction.federation.federated.text": "來自聯盟宇宙中其他站台的公開嘟文將會在站點聯盟時間軸中顯示。", "introduction.federation.home.headline": "首頁", - "introduction.federation.home.text": "您所關注使用者所發的嘟文將顯示在首頁的訊息來源。您能關注任何伺服器上的任何人!", - "introduction.federation.local.headline": "本地", - "introduction.federation.local.text": "跟您同伺服器之使用者所發的公開嘟文將會顯示在本地時間軸中。", + "introduction.federation.home.text": "你關注使用者的嘟文將會在首頁動態中顯示。你可以關注任何伺服器上的任何人!", + "introduction.federation.local.headline": "本機", + "introduction.federation.local.text": "跟您同伺服器之使用者所發的公開嘟文將會顯示在本機時間軸中。", "introduction.interactions.action": "完成教學!", - "introduction.interactions.favourite.headline": "最愛", - "introduction.interactions.favourite.text": "您能稍候儲存嘟文,或者將嘟文加到最愛,讓作者知道您喜歡這嘟文。", + "introduction.interactions.favourite.headline": "關注", + "introduction.interactions.favourite.text": "您能儲存嘟文供稍候觀看,或者收藏嘟文,讓作者知道您喜歡這則嘟文。", "introduction.interactions.reblog.headline": "轉嘟", - "introduction.interactions.reblog.text": "您能透過轉嘟他人嘟文來分享給您的關注者。", + "introduction.interactions.reblog.text": "您能藉由轉嘟他人嘟文來分享給您的關注者。", "introduction.interactions.reply.headline": "回覆", - "introduction.interactions.reply.text": "您能回覆其他人或自己的嘟文。將會把這些回覆串成一串對話。", - "introduction.welcome.action": "開始!", + "introduction.interactions.reply.text": "您能回覆其他人或自己的嘟文,這麼做會把這些回覆串成一串對話。", + "introduction.welcome.action": "開始旅程吧!", "introduction.welcome.headline": "第一步", - "introduction.welcome.text": "歡迎來到聯邦!稍候您將可以廣播訊息並跨各種各式各樣的伺服器與朋友聊天。但這台伺服器,{domain},十分特殊 -- 它寄管了您的個人資料,所以請記住這台伺服器的名稱。", + "introduction.welcome.text": "歡迎來到聯盟宇宙!等等你就可以廣播訊息及跨越各種各式各樣的伺服器與朋友聊天。但這台伺服器,{domain},非常特別 - 它寄管了你的個人資料,所以請記住它的名字。", "keyboard_shortcuts.back": "返回上一頁", - "keyboard_shortcuts.blocked": "開啟「封鎖的使用者」名單", + "keyboard_shortcuts.blocked": "開啟「封鎖使用者」名單", "keyboard_shortcuts.boost": "轉嘟", "keyboard_shortcuts.column": "將焦點放在其中一欄的嘟文", "keyboard_shortcuts.compose": "將焦點移至撰寫文字區塊", "keyboard_shortcuts.description": "描述", "keyboard_shortcuts.direct": "開啟私訊欄", - "keyboard_shortcuts.down": "在名單中往下移動", + "keyboard_shortcuts.down": "往下移動名單項目", "keyboard_shortcuts.enter": "檢視嘟文", - "keyboard_shortcuts.favourite": "加入最愛", - "keyboard_shortcuts.favourites": "開啟最愛名單", - "keyboard_shortcuts.federated": "開啟聯邦時間軸", + "keyboard_shortcuts.favourite": "收藏", + "keyboard_shortcuts.favourites": "開啟收藏名單", + "keyboard_shortcuts.federated": "開啟站點聯盟時間軸", "keyboard_shortcuts.heading": "鍵盤快速鍵", "keyboard_shortcuts.home": "開啟首頁時間軸", "keyboard_shortcuts.hotkey": "快速鍵", - "keyboard_shortcuts.legend": "顯示此說明", - "keyboard_shortcuts.local": "開啟本地時間軸", + "keyboard_shortcuts.legend": "顯示此列表", + "keyboard_shortcuts.local": "開啟本機時間軸", "keyboard_shortcuts.mention": "提及作者", "keyboard_shortcuts.muted": "開啟靜音使用者名單", "keyboard_shortcuts.my_profile": "開啟個人資料頁面", "keyboard_shortcuts.notifications": "開啟通知欄", "keyboard_shortcuts.pinned": "開啟釘選的嘟文名單", - "keyboard_shortcuts.profile": "開啟作者的個人資料頁", - "keyboard_shortcuts.reply": "回應嘟文", + "keyboard_shortcuts.profile": "開啟作者的個人資料頁面", + "keyboard_shortcuts.reply": "回覆", "keyboard_shortcuts.requests": "開啟關注請求名單", "keyboard_shortcuts.search": "將焦點移至搜尋框", "keyboard_shortcuts.start": "開啟「開始使用」欄位", "keyboard_shortcuts.toggle_hidden": "顯示/隱藏在內容警告之後的正文", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "顯示 / 隱藏媒體", "keyboard_shortcuts.toot": "開始發出新嘟文", "keyboard_shortcuts.unfocus": "取消輸入文字區塊 / 搜尋的焦點", - "keyboard_shortcuts.up": "在名單中往上移動", + "keyboard_shortcuts.up": "往上移動名單項目", "lightbox.close": "關閉", "lightbox.next": "下一步", "lightbox.previous": "上一步", - "lightbox.view_context": "View context", + "lightbox.view_context": "檢視內文", "lists.account.add": "新增至名單", "lists.account.remove": "從名單中移除", "lists.delete": "刪除名單", @@ -221,23 +243,24 @@ "lists.new.title_placeholder": "新名單標題", "lists.search": "搜尋您關注的使用者", "lists.subheading": "您的名單", + "load_pending": "{count, plural, other {# 個新項目}}", "loading_indicator.label": "讀取中...", "media_gallery.toggle_visible": "切換可見性", "missing_indicator.label": "找不到", "missing_indicator.sublabel": "找不到此資源", - "mute_modal.hide_notifications": "隱藏來自這個使用者的通知?", - "navigation_bar.apps": "行動應用程式", - "navigation_bar.blocks": "封鎖的使用者", - "navigation_bar.community_timeline": "本地時間軸", + "mute_modal.hide_notifications": "隱藏來自這位使用者的通知?", + "navigation_bar.apps": "封鎖的使用者", + "navigation_bar.blocks": "封鎖使用者", + "navigation_bar.community_timeline": "本機時間軸", "navigation_bar.compose": "撰寫新嘟文", "navigation_bar.direct": "私訊", "navigation_bar.discover": "探索", "navigation_bar.domain_blocks": "隱藏的網域", "navigation_bar.edit_profile": "編輯個人資料", - "navigation_bar.favourites": "最愛內容", + "navigation_bar.favourites": "收藏", "navigation_bar.filters": "靜音詞彙", "navigation_bar.follow_requests": "關注請求", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "關注及關注者", "navigation_bar.info": "關於此伺服器", "navigation_bar.keyboard_shortcuts": "快速鍵", "navigation_bar.lists": "名單", @@ -246,7 +269,6 @@ "navigation_bar.personal": "個人", "navigation_bar.pins": "釘選的嘟文", "navigation_bar.preferences": "偏好設定", - "navigation_bar.profile_directory": "Profile directory", "navigation_bar.public_timeline": "聯邦時間軸", "navigation_bar.security": "安全性", "notification.favourite": "{name} 把你的嘟文加入了最愛", @@ -277,8 +299,10 @@ "notifications.group": "{count} 條通知", "poll.closed": "已關閉", "poll.refresh": "重新整理", + "poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_votes": "{count, plural, one {# 個投票} other {# 個投票}}", "poll.vote": "投票", + "poll.voted": "你已對此問題投票", "poll_button.add_poll": "建立投票", "poll_button.remove_poll": "移除投票", "privacy.change": "調整隱私狀態", @@ -290,6 +314,7 @@ "privacy.public.short": "公開", "privacy.unlisted.long": "公開,但不會顯示在公開時間軸", "privacy.unlisted.short": "不公開", + "refresh": "Refresh", "regeneration_indicator.label": "載入中…", "regeneration_indicator.sublabel": "你的主頁時間軸正在準備中!", "relative_time.days": "{number} 天", @@ -314,6 +339,7 @@ "search_results.accounts": "使用者", "search_results.hashtags": "主題標籤", "search_results.statuses": "嘟文", + "search_results.statuses_fts_disabled": "「依內容搜尋嘟文」未在此 Mastodon 伺服器啟用。", "search_results.total": "{count, number} 項結果", "status.admin_account": "開啟 @{name} 的管理介面", "status.admin_status": "在管理介面開啟此嘟文", @@ -352,6 +378,7 @@ "status.show_more": "顯示更多", "status.show_more_all": "顯示更多這類嘟文", "status.show_thread": "顯示討論串", + "status.uncached_media_warning": "無法使用", "status.unmute_conversation": "解除此對話的靜音", "status.unpin": "解除置頂", "suggestions.dismiss": "關閉建議", @@ -367,14 +394,22 @@ "time_remaining.moments": "剩餘時間", "time_remaining.seconds": "剩餘 {number, plural, one {# 秒} other {# 秒}}", "trends.count_by_accounts": "{count} 位使用者在討論", + "trends.trending_now": "目前趨勢", "ui.beforeunload": "如果離開 Mastodon,你的草稿將會不見。", "upload_area.title": "拖放來上傳", "upload_button.label": "上傳媒體檔案 (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "已達到檔案上傳限制。", "upload_error.poll": "不允許在投票上傳檔案。", "upload_form.description": "為視障人士增加文字說明", - "upload_form.focus": "變更預覽", + "upload_form.edit": "編輯", "upload_form.undo": "刪除", + "upload_modal.analyzing_picture": "正在分析圖片…", + "upload_modal.apply": "套用", + "upload_modal.description_placeholder": "A quick brown fox 跳過那隻懶狗", + "upload_modal.detect_text": "從圖片偵測文字", + "upload_modal.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.preview_label": "預覽 ({ratio})", "upload_progress.label": "上傳中...", "video.close": "關閉影片", "video.exit_fullscreen": "退出全螢幕", diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js index 089d920c3..c62ab0dfd 100644 --- a/app/javascript/mastodon/reducers/alerts.js +++ b/app/javascript/mastodon/reducers/alerts.js @@ -14,6 +14,7 @@ export default function alerts(state = initialState, action) { key: state.size > 0 ? state.last().get('key') + 1 : 0, title: action.title, message: action.message, + message_values: action.message_values, })); case ALERT_DISMISS: return state.filterNot(item => item.get('key') === action.alert.key); diff --git a/app/javascript/mastodon/reducers/blocks.js b/app/javascript/mastodon/reducers/blocks.js new file mode 100644 index 000000000..1b6507163 --- /dev/null +++ b/app/javascript/mastodon/reducers/blocks.js @@ -0,0 +1,22 @@ +import Immutable from 'immutable'; + +import { + BLOCKS_INIT_MODAL, +} from '../actions/blocks'; + +const initialState = Immutable.Map({ + new: Immutable.Map({ + account_id: null, + }), +}); + +export default function mutes(state = initialState, action) { + switch (action.type) { + case BLOCKS_INIT_MODAL: + return state.withMutations((state) => { + state.setIn(['new', 'account_id'], action.account.get('id')); + }); + default: + return state; + } +} diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 29c691144..c6653fe4c 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -61,6 +61,7 @@ const initialState = ImmutableMap({ is_uploading: false, progress: 0, media_attachments: ImmutableList(), + pending_media_attachments: 0, poll: null, suggestion_token: null, suggestions: ImmutableList(), @@ -103,14 +104,18 @@ function clearAll(state) { }); }; -function appendMedia(state, media) { +function appendMedia(state, media, file) { const prevSize = state.get('media_attachments').size; return state.withMutations(map => { + if (media.get('type') === 'image') { + media = media.set('file', file); + } map.update('media_attachments', list => list.push(media)); map.set('is_uploading', false); map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); map.set('idempotencyKey', uuid()); + map.update('pending_media_attachments', n => n - 1); if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { map.set('sensitive', true); @@ -144,15 +149,20 @@ const insertSuggestion = (state, position, token, completion, path) => { }); }; -const updateSuggestionTags = (state, token) => { - const prefix = token.slice(1); +const sortHashtagsByUse = (state, tags) => { + const personalHistory = state.get('tagHistory'); - return state.merge({ - suggestions: state.get('tagHistory') - .filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase())) - .slice(0, 4) - .map(tag => '#' + tag), - suggestion_token: token, + return tags.sort((a, b) => { + const usedA = personalHistory.includes(a.name); + const usedB = personalHistory.includes(b.name); + + if (usedA === usedB) { + return 0; + } else if (usedA && !usedB) { + return -1; + } else { + return 1; + } }); }; @@ -195,6 +205,42 @@ const expandMentions = status => { return fragment.innerHTML; }; +const expiresInFromExpiresAt = expires_at => { + if (!expires_at) return 24 * 3600; + const delta = (new Date(expires_at).getTime() - Date.now()) / 1000; + return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600; +}; + +const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => { + prefix = prefix.toLowerCase(); + if (suggestions.length < 4) { + const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase())); + return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag }))); + } else { + return suggestions; + } +}; + +const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => { + if (accounts) { + return accounts.map(item => ({ id: item.id, type: 'account' })); + } else if (emojis) { + return emojis.map(item => ({ ...item, type: 'emoji' })); + } else { + return mergeLocalHashtagResults(sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' }))), token.slice(1), state.get('tagHistory')); + } +}; + +const updateSuggestionTags = (state, token) => { + const prefix = token.slice(1); + + const suggestions = state.get('suggestions').toJS(); + return state.merge({ + suggestions: ImmutableList(mergeLocalHashtagResults(suggestions, prefix, state.get('tagHistory'))), + suggestion_token: token, + }); +}; + export default function compose(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: @@ -224,6 +270,7 @@ export default function compose(state = initialState, action) { } }); case COMPOSE_SPOILER_TEXT_CHANGE: + if (!state.get('spoiler')) return state; return state .set('spoiler_text', action.text) .set('idempotencyKey', uuid()); @@ -277,11 +324,11 @@ export default function compose(state = initialState, action) { case COMPOSE_UPLOAD_CHANGE_FAIL: return state.set('is_changing_upload', false); case COMPOSE_UPLOAD_REQUEST: - return state.set('is_uploading', true); + return state.set('is_uploading', true).update('pending_media_attachments', n => n + 1); case COMPOSE_UPLOAD_SUCCESS: - return appendMedia(state, fromJS(action.media)); + return appendMedia(state, fromJS(action.media), action.file); case COMPOSE_UPLOAD_FAIL: - return state.set('is_uploading', false); + return state.set('is_uploading', false).update('pending_media_attachments', n => n - 1); case COMPOSE_UPLOAD_UNDO: return removeMedia(state, action.media_id); case COMPOSE_UPLOAD_PROGRESS: @@ -304,7 +351,7 @@ export default function compose(state = initialState, action) { case COMPOSE_SUGGESTIONS_CLEAR: return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); + return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion, action.path); case COMPOSE_SUGGESTION_TAGS_UPDATE: @@ -352,7 +399,7 @@ export default function compose(state = initialState, action) { map.set('poll', ImmutableMap({ options: action.status.getIn(['poll', 'options']).map(x => x.get('title')), multiple: action.status.getIn(['poll', 'multiple']), - expires_in: 24 * 3600, + expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])), })); } }); diff --git a/app/javascript/mastodon/reducers/conversations.js b/app/javascript/mastodon/reducers/conversations.js index 9564bffcd..975418eda 100644 --- a/app/javascript/mastodon/reducers/conversations.js +++ b/app/javascript/mastodon/reducers/conversations.js @@ -7,7 +7,10 @@ import { CONVERSATIONS_FETCH_FAIL, CONVERSATIONS_UPDATE, CONVERSATIONS_READ, + CONVERSATIONS_DELETE_SUCCESS, } from '../actions/conversations'; +import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts'; +import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import compareId from '../compare_id'; const initialState = ImmutableMap({ @@ -74,6 +77,10 @@ const expandNormalizedConversations = (state, conversations, next, isLoadingRece }); }; +const filterConversations = (state, accountIds) => { + return state.update('items', list => list.filterNot(item => item.get('accounts').some(accountId => accountIds.includes(accountId)))); +}; + export default function conversations(state = initialState, action) { switch (action.type) { case CONVERSATIONS_FETCH_REQUEST: @@ -96,6 +103,13 @@ export default function conversations(state = initialState, action) { return item; })); + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return filterConversations(state, [action.relationship.id]); + case DOMAIN_BLOCK_SUCCESS: + return filterConversations(state, action.accounts); + case CONVERSATIONS_DELETE_SUCCESS: + return state.update('items', list => list.filterNot(item => item.get('id') === action.id)); default: return state; } diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 981ad8e64..b8d608888 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -15,6 +15,7 @@ import settings from './settings'; import push_notifications from './push_notifications'; import status_lists from './status_lists'; import mutes from './mutes'; +import blocks from './blocks'; import reports from './reports'; import contexts from './contexts'; import compose from './compose'; @@ -31,6 +32,8 @@ import conversations from './conversations'; import suggestions from './suggestions'; import polls from './polls'; import identity_proofs from './identity_proofs'; +import trends from './trends'; +import missed_updates from './missed_updates'; const reducers = { dropdown_menu, @@ -49,6 +52,7 @@ const reducers = { settings, push_notifications, mutes, + blocks, reports, contexts, compose, @@ -65,6 +69,8 @@ const reducers = { conversations, suggestions, polls, + trends, + missed_updates, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/missed_updates.js b/app/javascript/mastodon/reducers/missed_updates.js new file mode 100644 index 000000000..b71d62d82 --- /dev/null +++ b/app/javascript/mastodon/reducers/missed_updates.js @@ -0,0 +1,21 @@ +import { Map as ImmutableMap } from 'immutable'; +import { NOTIFICATIONS_UPDATE } from 'mastodon/actions/notifications'; +import { APP_FOCUS, APP_UNFOCUS } from 'mastodon/actions/app'; + +const initialState = ImmutableMap({ + focused: true, + unread: 0, +}); + +export default function missed_updates(state = initialState, action) { + switch(action.type) { + case APP_FOCUS: + return state.set('focused', true).set('unread', 0); + case APP_UNFOCUS: + return state.set('focused', false); + case NOTIFICATIONS_UPDATE: + return state.get('focused') ? state : state.update('unread', x => x + 1); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/modal.js b/app/javascript/mastodon/reducers/modal.js index 599a2443e..a30da2db1 100644 --- a/app/javascript/mastodon/reducers/modal.js +++ b/app/javascript/mastodon/reducers/modal.js @@ -10,7 +10,7 @@ export default function modal(state = initialState, action) { case MODAL_OPEN: return { modalType: action.modalType, modalProps: action.modalProps }; case MODAL_CLOSE: - return initialState; + return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state; default: return state; } diff --git a/app/javascript/mastodon/reducers/mutes.js b/app/javascript/mastodon/reducers/mutes.js index a96232dbd..4672e5097 100644 --- a/app/javascript/mastodon/reducers/mutes.js +++ b/app/javascript/mastodon/reducers/mutes.js @@ -7,7 +7,6 @@ import { const initialState = Immutable.Map({ new: Immutable.Map({ - isSubmitting: false, account: null, notifications: true, }), @@ -17,7 +16,6 @@ export default function mutes(state = initialState, action) { switch (action.type) { case MUTES_INIT_MODAL: return state.withMutations((state) => { - state.setIn(['new', 'isSubmitting'], false); state.setIn(['new', 'account'], action.account); state.setIn(['new', 'notifications'], true); }); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 4d9604de9..60e901e39 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -6,19 +6,27 @@ import { NOTIFICATIONS_FILTER_SET, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, + NOTIFICATIONS_LOAD_PENDING, + NOTIFICATIONS_MOUNT, + NOTIFICATIONS_UNMOUNT, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, + FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + FOLLOW_REQUEST_REJECT_SUCCESS, } from '../actions/accounts'; +import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from '../compare_id'; const initialState = ImmutableMap({ + pendingItems: ImmutableList(), items: ImmutableList(), hasMore: true, top: false, + mounted: false, unread: 0, isLoading: false, }); @@ -31,9 +39,13 @@ const notificationToMap = notification => ImmutableMap({ status: notification.status ? notification.status.id : null, }); -const normalizeNotification = (state, notification) => { +const normalizeNotification = (state, notification, usePendingItems) => { const top = state.get('top'); + if (usePendingItems || !state.get('pendingItems').isEmpty()) { + return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1); + } + if (!top) { state = state.update('unread', unread => unread + 1); } @@ -47,7 +59,7 @@ const normalizeNotification = (state, notification) => { }); }; -const expandNormalizedNotifications = (state, notifications, next) => { +const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => { let items = ImmutableList(); notifications.forEach((n, i) => { @@ -56,7 +68,9 @@ const expandNormalizedNotifications = (state, notifications, next) => { return state.withMutations(mutable => { if (!items.isEmpty()) { - mutable.update('items', list => { + usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty()); + + mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) ); @@ -77,48 +91,63 @@ const expandNormalizedNotifications = (state, notifications, next) => { }); }; -const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); +const filterNotifications = (state, accountIds, type) => { + const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type'))); + return state.update('items', helper).update('pendingItems', helper); }; const updateTop = (state, top) => { if (top) { - state = state.set('unread', 0); + state = state.set('unread', state.get('pendingItems').size); } return state.set('top', top); }; const deleteByStatus = (state, statusId) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); + const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + return state.update('items', helper).update('pendingItems', helper); }; export default function notifications(state = initialState, action) { switch(action.type) { + case NOTIFICATIONS_LOAD_PENDING: + return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); case NOTIFICATIONS_EXPAND_REQUEST: return state.set('isLoading', true); case NOTIFICATIONS_EXPAND_FAIL: return state.set('isLoading', false); case NOTIFICATIONS_FILTER_SET: - return state.set('items', ImmutableList()).set('hasMore', true); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification); + return normalizeNotification(state, action.notification, action.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next); + return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingRecent, action.usePendingItems); case ACCOUNT_BLOCK_SUCCESS: - return filterNotifications(state, action.relationship); + return filterNotifications(state, [action.relationship.id]); + case ACCOUNT_MUTE_SUCCESS: + return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state; + case DOMAIN_BLOCK_SUCCESS: + return filterNotifications(state, action.accounts); + case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: + case FOLLOW_REQUEST_REJECT_SUCCESS: + return filterNotifications(state, [action.id], 'follow_request'); case ACCOUNT_MUTE_SUCCESS: - return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state; + return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state; case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('hasMore', false); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); case TIMELINE_DELETE: return deleteByStatus(state, action.id); case TIMELINE_DISCONNECT: return action.timeline === 'home' ? - state.update('items', items => items.first() ? items.unshift(null) : items) : + state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; + case NOTIFICATIONS_MOUNT: + return state.set('mounted', true); + case NOTIFICATIONS_UNMOUNT: + return state.set('mounted', false); default: return state; } diff --git a/app/javascript/mastodon/reducers/push_notifications.js b/app/javascript/mastodon/reducers/push_notifications.js index 317352b79..c48cfb705 100644 --- a/app/javascript/mastodon/reducers/push_notifications.js +++ b/app/javascript/mastodon/reducers/push_notifications.js @@ -6,6 +6,7 @@ const initialState = Immutable.Map({ subscription: null, alerts: new Immutable.Map({ follow: false, + follow_request: false, favourite: false, reblog: false, mention: false, diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js index 4758defb1..875b2d92b 100644 --- a/app/javascript/mastodon/reducers/search.js +++ b/app/javascript/mastodon/reducers/search.js @@ -3,6 +3,7 @@ import { SEARCH_CLEAR, SEARCH_FETCH_SUCCESS, SEARCH_SHOW, + SEARCH_EXPAND_SUCCESS, } from '../actions/search'; import { COMPOSE_MENTION, @@ -16,6 +17,7 @@ const initialState = ImmutableMap({ submitted: false, hidden: false, results: ImmutableMap(), + searchTerm: '', }); export default function search(state = initialState, action) { @@ -40,7 +42,10 @@ export default function search(state = initialState, action) { accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), hashtags: fromJS(action.results.hashtags), - })).set('submitted', true); + })).set('submitted', true).set('searchTerm', action.searchTerm); + case SEARCH_EXPAND_SUCCESS: + const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id); + return state.updateIn(['results', action.searchType], list => list.concat(results)); default: return state; } diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index a0eea137f..efef2ad9a 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -10,10 +10,12 @@ import uuid from '../uuid'; const initialState = ImmutableMap({ saved: true, - onboarded: false, - skinTone: 1, + trends: ImmutableMap({ + show: true, + }), + home: ImmutableMap({ shows: ImmutableMap({ reblog: true, @@ -28,6 +30,7 @@ const initialState = ImmutableMap({ notifications: ImmutableMap({ alerts: ImmutableMap({ follow: true, + follow_request: false, favourite: true, reblog: true, mention: true, @@ -42,6 +45,7 @@ const initialState = ImmutableMap({ shows: ImmutableMap({ follow: true, + follow_request: false, favourite: true, reblog: true, mention: true, @@ -50,6 +54,7 @@ const initialState = ImmutableMap({ sounds: ImmutableMap({ follow: true, + follow_request: false, favourite: true, reblog: true, mention: true, @@ -74,10 +79,6 @@ const initialState = ImmutableMap({ body: '', }), }), - - trends: ImmutableMap({ - show: true, - }), }); const defaultColumns = fromJS([ diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 6c5f33557..9f8f28dee 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -7,12 +7,22 @@ import { FAVOURITED_STATUSES_EXPAND_FAIL, } from '../actions/favourites'; import { + BOOKMARKED_STATUSES_FETCH_REQUEST, + BOOKMARKED_STATUSES_FETCH_SUCCESS, + BOOKMARKED_STATUSES_FETCH_FAIL, + BOOKMARKED_STATUSES_EXPAND_REQUEST, + BOOKMARKED_STATUSES_EXPAND_SUCCESS, + BOOKMARKED_STATUSES_EXPAND_FAIL, +} from '../actions/bookmarks'; +import { PINNED_STATUSES_FETCH_SUCCESS, } from '../actions/pin_statuses'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { FAVOURITE_SUCCESS, UNFAVOURITE_SUCCESS, + BOOKMARK_SUCCESS, + UNBOOKMARK_SUCCESS, PIN_SUCCESS, UNPIN_SUCCESS, } from '../actions/interactions'; @@ -23,6 +33,11 @@ const initialState = ImmutableMap({ loaded: false, items: ImmutableList(), }), + bookmarks: ImmutableMap({ + next: null, + loaded: false, + items: ImmutableList(), + }), pins: ImmutableMap({ next: null, loaded: false, @@ -71,10 +86,24 @@ export default function statusLists(state = initialState, action) { return normalizeList(state, 'favourites', action.statuses, action.next); case FAVOURITED_STATUSES_EXPAND_SUCCESS: return appendToList(state, 'favourites', action.statuses, action.next); + case BOOKMARKED_STATUSES_FETCH_REQUEST: + case BOOKMARKED_STATUSES_EXPAND_REQUEST: + return state.setIn(['bookmarks', 'isLoading'], true); + case BOOKMARKED_STATUSES_FETCH_FAIL: + case BOOKMARKED_STATUSES_EXPAND_FAIL: + return state.setIn(['bookmarks', 'isLoading'], false); + case BOOKMARKED_STATUSES_FETCH_SUCCESS: + return normalizeList(state, 'bookmarks', action.statuses, action.next); + case BOOKMARKED_STATUSES_EXPAND_SUCCESS: + return appendToList(state, 'bookmarks', action.statuses, action.next); case FAVOURITE_SUCCESS: return prependOneToList(state, 'favourites', action.status); case UNFAVOURITE_SUCCESS: return removeOneFromList(state, 'favourites', action.status); + case BOOKMARK_SUCCESS: + return prependOneToList(state, 'bookmarks', action.status); + case UNBOOKMARK_SUCCESS: + return removeOneFromList(state, 'bookmarks', action.status); case PINNED_STATUSES_FETCH_SUCCESS: return normalizeList(state, 'pins', action.statuses, action.next); case PIN_SUCCESS: diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 885cc221c..772f98bcb 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -3,6 +3,9 @@ import { REBLOG_FAIL, FAVOURITE_REQUEST, FAVOURITE_FAIL, + UNFAVOURITE_SUCCESS, + BOOKMARK_REQUEST, + BOOKMARK_FAIL, } from '../actions/interactions'; import { STATUS_MUTE_SUCCESS, @@ -37,8 +40,15 @@ export default function statuses(state = initialState, action) { return importStatuses(state, action.statuses); case FAVOURITE_REQUEST: return state.setIn([action.status.get('id'), 'favourited'], true); + case UNFAVOURITE_SUCCESS: + const favouritesCount = action.status.get('favourites_count'); + return state.setIn([action.status.get('id'), 'favourites_count'], favouritesCount - 1); case FAVOURITE_FAIL: return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false); + case BOOKMARK_REQUEST: + return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], true); + case BOOKMARK_FAIL: + return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false); case REBLOG_REQUEST: return state.setIn([action.status.get('id'), 'reblogged'], true); case REBLOG_FAIL: diff --git a/app/javascript/mastodon/reducers/suggestions.js b/app/javascript/mastodon/reducers/suggestions.js index 9f4b89d58..834be728f 100644 --- a/app/javascript/mastodon/reducers/suggestions.js +++ b/app/javascript/mastodon/reducers/suggestions.js @@ -4,6 +4,8 @@ import { SUGGESTIONS_FETCH_FAIL, SUGGESTIONS_DISMISS, } from '../actions/suggestions'; +import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts'; +import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ @@ -24,6 +26,11 @@ export default function suggestionsReducer(state = initialState, action) { return state.set('isLoading', false); case SUGGESTIONS_DISMISS: return state.update('items', list => list.filterNot(id => id === action.id)); + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return state.update('items', list => list.filterNot(id => id === action.relationship.id)); + case DOMAIN_BLOCK_SUCCESS: + return state.update('items', list => list.filterNot(id => action.accounts.includes(id))); default: return state; } diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 309a95a19..0d7222e10 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -8,6 +8,7 @@ import { TIMELINE_SCROLL_TOP, TIMELINE_CONNECT, TIMELINE_DISCONNECT, + TIMELINE_LOAD_PENDING, } from '../actions/timelines'; import { ACCOUNT_BLOCK_SUCCESS, @@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({ top: true, isLoading: false, hasMore: true, + pendingItems: ImmutableList(), items: ImmutableList(), }); -const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => { +const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); mMap.set('isPartial', isPartial); @@ -38,7 +40,9 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is if (timeline.endsWith(':pinned')) { mMap.set('items', statuses.map(status => status.get('id'))); } else if (!statuses.isEmpty()) { - mMap.update('items', ImmutableList(), oldIds => { + usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('pendingItems').isEmpty()); + + mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; @@ -57,8 +61,17 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is })); }; -const updateTimeline = (state, timeline, status) => { - const top = state.getIn([timeline, 'top']); +const updateTimeline = (state, timeline, status, usePendingItems) => { + const top = state.getIn([timeline, 'top']); + + if (usePendingItems || !state.getIn([timeline, 'pendingItems']).isEmpty()) { + if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { + return state; + } + + return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id'))).update('unread', unread => unread + 1)); + } + const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); const unread = state.getIn([timeline, 'unread'], 0); @@ -78,8 +91,10 @@ const updateTimeline = (state, timeline, status) => { const deleteStatus = (state, id, accountId, references, exclude_account = null) => { state.keySeq().forEach(timeline => { - if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) { + const helper = list => list.filterNot(item => item === id); + state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper); + } }); // Remove reblogs of deleted status @@ -109,29 +124,31 @@ const filterTimelines = (state, relationship, statuses) => { return state; }; -const filterTimeline = (timeline, state, relationship, statuses) => - state.updateIn([timeline, 'items'], ImmutableList(), list => - list.filterNot(statusId => - statuses.getIn([statusId, 'account']) === relationship.id - )); +const filterTimeline = (timeline, state, relationship, statuses) => { + const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id); + return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper); +}; const updateTop = (state, timeline, top) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - if (top) mMap.set('unread', 0); + if (top) mMap.set('unread', mMap.get('pendingItems').size); mMap.set('top', top); })); }; export default function timelines(state = initialState, action) { switch(action.type) { + case TIMELINE_LOAD_PENDING: + return state.update(action.timeline, initialTimeline, map => + map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0)); case TIMELINE_EXPAND_REQUEST: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_EXPAND_SUCCESS: - return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent); + return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status)); + return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case TIMELINE_CLEAR: @@ -149,7 +166,7 @@ export default function timelines(state = initialState, action) { return state.update( action.timeline, initialTimeline, - map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items) + map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) ); default: return state; diff --git a/app/javascript/mastodon/reducers/trends.js b/app/javascript/mastodon/reducers/trends.js new file mode 100644 index 000000000..5cecc8fca --- /dev/null +++ b/app/javascript/mastodon/reducers/trends.js @@ -0,0 +1,23 @@ +import { TRENDS_FETCH_REQUEST, TRENDS_FETCH_SUCCESS, TRENDS_FETCH_FAIL } from '../actions/trends'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; + +const initialState = ImmutableMap({ + items: ImmutableList(), + isLoading: false, +}); + +export default function trendsReducer(state = initialState, action) { + switch(action.type) { + case TRENDS_FETCH_REQUEST: + return state.set('isLoading', true); + case TRENDS_FETCH_SUCCESS: + return state.withMutations(map => { + map.set('items', fromJS(action.trends)); + map.set('isLoading', false); + }); + case TRENDS_FETCH_FAIL: + return state.set('isLoading', false); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 8db18c5dc..a7853452f 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -1,4 +1,7 @@ import { + NOTIFICATIONS_UPDATE, +} from '../actions/notifications'; +import { FOLLOWERS_FETCH_SUCCESS, FOLLOWERS_EXPAND_SUCCESS, FOLLOWING_FETCH_SUCCESS, @@ -20,6 +23,14 @@ import { MUTES_FETCH_SUCCESS, MUTES_EXPAND_SUCCESS, } from '../actions/mutes'; +import { + DIRECTORY_FETCH_REQUEST, + DIRECTORY_FETCH_SUCCESS, + DIRECTORY_FETCH_FAIL, + DIRECTORY_EXPAND_REQUEST, + DIRECTORY_EXPAND_SUCCESS, + DIRECTORY_EXPAND_FAIL, +} from 'mastodon/actions/directory'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; const initialState = ImmutableMap({ @@ -45,6 +56,12 @@ const appendToList = (state, type, id, accounts, next) => { }); }; +const normalizeFollowRequest = (state, notification) => { + return state.updateIn(['follow_requests', 'items'], list => { + return list.filterNot(item => item === notification.account.id).unshift(notification.account.id); + }); +}; + export default function userLists(state = initialState, action) { switch(action.type) { case FOLLOWERS_FETCH_SUCCESS: @@ -59,6 +76,8 @@ export default function userLists(state = initialState, action) { return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id))); case FAVOURITES_FETCH_SUCCESS: return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id))); + case NOTIFICATIONS_UPDATE: + return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state; case FOLLOW_REQUESTS_FETCH_SUCCESS: return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); case FOLLOW_REQUESTS_EXPAND_SUCCESS: @@ -74,6 +93,16 @@ export default function userLists(state = initialState, action) { return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); case MUTES_EXPAND_SUCCESS: return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + case DIRECTORY_FETCH_SUCCESS: + return state.setIn(['directory', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false); + case DIRECTORY_EXPAND_SUCCESS: + return state.updateIn(['directory', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false); + case DIRECTORY_FETCH_REQUEST: + case DIRECTORY_EXPAND_REQUEST: + return state.setIn(['directory', 'isLoading'], true); + case DIRECTORY_FETCH_FAIL: + case DIRECTORY_EXPAND_FAIL: + return state.setIn(['directory', 'isLoading'], false); default: return state; } diff --git a/app/javascript/mastodon/rtl.js b/app/javascript/mastodon/rtl.js index 00870a15d..89bed6de8 100644 --- a/app/javascript/mastodon/rtl.js +++ b/app/javascript/mastodon/rtl.js @@ -20,6 +20,7 @@ export function isRtl(text) { text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, ''); text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, ''); text = text.replace(/\s+/g, ''); + text = text.replace(/(\w\S+\.\w{2,}\S*)/g, ''); const matches = text.match(rtlChars); diff --git a/app/javascript/mastodon/scroll.js b/app/javascript/mastodon/scroll.js index 2af07e0fb..84fe58269 100644 --- a/app/javascript/mastodon/scroll.js +++ b/app/javascript/mastodon/scroll.js @@ -26,5 +26,7 @@ const scroll = (node, key, target) => { }; }; -export const scrollRight = (node, position) => scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => scroll(node, 'scrollTop', 0); +const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; + +export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); +export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 70f08a8eb..6f1ce9602 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; +import { List as ImmutableList, is } from 'immutable'; import { me } from '../initial_state'; const getAccountBase = (state, id) => state.getIn(['accounts', id], null); @@ -36,12 +36,10 @@ const toServerSideType = columnType => { } }; -export const getFilters = (state, { contextType }) => state.get('filters', ImmutableList()).filter(filter => contextType && filter.get('context').includes(toServerSideType(contextType)) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); - const escapeRegExp = string => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -export const regexFromFilters = filters => { +const regexFromFilters = filters => { if (filters.size === 0) { return null; } @@ -63,6 +61,27 @@ export const regexFromFilters = filters => { }).join('|'), 'i'); }; +// Memoize the filter regexps for each valid server contextType +const makeGetFiltersRegex = () => { + let memo = {}; + + return (state, { contextType }) => { + if (!contextType) return ImmutableList(); + + const serverSideType = toServerSideType(contextType); + const filters = state.get('filters', ImmutableList()).filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); + + if (!memo[serverSideType] || !is(memo[serverSideType].filters, filters)) { + const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible'))); + const regex = regexFromFilters(filters); + memo[serverSideType] = { filters: filters, results: [dropRegex, regex] }; + } + return memo[serverSideType].results; + }; +}; + +export const getFiltersRegex = makeGetFiltersRegex(); + export const makeGetStatus = () => { return createSelector( [ @@ -70,10 +89,10 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), - getFilters, + getFiltersRegex, ], - (statusBase, statusReblog, accountBase, accountReblog, filters) => { + (statusBase, statusReblog, accountBase, accountReblog, filtersRegex) => { if (!statusBase) { return null; } @@ -84,8 +103,13 @@ export const makeGetStatus = () => { statusReblog = null; } - const regex = (accountReblog || accountBase).get('id') !== me && regexFromFilters(filters); - const filtered = regex && regex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index')); + const dropRegex = (accountReblog || accountBase).get('id') !== me && filtersRegex[0]; + if (dropRegex && dropRegex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index'))) { + return null; + } + + const regex = (accountReblog || accountBase).get('id') !== me && filtersRegex[1]; + const filtered = regex && regex.test(statusBase.get('reblog') ? statusReblog.get('search_index') : statusBase.get('search_index')); return statusBase.withMutations(map => { map.set('reblog', statusReblog); @@ -104,6 +128,7 @@ export const getAlerts = createSelector([getAlertsBase], (base) => { base.forEach(item => { arr.push({ message: item.get('message'), + message_values: item.get('message_values'), title: item.get('title'), key: item.get('key'), dismissAfter: 5000, diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js index 5ce8c7b50..1265f3cfa 100644 --- a/app/javascript/mastodon/service_worker/web_push_locales.js +++ b/app/javascript/mastodon/service_worker/web_push_locales.js @@ -16,6 +16,7 @@ filenames.forEach(filename => { filtered[locale] = { 'notification.favourite': full['notification.favourite'] || '', 'notification.follow': full['notification.follow'] || '', + 'notification.follow_request': full['notification.follow_request'] || '', 'notification.mention': full['notification.mention'] || '', 'notification.reblog': full['notification.reblog'] || '', 'notification.poll': full['notification.poll'] || '', diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js index c4642344f..50f90d44c 100644 --- a/app/javascript/mastodon/stream.js +++ b/app/javascript/mastodon/stream.js @@ -1,4 +1,4 @@ -import WebSocketClient from 'websocket.js'; +import WebSocketClient from '@gamestdio/websocket'; const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); diff --git a/app/javascript/mastodon/utils/log_out.js b/app/javascript/mastodon/utils/log_out.js new file mode 100644 index 000000000..b43417f4b --- /dev/null +++ b/app/javascript/mastodon/utils/log_out.js @@ -0,0 +1,33 @@ +import Rails from 'rails-ujs'; + +export const logOut = () => { + const form = document.createElement('form'); + + const methodInput = document.createElement('input'); + methodInput.setAttribute('name', '_method'); + methodInput.setAttribute('value', 'delete'); + methodInput.setAttribute('type', 'hidden'); + form.appendChild(methodInput); + + const csrfToken = Rails.csrfToken(); + const csrfParam = Rails.csrfParam(); + + if (csrfParam && csrfToken) { + const csrfInput = document.createElement('input'); + csrfInput.setAttribute('name', csrfParam); + csrfInput.setAttribute('value', csrfToken); + csrfInput.setAttribute('type', 'hidden'); + form.appendChild(csrfInput); + } + + const submitButton = document.createElement('input'); + submitButton.setAttribute('type', 'submit'); + form.appendChild(submitButton); + + form.method = 'post'; + form.action = '/auth/sign_out'; + form.style.display = 'none'; + + document.body.appendChild(form); + submitButton.click(); +}; diff --git a/app/javascript/mastodon/utils/numbers.js b/app/javascript/mastodon/utils/numbers.js index fdd8269ae..f7e4ceb93 100644 --- a/app/javascript/mastodon/utils/numbers.js +++ b/app/javascript/mastodon/utils/numbers.js @@ -4,7 +4,9 @@ import { FormattedNumber } from 'react-intl'; export const shortNumberFormat = number => { if (number < 1000) { return <FormattedNumber value={number} />; - } else { + } else if (number < 1000000) { return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>; + } else { + return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>; } }; diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js index bbdbc865e..7196dc96b 100644 --- a/app/javascript/mastodon/utils/resize_image.js +++ b/app/javascript/mastodon/utils/resize_image.js @@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => { }); const getOrientation = (img, type = 'image/png') => new Promise(resolve => { - if (!['image/jpeg', 'image/webp'].includes(type)) { + if (type !== 'image/jpeg') { resolve(1); return; } @@ -67,6 +67,14 @@ const processImage = (img, { width, height, orientation, type = 'image/png' }) = context.drawImage(img, 0, 0, width, height); + // The Tor Browser and maybe other browsers may prevent reading from canvas + // and return an all-white image instead. Assume reading failed if the resized + // image is perfectly white. + const imageData = context.getImageData(0, 0, width, height); + if (imageData.data.every(value => value === 255)) { + throw 'Failed to read from canvas'; + } + canvas.toBlob(resolve, type); }); diff --git a/app/javascript/mastodon/utils/scrollbar.js b/app/javascript/mastodon/utils/scrollbar.js new file mode 100644 index 000000000..6f0ee010b --- /dev/null +++ b/app/javascript/mastodon/utils/scrollbar.js @@ -0,0 +1,36 @@ +import { isMobile } from '../is_mobile'; + +/** @type {number | null} */ +let cachedScrollbarWidth = null; + +/** + * @return {number} + */ +const getActualScrollbarWidth = () => { + const outer = document.createElement('div'); + outer.style.visibility = 'hidden'; + outer.style.overflow = 'scroll'; + document.body.appendChild(outer); + + const inner = document.createElement('div'); + outer.appendChild(inner); + + const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; + outer.parentNode.removeChild(outer); + + return scrollbarWidth; +}; + +/** + * @return {number} + */ +export const getScrollbarWidth = () => { + if (cachedScrollbarWidth !== null) { + return cachedScrollbarWidth; + } + + const scrollbarWidth = isMobile(window.innerWidth) ? 0 : getActualScrollbarWidth(); + cachedScrollbarWidth = scrollbarWidth; + + return scrollbarWidth; +}; diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 69e6ba0ec..3eae1a457 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -1,6 +1,7 @@ import loadPolyfills from '../mastodon/load_polyfills'; import ready from '../mastodon/ready'; import { start } from '../mastodon/common'; +import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; start(); @@ -14,10 +15,10 @@ function main() { const React = require('react'); const ReactDOM = require('react-dom'); const Rellax = require('rellax'); - const createHistory = require('history').createBrowserHistory; + const { createBrowserHistory } = require('history'); const scrollToDetailedStatus = () => { - const history = createHistory(); + const history = createBrowserHistory(); const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status'); const location = history.location; @@ -27,6 +28,12 @@ function main() { } }; + const getEmojiAnimationHandler = (swapTo) => { + return ({ target }) => { + target.src = target.getAttribute(swapTo); + }; + }; + ready(() => { const locale = document.documentElement.lang; @@ -92,16 +99,33 @@ function main() { new Rellax('.parallax', { speed: -1 }); } - if (document.body.classList.contains('with-modals')) { - const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; - const scrollbarWidthStyle = document.createElement('style'); - scrollbarWidthStyle.id = 'scrollbar-width'; - document.head.appendChild(scrollbarWidthStyle); - scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0); + delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original')); + delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static')); + + delegate(document, '.blocks-table button.icon-button', 'click', function(e) { + e.preventDefault(); + + const classList = this.firstElementChild.classList; + classList.toggle('fa-chevron-down'); + classList.toggle('fa-chevron-up'); + this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden'); + }); + }); + + delegate(document, '.sidebar__toggle__icon', 'click', () => { + const target = document.querySelector('.sidebar ul'); + + if (target.style.display === 'block') { + target.style.display = 'none'; + } else { + target.style.display = 'block'; } }); } -loadPolyfills().then(main).catch(error => { - console.error(error); -}); +loadPolyfills() + .then(main) + .then(loadKeyboardExtensions) + .catch(error => { + console.error(error); + }); diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 6db3bc3dc..8ebc45b62 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -13,7 +13,7 @@ @import 'mastodon/widgets'; @import 'mastodon/forms'; @import 'mastodon/accounts'; -@import 'mastodon/stream_entries'; +@import 'mastodon/statuses'; @import 'mastodon/boost'; @import 'mastodon/components'; @import 'mastodon/polls'; diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss index b4fb1d709..e25a80c04 100644 --- a/app/javascript/styles/mailer.scss +++ b/app/javascript/styles/mailer.scss @@ -457,6 +457,13 @@ h5 { .status { padding-bottom: 32px; + &--highlighted { + border: 1px solid lighten($ui-base-color, 8%); + border-radius: 4px; + padding-bottom: 16px; + margin-bottom: 16px; + } + .status-header { td { font-size: 14px; diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index ee8a7d265..05e52966b 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -14,15 +14,49 @@ html { } } +.status-card__actions button, +.status-card__actions a { + color: rgba($white, 0.8); + + &:hover, + &:active, + &:focus { + color: $white; + } +} + // Change default background colors of columns .column > .scrollable, .getting-started, -.column-inline-form { +.column-inline-form, +.error-column, +.regeneration-indicator { background: $white; border: 1px solid lighten($ui-base-color, 8%); border-top: 0; } +.directory__card__img { + background: lighten($ui-base-color, 12%); +} + +.filter-form, +.directory__card__bar { + background: $white; + border-bottom: 1px solid lighten($ui-base-color, 8%); +} + +.scrollable .directory__list { + width: calc(100% + 2px); + margin-left: -1px; + margin-right: -1px; +} + +.directory__card, +.table-of-contents { + border: 1px solid lighten($ui-base-color, 8%); +} + .column-back-button, .column-header { background: $white; @@ -33,16 +67,16 @@ html { } &--slim-button { - border: 0; - top: -49px; - right: 1px; + top: -50px; + right: 0; } } .column-header__back-button, .column-header__button, .column-header__button.active, -.account__header__bar { +.account__header__bar, +.directory__card__extra { background: $white; } @@ -66,6 +100,19 @@ html { text-decoration: underline; } +.confirmation-modal__secondary-button, +.confirmation-modal__cancel-button, +.mute-modal__cancel-button, +.block-modal__cancel-button { + color: lighten($ui-base-color, 26%); + + &:hover, + &:focus, + &:active { + color: $primary-text-color; + } +} + .column-subheading { background: darken($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 8%); @@ -104,7 +151,8 @@ html { .box-widget input[type="email"], .box-widget input[type="password"], .box-widget textarea, -.statuses-grid .detailed-status { +.statuses-grid .detailed-status, +.audio-player { border: 1px solid lighten($ui-base-color, 8%); } @@ -309,11 +357,23 @@ html { .boost-modal, .confirmation-modal, .mute-modal, +.block-modal, .report-modal, .embed-modal, .error-modal, -.onboarding-modal { - background: $ui-base-color; +.onboarding-modal, +.report-modal__comment .setting-text__wrapper, +.report-modal__comment .setting-text { + background: $white; + border: 1px solid lighten($ui-base-color, 8%); +} + +.report-modal__comment { + border-right-color: lighten($ui-base-color, 8%); +} + +.report-modal__container { + border-top-color: lighten($ui-base-color, 8%); } .column-header__collapsible-inner { @@ -322,9 +382,14 @@ html { border-top: 0; } +.focal-point__preview strong { + color: $white; +} + .boost-modal__action-bar, .confirmation-modal__action-bar, .mute-modal__action-bar, +.block-modal__action-bar, .onboarding-modal__paginator, .error-modal__footer { background: darken($ui-base-color, 6%); @@ -345,9 +410,11 @@ html { .embed-modal .embed-modal__container .embed-modal__html { background: $white; + border: 1px solid lighten($ui-base-color, 8%); &:focus { - background: darken($ui-base-color, 6%); + border-color: lighten($ui-base-color, 12%); + background: $white; } } @@ -700,3 +767,10 @@ html { .compose-form .compose-form__warning { box-shadow: none; } + +.audio-player .video-player__controls button, +.audio-player .video-player__time-sep, +.audio-player .video-player__time-current, +.audio-player .video-player__time-total { + color: $primary-text-color; +} diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss index 01748148f..c68944528 100644 --- a/app/javascript/styles/mastodon-light/variables.scss +++ b/app/javascript/styles/mastodon-light/variables.scss @@ -8,7 +8,7 @@ $classic-secondary-color: #d9e1e8; $classic-highlight-color: #2b90d9; // Differences -$success-green: #3c754d; +$success-green: lighten(#3c754d, 8%); $base-overlay-background: $white !default; $valid-value-color: $success-green !default; diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss index faaffb30f..68cad0fde 100644 --- a/app/javascript/styles/mastodon/_mixins.scss +++ b/app/javascript/styles/mastodon/_mixins.scss @@ -22,24 +22,6 @@ color: $darker-text-color; font-size: 14px; margin: 0; - - &::-moz-focus-inner { - border: 0; - } - - &::-moz-focus-inner, - &:focus, - &:active { - outline: 0 !important; - } - - &:focus { - background: lighten($ui-base-color, 4%); - } - - @media screen and (max-width: 600px) { - font-size: 16px; - } } @mixin search-popout { diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index 61637ce96..cf16b54ac 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -17,109 +17,102 @@ $small-breakpoint: 960px; .rich-formatting { font-family: $font-sans-serif, sans-serif; - font-size: 16px; + font-size: 14px; font-weight: 400; - font-size: 16px; - line-height: 30px; + line-height: 1.7; + word-wrap: break-word; color: $darker-text-color; - padding-right: 10px; a { color: $highlight-text-color; text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } } p, li { - font-family: $font-sans-serif, sans-serif; - font-size: 16px; - font-weight: 400; - font-size: 16px; - line-height: 30px; - margin-bottom: 12px; color: $darker-text-color; + } - a { - color: $highlight-text-color; - text-decoration: underline; - } + p { + margin-top: 0; + margin-bottom: .85em; &:last-child { margin-bottom: 0; } } - strong, - em { + strong { font-weight: 700; - color: lighten($darker-text-color, 10%); + color: $secondary-text-color; } - h1 { - font-family: $font-display, sans-serif; - font-size: 26px; - line-height: 30px; - font-weight: 500; - margin-bottom: 20px; + em { + font-style: italic; color: $secondary-text-color; + } - small { - font-family: $font-sans-serif, sans-serif; - display: block; - font-size: 18px; - font-weight: 400; - color: lighten($darker-text-color, 10%); - } + code { + font-size: 0.85em; + background: darken($ui-base-color, 8%); + border-radius: 4px; + padding: 0.2em 0.3em; } - h2 { + h1, + h2, + h3, + h4, + h5, + h6 { font-family: $font-display, sans-serif; - font-size: 22px; - line-height: 26px; + margin-top: 1.275em; + margin-bottom: .85em; font-weight: 500; - margin-bottom: 20px; color: $secondary-text-color; } + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.75em; + } + h3 { - font-family: $font-display, sans-serif; - font-size: 18px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + font-size: 1.5em; } h4 { - font-family: $font-display, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + font-size: 1.25em; } - h5 { - font-family: $font-display, sans-serif; - font-size: 14px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + h5, + h6 { + font-size: 1em; } - h6 { - font-family: $font-display, sans-serif; - font-size: 12px; - line-height: 24px; - font-weight: 500; - margin-bottom: 20px; - color: $secondary-text-color; + ul { + list-style: disc; + } + + ol { + list-style: decimal; } ul, ol { - margin-left: 20px; + margin: 0; + padding: 0; + padding-left: 2em; + margin-bottom: 0.85em; &[type='a'] { list-style-type: lower-alpha; @@ -130,31 +123,74 @@ $small-breakpoint: 960px; } } - ul { - list-style: disc; - } - - ol { - list-style: decimal; - } - - li > ol, - li > ul { - margin-top: 6px; - } - hr { width: 100%; height: 0; border: 0; - border-bottom: 1px solid rgba($ui-base-lighter-color, .6); - margin: 20px 0; + border-bottom: 1px solid lighten($ui-base-color, 4%); + margin: 1.7em 0; &.spacer { height: 1px; border: 0; } } + + table { + width: 100%; + border-collapse: collapse; + break-inside: auto; + margin-top: 24px; + margin-bottom: 32px; + + thead tr, + tbody tr { + border-bottom: 1px solid lighten($ui-base-color, 4%); + font-size: 1em; + line-height: 1.625; + font-weight: 400; + text-align: left; + color: $darker-text-color; + } + + thead tr { + border-bottom-width: 2px; + line-height: 1.5; + font-weight: 500; + color: $dark-text-color; + } + + th, + td { + padding: 8px; + align-self: start; + align-items: start; + word-break: break-all; + + &.nowrap { + width: 25%; + position: relative; + + &::before { + content: ' '; + visibility: hidden; + } + + span { + position: absolute; + left: 8px; + right: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } + + & > :first-child { + margin-top: 0; + } } .information-board { @@ -416,7 +452,7 @@ $small-breakpoint: 960px; } &__call-to-action { - background: darken($ui-base-color, 4%); + background: $ui-base-color; border-radius: 4px; padding: 25px 40px; overflow: hidden; diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index f95313a25..5dc067f0e 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -224,6 +224,7 @@ } .account__header__fields { + max-width: 100vw; padding: 0; margin: 15px -15px -15px; border: 0 none; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 692d86852..de95d82bf 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -5,21 +5,66 @@ $content-width: 840px; .admin-wrapper { display: flex; justify-content: center; - height: 100%; + width: 100%; + min-height: 100vh; .sidebar-wrapper { - flex: 1 1 $sidebar-width; - height: 100%; - background: $ui-base-color; - display: flex; - justify-content: flex-end; + min-height: 100vh; + overflow: hidden; + pointer-events: none; + flex: 1 1 auto; + + &__inner { + display: flex; + justify-content: flex-end; + background: $ui-base-color; + height: 100%; + } } .sidebar { width: $sidebar-width; - height: 100%; padding: 0; - overflow-y: auto; + pointer-events: auto; + + &__toggle { + display: none; + background: lighten($ui-base-color, 8%); + height: 48px; + + &__logo { + flex: 1 1 auto; + + a { + display: inline-block; + padding: 15px; + } + + svg { + fill: $primary-text-color; + height: 20px; + position: relative; + bottom: -2px; + } + } + + &__icon { + display: block; + color: $darker-text-color; + text-decoration: none; + flex: 0 0 auto; + font-size: 20px; + padding: 15px; + } + + a { + &:hover, + &:focus, + &:active { + background: lighten($ui-base-color, 12%); + } + } + } .logo { display: block; @@ -52,6 +97,9 @@ $content-width: 840px; transition: all 200ms linear; transition-property: color, background-color; border-radius: 4px 0 0 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; i.fa { margin-right: 5px; @@ -99,12 +147,30 @@ $content-width: 840px; } .content-wrapper { - flex: 2 1 $content-width; - overflow: auto; + box-sizing: border-box; + width: 100%; + max-width: $content-width; + flex: 1 1 auto; + } + + @media screen and (max-width: $content-width + $sidebar-width) { + .sidebar-wrapper--empty { + display: none; + } + + .sidebar-wrapper { + width: $sidebar-width; + flex: 0 0 auto; + } + } + + @media screen and (max-width: $no-columns-breakpoint) { + .sidebar-wrapper { + width: 100%; + } } .content { - max-width: $content-width; padding: 20px 15px; padding-top: 60px; padding-left: 25px; @@ -123,6 +189,12 @@ $content-width: 840px; padding-bottom: 40px; border-bottom: 1px solid lighten($ui-base-color, 8%); margin-bottom: 40px; + + @media screen and (max-width: $no-columns-breakpoint) { + border-bottom: 0; + padding-bottom: 0; + font-weight: 700; + } } h3 { @@ -147,7 +219,7 @@ $content-width: 840px; font-size: 16px; color: $secondary-text-color; line-height: 28px; - font-weight: 400; + font-weight: 500; } .fields-group h6 { @@ -176,7 +248,7 @@ $content-width: 840px; & > p { font-size: 14px; - line-height: 18px; + line-height: 21px; color: $secondary-text-color; margin-bottom: 20px; @@ -204,49 +276,86 @@ $content-width: 840px; border: 0; } } - - .muted-hint { - color: $darker-text-color; - - a { - color: $highlight-text-color; - } - } - - .positive-hint { - color: $valid-value-color; - font-weight: 500; - } - - .negative-hint { - color: $error-value-color; - font-weight: 500; - } - - .neutral-hint { - color: $dark-text-color; - font-weight: 500; - } } @media screen and (max-width: $no-columns-breakpoint) { display: block; - overflow-y: auto; - -webkit-overflow-scrolling: touch; - .sidebar-wrapper, - .content-wrapper { - flex: 0 0 auto; - height: auto; - overflow: initial; + .sidebar-wrapper { + min-height: 0; } .sidebar { width: 100%; padding: 0; height: auto; + + &__toggle { + display: flex; + } + + & > ul { + display: none; + } + + ul a, + ul ul a { + border-radius: 0; + border-bottom: 1px solid lighten($ui-base-color, 4%); + transition: none; + + &:hover { + transition: none; + } + } + + ul ul { + border-radius: 0; + } + + ul .simple-navigation-active-leaf a { + border-bottom-color: $ui-highlight-color; + } + } + } +} + +hr.spacer { + width: 100%; + border: 0; + margin: 20px 0; + height: 1px; +} + +body, +.admin-wrapper .content { + .muted-hint { + color: $darker-text-color; + + a { + color: $highlight-text-color; } } + + .positive-hint { + color: $valid-value-color; + font-weight: 500; + } + + .negative-hint { + color: $error-value-color; + font-weight: 500; + } + + .neutral-hint { + color: $dark-text-color; + font-weight: 500; + } + + .warning-hint { + color: $gold-star; + font-weight: 500; + } } .filters { @@ -255,10 +364,10 @@ $content-width: 840px; .filter-subset { flex: 0 0 auto; - margin: 0 40px 10px 0; + margin: 0 40px 20px 0; &:last-child { - margin-bottom: 20px; + margin-bottom: 30px; } ul { @@ -321,6 +430,22 @@ $content-width: 840px; } } +.flavour-screen { + display: block; + margin: 10px auto; + max-width: 100%; +} + +.flavour-description { + display: block; + font-size: 16px; + margin: 10px 0; + + & > p { + margin: 10px 0; + } +} + .report-accounts { display: flex; flex-wrap: wrap; @@ -720,3 +845,47 @@ a.name-tag, text-overflow: ellipsis; vertical-align: middle; } + +.admin-account-bio { + display: flex; + flex-wrap: wrap; + margin: 0 -5px; + margin-top: 20px; + + > div { + box-sizing: border-box; + padding: 0 5px; + margin-bottom: 10px; + flex: 1 0 50%; + } + + .account__header__fields, + .account__header__content { + background: lighten($ui-base-color, 8%); + border-radius: 4px; + height: 100%; + } + + .account__header__fields { + margin: 0; + border: 0; + + a { + color: lighten($ui-highlight-color, 8%); + } + + dl:first-child .verified { + border-radius: 0 4px 0 0; + } + + .verified a { + color: $valid-value-color; + } + } + + .account__header__content { + box-sizing: border-box; + padding: 20px; + color: $primary-text-color; + } +} diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index b5a77ce94..2b10b5ad3 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -8,7 +8,7 @@ body { font-family: $font-sans-serif, sans-serif; - background: darken($ui-base-color, 8%); + background: darken($ui-base-color, 7%); font-size: 13px; line-height: 18px; font-weight: 400; @@ -35,11 +35,19 @@ body { } &.app-body { - position: absolute; - width: 100%; - height: 100%; padding: 0; - background: $ui-base-color; + + &.layout-single-column { + height: auto; + min-height: 100vh; + overflow-y: scroll; + } + + &.layout-multiple-columns { + position: absolute; + width: 100%; + height: 100%; + } &.with-modals--active { overflow-y: hidden; @@ -56,7 +64,6 @@ body { &--active { overflow-y: hidden; - margin-right: 13px; } } @@ -79,9 +86,6 @@ body { &.admin { background: darken($ui-base-color, 4%); - position: fixed; - width: 100%; - height: 100%; padding: 0; } @@ -131,12 +135,97 @@ button { .app-holder { &, - & > div { + & > div, + & > noscript { display: flex; width: 100%; - height: 100%; align-items: center; justify-content: center; outline: 0 !important; } + + & > noscript { + height: 100vh; + } +} + +.layout-single-column .app-holder { + &, + & > div { + min-height: 100vh; + } +} + +.layout-multiple-columns .app-holder { + &, + & > div { + height: 100%; + } +} + +.error-boundary, +.app-holder noscript { + flex-direction: column; + font-size: 16px; + font-weight: 400; + line-height: 1.7; + color: lighten($error-red, 4%); + text-align: center; + + & > div { + max-width: 500px; + } + + p { + margin-bottom: .85em; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: $highlight-text-color; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + + &__footer { + color: $dark-text-color; + font-size: 13px; + + a { + color: $dark-text-color; + } + } + + button { + display: inline; + border: 0; + background: transparent; + color: $dark-text-color; + font: inherit; + padding: 0; + margin: 0; + line-height: inherit; + cursor: pointer; + outline: 0; + transition: color 300ms linear; + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + + &.copied { + color: $valid-value-color; + transition: none; + } + } } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 597a8d1dc..0ec25e3f8 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3,6 +3,27 @@ -ms-overflow-style: -ms-autohiding-scrollbar; } +.link-button { + display: block; + font-size: 15px; + line-height: 20px; + color: $ui-highlight-color; + border: 0; + background: transparent; + padding: 0; + cursor: pointer; + + &:hover, + &:active { + text-decoration: underline; + } + + &:disabled { + color: $ui-primary-color; + cursor: default; + } +} + .button { background-color: $ui-highlight-color; border: 10px none; @@ -129,19 +150,28 @@ padding: 0; color: $action-button-color; border: 0; + border-radius: 4px; background: transparent; cursor: pointer; - transition: color 100ms ease-in; + transition: all 100ms ease-in; + transition-property: background-color, color; &:hover, &:active, &:focus { color: lighten($action-button-color, 7%); - transition: color 200ms ease-out; + background-color: rgba($action-button-color, 0.15); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + + &:focus { + background-color: rgba($action-button-color, 0.3); } &.disabled { color: darken($action-button-color, 13%); + background-color: transparent; cursor: default; } @@ -166,10 +196,16 @@ &:active, &:focus { color: darken($lighter-text-color, 7%); + background-color: rgba($lighter-text-color, 0.15); + } + + &:focus { + background-color: rgba($lighter-text-color, 0.3); } &.disabled { color: lighten($lighter-text-color, 7%); + background-color: transparent; } &.active { @@ -197,6 +233,7 @@ .text-icon-button { color: $lighter-text-color; border: 0; + border-radius: 4px; background: transparent; cursor: pointer; font-weight: 600; @@ -204,17 +241,25 @@ padding: 0 3px; line-height: 27px; outline: 0; - transition: color 100ms ease-in; + transition: all 100ms ease-in; + transition-property: background-color, color; &:hover, &:active, &:focus { color: darken($lighter-text-color, 7%); - transition: color 200ms ease-out; + background-color: rgba($lighter-text-color, 0.15); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + + &:focus { + background-color: rgba($lighter-text-color, 0.3); } &.disabled { color: lighten($lighter-text-color, 20%); + background-color: transparent; cursor: default; } @@ -347,6 +392,7 @@ .autosuggest-input, .spoiler-input { position: relative; + width: 100%; } .spoiler-input { @@ -376,6 +422,10 @@ border: 0; outline: 0; + &::placeholder { + color: $dark-text-color; + } + &:focus { outline: 0; } @@ -445,7 +495,8 @@ } .autosuggest-account, - .autosuggest-emoji { + .autosuggest-emoji, + .autosuggest-hashtag { display: flex; flex-direction: row; align-items: center; @@ -454,6 +505,29 @@ font-size: 14px; } + .autosuggest-hashtag { + justify-content: space-between; + + &__name { + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + strong { + font-weight: 500; + } + + &__uses { + flex: 0 0 auto; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + .autosuggest-account-icon, .autosuggest-emoji img { display: block; @@ -557,6 +631,7 @@ .compose-form__upload-thumbnail { border-radius: 4px; + background-color: $base-shadow-color; background-position: center; background-size: cover; background-repeat: no-repeat; @@ -594,7 +669,8 @@ } } - .icon-button { + .icon-button, + .text-icon-button { box-sizing: content-box; padding: 0 3px; } @@ -602,18 +678,6 @@ .character-counter__wrapper { align-self: center; margin-right: 4px; - - .character-counter { - cursor: default; - font-family: $font-sans-serif, sans-serif; - font-size: 14px; - font-weight: 600; - color: $lighter-text-color; - - &.character-counter--over { - color: $warning-red; - } - } } } @@ -630,6 +694,18 @@ } } +.character-counter { + cursor: default; + font-family: $font-sans-serif, sans-serif; + font-size: 14px; + font-weight: 600; + color: $lighter-text-color; + + &.character-counter--over { + color: $warning-red; + } +} + .no-reduce-motion .spoiler-input { transition: height 0.4s ease, opacity 0.4s ease; } @@ -721,7 +797,7 @@ white-space: pre-wrap; &:last-child { - margin-bottom: 2px; + margin-bottom: 0; } } @@ -752,6 +828,10 @@ } } + a.unhandled-link { + color: lighten($ui-highlight-color, 8%); + } + .status__content__spoiler-link { background: $action-button-color; @@ -876,7 +956,8 @@ opacity: 1; animation: fade 150ms linear; - .video-player { + .video-player, + .audio-player { margin-top: 8px; } @@ -971,7 +1052,8 @@ white-space: normal; } - .video-player { + .video-player, + .audio-player { margin-top: 8px; max-width: 250px; } @@ -1082,7 +1164,8 @@ } } - .video-player { + .video-player, + .audio-player { margin-top: 8px; } } @@ -1194,14 +1277,28 @@ &-composite { @include avatar-radius; + border-radius: 50%; overflow: hidden; + position: relative; + cursor: default; & > div { - @include avatar-radius; float: left; position: relative; box-sizing: border-box; } + + &__label { + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: $primary-text-color; + text-shadow: 1px 1px 2px $base-shadow-color; + font-weight: 700; + font-size: 15px; + } } } @@ -1390,6 +1487,10 @@ a.account__display-name { color: inherit; } +.detailed-status .button.logo-button { + margin-bottom: 15px; +} + .detailed-status__display-name { color: $secondary-text-color; display: block; @@ -1433,6 +1534,7 @@ a.account__display-name { } .muted { + .status__content, .status__content p, .status__content a { color: $dark-text-color; @@ -1490,6 +1592,24 @@ a.account__display-name { color: $gold-star; } +.bookmark-icon.active { + color: $red-bookmark; +} + +.no-reduce-motion .icon-button.star-icon { + &.activate { + & > .fa-star { + animation: spring-rotate-in 1s linear; + } + } + + &.deactivate { + & > .fa-star { + animation: spring-rotate-out 1s linear; + } + } +} + .notification__display-name { color: inherit; font-weight: 500; @@ -1803,6 +1923,7 @@ a.account__display-name { justify-content: center; width: 100%; height: 100%; + min-height: 100vh; &__pane { height: 100%; @@ -1810,12 +1931,14 @@ a.account__display-name { pointer-events: none; display: flex; justify-content: flex-end; + min-width: 285px; &--start { justify-content: flex-start; } &__inner { + position: fixed; width: 285px; pointer-events: auto; height: 100%; @@ -1826,6 +1949,7 @@ a.account__display-name { box-sizing: border-box; width: 100%; max-width: 600px; + flex: 0 0 auto; display: flex; flex-direction: column; @@ -1836,6 +1960,26 @@ a.account__display-name { } } +.tabs-bar__wrapper { + background: darken($ui-base-color, 8%); + position: sticky; + top: 0; + z-index: 2; + padding-top: 0; + + @media screen and (min-width: $no-gap-breakpoint) { + padding-top: 10px; + } + + .tabs-bar { + margin-bottom: 0; + + @media screen and (min-width: $no-gap-breakpoint) { + margin-bottom: 10px; + } + } +} + .react-swipeable-view-container { &, .columns-area, @@ -1870,7 +2014,6 @@ a.account__display-name { flex-direction: column; width: 100%; height: 100%; - background: darken($ui-base-color, 7%); } .drawer { @@ -1996,6 +2139,24 @@ a.account__display-name { padding: 0; } + .directory__list { + display: grid; + grid-gap: 10px; + grid-template-columns: minmax(0, 50%) minmax(0, 50%); + + @media screen and (max-width: $no-gap-breakpoint) { + display: block; + } + } + + .directory__card { + margin-bottom: 0; + } + + .filter-form { + display: flex; + } + .autosuggest-textarea__textarea { font-size: 16px; } @@ -2011,8 +2172,17 @@ a.account__display-name { top: 15px; } + .scrollable { + overflow: visible; + + @supports(display: grid) { + contain: content; + } + } + @media screen and (min-width: $no-gap-breakpoint) { padding: 10px 0; + padding-top: 0; } @media screen and (min-width: 630px) { @@ -2020,7 +2190,8 @@ a.account__display-name { padding: 15px; .media-gallery, - .video-player { + .video-player, + .audio-player { margin-top: 15px; } } @@ -2062,7 +2233,8 @@ a.account__display-name { .media-gallery, &__action-bar, - .video-player { + .video-player, + .audio-player { margin-top: 10px; } } @@ -2127,17 +2299,14 @@ a.account__display-name { @media screen and (min-width: $no-gap-breakpoint) { .tabs-bar { - margin: 10px auto; - margin-bottom: 0; width: 100%; } .react-swipeable-view-container .columns-area--mobile { - height: calc(100% - 20px) !important; + height: calc(100% - 10px) !important; } .getting-started__wrapper, - .getting-started__trends, .search { margin-bottom: 10px; } @@ -2244,13 +2413,24 @@ a.account__display-name { margin-bottom: 10px; height: calc(100% - 20px); overflow-y: auto; + display: flex; + flex-direction: column; + + & > a { + flex: 0 0 auto; + } hr { + flex: 0 0 auto; border: 0; background: transparent; border-top: 1px solid lighten($ui-base-color, 4%); margin: 10px 0; } + + .flex-spacer { + background: transparent; + } } .drawer__pager { @@ -2361,6 +2541,8 @@ a.account__display-name { } .column-back-button { + box-sizing: border-box; + width: 100%; background: lighten($ui-base-color, 4%); color: $highlight-text-color; cursor: pointer; @@ -2640,8 +2822,19 @@ a.account__display-name { } &__trends { - background: $ui-base-color; flex: 0 1 auto; + opacity: 1; + animation: fade 150ms linear; + margin-top: 10px; + + h4 { + font-size: 12px; + text-transform: uppercase; + color: $darker-text-color; + padding: 10px; + font-weight: 500; + border-bottom: 1px solid lighten($ui-base-color, 8%); + } @media screen and (max-height: 810px) { .trends__item:nth-child(3) { @@ -2658,11 +2851,15 @@ a.account__display-name { @media screen and (max-height: 670px) { display: none; } - } - &__scrollable { - max-height: 100%; - overflow-y: auto; + .trends__item { + border-bottom: 0; + padding: 10px; + + &__current { + color: $darker-text-color; + } + } } } @@ -2949,37 +3146,27 @@ a.status-card.compact:hover { cursor: default; display: flex; flex: 1 1 auto; + flex-direction: column; align-items: center; justify-content: center; padding: 20px; - & > div { - width: 100%; - background: transparent; - padding-top: 0; - } - &__figure { - background: url('~images/elephant_ui_working.svg') no-repeat center 0; - width: 100%; - height: 160px; - background-size: contain; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + &, + img { + display: block; + width: auto; + height: 160px; + margin: 0; + } } - &.missing-indicator { + &--without-header { padding-top: 20px + 48px; - - .regeneration-indicator__figure { - background-image: url('~images/elephant_ui_disappointed.svg'); - } } &__label { - margin-top: 200px; + margin-top: 30px; strong { display: block; @@ -3205,6 +3392,50 @@ a.status-card.compact:hover { animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1); } +@keyframes spring-rotate-in { + 0% { + transform: rotate(0deg); + } + + 30% { + transform: rotate(-484.8deg); + } + + 60% { + transform: rotate(-316.7deg); + } + + 90% { + transform: rotate(-375deg); + } + + 100% { + transform: rotate(-360deg); + } +} + +@keyframes spring-rotate-out { + 0% { + transform: rotate(-360deg); + } + + 30% { + transform: rotate(124.8deg); + } + + 60% { + transform: rotate(-43.27deg); + } + + 90% { + transform: rotate(15deg); + } + + 100% { + transform: rotate(0deg); + } +} + @keyframes loader-figure { 0% { width: 0; @@ -3299,6 +3530,10 @@ a.status-card.compact:hover { height: auto; } + &--click-thru { + pointer-events: none; + } + &--hidden { display: none; } @@ -3327,6 +3562,12 @@ a.status-card.compact:hover { background: rgba($base-overlay-background, 0.8); } } + + &:disabled { + .spoiler-button__overlay__label { + background: rgba($base-overlay-background, 0.5); + } + } } } @@ -3370,6 +3611,28 @@ a.status-card.compact:hover { .column-select { &__control { @include search-input; + + &::placeholder { + color: lighten($darker-text-color, 4%); + } + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus, + &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($ui-base-color, 4%); + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } } &__placeholder { @@ -3883,6 +4146,28 @@ a.status-card.compact:hover { padding-right: 30px; line-height: 18px; font-size: 16px; + + &::placeholder { + color: lighten($darker-text-color, 4%); + } + + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus, + &:active { + outline: 0 !important; + } + + &:focus { + background: lighten($ui-base-color, 4%); + } + + @media screen and (max-width: 600px) { + font-size: 16px; + } } .search__icon { @@ -3995,6 +4280,12 @@ a.status-card.compact:hover { } } +.search-results__info { + padding: 20px; + color: $darker-text-color; + text-align: center; +} + .modal-root { position: relative; transition: opacity 0.3s linear; @@ -4033,10 +4324,13 @@ a.status-card.compact:hover { z-index: 9999; } -.video-modal { +.video-modal__container { max-width: 100vw; max-height: 100vh; - position: relative; +} + +.audio-modal__container { + width: 50vw; } .media-modal { @@ -4136,6 +4430,7 @@ a.status-card.compact:hover { } a { + pointer-events: auto; text-decoration: none; font-weight: 500; color: $ui-secondary-color; @@ -4299,7 +4594,8 @@ a.status-card.compact:hover { .confirmation-modal, .report-modal, .actions-modal, -.mute-modal { +.mute-modal, +.block-modal { background: lighten($ui-secondary-color, 8%); color: $inverted-text-color; border-radius: 8px; @@ -4353,7 +4649,8 @@ a.status-card.compact:hover { .boost-modal__action-bar, .confirmation-modal__action-bar, -.mute-modal__action-bar { +.mute-modal__action-bar, +.block-modal__action-bar { display: flex; justify-content: space-between; background: $ui-secondary-color; @@ -4381,11 +4678,13 @@ a.status-card.compact:hover { font-size: 14px; } -.mute-modal { +.mute-modal, +.block-modal { line-height: 24px; } -.mute-modal .react-toggle { +.mute-modal .react-toggle, +.block-modal .react-toggle { vertical-align: middle; } @@ -4414,7 +4713,8 @@ a.status-card.compact:hover { } } -.report-modal__statuses { +.report-modal__statuses, +.focal-point-modal__content { flex: 1 1 auto; min-height: 20vh; max-height: 80vh; @@ -4435,6 +4735,12 @@ a.status-card.compact:hover { } } +.focal-point-modal__content { + @media screen and (max-width: 480px) { + max-height: 40vh; + } +} + .report-modal__comment { padding: 20px; border-right: 1px solid $ui-secondary-color; @@ -4456,16 +4762,56 @@ a.status-card.compact:hover { padding: 10px; font-family: inherit; font-size: 14px; - resize: vertical; + resize: none; border: 0; outline: 0; border-radius: 4px; border: 1px solid $ui-secondary-color; - margin-bottom: 20px; + min-height: 100px; + max-height: 50vh; + margin-bottom: 10px; &:focus { border: 1px solid darken($ui-secondary-color, 8%); } + + &__wrapper { + background: $white; + border: 1px solid $ui-secondary-color; + margin-bottom: 10px; + border-radius: 4px; + + .setting-text { + border: 0; + margin-bottom: 0; + border-radius: 0; + + &:focus { + border: 0; + } + } + + &__modifiers { + color: $inverted-text-color; + font-family: inherit; + font-size: 14px; + background: $white; + } + } + + &__toolbar { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + } + } + + .setting-text-label { + display: block; + color: $inverted-text-color; + font-size: 14px; + font-weight: 500; + margin-bottom: 10px; } .setting-toggle { @@ -4549,33 +4895,36 @@ a.status-card.compact:hover { } .confirmation-modal__action-bar, -.mute-modal__action-bar { - .confirmation-modal__secondary-button, - .confirmation-modal__cancel-button, - .mute-modal__cancel-button { - background-color: transparent; - color: $lighter-text-color; - font-size: 14px; - font-weight: 500; - - &:hover, - &:focus, - &:active { - color: darken($lighter-text-color, 4%); - } - } - +.mute-modal__action-bar, +.block-modal__action-bar { .confirmation-modal__secondary-button { flex-shrink: 1; } } +.confirmation-modal__secondary-button, +.confirmation-modal__cancel-button, +.mute-modal__cancel-button, +.block-modal__cancel-button { + background-color: transparent; + color: $lighter-text-color; + font-size: 14px; + font-weight: 500; + + &:hover, + &:focus, + &:active { + color: darken($lighter-text-color, 4%); + background-color: transparent; + } +} + .confirmation-modal__container, .mute-modal__container, +.block-modal__container, .report-modal__target { padding: 30px; font-size: 16px; - text-align: center; strong { font-weight: 500; @@ -4588,11 +4937,36 @@ a.status-card.compact:hover { } } +.confirmation-modal__container, .report-modal__target { - padding: 20px; + text-align: center; +} + +.block-modal, +.mute-modal { + &__explanation { + margin-top: 20px; + } + + .setting-toggle { + margin-top: 20px; + margin-bottom: 24px; + display: flex; + align-items: center; + + &__label { + color: $inverted-text-color; + margin: 0; + margin-left: 8px; + } + } +} + +.report-modal__target { + padding: 15px; .media-modal__close { - top: 19px; + top: 14px; right: 15px; } } @@ -4603,6 +4977,7 @@ a.status-card.compact:hover { position: absolute; top: 0; left: 0; + z-index: 9999; } .media-gallery__gifv__label { @@ -4788,70 +5163,64 @@ a.status-card.compact:hover { } /* End Media Gallery */ -/* Status Video Player */ -.status__video-player { - background: $base-overlay-background; - box-sizing: border-box; - cursor: default; /* May not be needed */ - margin-top: 8px; - overflow: hidden; - position: relative; -} +.detailed, +.fullscreen { + .video-player__volume__current, + .video-player__volume::before { + bottom: 27px; + } -.status__video-player-video { - height: 100%; - object-fit: cover; - position: relative; - top: 50%; - transform: translateY(-50%); - width: 100%; - z-index: 1; -} + .video-player__volume__handle { + bottom: 23px; + } -.status__video-player-expand, -.status__video-player-mute { - color: $primary-text-color; - opacity: 0.8; - position: absolute; - right: 4px; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; } -.status__video-player-spoiler { - display: none; - color: $primary-text-color; - left: 4px; - position: absolute; - text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color; - top: 4px; - z-index: 100; +.audio-player { + box-sizing: border-box; + position: relative; + background: darken($ui-base-color, 8%); + border-radius: 4px; + padding-bottom: 44px; + direction: ltr; - &.status__video-player-spoiler--visible { - display: block; + &.editable { + border-radius: 0; + height: 100%; } -} -.status__video-player-expand { - bottom: 4px; - z-index: 100; -} + &__waveform { + padding: 15px 0; + position: relative; + overflow: hidden; -.status__video-player-mute { - top: 4px; - z-index: 5; -} + &::before { + content: ""; + display: block; + position: absolute; + border-top: 1px solid lighten($ui-base-color, 4%); + width: 100%; + height: 0; + left: 0; + top: calc(50% + 1px); + } + } -.detailed, -.fullscreen { - .video-player__volume__current, - .video-player__volume::before { - bottom: 27px; + &__progress-placeholder { + background-color: rgba(lighten($ui-highlight-color, 8%), 0.5); } - .video-player__volume__handle { - bottom: 23px; + &__wave-placeholder { + background-color: lighten($ui-base-color, 16%); } + .video-player__controls { + padding: 0 15px; + padding-top: 10px; + background: darken($ui-base-color, 8%); + border-top: 1px solid lighten($ui-base-color, 4%); + border-radius: 0 0 4px 4px; + } } .video-player { @@ -4860,6 +5229,13 @@ a.status-card.compact:hover { background: $base-shadow-color; max-width: 100%; border-radius: 4px; + box-sizing: border-box; + direction: ltr; + + &.editable { + border-radius: 0; + height: 100% !important; + } &:focus { outline: 0; @@ -4881,6 +5257,7 @@ a.status-card.compact:hover { max-height: 100% !important; width: 100% !important; height: 100% !important; + outline: 0; } } @@ -4958,6 +5335,10 @@ a.status-card.compact:hover { display: flex; justify-content: space-between; padding-bottom: 10px; + + .video-player__download__icon { + color: inherit; + } } &__buttons { @@ -5141,28 +5522,137 @@ a.status-card.compact:hover { } } -.media-spoiler-video { - background-size: cover; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; - margin-top: 8px; - position: relative; - border: 0; - display: block; -} +.directory { + &__list { + width: 100%; + margin: 10px 0; + transition: opacity 100ms ease-in; -.media-spoiler-video-play-icon { - border-radius: 100px; - color: rgba($primary-text-color, 0.8); - font-size: 36px; - left: 50%; - padding: 5px; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); + &.loading { + opacity: 0.7; + } + + @media screen and (max-width: $no-gap-breakpoint) { + margin: 0; + } + } + + &__card { + box-sizing: border-box; + margin-bottom: 10px; + + &__img { + height: 125px; + position: relative; + background: darken($ui-base-color, 12%); + overflow: hidden; + + img { + display: block; + width: 100%; + height: 100%; + margin: 0; + object-fit: cover; + } + } + + &__bar { + display: flex; + align-items: center; + background: lighten($ui-base-color, 4%); + padding: 10px; + + &__name { + flex: 1 1 auto; + display: flex; + align-items: center; + text-decoration: none; + overflow: hidden; + } + + &__relationship { + width: 23px; + min-height: 1px; + flex: 0 0 auto; + } + + .avatar { + flex: 0 0 auto; + width: 48px; + height: 48px; + padding-top: 2px; + + img { + width: 100%; + height: 100%; + display: block; + margin: 0; + border-radius: 4px; + background: darken($ui-base-color, 8%); + object-fit: cover; + } + } + + .display-name { + margin-left: 15px; + text-align: left; + + strong { + font-size: 15px; + color: $primary-text-color; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + } + + span { + display: block; + font-size: 14px; + color: $darker-text-color; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + &__extra { + background: $ui-base-color; + display: flex; + align-items: center; + justify-content: center; + + .accounts-table__count { + width: 33.33%; + flex: 0 0 auto; + padding: 15px 0; + } + + .account__header__content { + box-sizing: border-box; + padding: 15px 10px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + width: 100%; + min-height: 18px + 30px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + p { + display: none; + + &:first-child { + display: inline; + } + } + + br { + display: none; + } + } + } + } } -/* End Video Player */ .account-gallery__container { display: flex; @@ -5238,6 +5728,73 @@ a.status-card.compact:hover { } } } + + &.directory__section-headline { + background: darken($ui-base-color, 2%); + border-bottom-color: transparent; + + a, + button { + &.active { + &::before { + display: none; + } + + &::after { + border-color: transparent transparent darken($ui-base-color, 7%); + } + } + } + } +} + +.filter-form { + background: $ui-base-color; + + &__column { + padding: 10px 15px; + } + + .radio-button { + display: block; + } +} + +.radio-button { + font-size: 14px; + position: relative; + display: inline-block; + padding: 6px 0; + line-height: 18px; + cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + + input[type=radio], + input[type=checkbox] { + display: none; + } + + &__input { + display: inline-block; + position: relative; + border: 1px solid $ui-primary-color; + box-sizing: border-box; + width: 18px; + height: 18px; + flex: 0 0 auto; + margin-right: 10px; + top: -1px; + border-radius: 50%; + vertical-align: middle; + + &.checked { + border-color: lighten($ui-highlight-color, 8%); + background: lighten($ui-highlight-color, 8%); + } + } } ::-webkit-scrollbar-thumb { @@ -5362,6 +5919,7 @@ noscript { } .embed-modal { + width: auto; max-width: 80vw; max-height: 80vh; @@ -5392,6 +5950,7 @@ noscript { font-size: 14px; margin: 0; margin-bottom: 15px; + border-radius: 4px; &::-moz-focus-inner { border: 0; @@ -5417,6 +5976,7 @@ noscript { max-width: 100%; overflow: hidden; border: 0; + border-radius: 4px; } } } @@ -5589,27 +6149,26 @@ noscript { } } -.focal-point-modal { - max-width: 80vw; - max-height: 80vh; - position: relative; -} - .focal-point { position: relative; - cursor: pointer; + cursor: move; overflow: hidden; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background: $base-shadow-color; - &.dragging { - cursor: move; - } - - img { - max-width: 80vw; + img, + video, + canvas { + display: block; max-height: 80vh; - width: auto; + width: 100%; height: auto; - margin: auto; + margin: 0; + object-fit: contain; + background: $base-shadow-color; } &__reticle { @@ -5629,6 +6188,43 @@ noscript { top: 0; left: 0; } + + &__preview { + position: absolute; + bottom: 10px; + right: 10px; + z-index: 2; + cursor: move; + transition: opacity 0.1s ease; + + &:hover { + opacity: 0.5; + } + + strong { + color: $primary-text-color; + font-size: 14px; + font-weight: 500; + display: block; + margin-bottom: 5px; + } + + div { + border-radius: 4px; + box-shadow: 0 0 14px rgba($base-shadow-color, 0.2); + } + } + + @media screen and (max-width: 480px) { + img, + video { + max-height: 100%; + } + + &__preview { + display: none; + } + } } .account__header__content { @@ -5881,11 +6477,12 @@ noscript { &__current { flex: 0 0 auto; - width: 100px; font-size: 24px; line-height: 36px; font-weight: 500; - text-align: center; + text-align: right; + padding-right: 15px; + margin-left: 5px; color: $secondary-text-color; } @@ -5893,55 +6490,104 @@ noscript { flex: 0 0 auto; width: 50px; - path { + path:first-child { + fill: rgba($highlight-text-color, 0.25) !important; + fill-opacity: 1 !important; + } + + path:last-child { stroke: lighten($highlight-text-color, 6%) !important; } } } } -.layout-toggle { +.conversation { display: flex; + border-bottom: 1px solid lighten($ui-base-color, 8%); padding: 5px; + padding-bottom: 0; - button { - box-sizing: border-box; - flex: 0 0 50%; - background: transparent; - padding: 5px; - border: 0; + &:focus { + background: lighten($ui-base-color, 2%); + outline: 0; + } + + &__avatar { + flex: 0 0 auto; + padding: 10px; + padding-top: 12px; position: relative; + } - &:hover, - &:focus, - &:active { - svg path:first-child { - fill: lighten($ui-base-color, 16%); + &__unread { + display: inline-block; + background: $highlight-text-color; + border-radius: 50%; + width: 0.625rem; + height: 0.625rem; + margin: -.1ex .15em .1ex; + } + + &__content { + flex: 1 1 auto; + padding: 10px 5px; + padding-right: 15px; + overflow: hidden; + + &__info { + overflow: hidden; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + } + + &__relative-time { + font-size: 15px; + color: $darker-text-color; + padding-left: 15px; + } + + &__names { + color: $darker-text-color; + font-size: 15px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; + flex-basis: 90px; + flex-grow: 1; + + a { + color: $primary-text-color; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } } } + + a { + word-break: break-word; + } } - svg { - width: 100%; - height: auto; + &--unread { + background: lighten($ui-base-color, 2%); - path:first-child { - fill: lighten($ui-base-color, 12%); + &:focus { + background: lighten($ui-base-color, 4%); } - path:last-child { - fill: darken($ui-base-color, 14%); + .conversation__content__info { + font-weight: 700; } - } - &__active { - color: $ui-highlight-color; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: lighten($ui-base-color, 12%); - border-radius: 50%; - padding: 0.35rem; + .conversation__content__relative-time { + color: $primary-text-color; + } } } diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 3564bf07b..51d9b46b0 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -141,10 +141,71 @@ grid-row: 3; } + @media screen and (max-width: $no-gap-breakpoint) { + grid-gap: 0; + grid-template-columns: minmax(0, 100%); + + .column-0 { + grid-column: 1; + } + + .column-1 { + grid-column: 1; + grid-row: 3; + } + + .column-2 { + grid-column: 1; + grid-row: 2; + } + + .column-3 { + grid-column: 1; + grid-row: 4; + } + } +} + +.grid-4 { + display: grid; + grid-gap: 10px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-auto-columns: 25%; + grid-auto-rows: max-content; + + .column-0 { + grid-column: 1 / 5; + grid-row: 1; + } + + .column-1 { + grid-column: 1 / 4; + grid-row: 2; + } + + .column-2 { + grid-column: 4; + grid-row: 2; + } + + .column-3 { + grid-column: 2 / 5; + grid-row: 3; + } + + .column-4 { + grid-column: 1; + grid-row: 3; + } + .landing-page__call-to-action { min-height: 100%; } + .flash-message { + margin-bottom: 10px; + } + @media screen and (max-width: 738px) { grid-template-columns: minmax(0, 50%) minmax(0, 50%); @@ -186,6 +247,11 @@ .column-3 { grid-column: 1; + grid-row: 5; + } + + .column-4 { + grid-column: 1; grid-row: 4; } } @@ -346,6 +412,20 @@ } } + .directory__card { + border-radius: 4px; + + @media screen and (max-width: $no-gap-breakpoint) { + border-radius: 0; + } + } + + .page-header { + @media screen and (max-width: $no-gap-breakpoint) { + border-bottom: 0; + } + } + .public-account-header { overflow: hidden; margin-bottom: 10px; @@ -566,7 +646,7 @@ } .counter { - width: 33.3%; + min-width: 33.3%; box-sizing: border-box; flex: 0 0 auto; color: $darker-text-color; @@ -749,14 +829,22 @@ } } - .static-icon-button { - color: $action-button-color; - font-size: 18px; + .directory__list { + display: grid; + grid-gap: 10px; + grid-template-columns: minmax(0, 50%) minmax(0, 50%); - & > span { - font-size: 14px; - font-weight: 500; + @media screen and (max-width: $no-gap-breakpoint) { + display: block; } + + .icon-button { + font-size: 18px; + } + } + + .directory__card { + margin-bottom: 0; } .card-grid { diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss index e4564f062..c0944d417 100644 --- a/app/javascript/styles/mastodon/dashboard.scss +++ b/app/javascript/styles/mastodon/dashboard.scss @@ -15,6 +15,8 @@ padding: 20px; background: lighten($ui-base-color, 4%); border-radius: 4px; + box-sizing: border-box; + height: 100%; } & > a { diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss index f74c004e9..00d290883 100644 --- a/app/javascript/styles/mastodon/footer.scss +++ b/app/javascript/styles/mastodon/footer.scss @@ -128,7 +128,7 @@ &:hover, &:focus, &:active { - svg path { + svg { fill: lighten($ui-base-color, 38%); } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 456ee4e0d..a0478bf7f 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -112,6 +112,15 @@ code { padding: 0.2em 0.4em; background: darken($ui-base-color, 12%); } + + li { + list-style: disc; + margin-left: 18px; + } + } + + ul.hint { + margin-bottom: 15px; } span.hint { @@ -245,6 +254,10 @@ code { &-6 { max-width: 50%; } + + .actions { + margin-top: 27px; + } } .fields-group:last-child, @@ -300,6 +313,13 @@ code { } } + .input.static .label_input__wrapper { + font-size: 16px; + padding: 10px; + border: 1px solid $dark-text-color; + border-radius: 4px; + } + input[type=text], input[type=number], input[type=email], @@ -318,6 +338,10 @@ code { border-radius: 4px; padding: 10px; + &::placeholder { + color: lighten($darker-text-color, 4%); + } + &:invalid { box-shadow: none; } @@ -489,6 +513,10 @@ code { &__overlay-area { position: relative; + &__blurred form { + filter: blur(2px); + } + &__overlay { position: absolute; top: 0; @@ -499,8 +527,10 @@ code { justify-content: center; align-items: center; background: rgba($ui-base-color, 0.65); - backdrop-filter: blur(2px); border-radius: 4px; + margin-left: -4px; + margin-top: -4px; + padding: 4px; &__content { text-align: center; diff --git a/app/javascript/styles/mastodon/introduction.scss b/app/javascript/styles/mastodon/introduction.scss index 222d8f60e..b44ae7306 100644 --- a/app/javascript/styles/mastodon/introduction.scss +++ b/app/javascript/styles/mastodon/introduction.scss @@ -3,9 +3,10 @@ flex-direction: column; justify-content: center; align-items: center; + height: 100vh; + background: $ui-base-color; @media screen and (max-width: 920px) { - background: darken($ui-base-color, 8%); display: block !important; } diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index 12f57b7a9..d7d850a1e 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -5,7 +5,6 @@ li { margin-bottom: 10px; position: relative; - height: 18px + 12px; } &__chart { @@ -24,13 +23,11 @@ &__text { position: relative; - display: inline-block; + display: flex; padding: 6px 0; line-height: 18px; cursor: default; - white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; input[type=radio], input[type=checkbox] { @@ -82,6 +79,9 @@ top: -1px; border-radius: 50%; vertical-align: middle; + margin-top: auto; + margin-bottom: auto; + flex: 0 0 18px; &.checkbox { border-radius: 4px; @@ -91,14 +91,40 @@ border-color: $valid-value-color; background: $valid-value-color; } + + &:active, + &:focus, + &:hover { + border-width: 4px; + background: none; + } + + &::-moz-focus-inner { + outline: 0 !important; + border: 0; + } + + &:focus, + &:active { + outline: 0 !important; + } } &__number { display: inline-block; - width: 36px; + width: 52px; font-weight: 700; padding: 0 10px; + padding-left: 8px; text-align: right; + margin-top: auto; + margin-bottom: auto; + flex: 0 0 52px; + } + + &__vote__mark { + float: left; + line-height: 18px; } &__footer { @@ -151,6 +177,10 @@ button, select { flex: 1 1 50%; + + &:focus { + border-color: $highlight-text-color; + } } } diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index 58bc53b14..ecd166253 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -7,6 +7,34 @@ body.rtl { padding-right: 15px; } + .radio-button__input { + margin-right: 0; + margin-left: 10px; + } + + .directory__card__bar .display-name { + margin-left: 0; + margin-right: 15px; + } + + .display-name { + text-align: right; + } + + .notification__message { + margin-left: 0; + margin-right: 68px; + } + + .drawer__inner__mastodon > img { + transform: scaleX(-1); + } + + .notification__favourite-icon-wrapper { + left: auto; + right: -26px; + } + .landing-page__logo { margin-right: 0; margin-left: 20px; @@ -135,7 +163,6 @@ body.rtl { } .status__action-bar { - &__counter { margin-right: 0; margin-left: 11px; @@ -328,6 +355,12 @@ body.rtl { } } + .columns-area--mobile .column, + .columns-area--mobile .drawer { + padding-left: 0; + padding-right: 0; + } + .public-layout { .header { .nav-button { diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/statuses.scss index 19ce0ab8f..19ce0ab8f 100644 --- a/app/javascript/styles/mastodon/stream_entries.scss +++ b/app/javascript/styles/mastodon/statuses.scss diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 11ac6dfeb..62f5554ff 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -149,10 +149,6 @@ a.table-action-link { margin-top: 0; } } - - @media screen and (max-width: $no-gap-breakpoint) { - display: none; - } } &__actions, @@ -174,9 +170,17 @@ a.table-action-link { text-align: right; padding-right: 16px - 5px; } + } - @media screen and (max-width: $no-gap-breakpoint) { - display: none; + &__form { + padding: 16px; + border: 1px solid darken($ui-base-color, 8%); + border-top: 0; + background: $ui-base-color; + + .fields-row { + padding-top: 0; + margin-bottom: 0; } } @@ -186,7 +190,7 @@ a.table-action-link { background: darken($ui-base-color, 4%); @media screen and (max-width: $no-gap-breakpoint) { - &:first-child { + .optional &:first-child { border-top: 1px solid darken($ui-base-color, 8%); } } @@ -210,6 +214,52 @@ a.table-action-link { &--unpadded { padding: 0; } + + &--with-image { + display: flex; + align-items: center; + } + + &__image { + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + margin-right: 10px; + + .emojione { + width: 32px; + height: 32px; + } + } + + &__text { + flex: 1 1 auto; + } + + &__extra { + flex: 0 0 auto; + text-align: right; + color: $darker-text-color; + font-weight: 500; + } + } + + .directory__tag { + margin: 0; + width: 100%; + + a { + background: transparent; + border-radius: 0; + } + } + } + + &.optional .batch-table__toolbar, + &.optional .batch-table__row__select { + @media screen and (max-width: $no-gap-breakpoint) { + display: none; } } diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index a82c44229..8602c3dde 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -6,6 +6,8 @@ $error-red: #df405a !default; // Cerise $warning-red: #ff5050 !default; // Sunset Orange $gold-star: #ca8f04 !default; // Dark Goldenrod +$red-bookmark: $warning-red; + // Values from the classic Mastodon UI $classic-base-color: #282c37; // Midnight Express $classic-primary-color: #9baec8; // Echo Blue diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index acaf5b024..ca050a8d9 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -100,6 +100,16 @@ background-size: 44px 44px; } } + + .trends__item { + padding: 10px; + } +} + +.trends-widget { + h4 { + color: $darker-text-color; + } } .box-widget { @@ -109,41 +119,52 @@ box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); } -.contact-widget, -.landing-page__information.contact-widget { - box-sizing: border-box; - padding: 20px; - min-height: 100%; +.placeholder-widget { + padding: 16px; border-radius: 4px; - background: $ui-base-color; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + border: 2px dashed $dark-text-color; + text-align: center; + color: $darker-text-color; + margin-bottom: 10px; } .contact-widget { + min-height: 100%; font-size: 15px; color: $darker-text-color; line-height: 20px; word-wrap: break-word; font-weight: 400; + padding: 0; - strong { - font-weight: 500; + h4 { + padding: 10px; + text-transform: uppercase; + font-weight: 700; + font-size: 13px; + color: $darker-text-color; } - p { - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } + .account { + border-bottom: 0; + padding: 10px 0; + padding-top: 5px; } - &__mail { - margin-top: 10px; + & > a { + display: inline-block; + padding: 10px; + padding-top: 0; + color: $darker-text-color; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; - a { - color: $primary-text-color; - text-decoration: none; + &:hover, + &:focus, + &:active { + text-decoration: underline; } } } @@ -324,7 +345,8 @@ &.active h4 { &, .fa, - small { + small, + .trends__item__current { color: $primary-text-color; } } @@ -337,6 +359,10 @@ &.active .avatar-stack .account__avatar { border-color: $ui-highlight-color; } + + .trends__item__current { + padding-right: 0; + } } } @@ -521,6 +547,12 @@ $fluid-breakpoint: $maximum-width + 20px; a { font-size: 14px; line-height: 20px; + } +} + +.notice-widget, +.placeholder-widget { + a { text-decoration: none; font-weight: 500; color: $ui-highlight-color; @@ -532,3 +564,38 @@ $fluid-breakpoint: $maximum-width + 20px; } } } + +.table-of-contents { + background: darken($ui-base-color, 4%); + min-height: 100%; + font-size: 14px; + border-radius: 4px; + + li a { + display: block; + font-weight: 500; + padding: 15px; + overflow: hidden; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-decoration: none; + color: $primary-text-color; + border-bottom: 1px solid lighten($ui-base-color, 4%); + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + li:last-child a { + border-bottom: 0; + } + + li ul { + padding-left: 20px; + border-bottom: 1px solid lighten($ui-base-color, 4%); + } +} |