2
0
Fork 0

Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Essem 2023-07-31 12:24:48 -05:00
commit d9ea6abec0
No known key found for this signature in database
GPG key ID: 7D497397CC3A2A8C
785 changed files with 17127 additions and 19813 deletions

View file

@ -0,0 +1,172 @@
/*
This script is meant to to be used in an `iframe` with the sole purpose of doing webfinger queries
client-side without being restricted by a strict `connect-src` Content-Security-Policy directive.
It communicates with the parent window through message events that are authenticated by origin,
and performs no other task.
*/
import 'packs/public-path';
import axios from 'axios';
interface JRDLink {
rel: string;
template?: string;
href?: string;
}
const isJRDLink = (link: unknown): link is JRDLink =>
typeof link === 'object' &&
link !== null &&
'rel' in link &&
typeof link.rel === 'string' &&
(!('template' in link) || typeof link.template === 'string') &&
(!('href' in link) || typeof link.href === 'string');
const findLink = (rel: string, data: unknown): JRDLink | undefined => {
if (
typeof data === 'object' &&
data !== null &&
'links' in data &&
data.links instanceof Array
) {
return data.links.find(
(link): link is JRDLink => isJRDLink(link) && link.rel === rel,
);
} else {
return undefined;
}
};
const findTemplateLink = (data: unknown) =>
findLink('http://ostatus.org/schema/1.0/subscribe', data)?.template;
const fetchInteractionURLSuccess = (
uri_or_domain: string,
template: string,
) => {
window.parent.postMessage(
{
type: 'fetchInteractionURL-success',
uri_or_domain,
template,
},
window.origin,
);
};
const fetchInteractionURLFailure = () => {
window.parent.postMessage(
{
type: 'fetchInteractionURL-failure',
},
window.origin,
);
};
const isValidDomain = (value: string) => {
const url = new URL('https:///path');
url.hostname = value;
return url.hostname === value;
};
// Attempt to find a remote interaction URL from a domain
const fromDomain = (domain: string) => {
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
axios
.get(`https://${domain}/.well-known/webfinger`, {
params: { resource: `https://${domain}` },
})
.then(({ data }) => {
const template = findTemplateLink(data);
fetchInteractionURLSuccess(domain, template ?? fallbackTemplate);
return;
})
.catch(() => {
fetchInteractionURLSuccess(domain, fallbackTemplate);
});
};
// Attempt to find a remote interaction URL from an arbitrary URL
const fromURL = (url: string) => {
const domain = new URL(url).host;
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
axios
.get(`https://${domain}/.well-known/webfinger`, {
params: { resource: url },
})
.then(({ data }) => {
const template = findTemplateLink(data);
fetchInteractionURLSuccess(url, template ?? fallbackTemplate);
return;
})
.catch(() => {
fromDomain(domain);
});
};
// Attempt to find a remote interaction URL from a `user@domain` string
const fromAcct = (acct: string) => {
acct = acct.replace(/^@/, '');
const segments = acct.split('@');
if (segments.length !== 2 || !segments[0] || !isValidDomain(segments[1])) {
fetchInteractionURLFailure();
return;
}
const domain = segments[1];
const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
axios
.get(`https://${domain}/.well-known/webfinger`, {
params: { resource: `acct:${acct}` },
})
.then(({ data }) => {
const template = findTemplateLink(data);
fetchInteractionURLSuccess(acct, template ?? fallbackTemplate);
return;
})
.catch(() => {
// TODO: handle host-meta?
fromDomain(domain);
});
};
const fetchInteractionURL = (uri_or_domain: string) => {
if (/^https?:\/\//.test(uri_or_domain)) {
fromURL(uri_or_domain);
} else if (uri_or_domain.includes('@')) {
fromAcct(uri_or_domain);
} else {
fromDomain(uri_or_domain);
}
};
window.addEventListener('message', (event: MessageEvent<unknown>) => {
// Check message origin
if (
!window.origin ||
window.parent !== event.source ||
event.origin !== window.origin
) {
return;
}
if (
event.data &&
typeof event.data === 'object' &&
'type' in event.data &&
event.data.type === 'fetchInteractionURL' &&
'uri_or_domain' in event.data &&
typeof event.data.uri_or_domain === 'string'
) {
fetchInteractionURL(event.data.uri_or_domain);
}
});

View file

@ -18,3 +18,4 @@ pack:
settings: settings.js
sign_up:
share:
remote_interaction_helper: remote_interaction_helper.ts

View file

@ -81,7 +81,7 @@ export function importFetchedStatuses(statuses) {
}
if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll));
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
}
}
@ -95,7 +95,7 @@ export function importFetchedStatuses(statuses) {
}
export function importFetchedPoll(poll) {
return dispatch => {
dispatch(importPolls([normalizePoll(poll)]));
return (dispatch, getState) => {
dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))]));
};
}

View file

@ -75,6 +75,7 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.translation = normalOldStatus.get('translation');
} else {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
@ -86,6 +87,18 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
}
if (normalOldStatus) {
const list = normalOldStatus.get('media_attachments');
if (normalStatus.media_attachments && list) {
normalStatus.media_attachments.forEach(item => {
const oldItem = list.find(i => i.get('id') === item.id);
if (oldItem && oldItem.get('description') === item.description) {
item.translation = oldItem.get('translation')
}
});
}
}
return normalStatus;
}
@ -104,15 +117,23 @@ export function normalizeStatusTranslation(translation, status) {
return normalTranslation;
}
export function normalizePoll(poll) {
export function normalizePoll(poll, normalOldPoll) {
const normalPoll = { ...poll };
const emojiMap = makeEmojiMap(poll.emojis);
normalPoll.options = poll.options.map((option, index) => ({
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}));
normalPoll.options = poll.options.map((option, index) => {
const normalOption = {
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
}
return normalOption
});
return normalPoll;
}

View file

@ -15,6 +15,9 @@ 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 const SEARCH_RESULT_CLICK = 'SEARCH_RESULT_CLICK';
export const SEARCH_RESULT_FORGET = 'SEARCH_RESULT_FORGET';
export function changeSearch(value) {
return {
type: SEARCH_CHANGE,
@ -28,7 +31,7 @@ export function clearSearch() {
};
}
export function submitSearch() {
export function submitSearch(type) {
return (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const signedIn = !!getState().getIn(['meta', 'me']);
@ -45,6 +48,7 @@ export function submitSearch() {
q: value,
resolve: signedIn,
limit: 10,
type,
},
}).then(response => {
if (response.data.accounts) {
@ -131,3 +135,42 @@ export const expandSearchFail = error => ({
export const showSearch = () => ({
type: SEARCH_SHOW,
});
export const openURL = routerHistory => (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const signedIn = !!getState().getIn(['meta', 'me']);
if (!signedIn) {
return;
}
dispatch(fetchSearchRequest());
api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
if (response.data.accounts?.length > 0) {
dispatch(importFetchedAccounts(response.data.accounts));
routerHistory.push(`/@${response.data.accounts[0].acct}`);
} else if (response.data.statuses?.length > 0) {
dispatch(importFetchedStatuses(response.data.statuses));
routerHistory.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
}
dispatch(fetchSearchSuccess(response.data, value));
}).catch(err => {
dispatch(fetchSearchFail(err));
});
};
export const clickSearchResult = (q, type) => ({
type: SEARCH_RESULT_CLICK,
result: {
type,
q,
},
});
export const forgetSearchResult = q => ({
type: SEARCH_RESULT_FORGET,
q,
});

View file

@ -20,9 +20,7 @@ export default class ColumnBackButton extends PureComponent {
handleClick = () => {
const { router } = this.context;
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
if (router.route.location.key) {
if (router.history.location?.state?.fromMastodon) {
router.history.goBack();
} else {
router.history.push('/');

View file

@ -65,9 +65,7 @@ class ColumnHeader extends PureComponent {
handleBackClick = () => {
const { router } = this.context;
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
if (router.route.location.key) {
if (router.history.location?.state?.fromMastodon) {
router.history.goBack();
} else {
router.history.push('/');
@ -87,6 +85,7 @@ class ColumnHeader extends PureComponent {
};
render () {
const { router } = this.context;
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
const { collapsed, animating } = this.state;
@ -130,7 +129,7 @@ class ColumnHeader extends PureComponent {
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)) {
if (!pinned && ((multiColumn && router.history.location?.state?.fromMastodon) || showBackButton)) {
backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'>
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />

View file

@ -7,8 +7,7 @@ import { Helmet } from 'react-helmet';
import StackTrace from 'stacktrace-js';
import { source_url } from 'flavours/glitch/initial_state';
import { preferencesLink } from 'flavours/glitch/utils/backend_links';
import { version, source_url } from 'flavours/glitch/initial_state';
export default class ErrorBoundary extends PureComponent {
@ -24,7 +23,7 @@ export default class ErrorBoundary extends PureComponent {
componentStack: undefined,
};
componentDidCatch(error, info) {
componentDidCatch (error, info) {
this.setState({
hasError: true,
errorMessage: error.toString(),
@ -44,88 +43,62 @@ export default class ErrorBoundary extends PureComponent {
});
}
handleReload(e) {
e.preventDefault();
window.location.reload();
}
handleCopyStackTrace = () => {
const { errorMessage, stackTrace, mappedStackTrace } = this.state;
const textarea = document.createElement('textarea');
let contents = [errorMessage, stackTrace];
if (mappedStackTrace) {
contents.push(mappedStackTrace);
}
textarea.textContent = contents.join('\n\n\n');
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, errorMessage, stackTrace, mappedStackTrace, componentStack } = this.state;
const { hasError, copied, errorMessage } = this.state;
if (!hasError) return this.props.children;
if (!hasError) {
return this.props.children;
}
const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError');
let debugInfo = '';
if (stackTrace) {
debugInfo += 'Stack trace\n-----------\n\n```\n' + errorMessage + '\n' + stackTrace.toString() + '\n```';
}
if (mappedStackTrace) {
debugInfo += 'Mapped stack trace\n-----------\n\n```\n' + errorMessage + '\n' + mappedStackTrace.toString() + '\n```';
}
if (componentStack) {
if (debugInfo) {
debugInfo += '\n\n\n';
}
debugInfo += 'React component stack\n---------------------\n\n```\n' + componentStack.toString() + '\n```';
}
let issueTracker = source_url;
if (source_url.match(/^https:\/\/github\.com\/[^/]+\/[^/]+\/?$/)) {
issueTracker = source_url + '/issues';
}
return (
<div tabIndex={-1}>
<div className='error-boundary'>
<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:' />
</p>
<ul>
{ likelyBrowserAddonIssue && (
<li>
<FormattedMessage
id='web_app_crash.disable_addons'
defaultMessage='Disable browser add-ons or built-in translation tools'
/>
</li>
) }
<li>
<FormattedMessage
id='web_app_crash.report_issue'
defaultMessage='Report a bug in the {issuetracker}'
values={{ issuetracker: <a href={issueTracker} 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: <button onClick={this.handleReload}><FormattedMessage id='web_app_crash.reload' defaultMessage='Reload' /></button> }}
/>
</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>
<div className='error-boundary'>
<div>
<p className='error-boundary__error'>
{ likelyBrowserAddonIssue ? (
<FormattedMessage id='error.unexpected_crash.explanation_addons' defaultMessage='This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.' />
) : (
<FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' />
)}
</ul>
</p>
<p>
{ likelyBrowserAddonIssue ? (
<FormattedMessage id='error.unexpected_crash.next_steps_addons' defaultMessage='Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
) : (
<FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
)}
</p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div>
<Helmet>

View file

@ -1,16 +1,26 @@
import type { PropsWithChildren } from 'react';
import React from 'react';
import type { History } from 'history';
import { createBrowserHistory } from 'history';
import { Router as OriginalRouter } from 'react-router';
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
const browserHistory = createBrowserHistory();
const originalPush = browserHistory.push.bind(browserHistory);
interface MastodonLocationState {
fromMastodon?: boolean;
mastodonModalKey?: string;
}
const browserHistory = createBrowserHistory<
MastodonLocationState | undefined
>();
const originalPush = browserHistory.push.bind(browserHistory);
const originalReplace = browserHistory.replace.bind(browserHistory);
browserHistory.push = (path: string, state?: MastodonLocationState) => {
state = state ?? {};
state.fromMastodon = true;
browserHistory.push = (path: string, state: History.LocationState) => {
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
originalPush(`/deck${path}`, state);
} else {
@ -18,6 +28,19 @@ browserHistory.push = (path: string, state: History.LocationState) => {
}
};
browserHistory.replace = (path: string, state?: MastodonLocationState) => {
if (browserHistory.location.state?.fromMastodon) {
state = state ?? {};
state.fromMastodon = true;
}
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
originalReplace(`/deck${path}`, state);
} else {
originalReplace(path, state);
}
};
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
};

View file

@ -32,7 +32,7 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
react: { id: 'status.react', defaultMessage: 'React' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },

View file

@ -55,7 +55,7 @@ export default class StatusPrepend extends PureComponent {
return (
<FormattedMessage
id='notification.favourite'
defaultMessage='{name} favourited your status'
defaultMessage='{name} favorited your status'
values={{ name : link }}
/>
);

View file

@ -49,7 +49,7 @@ 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? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
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?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
@ -303,7 +303,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
modalProps: {
type,
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
url: status.get('uri'),
},
}));
},

View file

@ -46,7 +46,7 @@ const messages = defineMessages({
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },

View file

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

View file

@ -83,7 +83,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
modalProps: {
type: 'follow',
accountId: account.get('id'),
url: account.get('url'),
url: account.get('uri'),
},
}));
},

View file

@ -477,6 +477,7 @@ class Audio extends PureComponent {
const progress = Math.min((currentTime / duration) * 100, 100);
let warning;
if (sensitive) {
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
} else {
@ -522,7 +523,10 @@ class Audio extends PureComponent {
<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>
<span className='spoiler-button__overlay__label'>
{warning}
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
</span>
</button>
</div>

View file

@ -14,7 +14,7 @@ const messages = defineMessages({
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },

View file

@ -7,39 +7,21 @@ import {
defineMessages,
} from 'react-intl';
import Overlay from 'react-overlays/Overlay';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Icon } from 'flavours/glitch/components/icon';
import { searchEnabled } from 'flavours/glitch/initial_state';
import { focusRoot } from 'flavours/glitch/utils/dom_helpers';
import { HASHTAG_REGEX } from 'flavours/glitch/utils/hashtags';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
});
class SearchPopout extends PureComponent {
render () {
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
return (
<div className='search-popout'>
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
<ul>
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
<li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
</ul>
{extraInformation}
</div>
);
}
}
// The component.
class Search extends PureComponent {
@ -50,9 +32,13 @@ class Search extends PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
recent: ImmutablePropTypes.orderedSet,
submitted: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
onClickSearchResult: PropTypes.func.isRequired,
onForgetSearchResult: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired,
openInRoute: PropTypes.bool,
@ -62,59 +48,104 @@ class Search extends PureComponent {
state = {
expanded: false,
selectedOption: -1,
options: [],
};
setRef = c => {
this.searchForm = c;
};
handleChange = (e) => {
handleChange = ({ target }) => {
const { onChange } = this.props;
if (onChange) {
onChange(e.target.value);
}
onChange(target.value);
this._calculateOptions(target.value);
};
handleClear = (e) => {
handleClear = e => {
const {
onClear,
submitted,
value,
} = this.props;
e.preventDefault(); // Prevents focus change ??
if (onClear && (submitted || value && value.length)) {
if (value.length > 0 || submitted) {
onClear();
this.setState({ options: [], selectedOption: -1 })
}
};
handleBlur = () => {
this.setState({ expanded: false });
this.setState({ expanded: false, selectedOption: -1 });
};
handleFocus = () => {
this.setState({ expanded: true });
this.props.onShow();
const { onShow, singleColumn } = this.props;
if (this.searchForm && !this.props.singleColumn) {
this.setState({ expanded: true, selectedOption: -1 });
onShow();
if (this.searchForm && !singleColumn) {
const { left, right } = this.searchForm.getBoundingClientRect();
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
this.searchForm.scrollIntoView();
}
}
};
handleKeyUp = (e) => {
const { onSubmit } = this.props;
switch (e.key) {
case 'Enter':
onSubmit();
handleKeyDown = (e) => {
const { selectedOption } = this.state;
const options = this._getOptions();
if (this.props.openInRoute) {
this.context.router.history.push('/search');
switch(e.key) {
case 'Escape':
e.preventDefault();
focusRoot();
break;
case 'ArrowDown':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) });
}
break;
case 'ArrowUp':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.max(selectedOption - 1, -1) });
}
break;
case 'Enter':
e.preventDefault();
if (selectedOption === -1) {
this._submit();
} else if (options.length > 0) {
options[selectedOption].action();
}
this._unfocus();
break;
case 'Delete':
if (selectedOption > -1 && options.length > 0) {
const search = options[selectedOption];
if (typeof search.forget === 'function') {
e.preventDefault();
search.forget(e);
}
}
break;
case 'Escape':
focusRoot();
}
};
@ -122,14 +153,141 @@ class Search extends PureComponent {
return this.searchForm;
};
handleHashtagClick = () => {
const { router } = this.context;
const { value, onClickSearchResult } = this.props;
const query = value.trim().replace(/^#/, '');
router.history.push(`/tags/${query}`);
onClickSearchResult(query, 'hashtag');
};
handleAccountClick = () => {
const { router } = this.context;
const { value, onClickSearchResult } = this.props;
const query = value.trim().replace(/^@/, '');
router.history.push(`/@${query}`);
onClickSearchResult(query, 'account');
};
handleURLClick = () => {
const { router } = this.context;
const { onOpenURL } = this.props;
onOpenURL(router.history);
};
handleStatusSearch = () => {
this._submit('statuses');
};
handleAccountSearch = () => {
this._submit('accounts');
};
handleRecentSearchClick = search => {
const { router } = this.context;
if (search.get('type') === 'account') {
router.history.push(`/@${search.get('q')}`);
} else if (search.get('type') === 'hashtag') {
router.history.push(`/tags/${search.get('q')}`);
}
};
handleForgetRecentSearchClick = search => {
const { onForgetSearchResult } = this.props;
onForgetSearchResult(search.get('q'));
};
_unfocus () {
document.querySelector('.ui').parentElement.focus();
}
_submit (type) {
const { onSubmit, openInRoute } = this.props;
const { router } = this.context;
onSubmit(type);
if (openInRoute) {
router.history.push('/search');
}
}
_getOptions () {
const { options } = this.state;
if (options.length > 0) {
return options;
}
const { recent } = this.props;
return recent.toArray().map(search => ({
label: search.get('type') === 'account' ? `@${search.get('q')}` : `#${search.get('q')}`,
action: () => this.handleRecentSearchClick(search),
forget: e => {
e.stopPropagation();
this.handleForgetRecentSearchClick(search);
},
}));
}
_calculateOptions (value) {
const trimmedValue = value.trim();
const options = [];
if (trimmedValue.length > 0) {
const couldBeURL = trimmedValue.startsWith('https://') && !trimmedValue.includes(' ');
if (couldBeURL) {
options.push({ key: 'open-url', label: <FormattedMessage id='search.quick_action.open_url' defaultMessage='Open URL in Mastodon' />, action: this.handleURLClick });
}
const couldBeHashtag = (trimmedValue.startsWith('#') && trimmedValue.length > 1) || trimmedValue.match(HASHTAG_REGEX);
if (couldBeHashtag) {
options.push({ key: 'go-to-hashtag', label: <FormattedMessage id='search.quick_action.go_to_hashtag' defaultMessage='Go to hashtag {x}' values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }} />, action: this.handleHashtagClick });
}
const couldBeUsername = trimmedValue.match(/^@?[a-z0-9_-]+(@[^\s]+)?$/i);
if (couldBeUsername) {
options.push({ key: 'go-to-account', label: <FormattedMessage id='search.quick_action.go_to_account' defaultMessage='Go to profile {x}' values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }} />, action: this.handleAccountClick });
}
const couldBeStatusSearch = searchEnabled;
if (couldBeStatusSearch) {
options.push({ key: 'status-search', label: <FormattedMessage id='search.quick_action.status_search' defaultMessage='Posts matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleStatusSearch });
}
const couldBeUserSearch = true;
if (couldBeUserSearch) {
options.push({ key: 'account-search', label: <FormattedMessage id='search.quick_action.account_search' defaultMessage='Profiles matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleAccountSearch });
}
}
this.setState({ options });
}
render () {
const { intl, value, submitted } = this.props;
const { expanded } = this.state;
const { intl, value, submitted, recent } = this.props;
const { expanded, options, selectedOption } = this.state;
const { signedIn } = this.context.identity;
const hasValue = value.length > 0 || submitted;
return (
<div className='search'>
<div className={classNames('search', { active: expanded })}>
<input
ref={this.setRef}
className='search__input'
@ -138,7 +296,7 @@ class Search extends PureComponent {
aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
value={value || ''}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
@ -147,15 +305,39 @@ class Search extends PureComponent {
<Icon id='search' className={hasValue ? '' : 'active'} />
<Icon id='times-circle' className={hasValue ? 'active' : ''} />
</div>
<Overlay show={expanded && !hasValue} placement='bottom' target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
{({ props, placement }) => (
<div {...props} style={{ ...props.style, width: 285, zIndex: 2 }}>
<div className={`dropdown-animation ${placement}`}>
<SearchPopout />
<div className='search__popout'>
{options.length === 0 && (
<>
<h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4>
<div className='search__popout__menu'>
{recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => (
<button key={label} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}><Icon id='times' /></button>
</button>
)) : (
<div className='search__popout__menu__message'>
<FormattedMessage id='search.no_recent_searches' defaultMessage='No recent searches' />
</div>
)}
</div>
</div>
</>
)}
</Overlay>
{options.length > 0 && (
<>
<h4><FormattedMessage id='search_popout.quick_actions' defaultMessage='Quick actions' /></h4>
<div className='search__popout__menu'>
{options.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}>
{label}
</button>
))}
</div>
</>
)}
</div>
</div>
);
}

View file

@ -89,7 +89,7 @@ class SearchResults extends ImmutablePureComponent {
count += results.get('accounts').size;
accounts = (
<section className='search-results__section'>
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></h5>
{results.get('accounts').map(accountId => <AccountContainer id={accountId} key={accountId} />)}

View file

@ -5,6 +5,9 @@ import {
clearSearch,
submitSearch,
showSearch,
openURL,
clickSearchResult,
forgetSearchResult,
} from 'flavours/glitch/actions/search';
import Search from '../components/search';
@ -12,6 +15,7 @@ import Search from '../components/search';
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']),
recent: state.getIn(['search', 'recent']),
});
const mapDispatchToProps = dispatch => ({
@ -24,14 +28,26 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSearch());
},
onSubmit () {
dispatch(submitSearch());
onSubmit (type) {
dispatch(submitSearch(type));
},
onShow () {
dispatch(showSearch());
},
onOpenURL (routerHistory) {
dispatch(openURL(routerHistory));
},
onClickSearchResult (q, type) {
dispatch(clickSearchResult(q, type));
},
onForgetSearchResult (q) {
dispatch(forgetSearchResult(q));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Search);

View file

@ -6,37 +6,14 @@ import { connect } from 'react-redux';
import { me } from 'flavours/glitch/initial_state';
import { profileLink, privacyPolicyLink } from 'flavours/glitch/utils/backend_links';
import { HASHTAG_PATTERN_REGEX } from 'flavours/glitch/utils/hashtags';
import Warning from '../components/warning';
const buildHashtagRE = () => {
try {
const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
const ALPHA = '\\p{L}\\p{M}';
const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
return new RegExp(
'(?:^|[^\\/\\)\\w])#((' +
'[' + WORD + '_]' +
'[' + WORD + HASHTAG_SEPARATORS + ']*' +
'[' + ALPHA + HASHTAG_SEPARATORS + ']' +
'[' + WORD + HASHTAG_SEPARATORS +']*' +
'[' + WORD + '_]' +
')|(' +
'[' + WORD + '_]*' +
'[' + ALPHA + ']' +
'[' + WORD + '_]*' +
'))', 'iu',
);
} catch {
return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
}
};
const APPROX_HASHTAG_RE = buildHashtagRE();
const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
});

View file

@ -1,10 +1,13 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { accountsCountRenderer } from 'flavours/glitch/components/hashtag';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { Skeleton } from 'flavours/glitch/components/skeleton';
@ -14,10 +17,14 @@ export default class Story extends PureComponent {
static propTypes = {
url: PropTypes.string,
title: PropTypes.string,
lang: PropTypes.string,
publisher: PropTypes.string,
publishedAt: PropTypes.string,
author: PropTypes.string,
sharedTimes: PropTypes.number,
thumbnail: PropTypes.string,
blurhash: PropTypes.string,
expanded: PropTypes.bool,
};
state = {
@ -27,16 +34,16 @@ export default class Story extends PureComponent {
handleImageLoad = () => this.setState({ thumbnailLoaded: true });
render () {
const { url, title, publisher, sharedTimes, thumbnail, blurhash } = this.props;
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, blurhash } = this.props;
const { thumbnailLoaded } = this.state;
return (
<a className='story' href={url} target='blank' rel='noopener'>
<a className={classNames('story', { expanded })} href={url} target='blank' rel='noopener'>
<div className='story__details'>
<div className='story__details__publisher'>{publisher ? publisher : <Skeleton width={50} />}</div>
<div className='story__details__title'>{title ? title : <Skeleton />}</div>
<div className='story__details__shared'>{typeof sharedTimes === 'number' ? <ShortNumber value={sharedTimes} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}</div>
<div className='story__details__publisher'>{publisher ? <span lang={lang}>{publisher}</span> : <Skeleton width={50} />}{publishedAt && <> · <RelativeTimestamp timestamp={publishedAt} /></>}</div>
<div className='story__details__title' lang={lang}>{title ? title : <Skeleton />}</div>
<div className='story__details__shared'>{author && <><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{author}</strong> }} /> · </>}{typeof sharedTimes === 'number' ? <ShortNumber value={sharedTimes} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}</div>
</div>
<div className='story__thumbnail'>

View file

@ -55,12 +55,16 @@ class Links extends PureComponent {
<div className='explore__links'>
{banner}
{isLoading ? (<LoadingIndicator />) : links.map(link => (
{isLoading ? (<LoadingIndicator />) : links.map((link, i) => (
<Story
key={link.get('id')}
expanded={i === 0}
lang={link.get('language')}
url={link.get('url')}
title={link.get('title')}
publisher={link.get('provider_name')}
publishedAt={link.get('published_at')}
author={link.get('author_name')}
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
thumbnail={link.get('image')}
blurhash={link.get('blurhash')}

View file

@ -111,7 +111,7 @@ class Results extends PureComponent {
<>
<div className='account__section-headline'>
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='People' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
</div>

View file

@ -47,7 +47,7 @@ class Statuses extends PureComponent {
return (
<>
<DismissableBanner id='explore/statuses'>
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.' />
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' />
</DismissableBanner>
<StatusList

View file

@ -18,7 +18,7 @@ import Column from 'flavours/glitch/features/ui/components/column';
import { getStatusList } from 'flavours/glitch/selectors';
const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
heading: { id: 'column.favourites', defaultMessage: 'Favorites' },
});
const mapStateToProps = state => ({
@ -74,7 +74,7 @@ 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 posts yet. When you favourite one, it will show up here." />;
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favorite posts yet. When you favorite one, it will show up here." />;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}>

View file

@ -71,7 +71,7 @@ class Favourites extends ImmutablePureComponent {
);
}
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favourited this post yet. When someone does, they will show up here.' />;
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favorited this post yet. When someone does, they will show up here.' />;
return (
<Column ref={this.setRef}>

View file

@ -76,12 +76,12 @@ const Firehose = ({ feedType, multiColumn }) => {
const { signedIn } = useIdentity();
const columnRef = useRef(null);
const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false));
const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0);
const allowLocalOnly = useAppSelector((state) => state.getIn(['settings', 'firehose', 'allowLocalOnly']));
const regex = useAppSelector((state) => state.getIn(['settings', 'firehose', 'regex', 'body']));
const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false));
const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${feedType === 'public' && allowLocalOnly ? ':allow_local_only' : ''}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0);
const handlePin = useCallback(
() => {
switch(feedType) {
@ -129,7 +129,7 @@ const Firehose = ({ feedType, multiColumn }) => {
}
break;
case 'public':
dispatch(expandPublicTimeline({ onlyMedia }));
dispatch(expandPublicTimeline({ onlyMedia, allowLocalOnly }));
if (signedIn) {
disconnect = dispatch(connectPublicStream({ onlyMedia, allowLocalOnly }));
}

View file

@ -15,7 +15,7 @@ import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subh
const messages = defineMessages({
heading: { id: 'column.heading', defaultMessage: 'Misc' },
subheading: { id: 'column.subheading', defaultMessage: 'Miscellaneous options' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },

View file

@ -22,7 +22,7 @@ export const ExplorePrompt = () => (
<p>
<FormattedMessage
id='home.explore_prompt.body'
defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:"
defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:"
/>
</p>

View file

@ -1,95 +1,296 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { throttle, escapeRegExp } from 'lodash';
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
import api from 'flavours/glitch/api';
import Button from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { registrationsOpen } from 'flavours/glitch/initial_state';
const messages = defineMessages({
loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' },
});
const mapStateToProps = (state, { accountId }) => ({
displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
});
const mapDispatchToProps = (dispatch) => ({
onSignupClick() {
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
dispatch(closeModal());
dispatch(openModal('CLOSED_REGISTRATIONS'));
},
});
class Copypaste extends PureComponent {
const PERSISTENCE_KEY = 'flavours/glitch_home';
const isValidDomain = value => {
const url = new URL('https:///path');
url.hostname = value;
return url.hostname === value;
};
const valueToDomain = value => {
// If the user starts typing an URL
if (/^https?:\/\//.test(value)) {
try {
const url = new URL(value);
// Consider that if there is a path, the URL is more meaningful than a bare domain
if (url.pathname.length > 1) {
return '';
}
return url.host;
} catch {
return undefined;
}
// If the user writes their full handle including username
} else if (value.includes('@')) {
if (value.replace(/^@/, '').split('@').length > 2) {
return undefined;
}
return '';
}
return value;
};
const addInputToOptions = (value, options) => {
value = value.trim();
if (value.includes('.') && isValidDomain(value)) {
return [value].concat(options.filter((x) => x !== value));
}
return options;
};
class LoginForm extends React.PureComponent {
static propTypes = {
value: PropTypes.string,
resourceUrl: PropTypes.string,
intl: PropTypes.object.isRequired,
};
state = {
copied: false,
value: localStorage ? (localStorage.getItem(PERSISTENCE_KEY) || '') : '',
expanded: false,
selectedOption: -1,
isLoading: false,
isSubmitting: false,
error: false,
options: [],
networkOptions: [],
};
setRef = c => {
this.input = c;
};
handleInputClick = () => {
this.setState({ copied: false });
this.input.focus();
this.input.select();
this.input.setSelectionRange(0, this.input.value.length);
handleChange = ({ target }) => {
this.setState(state => ({ value: target.value, isLoading: true, error: false, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
};
handleButtonClick = () => {
const { value } = this.props;
navigator.clipboard.writeText(value);
this.input.blur();
this.setState({ copied: true });
this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
handleMessage = (event) => {
const { resourceUrl } = this.props;
if (event.origin !== window.origin || event.source !== this.iframeRef.contentWindow) {
return;
}
if (event.data?.type === 'fetchInteractionURL-failure') {
this.setState({ isSubmitting: false, error: true });
} else if (event.data?.type === 'fetchInteractionURL-success') {
if (/^https?:\/\//.test(event.data.template)) {
if (localStorage) {
localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
}
window.location.href = event.data.template.replace('{uri}', encodeURIComponent(resourceUrl));
} else {
this.setState({ isSubmitting: false, error: true });
}
}
};
componentWillUnmount () {
if (this.timeout) clearTimeout(this.timeout);
componentDidMount () {
window.addEventListener('message', this.handleMessage);
}
componentWillUnmount () {
window.removeEventListener('message', this.handleMessage);
}
handleSubmit = () => {
const { value } = this.state;
this.setState({ isSubmitting: true });
this.iframeRef.contentWindow.postMessage({
type: 'fetchInteractionURL',
uri_or_domain: value.trim(),
}, window.origin);
};
setIFrameRef = (iframe) => {
this.iframeRef = iframe;
}
handleFocus = () => {
this.setState({ expanded: true });
};
handleBlur = () => {
this.setState({ expanded: false });
};
handleKeyDown = (e) => {
const { options, selectedOption } = this.state;
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) });
}
break;
case 'ArrowUp':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.max(selectedOption - 1, -1) });
}
break;
case 'Enter':
e.preventDefault();
if (selectedOption === -1) {
this.handleSubmit();
} else if (options.length > 0) {
this.setState({ value: options[selectedOption], error: false }, () => this.handleSubmit());
}
break;
}
};
handleOptionClick = e => {
const index = Number(e.currentTarget.getAttribute('data-index'));
const option = this.state.options[index];
e.preventDefault();
this.setState({ selectedOption: index, value: option, error: false }, () => this.handleSubmit());
};
_loadOptions = throttle(() => {
const { value } = this.state;
const domain = valueToDomain(value.trim());
if (typeof domain === 'undefined') {
this.setState({ options: [], networkOptions: [], isLoading: false, error: true });
return;
}
if (domain.length === 0) {
this.setState({ options: [], networkOptions: [], isLoading: false });
return;
}
api().get('/api/v1/peers/search', { params: { q: domain } }).then(({ data }) => {
if (!data) {
data = [];
}
this.setState((state) => ({ networkOptions: data, options: addInputToOptions(state.value, data), isLoading: false }));
}).catch(() => {
this.setState({ isLoading: false });
});
}, 200, { leading: true, trailing: true });
render () {
const { value } = this.props;
const { copied } = this.state;
const { intl } = this.props;
const { value, expanded, options, selectedOption, error, isSubmitting } = this.state;
const domain = (valueToDomain(value) || '').trim();
const domainRegExp = new RegExp(`(${escapeRegExp(domain)})`, 'gi');
const hasPopOut = domain.length > 0 && options.length > 0;
return (
<div className={classNames('copypaste', { copied })}>
<input
type='text'
ref={this.setRef}
value={value}
readOnly
onClick={this.handleInputClick}
<div className={classNames('interaction-modal__login', { focused: expanded, expanded: hasPopOut, invalid: error })}>
<iframe
ref={this.setIFrameRef}
style={{display: 'none'}}
src='/remote_interaction_helper'
sandbox='allow-scripts allow-same-origin'
title='remote interaction helper'
/>
<button className='button' onClick={this.handleButtonClick}>
{copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy' defaultMessage='Copy' />}
</button>
<div className='interaction-modal__login__input'>
<input
ref={this.setRef}
type='text'
value={value}
placeholder={intl.formatMessage(messages.loginPrompt)}
aria-label={intl.formatMessage(messages.loginPrompt)}
autoFocus
onChange={this.handleChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
/>
<Button onClick={this.handleSubmit} disabled={isSubmitting}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
</div>
{hasPopOut && (
<div className='search__popout'>
<div className='search__popout__menu'>
{options.map((option, i) => (
<button key={option} onMouseDown={this.handleOptionClick} data-index={i} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}>
{option.split(domainRegExp).map((part, i) => (
part.toLowerCase() === domain.toLowerCase() ? (
<mark key={i}>
{part}
</mark>
) : (
<span key={i}>
{part}
</span>
)
))}
</button>
))}
</div>
</div>
)}
</div>
);
}
}
class InteractionModal extends PureComponent {
const IntlLoginForm = injectIntl(LoginForm);
class InteractionModal extends React.PureComponent {
static propTypes = {
displayNameHtml: PropTypes.string,
url: PropTypes.string,
type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']),
onSignupClick: PropTypes.func.isRequired,
signupUrl: PropTypes.string.isRequired,
};
handleSignupClick = () => {
@ -97,7 +298,7 @@ class InteractionModal extends PureComponent {
};
render () {
const { url, type, displayNameHtml, signupUrl } = this.props;
const { url, type, displayNameHtml } = this.props;
const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />;
@ -116,8 +317,8 @@ class InteractionModal extends PureComponent {
break;
case 'favourite':
icon = <Icon id='star' />;
title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favourite {name}'s post" values={{ name }} />;
actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.' />;
title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favorite {name}'s post" values={{ name }} />;
actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.' />;
break;
case 'follow':
icon = <Icon id='user-plus' />;
@ -130,13 +331,13 @@ class InteractionModal extends PureComponent {
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button button--block button-tertiary'>
<a href='/auth/sign_up' className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button button--block button-tertiary' onClick={this.handleSignupClick}>
<button className='link-button' onClick={this.handleSignupClick}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
@ -146,22 +347,13 @@ class InteractionModal extends PureComponent {
<div className='modal-root__modal interaction-modal'>
<div className='interaction-modal__lead'>
<h3><span className='interaction-modal__icon'>{icon}</span> {title}</h3>
<p>{actionDescription} <FormattedMessage id='interaction_modal.preamble' defaultMessage="Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one." /></p>
<p>{actionDescription} <strong><FormattedMessage id='interaction_modal.sign_in' defaultMessage='You are not logged in to this server. Where is your account hosted?' /></strong></p>
</div>
<div className='interaction-modal__choices'>
<div className='interaction-modal__choices__choice'>
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
{signupButton}
</div>
<IntlLoginForm resourceUrl={url} />
<div className='interaction-modal__choices__choice'>
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3>
<p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.' /></p>
<Copypaste value={url} />
</div>
</div>
<p className='hint'><FormattedMessage id='interaction_modal.sign_in_hint' defaultMessage="Tip: That's the website where you signed up. If you don't remember, look for the welcome e-mail in your inbox. You can also enter your full username! (e.g. @Mastodon@mastodon.social)" /></p>
<p><FormattedMessage id='interaction_modal.no_account_yet' defaultMessage='Not on Mastodon?' /> {signupButton}</p>
</div>
);
}

View file

@ -61,7 +61,7 @@ class KeyboardShortcuts extends ImmutablePureComponent {
</tr>
<tr>
<td><kbd>f</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favourite' /></td>
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favorite' /></td>
</tr>
<tr>
<td><kbd>b</kbd></td>

View file

@ -60,7 +60,6 @@ export default class LocalSettingsPage extends PureComponent {
else if (onNavigate) return (
<button
onClick={handleClick}
tabIndex={0}
className={finalClassName}
title={title}
aria-label={title}

View file

@ -110,7 +110,7 @@ export default class ColumnSettings extends PureComponent {
</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>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></span>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />

View file

@ -7,7 +7,7 @@ import { Icon } from 'flavours/glitch/components/icon';
const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },
reactions: { id: 'notifications.filter.reactions', defaultMessage: 'Reactions' },
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },

View file

@ -23,7 +23,7 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
@ -93,7 +93,7 @@ class Footer extends ImmutablePureComponent {
modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
url: status.get('uri'),
},
}));
}
@ -115,7 +115,7 @@ class Footer extends ImmutablePureComponent {
modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
url: status.get('uri'),
},
}));
}
@ -144,7 +144,7 @@ class Footer extends ImmutablePureComponent {
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
url: status.get('uri'),
},
}));
}

View file

@ -30,7 +30,7 @@ const mapStateToProps = (state, { columnId }) => {
const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
const allowLocalOnly = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'allowLocalOnly']) : state.getIn(['settings', 'public', 'other', 'allowLocalOnly']);
const regex = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'regex', 'body']) : state.getIn(['settings', 'public', 'regex', 'body']);
const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
const timelineState = state.getIn(['timelines', `public${onlyRemote ? ':remote' : allowLocalOnly ? ':allow_local_only' : ''}${onlyMedia ? ':media' : ''}`]);
return {
hasUnread: !!timelineState && timelineState.get('unread') > 0,

View file

@ -25,7 +25,7 @@ const messages = defineMessages({
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
react: { id: 'status.react', defaultMessage: 'React' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
more: { id: 'status.more', defaultMessage: 'More' },

View file

@ -181,7 +181,10 @@ export default class Card extends PureComponent {
let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='spoiler-button__overlay__label'>
<FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
</span>
</button>
);
spoilerButton = (

View file

@ -35,7 +35,7 @@ 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? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
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?' },
});

View file

@ -68,7 +68,7 @@ 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? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
@ -194,8 +194,8 @@ class Status extends ImmutablePureComponent {
status: ImmutablePropTypes.map,
isLoading: PropTypes.bool,
settings: ImmutablePropTypes.map.isRequired,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
ancestorsIds: ImmutablePropTypes.list.isRequired,
descendantsIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
@ -219,16 +219,6 @@ class Status extends ImmutablePureComponent {
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
this.props.dispatch(fetchStatus(this.props.params.statusId));
const { status, ancestorsIds } = this.props;
if (status && ancestorsIds && ancestorsIds.size > 0) {
const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1];
window.requestAnimationFrame(() => {
element.scrollIntoView(true);
});
}
}
static getDerivedStateFromProps(props, state) {
@ -307,7 +297,7 @@ class Status extends ImmutablePureComponent {
modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
url: status.get('uri'),
},
}));
}
@ -358,7 +348,7 @@ class Status extends ImmutablePureComponent {
modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
url: status.get('uri'),
},
}));
}
@ -392,7 +382,7 @@ class Status extends ImmutablePureComponent {
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
url: status.get('uri'),
},
}));
}
@ -640,16 +630,22 @@ class Status extends ImmutablePureComponent {
};
componentDidUpdate (prevProps) {
if (this.props.params.statusId && (this.props.params.statusId !== prevProps.params.statusId || prevProps.ancestorsIds.size < this.props.ancestorsIds.size)) {
const { status, ancestorsIds } = this.props;
const { status, ancestorsIds, multiColumn } = this.props;
if (status && ancestorsIds && ancestorsIds.size > 0) {
const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1];
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
window.requestAnimationFrame(() => {
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
window.requestAnimationFrame(() => {
element.scrollIntoView(true);
});
}
// In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that.
if (!multiColumn) {
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
if (offset) {
const scrollingElement = document.scrollingElement || document.body;
scrollingElement.scrollBy(0, -offset);
}
}
});
}
}
@ -716,16 +712,16 @@ class Status extends ImmutablePureComponent {
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}><Icon id={!isExpanded ? 'eye-slash' : 'eye'} /></button>
<button type='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}><Icon id={!isExpanded ? 'eye-slash' : 'eye'} /></button>
)}
/>
<ScrollContainer scrollKey='thread'>
<div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
{ancestors}
<HotKeys handlers={handlers}>
<div className='focusable' tabIndex={0} aria-label={textForScreenReader(intl, status, false, isExpanded)}>
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false, isExpanded)}>
<DetailedStatus
key={`details-${status.get('id')}`}
status={status}

View file

@ -0,0 +1,761 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { HotKeys } from 'react-hotkeys';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initBoostModal } from 'flavours/glitch/actions/boosts';
import {
replyCompose,
mentionCompose,
directCompose,
} from 'flavours/glitch/actions/compose';
import {
favourite,
unfavourite,
bookmark,
unbookmark,
reblog,
unreblog,
pin,
unpin,
} from 'flavours/glitch/actions/interactions';
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
import { openModal } from 'flavours/glitch/actions/modal';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initReport } from 'flavours/glitch/actions/reports';
import {
fetchStatus,
muteStatus,
unmuteStatus,
deleteStatus,
editStatus,
hideStatus,
revealStatus,
translateStatus,
undoStatusTranslation,
} from 'flavours/glitch/actions/statuses';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import StatusContainer from 'flavours/glitch/containers/status_container';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import Column from 'flavours/glitch/features/ui/components/column';
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state';
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import ColumnHeader from '../../components/column_header';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import ActionBar from './components/action_bar';
import DetailedStatus from './components/detailed_status';
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? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
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?' },
tootHeading: { id: 'account.posts_with_replies', defaultMessage: 'Posts and replies' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.List();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id && !mutable.includes(id)) {
mutable.unshift(id);
id = inReplyTos.get(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];
while (ids.length > 0) {
let id = ids.pop();
const replies = contextReplies.get(id);
if (statusId !== id) {
descendantsIds.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(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 {
isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']),
status,
ancestorsIds,
descendantsIds,
settings: state.get('local_settings'),
askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
};
};
return mapStateToProps;
};
const truncate = (str, num) => {
const arr = Array.from(str);
if (arr.length > num) {
return arr.slice(0, num).join('') + '…';
} else {
return str;
}
};
const titleFromStatus = (intl, status) => {
const displayName = status.getIn(['account', 'display_name']);
const username = status.getIn(['account', 'username']);
const user = displayName.trim().length === 0 ? username : displayName;
const text = status.get('search_index');
const attachmentCount = status.get('media_attachments').size;
return text ? `${user}: "${truncate(text, 30)}"` : intl.formatMessage(messages.statusTitleWithAttachments, { user, attachmentCount });
};
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object,
};
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
isLoading: PropTypes.bool,
settings: ImmutablePropTypes.map.isRequired,
ancestorsIds: ImmutablePropTypes.list.isRequired,
descendantsIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
domain: PropTypes.string.isRequired,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
};
state = {
fullscreen: false,
isExpanded: undefined,
threadExpanded: undefined,
statusId: undefined,
loadedStatusId: undefined,
showMedia: undefined,
revealBehindCW: undefined,
};
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
this.props.dispatch(fetchStatus(this.props.params.statusId));
}
static getDerivedStateFromProps(props, state) {
let update = {};
let updated = false;
if (props.params.statusId && state.statusId !== props.params.statusId) {
props.dispatch(fetchStatus(props.params.statusId));
update.threadExpanded = undefined;
update.statusId = props.params.statusId;
updated = true;
}
const revealBehindCW = props.settings.getIn(['media', 'reveal_behind_cw']);
if (revealBehindCW !== state.revealBehindCW) {
update.revealBehindCW = revealBehindCW;
if (revealBehindCW) update.showMedia = defaultMediaVisibility(props.status, props.settings);
updated = true;
}
if (props.status && state.loadedStatusId !== props.status.get('id')) {
update.showMedia = defaultMediaVisibility(props.status, props.settings);
update.loadedStatusId = props.status.get('id');
update.isExpanded = autoUnfoldCW(props.settings, props.status);
updated = true;
}
return updated ? update : null;
}
handleToggleHidden = () => {
const { status } = this.props;
if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
if (status.get('hidden')) {
this.props.dispatch(revealStatus(status.get('id')));
} else {
this.props.dispatch(hideStatus(status.get('id')));
}
} else if (this.props.status.get('spoiler_text')) {
this.setExpansion(!this.state.isExpanded);
}
};
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
};
handleModalFavourite = (status) => {
this.props.dispatch(favourite(status));
};
handleFavouriteClick = (status, e) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
if ((e && e.shiftKey) || !favouriteModal) {
this.handleModalFavourite(status);
} else {
dispatch(openModal({
modalType: 'FAVOURITE',
modalProps: {
status,
onFavourite: this.handleModalFavourite,
},
}));
}
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
handlePin = (status) => {
if (status.get('pinned')) {
this.props.dispatch(unpin(status));
} else {
this.props.dispatch(pin(status));
}
};
handleReplyClick = (status) => {
const { askReplyConfirmation, dispatch, intl } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (askReplyConfirmation) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
},
}));
} else {
dispatch(replyCompose(status, this.context.router.history));
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
handleModalReblog = (status, privacy) => {
const { dispatch } = this.props;
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status, privacy));
}
};
handleReblogClick = (status, e) => {
const { settings, dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
} else if ((e && e.shiftKey) || !boostModal) {
this.handleModalReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
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;
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
},
}));
}
};
handleEditClick = (status, history) => {
this.props.dispatch(editStatus(status.get('id'), history));
};
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
};
handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router));
};
handleOpenMedia = (media, index, lang) => {
this.props.dispatch(openModal({
modalType: 'MEDIA',
modalProps: { statusId: this.props.status.get('id'), media, index, lang },
}));
};
handleOpenVideo = (media, lang, options) => {
this.props.dispatch(openModal({
modalType: 'VIDEO',
modalProps: { statusId: this.props.status.get('id'), media, lang, options },
}));
};
handleHotkeyOpenMedia = e => {
const { status } = this.props;
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
};
handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
};
handleConversationMuteClick = (status) => {
if (status.get('muted')) {
this.props.dispatch(unmuteStatus(status.get('id')));
} else {
this.props.dispatch(muteStatus(status.get('id')));
}
};
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds, settings } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
let { isExpanded } = this.state;
if (settings.getIn(['content_warnings', 'shared_state']))
isExpanded = !status.get('hidden');
if (!isExpanded) {
this.props.dispatch(revealStatus(statusIds));
} else {
this.props.dispatch(hideStatus(statusIds));
}
this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded });
};
handleTranslate = status => {
const { dispatch } = this.props;
if (status.get('translation')) {
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
} else {
dispatch(translateStatus(status.get('id')));
}
};
handleBlockClick = (status) => {
const { dispatch } = this.props;
const account = status.get('account');
dispatch(initBlockModal(account));
};
handleReport = (status) => {
this.props.dispatch(initReport(status.get('account'), status));
};
handleEmbed = (status) => {
this.props.dispatch(openModal({
modalType: 'EMBED',
modalProps: { id: status.get('id') },
}));
};
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
};
handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id'));
};
handleHotkeyMoveDown = () => {
this.handleMoveDown(this.props.status.get('id'));
};
handleHotkeyReply = e => {
e.preventDefault();
this.handleReplyClick(this.props.status);
};
handleHotkeyFavourite = () => {
this.handleFavouriteClick(this.props.status);
};
handleHotkeyBoost = () => {
this.handleReblogClick(this.props.status);
};
handleHotkeyBookmark = () => {
this.handleBookmarkClick(this.props.status);
};
handleHotkeyMention = e => {
e.preventDefault();
this.handleMentionClick(this.props.status);
};
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
};
handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index, true);
} else {
this._selectChild(index - 1, true);
}
}
};
handleMoveDown = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false);
} else {
this._selectChild(index + 1, false);
}
}
};
_selectChild (index, align_top) {
const container = this.node;
const element = container.querySelectorAll('.focusable')[index];
if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus();
}
}
handleHeaderClick = () => {
this.column.scrollTop();
};
renderChildren (list, ancestors) {
const { params: { statusId } } = this.props;
return list.map((id, i) => (
<StatusContainer
key={id}
id={id}
expanded={this.state.threadExpanded}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 && list.get(i - 1)}
nextId={list.get(i + 1) || (ancestors && statusId)}
rootId={statusId}
/>
));
}
setExpansion = value => {
this.setState({ isExpanded: value });
};
setRef = c => {
this.node = c;
};
setColumnRef = c => {
this.column = c;
};
componentDidUpdate (prevProps) {
const { status, ancestorsIds, multiColumn } = this.props;
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
window.requestAnimationFrame(() => {
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that.
if (!multiColumn) {
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
if (offset) {
const scrollingElement = document.scrollingElement || document.body;
scrollingElement.scrollBy(0, -offset);
}
}
});
}
}
componentWillUnmount () {
detachFullscreenListener(this.onFullScreenChange);
}
onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
};
render () {
let ancestors, descendants;
const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
if (isLoading) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
if (status === null) {
return (
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
}
if (descendantsIds && descendantsIds.size > 0) {
descendants = <>{this.renderChildren(descendantsIds)}</>;
}
const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1;
const isIndexable = !status.getIn(['account', 'noindex']);
const handlers = {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
bookmark: this.handleHotkeyBookmark,
mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile,
toggleSpoiler: this.handleToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};
return (
<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 type='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}><Icon id={!isExpanded ? 'eye-slash' : 'eye'} /></button>
)}
/>
<ScrollContainer scrollKey='thread'>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
{ancestors}
<HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false, isExpanded)}>
<DetailedStatus
key={`details-${status.get('id')}`}
status={status}
settings={settings}
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
expanded={isExpanded}
onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate}
domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
/>
<ActionBar
key={`action-bar-${status.get('id')}`}
status={status}
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}
onMuteConversation={this.handleConversationMuteClick}
onBlock={this.handleBlockClick}
onReport={this.handleReport}
onPin={this.handlePin}
onEmbed={this.handleEmbed}
/>
</div>
</HotKeys>
{descendants}
</div>
</ScrollContainer>
<Helmet>
<title>{titleFromStatus(intl, status)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={status.get('url')} />
</Helmet>
</Column>
);
}
}
export default injectIntl(connect(makeMapStateToProps)(Status));

View file

@ -129,12 +129,12 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
/**
* Set options in the redux store
* @param opts
* @param {Object} opts
*/
setOpt: (opts) => dispatch(doodleSet(opts)),
/**
* Submit doodle for upload
* @param file
* @param {File} file
*/
submit: (file) => dispatch(uploadCompose([file])),
});
@ -240,7 +240,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Key up handler
* @param e
* @param {KeyboardEvent} e
*/
handleKeyUp = (e) => {
if (e.target.nodeName === 'INPUT') return;
@ -269,7 +269,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Key down handler
* @param e
* @param {KeyboardEvent} e
*/
handleKeyDown = (e) => {
if (e.key === 'Control' || e.key === 'Meta') {
@ -306,7 +306,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Set reference to the canvas element.
* This is called during component init
* @param elem - canvas element
* @param {HTMLCanvasElement} elem - canvas element
*/
setCanvasRef = (elem) => {
this.canvas = elem;
@ -347,7 +347,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Set up the sketcher instance
* @param canvas - canvas element. Null if we're just resizing
* @param {HTMLCanvasElement | null} canvas - canvas element. Null if we're just resizing
*/
initSketcher (canvas = null) {
const sizepreset = DOODLE_SIZES[this.size];
@ -445,7 +445,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Palette left click.
* Selects Fg color (or Bg, if Control/Meta is held)
* @param e - event
* @param {MouseEvent<HTMLButtonElement>} e - event
*/
onPaletteClick = (e) => {
const c = e.target.dataset.color;
@ -463,7 +463,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Palette right click.
* Selects Bg color
* @param e - event
* @param {MouseEvent<HTMLButtonElement>} e - event
*/
onPaletteRClick = (e) => {
this.bg = e.target.dataset.color;
@ -473,7 +473,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Handle click on the Draw mode button
* @param e - event
* @param {MouseEvent<HTMLButtonElement>} e - event
*/
setModeDraw = (e) => {
this.mode = 'draw';
@ -482,7 +482,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Handle click on the Fill mode button
* @param e - event
* @param {MouseEvent<HTMLButtonElement>} e - event
*/
setModeFill = (e) => {
this.mode = 'fill';
@ -491,7 +491,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Handle click on Smooth checkbox
* @param e - event
* @param {ChangeEvent<HTMLInputElement>} e - event
*/
tglSmooth = (e) => {
this.smoothing = !this.smoothing;
@ -500,7 +500,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Handle click on Adaptive checkbox
* @param e - event
* @param {ChangeEvent<HTMLInputElement>} e - event
*/
tglAdaptive = (e) => {
this.adaptiveStroke = !this.adaptiveStroke;
@ -509,7 +509,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Handle change of the Weight input field
* @param e - event
* @param {ChangeEvent<HTMLInputElement>} e - event
*/
setWeight = (e) => {
this.weight = +e.target.value || 1;
@ -517,7 +517,7 @@ class DoodleModal extends ImmutablePureComponent {
/**
* Set size - clalback from the select box
* @param e - event
* @param {ChangeEvent<HTMLSelectElement>} e - event
*/
changeSize = (e) => {
let newSize = e.target.value;

View file

@ -21,7 +21,7 @@ const messages = defineMessages({
explore: { id: 'explore.title', defaultMessage: 'Explore' },
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
@ -55,12 +55,12 @@ class NavigationPanel extends Component {
return (
<div className='navigation-panel'>
{transientSingleColumn && (
<>
<div className='navigation-panel__logo'>
<a href={`/deck${location.pathname}`} className='button button--block'>
{intl.formatMessage(messages.advancedInterface)}
</a>
<hr />
</>
</div>
)}
{signedIn && (

View file

@ -34,7 +34,7 @@ const SignInBanner = () => {
return (
<div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
{signupButton}
<a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</div>

View file

@ -191,6 +191,7 @@ class SwitchingColumnsArea extends PureComponent {
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
{!singleColumn && pathName === '/getting-started' ? <Redirect from='/getting-started' to='/deck/getting-started' exact /> : null}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
@ -528,11 +529,12 @@ class UI extends Component {
};
handleHotkeyBack = () => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history.state) {
this.props.history.goBack();
const { history } = this.props;
if (history.location?.state?.fromMastodon) {
history.goBack();
} else {
this.props.history.push('/');
history.push('/');
}
};

View file

@ -577,7 +577,10 @@ class Video extends PureComponent {
<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>
<span className='spoiler-button__overlay__label'>
{warning}
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
</span>
</button>
</div>

View file

@ -1,7 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Plasings van mense wat jy volg, kom chronologies in jou tuisvoer verby. Moenie huiwer nie. Volg na hartelus. As daar mense is wie se plasings jy nie meer wil sien nie, ontvolg hulle net!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",

View file

@ -1,10 +1,93 @@
{
"about.fork_disclaimer": "جلتش-سوك هو برنامج حر مفتوح المصدر متفرع عن ماستدون.",
"account.add_account_note": "إضافة ملاحظة لـ @{name}",
"account.disclaimer_full": "قد لا تعكِس المعلومات أدناه كامل الملف الشخصي للمستخدِم.",
"account.follows": "يتابِع",
"account.joined": "إنضم بتاريخ {date}",
"account.suspended_disclaimer_full": "تم تعليق هذا المستخدم من قبل المشرف.",
"account.view_full_profile": "عرض الملف الشخصي كاملاً",
"account_note.cancel": "إلغاء",
"account_note.edit": "تعديل",
"account_note.glitch_placeholder": "لم يُقدّم أي تعليق",
"account_note.save": "حفظ",
"advanced_options.icon_title": "خيارات متقدمة",
"advanced_options.local-only.long": "لا تنشر في خوادم أخرى",
"advanced_options.local-only.short": "المحلي فقط",
"advanced_options.local-only.tooltip": "هذا المنشور محلي فقط",
"advanced_options.threaded_mode.long": "تلقائياً يفتح رداً عند النشر",
"advanced_options.threaded_mode.short": "وضعُ النقاش الخيطي",
"advanced_options.threaded_mode.tooltip": "تم تمكين وضع النقاش الخيطي",
"boost_modal.missing_description": "هذا المنشور يحتوي على وسائط بلا وصف",
"column.favourited_by": "المفضلة من قبل",
"column.heading": "متنوعة",
"column.reblogged_by": "المرقى من قبل",
"column.subheading": "خيارات متنوعة",
"column_header.profile": "الملف الشخصي",
"column_subheading.lists": "القوائم",
"column_subheading.navigation": "التنقل",
"community.column_settings.allow_local_only": "إظهار المنشورات المحلية فقط",
"compose.attach": "أرفق...",
"compose.attach.doodle": "ارسم شيئاً",
"compose.attach.upload": "رفع ملف",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "ماركداون",
"compose.content-type.plain": "نص عادي",
"compose_form.poll.multiple_choices": "السماح بخيارات متعددة",
"compose_form.poll.single_choice": "السماح بخيار واحد",
"compose_form.spoiler": "إخفاء النص خلف تحذير",
"confirmation_modal.do_not_ask_again": "لا تطلب التأكيد مرة أخرى",
"confirmations.deprecated_settings.confirm": "استخدام تفضيلات ماستدون",
"confirmations.missing_media_description.confirm": "أرسل على أيّة حال",
"confirmations.missing_media_description.edit": "تعديل الوسائط",
"confirmations.unfilter.author": "المؤلف",
"confirmations.unfilter.confirm": "عرض",
"confirmations.unfilter.edit_filter": "تعديل عامل التصفية",
"content-type.change": "نوع المحتوى",
"direct.group_by_conversations": "تجميع حسب المحادثة",
"empty_column.follow_recommendations": "يبدو أنه لا يمكن إنشاء أي اقتراحات لك. يمكنك البحث عن أشخاص قد تعرفهم أو استكشاف الوسوم الرائجة.",
"endorsed_accounts_editor.endorsed_accounts": "الحسابات المميزة",
"favourite_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة",
"follow_recommendations.done": "تم",
"follow_recommendations.heading": "تابع الأشخاص الذين ترغب في رؤية منشوراتهم! إليك بعض الاقتراحات.",
"follow_recommendations.lead": "ستظهر منشورات الأشخاص الذين تُتابعتهم بترتيب تسلسلي زمني على صفحتك الرئيسية. لا تخف إذا ارتكبت أي أخطاء، تستطيع إلغاء متابعة أي شخص في أي وقت تريد!",
"getting_started.onboarding": "خذني في جولة",
"home.column_settings.advanced": "متقدم",
"home.column_settings.filter_regex": "إزالة باستخدام التعبيرات النمطية",
"home.settings": "إعدادات العمود",
"keyboard_shortcuts.bookmark": "لإضافة علامة",
"keyboard_shortcuts.secondary_toot": "لإرسال التبويق باستخدام إعدادات الخصوصية الثانوية",
"keyboard_shortcuts.toggle_collapse": "لطي/فتح التبويقات",
"media_gallery.sensitive": "حساس",
"moved_to_warning": "عُلِّم هذا الحساب بأنه انتقل إلى {moved_to_link}، لذا قد لا يقبل متابعات جديدة.",
"navigation_bar.app_settings": "إعدادات التطبيق",
"navigation_bar.featured_users": "مستخدمون مميزون",
"navigation_bar.keyboard_shortcuts": "اختصارات لوحة المفاتيح",
"navigation_bar.misc": "متنوع",
"notification.markForDeletion": "وضع علامة للحذف",
"notification_purge.btn_all": "تحديد الكل",
"notification_purge.btn_apply": "مسح المحدد",
"notification_purge.btn_invert": "عكس التحديد",
"notification_purge.btn_none": "إزالة التحديد",
"notification_purge.start": "أدخل وضع تنظيف الإشعارات",
"notifications.marked_clear": "مسح الإشعارات المحددة",
"notifications.marked_clear_confirmation": "هل أنت متأكد من أنك تريد مسح جميع الإشعارات المحددة نهائياً؟",
"onboarding.done": "تم",
"onboarding.next": "التالي",
"onboarding.page_five.public_timelines": "الجدول الزمني المحلي يظهر المشاركات العامة من الجميع على {domain}. ويظهر الخيط الفيدرالي المنشورات العامة لكل من يتابعهم الأشخاص في {domain}. هذه هي الخيوط الزمنية العامة، وهي طريقة رائعة لاكتشاف أناس جدد.",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.handle": "أنت على {domain}، لذا فإن حسابك الكامل هو {handle}",
"onboarding.page_one.welcome": "أهلاً بكم في {domain}!",
"onboarding.page_six.admin": "مشرف خادمك هو {admin}.",
"onboarding.page_six.almost_done": "على وشك الانتهاء...",
"onboarding.page_six.appetoot": "نشراً طيباً!",
"onboarding.page_six.apps_available": "هناك {apps} متوفرة على iOS و أندرويد و منصات أخرى.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "إرشادات المجتمع",
"onboarding.page_six.read_guidelines": "الرجاء قراءة {guidelines} من {domain}!",
"onboarding.skip": "تخطي",
"settings.close": "إغلاق",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
"settings.preferences": "Preferences",
"web_app_crash.settings": "الإعدادات",
"web_app_crash.title": "نحن آسفون، لقد حدث خطأ ما في تطبيق ماستدون."
}

View file

@ -1,8 +1,5 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "সম্পন্ন",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -32,10 +32,6 @@
"keyboard_shortcuts.bookmark": "Přidat do záložek",
"keyboard_shortcuts.secondary_toot": "Odeslat příspěvek s druhotným nastavením soukromí",
"keyboard_shortcuts.toggle_collapse": "Sbalit/rozbalit příspěvek",
"layout.auto": "Automatické",
"layout.hint.auto": "Vybrat rozložení automaticky v závislosti na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.",
"layout.hint.desktop": "Použít vícesloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.",
"layout.hint.single": "Použít jednosloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.",
"media_gallery.sensitive": "Citlivý obsah",
"navigation_bar.app_settings": "Nastavení aplikace",
"navigation_bar.featured_users": "Vybraní uživatelé",
@ -100,7 +96,6 @@
"settings.image_backgrounds_media_hint": "Pokud jsou k příspěvku přiložena média, použije se první z nich jako pozadí",
"settings.image_backgrounds_users": "Nastavit sbaleným příspěvkům obrázkové pozadí",
"settings.inline_preview_cards": "Zobrazit v časové ose náhledy externích odkazů",
"settings.layout": "Rozložení:",
"settings.layout_opts": "Možnosti rozvržení",
"settings.media": "Média",
"settings.media_fullwidth": "Zobrazit náhledy v plné šířce",

View file

@ -52,7 +52,7 @@
"favourite_modal.combo": "Mit {combo} wird dieses Fenster beim nächsten Mal nicht mehr angezeigt",
"follow_recommendations.done": "Fertig",
"follow_recommendations.heading": "Folge Leuten, deren Beiträge du sehen möchtest! Hier sind einige Vorschläge.",
"follow_recommendations.lead": "Beiträge von Profilen, denen du folgst, werden in chronologischer Reihenfolge auf deiner Startseite angezeigt. Sei unbesorgt, mal Fehler zu begehen. Du kannst diesen Konten ganz einfach und jederzeit wieder entfolgen.",
"follow_recommendations.lead": "Beiträge von Leuten, denen du folgst, werden in chronologischer Reihenfolge auf deiner Startseite angezeigt. Sei unbesorgt, mal Fehler zu begehen. Du kannst Leuten jederzeit ganz einfach wieder entfolgen!",
"getting_started.onboarding": "Führe mich herum",
"home.column_settings.advanced": "Erweitert",
"home.column_settings.filter_regex": "Mit regulären Ausdrücken herausfiltern",
@ -62,12 +62,6 @@
"keyboard_shortcuts.secondary_toot": "Toot mit sekundärer Privatsphäreeinstellung absenden",
"keyboard_shortcuts.toggle_collapse": "Toots ein-/ausklappen",
"tooltips.reactions": "Reaktionen",
"layout.auto": "Automatisch",
"layout.desktop": "Desktop",
"layout.hint.auto": "Automatisch das Layout anhand der Einstellung \"Erweitertes Webinterface verwenden\" und Bildschirmgröße auswählen.",
"layout.hint.desktop": "Das mehrspaltige Layout verwenden, unabhängig von der Einstellung \"Erweitertes Webinterface verwenden\".",
"layout.hint.single": "Das einspaltige Layout verwenden, unabhängig von der Einstellung \"Erweitertes Webinterface verwenden\".",
"layout.single": "Mobil",
"media_gallery.sensitive": "Empfindlich",
"moved_to_warning": "Dieses Konto ist als verschoben zu {moved_to_link} markiert und akzeptiert daher keine neuen Follower.",
"navigation_bar.app_settings": "App-Einstellungen",
@ -104,12 +98,6 @@
"onboarding.page_three.search": "Benutze die Suchleiste, um Leute zu finden und Hashtags anzusehen, wie etwa {illustration} und {introductions}. Um nach einer Person zu suchen, die nicht auf dieser Instanz ist, benutze deren vollständigen Nutzername.",
"onboarding.page_two.compose": "Schreibe Posts in der Verfassen-Spalte. Mit den Symbolen unten kannst du Bilder hochladen, Privatsphäre-Einstellungen ändern, und Inhaltswarnungen hinzufügen.",
"onboarding.skip": "Überspringen",
"search_popout.search_format": "Erweitertes Suchformat",
"search_popout.tips.full_text": "Simple Suchanfragen geben sowohl Beiträge, die du geschrieben, favorisiert oder geteilt hast oder in denen du erwähnt wurdest, als auch passende Nutzernamen, Anzeigenamen oder Hashtags, zurück.",
"search_popout.tips.hashtag": "Hashtag",
"search_popout.tips.status": "Beitrag",
"search_popout.tips.text": "Simple Suchanfragen geben passende Nutzernamen, Anzeigenamen oder Hashtags zurück",
"search_popout.tips.user": "Nutzer",
"settings.always_show_spoilers_field": "Das Inhaltswarnungs-Feld immer aktivieren",
"settings.auto_collapse": "Automatisches Einklappen",
"settings.auto_collapse_all": "Alles",
@ -146,7 +134,6 @@
"settings.image_backgrounds_media_hint": "Wenn der Post Anhänge hat, wird der erste als Hintergrund verwendet",
"settings.image_backgrounds_users": "Eingeklappten Toots einen Bild-Hintergrund geben",
"settings.inline_preview_cards": "Eingebettete Vorschaukarten für externe Links",
"settings.layout": "Layout:",
"settings.layout_opts": "Layout-Optionen",
"settings.media": "Medien",
"settings.media_fullwidth": "Medienvorschau in voller Breite",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -102,12 +102,6 @@
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"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",
"settings.always_show_spoilers_field": "Always enable the Content Warning field",
"settings.auto_collapse": "Automatic collapsing",
"settings.auto_collapse_all": "Everything",

View file

@ -14,10 +14,12 @@
"advanced_options.local-only.long": "Ne afiŝi al aliaj instancoj",
"advanced_options.local-only.short": "Nur loka",
"advanced_options.local-only.tooltip": "Ĉi tiu afiŝo estas nur-loka",
"advanced_options.threaded_mode.long": "Aŭtomate komencas respondon post afiŝado",
"advanced_options.threaded_mode.short": "Fadena reĝimo",
"advanced_options.threaded_mode.tooltip": "Fadena reĝimo ŝaltita",
"boost_modal.missing_description": "Ĉi tiu afiŝo enhavas plurmedion, ke ne havas priskribon",
"column.favourited_by": "Stelumita per",
"column.heading": "Diversaj aferoj",
"column.reblogged_by": "Diskonigita de",
"column.subheading": "Diversaj agordoj",
"column_header.profile": "Profilo",
@ -42,10 +44,13 @@
"confirmations.unfilter.author": "Aŭtoro",
"confirmations.unfilter.confirm": "Montri",
"confirmations.unfilter.edit_filter": "Redakti filtrilon",
"confirmations.unfilter.filters": "{count, plural, one {# filtrilo} other {# filtriloj}} kongruas",
"content-type.change": "Tipo de enhavo",
"empty_column.follow_recommendations": "Ŝajnas, ke neniuj sugestoj povis esti generitaj por vi. Vi povas provi uzi serĉon por serĉi homojn, kiujn vi eble konas, aŭ esplori tendencajn kradvortojn.",
"follow_recommendations.done": "Farita",
"follow_recommendations.heading": "Sekvi la personojn kies mesaĝojn vi volas vidi! Jen iom da sugestoj.",
"follow_recommendations.lead": "La mesaĝoj de personoj kiujn vi sekvas, aperos laŭ kronologia ordo en via hejma templinio. Ne timu erari, vi povas ĉesi sekvi facile iam ajn!",
"follow_recommendations.lead": "La mesaĝoj de personoj, kiujn vi sekvas, aperos laŭ kronologia ordo en via hejma templinio. Ne timu erari, vi povas ĉesi sekvi facile iam ajn!",
"home.column_settings.filter_regex": "Filtri per regulaj esprimoj",
"navigation_bar.keyboard_shortcuts": "Fulmoklavoj",
"notification_purge.btn_all": "Selekti ĉiujn",
"notification_purge.btn_apply": "Forigi selektajn",
@ -65,10 +70,6 @@
"onboarding.page_six.various_app": "poŝtelefonaj aplikaĵoj",
"onboarding.page_three.profile": "Redakti vian profilon por ŝanĝi vian profilbildon, biografion kaj montro-nomon. Vi povas ankaŭ trovi aliajn agordojn tie.",
"onboarding.skip": "Preterlasi",
"search_popout.search_format": "Detala serĉformato",
"search_popout.tips.hashtag": "kradvorto",
"search_popout.tips.text": "Simpla teksta serĉo montras la kongruajn afiŝitajn nomojn, uzantnomojn kaj kradvortojn",
"search_popout.tips.user": "uzanto",
"settings.auto_collapse_all": "Ĉiuj",
"settings.auto_collapse_lengthy": "Longaj afiŝoj",
"settings.auto_collapse_media": "Afiŝoj kun aŭdovidaĵoj",
@ -76,8 +77,12 @@
"settings.auto_collapse_reblogs": "Diskonigoj",
"settings.auto_collapse_replies": "Respondoj",
"settings.close": "Fermi",
"settings.content_warnings": "Content warnings",
"settings.content_warnings": "Enhavaj avertoj",
"settings.content_warnings.regexp": "Regula esprimo",
"settings.content_warnings_filter": "Enhavaj avertoj, kiujn ne aŭtomate malkaŝu:",
"settings.content_warnings_media_outside": "Montri plurmediojn ekstere de enhavaj avertoj",
"settings.content_warnings_media_outside_hint": "Fari same, kiel la originala programaro Mastodon, por ke la enhavaj avertoj ne influas plurmediojn",
"settings.image_backgrounds_media_hint": "Se la afiŝo havas plurmediojn, uzi la unuan kiel fono",
"settings.preferences": "Preferences",
"settings.shared_settings_link": "preferoj de uzanto",
"settings.side_arm": "Duaranga butono por afiŝi:",

View file

@ -4,7 +4,9 @@
"account.disclaimer_full": "La información aquí presentada puede reflejar de manera incompleta el perfil del usuario.",
"account.follows": "Sigue",
"account.joined": "Unido el {date}",
"account.mute_notifications": "Silenciar notificaciones de @{name}",
"account.suspended_disclaimer_full": "Este usuario ha sido suspendido por un moderador.",
"account.unmute_notifications": "Dejar de silenciar notificaciones de @{name}",
"account.view_full_profile": "Ver perfil completo",
"account_note.cancel": "Cancelar",
"account_note.edit": "Editar",
@ -50,6 +52,7 @@
"empty_column.follow_recommendations": "Parece que no se pudieron generar sugerencias para vos. Podés intentar buscar gente que conozcas o explorar las tendencias de las etiquetas.",
"endorsed_accounts_editor.endorsed_accounts": "Cuentas destacadas",
"favourite_modal.combo": "Puedes presionar {combo} para omitir esto la próxima vez",
"firehose.column_settings.allow_local_only": "Mostrar sólo mensajes locales en \"Todos\"",
"follow_recommendations.done": "Listo",
"follow_recommendations.heading": "¡Seguí cuentas cuyos mensajes te gustaría ver! Acá tenés algunas sugerencias.",
"follow_recommendations.lead": "Los mensajes de las cuentas que seguís aparecerán en orden cronológico en la columna \"Inicio\". No tengás miedo de meter la pata, ¡podés dejar de seguir cuentas fácilmente en cualquier momento!",
@ -61,12 +64,6 @@
"keyboard_shortcuts.bookmark": "a marcadores",
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
"keyboard_shortcuts.toggle_collapse": "para colapsar/descolapsar toots",
"layout.auto": "Automático",
"layout.desktop": "Escritorio",
"layout.hint.auto": "Seleccionar un diseño automáticamente basado en \"Habilitar interface web avanzada\" y tamaño de pantalla",
"layout.hint.desktop": "Utiliza el diseño multi-columna sin importar \"Habilitar interface web avanzada\" o tamaño de pantalla",
"layout.hint.single": "Utiliza el diseño de una columna sin importar \"Habilitar interface web avanzada\" o tamaño de pantalla",
"layout.single": "Móvil",
"media_gallery.sensitive": "Sensible",
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
"navigation_bar.app_settings": "Ajustes de aplicación",
@ -136,7 +133,6 @@
"settings.image_backgrounds_media_hint": "Si la publicación tiene algún archivo adjunto, utilice el primero como fondo",
"settings.image_backgrounds_users": "Darle fondo de imagen a toots colapsados",
"settings.inline_preview_cards": "Vista previa para enlaces externos",
"settings.layout": "Diseño",
"settings.layout_opts": "Opciones de diseño",
"settings.media": "Medios",
"settings.media_fullwidth": "Ancho completo al mostrar medios ",

View file

@ -61,12 +61,6 @@
"keyboard_shortcuts.bookmark": "a marcadores",
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
"keyboard_shortcuts.toggle_collapse": "para colapsar/descolapsar toots",
"layout.auto": "Automático",
"layout.desktop": "Escritorio",
"layout.hint.auto": "Seleccionar un diseño automáticamente basado en \"Habilitar interface web avanzada\" y tamaño de pantalla",
"layout.hint.desktop": "Utiliza el diseño multi-columna sin importar \"Habilitar interface web avanzada\" o tamaño de pantalla",
"layout.hint.single": "Utiliza el diseño de una columna sin importar \"Habilitar interface web avanzada\" o tamaño de pantalla",
"layout.single": "Móvil",
"media_gallery.sensitive": "Sensible",
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
"navigation_bar.app_settings": "Ajustes de aplicación",
@ -136,7 +130,6 @@
"settings.image_backgrounds_media_hint": "Si la publicación tiene algún archivo adjunto, utilice el primero como fondo",
"settings.image_backgrounds_users": "Darle fondo de imagen a toots colapsados",
"settings.inline_preview_cards": "Vista previa para enlaces externos",
"settings.layout": "Diseño",
"settings.layout_opts": "Opciones de diseño",
"settings.media": "Medios",
"settings.media_fullwidth": "Ancho completo al mostrar medios ",

View file

@ -61,12 +61,6 @@
"keyboard_shortcuts.bookmark": "a marcadores",
"keyboard_shortcuts.secondary_toot": "para enviar un toot usando lac onfiguración de privacidad secundaria",
"keyboard_shortcuts.toggle_collapse": "para colapsar/descolapsar toots",
"layout.auto": "Automático",
"layout.desktop": "Escritorio",
"layout.hint.auto": "Seleccionar un diseño automáticamente basado en \"Habilitar interface web avanzada\" y el tamaño de la pantalla.",
"layout.hint.desktop": "Utiliza el diseño multi-columna sin importar \"Habilitar interface web avanzada\" o el tamaño de la pantalla.",
"layout.hint.single": "Utiliza el diseño de una columna sin importar \"Habilitar interface web avanzada\" o el tamaño de la pantalla.",
"layout.single": "Móvil",
"media_gallery.sensitive": "Sensible",
"moved_to_warning": "Esta cuenta está marcada como movida a {moved_to_link}, y por lo tanto no aceptará nuevos seguimientos.",
"navigation_bar.app_settings": "Ajustes de la aplicación",
@ -136,7 +130,6 @@
"settings.image_backgrounds_media_hint": "Si la publicación tiene algún archivo adjunto, utilice el primero como fondo",
"settings.image_backgrounds_users": "Darle fondo de imagen a publicaciones colapsadas",
"settings.inline_preview_cards": "Vista previa para enlaces externos",
"settings.layout": "Diseño",
"settings.layout_opts": "Opciones de diseño",
"settings.media": "Medios",
"settings.media_fullwidth": "Ancho completo al mostrar medios ",

View file

@ -1,10 +1,57 @@
{
"about.fork_disclaimer": "Glitch-Goc یک نرم‌افزار آزاد است که از Mastodon انشعاب گرفته است.",
"account.add_account_note": "افزودن یادداشت برای @{name}",
"account.disclaimer_full": "اطلاعات زیر ممکن است نمایه کاربر را کامل منعکس نکند.",
"account.joined": "در {date} پیوست",
"account.view_full_profile": "مشاهده کامل نمایه",
"account_note.cancel": "لغو",
"account_note.edit": "ویرایش",
"account_note.save": "ذخیره",
"advanced_options.icon_title": "گزینه‌های پیشرفته",
"advanced_options.local-only.short": "فقط محلی",
"advanced_options.local-only.tooltip": "این فرسته فقط محلی است",
"column_header.profile": "نمایه",
"compose.attach.upload": "بارگذاری پرونده",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "مارک‌دون",
"compose.content-type.plain": "متن ساده",
"confirmations.missing_media_description.edit": "ویرایش رسانه",
"confirmations.unfilter.author": "نویسنده",
"confirmations.unfilter.confirm": "نمایش",
"confirmations.unfilter.edit_filter": "ویرایش پالایه",
"content-type.change": "نوع محتوا",
"empty_column.follow_recommendations": "به نظر نمی‌توان هیچ پیشنهادی برایتان ایجاد کرد. می‌توانید برای یافتن افرادی که ممکن است بشناسید از جست‌وجو یا کاوش برچسب‌های داغ استفاده کنید.",
"endorsed_accounts_editor.endorsed_accounts": "حساب‌های پیشنهاد شده",
"follow_recommendations.done": "انجام شد",
"follow_recommendations.heading": "افرادی را که می‌خواهید فرسته‌هایشان را ببینید پی‌گیری کنید! این‌ها تعدادی پیشنهاد هستند.",
"follow_recommendations.lead": "فرسته‌های افرادی که دنبال می‌کنید به ترتیب زمانی در خوراک خانه‌تان نشان داده خواهد شد. از اشتباه کردن نترسید. می‌توانید به همین سادگی در هر زمانی از دنبال کردن افراد دست بکشید!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"home.column_settings.advanced": "پیشرفته",
"navigation_bar.app_settings": "تنظیمات کاره",
"navigation_bar.featured_users": "کاربران پیشنهاد شده",
"navigation_bar.keyboard_shortcuts": "میان‌برهای صفحه‌کلید",
"notification.markForDeletion": "علامت‌گذاری برای حذف",
"notification_purge.btn_all": "انتخاب همه",
"onboarding.done": "انجام شد",
"onboarding.page_one.federation": "{domain} یک \"نمونه\" از ماستودون است. ماستودون شبکه ای از کارسازهای مستقل است که برای ایجاد یک شبکه اجتماعی بزرگتر به هم می پیوندند. ما این کارسازها را نمونه می نامیم.",
"onboarding.page_one.welcome": "به {domain} خوش آمدید!",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
"onboarding.page_six.various_app": "کاره‌های موبایل",
"settings.auto_collapse_reblogs": "تقویت‌ها",
"settings.auto_collapse_replies": "پاسخ‌ها",
"settings.close": "بستن",
"settings.content_warnings": "هشدارهای محتوا",
"settings.media": "رسانه",
"settings.pop_in_left": "چپ",
"settings.pop_in_right": "راست",
"settings.preferences": "ترجیحات کاربر",
"settings.shared_settings_link": "ترجیحات کاربر",
"settings.side_arm.none": "هیچ یک",
"settings.status_icons": "نقشک‌های توت",
"status.in_reply_to": "این توت یک پاسخ است",
"status.is_poll": "این توت یک نظرسنجی است",
"status.sensitive_toggle": "برای مشاهده کلیک کنید",
"web_app_crash.debug_info": "اطلاعات اشکال‌زدایی",
"web_app_crash.reload": "نوسازی",
"web_app_crash.settings": "تنظیمات",
"web_app_crash.title": "متأسفیم، اما مشکلی در کاره ماستودون رخ داد."
}

View file

@ -61,12 +61,6 @@
"keyboard_shortcuts.bookmark": "ajouter aux marque-pages",
"keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité",
"keyboard_shortcuts.toggle_collapse": "Plier/déplier les posts",
"layout.auto": "Auto",
"layout.desktop": "Ordinateur",
"layout.hint.auto": "Choisir automatiquement la mise en page selon l'option \"Activer l'interface Web avancée\" et la taille d'écran.",
"layout.hint.desktop": "Utiliser la mise en page en plusieurs colonnes indépendamment de l'option \"Activer l'interface Web avancée\" ou de la taille d'écran.",
"layout.hint.single": "Utiliser la mise en page à colonne unique indépendamment de l'option \"Activer l'interface Web avancée\" ou de la taille d'écran.",
"layout.single": "Téléphone",
"media_gallery.sensitive": "Sensible",
"moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.",
"navigation_bar.app_settings": "Paramètres de l'application",
@ -135,7 +129,6 @@
"settings.image_backgrounds_media_hint": "Si le post a un média attaché, utiliser le premier comme arrière-plan du post",
"settings.image_backgrounds_users": "Donner aux posts repliés une image en arrière-plan",
"settings.inline_preview_cards": "Cartes d'aperçu pour les liens externes",
"settings.layout": "Mise en page :",
"settings.layout_opts": "Mise en page",
"settings.media": "Média",
"settings.media_fullwidth": "Utiliser toute la largeur pour les aperçus",

View file

@ -62,12 +62,6 @@
"keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité",
"keyboard_shortcuts.toggle_collapse": "Plier/déplier les posts",
"tooltips.reactions": "Réactions",
"layout.auto": "Auto",
"layout.desktop": "Ordinateur",
"layout.hint.auto": "Choisir automatiquement la mise en page selon l'option \"Activer l'interface Web avancée\" et la taille d'écran.",
"layout.hint.desktop": "Utiliser la mise en page en plusieurs colonnes indépendamment de l'option \"Activer l'interface Web avancée\" ou de la taille d'écran.",
"layout.hint.single": "Utiliser la mise en page à colonne unique indépendamment de l'option \"Activer l'interface Web avancée\" ou de la taille d'écran.",
"layout.single": "Téléphone",
"media_gallery.sensitive": "Sensible",
"moved_to_warning": "Ce compte a déménagé vers {moved_to_link} et ne peut donc plus accepter de nouveaux abonné·e·s.",
"navigation_bar.app_settings": "Paramètres de l'application",
@ -139,7 +133,6 @@
"settings.image_backgrounds_media_hint": "Si le post a un média attaché, utiliser le premier comme arrière-plan du post",
"settings.image_backgrounds_users": "Donner aux posts repliés une image en arrière-plan",
"settings.inline_preview_cards": "Cartes d'aperçu pour les liens externes",
"settings.layout": "Mise en page :",
"settings.layout_opts": "Mise en page",
"settings.media": "Média",
"settings.media_fullwidth": "Utiliser toute la largeur pour les aperçus",

View file

@ -1,8 +1,6 @@
{
"empty_column.follow_recommendations": "Is cosúil nár fhéadfaí moltaí a ghineadh. D'fhéadfá cuardach a úsáid le teacht ar dhaoine a bhfuil aithne agat orthu, nó iniúchadh ar haischlibeanna atá ag treochtáil a dhéanamh.",
"follow_recommendations.done": "Déanta",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -2,7 +2,6 @@
"empty_column.follow_recommendations": "Čini se da se ne postoje sugestije generirane za tebe. Možeš pokušati koristiti pretragu kako bi pronašao osobe koje poznaš ili istraži popularne hashtagove.",
"follow_recommendations.done": "Učinjeno",
"follow_recommendations.heading": "Zaprati osobe čije objave želiš vidjeti! Evo nekoliko prijedloga.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -51,9 +51,6 @@
"keyboard_shortcuts.bookmark": "ブックマーク",
"keyboard_shortcuts.secondary_toot": "セカンダリートゥートの公開範囲でトゥートする",
"keyboard_shortcuts.toggle_collapse": "折りたたむ/折りたたみを解除",
"layout.auto": "自動",
"layout.desktop": "デスクトップ",
"layout.single": "モバイル",
"moved_to_warning": "このアカウント{moved_to_link}に引っ越したため、新しいフォロワーを受け入れていません。",
"navigation_bar.app_settings": "アプリ設定",
"navigation_bar.featured_users": "紹介しているアカウント",
@ -94,7 +91,6 @@
"settings.image_backgrounds_media": "折りたまれたメディア付きトゥートをプレビュー",
"settings.image_backgrounds_users": "折りたまれたトゥートの背景を変更する",
"settings.inline_preview_cards": "外部リンクに埋め込みプレビューを有効にする",
"settings.layout": "レイアウト",
"settings.layout_opts": "レイアウトの設定",
"settings.media": "メディア",
"settings.media_fullwidth": "全幅メディアプレビュー",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,5 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Immed",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -61,12 +61,6 @@
"keyboard_shortcuts.bookmark": "북마크",
"keyboard_shortcuts.secondary_toot": "보조 프라이버시 설정으로 글 보내기",
"keyboard_shortcuts.toggle_collapse": "글 접거나 펼치기",
"layout.auto": "자동",
"layout.desktop": "데스크탑",
"layout.hint.auto": "“고급 웹 인터페이스 활성화” 설정과 화면 크기에 따라 자동으로 레이아웃을 고릅니다.",
"layout.hint.desktop": "“고급 웹 인터페이스 활성화” 설정이나 화면 크기에 관계 없이 멀티 컬럼 레이아웃을 사용합니다.",
"layout.hint.single": "“고급 웹 인터페이스 활성화” 설정이나 화면 크기에 관계 없이 싱글 컬럼 레이아웃을 사용합니다.",
"layout.single": "모바일",
"media_gallery.sensitive": "민감함",
"moved_to_warning": "이 계정은 {moved_to_link}로 이동한 것으로 표시되었고, 새 팔로우를 받지 않는 것 같습니다.",
"navigation_bar.app_settings": "앱 설정",
@ -136,7 +130,6 @@
"settings.image_backgrounds_media_hint": "게시물이 미디어 첨부를 포함한다면, 첫번째를 배경으로 사용합니다",
"settings.image_backgrounds_users": "접힌 글에 이미지 배경 주기",
"settings.inline_preview_cards": "외부 링크에 대한 미리보기 카드를 같이 표시",
"settings.layout": "레이아웃:",
"settings.layout_opts": "레이아웃 옵션",
"settings.media": "미디어",
"settings.media_fullwidth": "최대폭 미디어 미리보기",

View file

@ -1,5 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Gwrys",
"follow_recommendations.heading": "Holyewgh tus a vynnowgh gweles postow anedha! Ottomma nebes profyansow.",
"follow_recommendations.lead": "Postow a dus a holyewgh a wra omdhiskwedhes omma yn aray termynel yn agas lin dre. Na borthewgh own a gammwul, hwi a yll p'eurpynag anholya tus mar es poran!",

View file

@ -1,8 +1,5 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Confectum",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,5 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "പൂര്‍ത്തിയായീ",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,5 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Acabat",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -4,7 +4,9 @@
"account.disclaimer_full": "Poniższe informacje mogą niekompletnie odzwierciedlać profil tego użytkownika.",
"account.follows": "Obserwuje",
"account.joined": "Konto utworzono {date}",
"account.mute_notifications": "Wycisz powiadomienia od @{name}",
"account.suspended_disclaimer_full": "Użytkownik został zawieszony przez moderatora.",
"account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}",
"account.view_full_profile": "Pokaż pełny profil",
"account_note.cancel": "Anuluj",
"account_note.edit": "Edytuj",
@ -37,6 +39,7 @@
"compose_form.spoiler": "Ukryj tekst za ostrzeżeniem",
"confirmation_modal.do_not_ask_again": "Więcej nie pytaj się o potwierdzenie",
"confirmations.deprecated_settings.confirm": "Użyj preferencji Mastodonu",
"confirmations.deprecated_settings.message": "Niektóre używane przez Ciebie {app_settings} glitch-soc zostały zastąpione przez {preferences} Mastodona:",
"confirmations.missing_media_description.confirm": "Zignoruj i wyślij",
"confirmations.missing_media_description.edit": "Edytuj załącznik multimedialny",
"confirmations.missing_media_description.message": "Co najmniej jednemu załącznikowi multimedialnemu brakuje opisu. Z uwagi na osoby z zaburzeniami widzenia rozważ opisanie wszystkich załączników przed opublikowaniem wpisu.",
@ -59,12 +62,6 @@
"keyboard_shortcuts.bookmark": "aby dodać do ulubionych",
"keyboard_shortcuts.secondary_toot": "aby opublikować wpis używając dodatkowych ustawień prywatności",
"keyboard_shortcuts.toggle_collapse": "aby zwinąć/rozwinąć wpisy",
"layout.auto": "Automatyczny",
"layout.desktop": "Desktopowy",
"layout.hint.auto": "Automatycznie wybierz układ na podstawie ustawienia „Włącz zaawansowany interfejs użytkownika” i rozmiaru ekranu.",
"layout.hint.desktop": "Użyj układu wielokolumnowego niezależnie od ustawienia „Włącz zaawansowany interfejs użytkownika” i rozmiaru ekranu.",
"layout.hint.single": "Użyj układu jednokolumnowego niezależnie od ustawienia „Włącz zaawansowany interfejs użytkownika” i rozmiaru ekranu.",
"layout.single": "Mobilny",
"media_gallery.sensitive": "Zawartość wrażliwa",
"moved_to_warning": "To konto oznaczone jest jako przeniesione do {moved_to_link} i może z tego powodu nie akceptować nowych obserwujących.",
"navigation_bar.app_settings": "Ustawienia aplikacji",
@ -98,12 +95,6 @@
"onboarding.page_three.search": "Użyj paska wyszukiwania aby znaleźć osoby i hasztagi, takie jak {illustration} i {introductions}. Aby znaleźć osobę niebędącą na tym serwerze użyj jej pełnego adresu.",
"onboarding.page_two.compose": "Twórz nowe wpisy w lewej kolumnie. Możesz wysłać zdjęcia, zmienić ustawienia prywatności i ukryć wpis za ostrzeżeniem używając poniższych ikon.",
"onboarding.skip": "Pomiń",
"search_popout.search_format": "Zaawansowane wyszukiwanie",
"search_popout.tips.full_text": "Proste wyszukiwanie twoich wpisów, ulubionych, podbić i nawiązań, a także pasujących pseudonimów, nazw użytkownika i hasztagów.",
"search_popout.tips.hashtag": "hasztag",
"search_popout.tips.status": "wpis",
"search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkownika i hasztagów",
"search_popout.tips.user": "użytkownik",
"settings.always_show_spoilers_field": "Zawsze pokazuj pole ostrzeżenia o zawartości",
"settings.auto_collapse": "Automatyczne zwijanie",
"settings.auto_collapse_all": "Wszystko",
@ -139,7 +130,6 @@
"settings.image_backgrounds_media_hint": "Jeśli wpis ma co najmniej jeden załącznik multimedialny, użyj pierwszego z nich, jako tła.",
"settings.image_backgrounds_users": "Nadaj tło zwiniętym wpisom",
"settings.inline_preview_cards": "Karty podglądu zewnętrznych linków w tekście",
"settings.layout": "Układ",
"settings.layout_opts": "Opcje układu",
"settings.media": "Zawartość multimedialna",
"settings.media_fullwidth": "Podgląd zawartości multimedialnej o pełnej szerokości",

View file

@ -61,12 +61,6 @@
"keyboard_shortcuts.bookmark": "para marcar",
"keyboard_shortcuts.secondary_toot": "para enviar toot usando a configuração de privacidade secundária",
"keyboard_shortcuts.toggle_collapse": "para recolher/mostrar toots",
"layout.auto": "Automático",
"layout.desktop": "Área de trabalho",
"layout.hint.auto": "Escolher automaticamente o layout baseado na configuração \"Habilitar interface web avançada\" e o tamanho da tela.",
"layout.hint.desktop": "Use o layout de várias colunas independentemente da configuração \"Habilitar interface web avançada\" ou do tamanho da tela.",
"layout.hint.single": "Use o layout de uma coluna independentemente da configuração \"Habilitar interface web avançada\" ou do tamanho da tela.",
"layout.single": "Celular",
"media_gallery.sensitive": "Sensível",
"moved_to_warning": "Esta conta foi como movida para {moved_to_link} e, portanto, pode não aceitar novos seguidores.",
"navigation_bar.app_settings": "Configurações do aplicativo",
@ -136,7 +130,6 @@
"settings.image_backgrounds_media_hint": "Se o post tiver algum anexo de mídia, use o primeiro em um plano de fundo",
"settings.image_backgrounds_users": "Dar a toots recolhidos uma imagem de fundo",
"settings.inline_preview_cards": "Cartões de pré-visualização em linha para links externos",
"settings.layout": "Layout:",
"settings.layout_opts": "Opções de layout",
"settings.media": "Mídia",
"settings.media_fullwidth": "Pré-visualização da mídia em largura total",

View file

@ -4,7 +4,9 @@
"account.disclaimer_full": "As informações abaixo podem não refletir completamente o perfil do utilizador.",
"account.follows": "A seguir",
"account.joined": "Juntou-se em {date}",
"account.mute_notifications": "Silenciar notificações de @{name}",
"account.suspended_disclaimer_full": "Este utilizador foi suspenso por um elemento da moderação.",
"account.unmute_notifications": "Desativar o silenciamento das notificações de @{name}",
"account.view_full_profile": "Ver o perfil completo",
"account_note.cancel": "Cancelar",
"account_note.edit": "Editar",
@ -18,6 +20,8 @@
"advanced_options.threaded_mode.short": "Modo de fio",
"advanced_options.threaded_mode.tooltip": "Modo de fio ativado",
"boost_modal.missing_description": "Este post contém alguns media sem descrição",
"column.favourited_by": "Adicionado aos favoritos de",
"column.heading": "Diversos",
"empty_column.follow_recommendations": "Parece que não foi possível gerar nenhuma sugestão para si. Pode tentar utilizar a pesquisa para procurar pessoas que conheça ou explorar as #etiquetas em destaque.",
"follow_recommendations.done": "Concluído",
"follow_recommendations.heading": "Siga pessoas das quais gostaria de ver publicações! Aqui estão algumas sugestões.",

View file

@ -1,5 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Fatu",
"follow_recommendations.heading": "Sighi gente de chie boles bìdere is publicatziones! Càstia custos cussìgios.",
"follow_recommendations.lead": "Is messàgios de gente a sa chi ses sighende ant a èssere ammustrados in òrdine cronològicu in sa lìnia de tempus printzipale tua. Non timas de fàghere errores, acabbare de sighire gente est fàtzile in cale si siat momentu!",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,10 +1,63 @@
{
"account.add_account_note": "@{name} için not ekleyin",
"account.disclaimer_full": "Aşağıdaki bilgi kullanıcının profilini hatalı olarak gösterebilir.",
"account.follows": "Takip Edilen",
"account.joined": "{date} tarihinde katıldı",
"account.suspended_disclaimer_full": "Bu kullanıcı, bir moderatör tarafından askıya alınmıştır.",
"account.view_full_profile": "Tam görünüm",
"account_note.cancel": "İptal",
"account_note.edit": "Düzenle",
"account_note.glitch_placeholder": "Yorum yok",
"account_note.save": "Kaydet",
"advanced_options.icon_title": "Gelişmiş seçenekler",
"advanced_options.local-only.long": "Başka örneklere paylaşım yapmayın",
"advanced_options.local-only.short": "Sadece yerel",
"advanced_options.local-only.tooltip": "Bu gönderi sadece yerel kullanıcılar içindir",
"advanced_options.threaded_mode.long": "Paylaşımda otomatik olarak yanıt oluşturur",
"advanced_options.threaded_mode.short": "Başlık modu",
"advanced_options.threaded_mode.tooltip": "Başlık modu aktif",
"boost_modal.missing_description": "Bu gönderi açıklaması olmayan medya içerir",
"column.favourited_by": "Tarafından favorilere eklendi",
"column.heading": "Diğer",
"column.reblogged_by": "Tarafından yükseltildi",
"column.subheading": "Diğer seçenekler",
"column_header.profile": "Profil",
"column_subheading.lists": "Listeler",
"column_subheading.navigation": "Gezinme",
"community.column_settings.allow_local_only": "Sadece yerel gönderileri göster",
"compose.attach": "Ekle...",
"compose.attach.doodle": "Herhangi bir şey çiz",
"compose.attach.upload": "Bir dosya yükle",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown modu",
"compose.content-type.plain": "Düz metin",
"compose_form.poll.multiple_choices": "Birden fazla seçeneğe izin ver",
"compose_form.poll.single_choice": "Bir seçeneğe izin ver",
"compose_form.spoiler": "Yazıyı uyarının arkasına gizle",
"confirmation_modal.do_not_ask_again": "Tekrardan onay istemeyin",
"confirmations.deprecated_settings.confirm": "Mastadon seçimlerini kullan",
"confirmations.deprecated_settings.message": "Kullanmakta olduğunuz bazı cihaz özgülü glitch-soc {app_settings} Mastadon {preferences} ile değiştirildi ve üzerine yazılacak:",
"confirmations.missing_media_description.confirm": "Yine de gönder",
"confirmations.missing_media_description.edit": "Medyayı düzenle",
"confirmations.missing_media_description.message": "En az bir medya eki açıklaması eksik. Gönderinizi göndermeden önce görme engelliler için tüm medya eklerini açıklamayı ön görün.",
"confirmations.unfilter.author": "Yazar",
"confirmations.unfilter.confirm": "Göster",
"confirmations.unfilter.edit_filter": "Filtreyi düzenle",
"confirmations.unfilter.filters": "Eşleşen {count, plural, one {filter} other {filters}}",
"content-type.change": "İçerik türü",
"direct.group_by_conversations": "Grup sohbeti",
"empty_column.follow_recommendations": "Öyle görünüyor ki sizin için hiçbir öneri oluşturulamıyor. Tanıdığınız kişileri aramak için aramayı kullanabilir veya öne çıkanlara bakabilirsiniz.",
"endorsed_accounts_editor.endorsed_accounts": "Öne çıkan hesaplar",
"favourite_modal.combo": "Bir sonraki sefer {combo} tuşuna basabilirsiniz",
"follow_recommendations.done": "Tamam",
"follow_recommendations.heading": "Gönderilerini görmek isteyeceğiniz kişileri takip edin! Burada bazı öneriler bulabilirsiniz.",
"follow_recommendations.lead": "Takip ettiğiniz kişilerin gönderileri anasayfa akışınızda kronolojik sırada görünmeye devam edecek. Hata yapmaktan çekinmeyin, kişileri istediğiniz anda kolayca takipten çıkabilirsiniz!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.always_show_spoilers_field": "Her zaman İçerik Uyarısı alanını etkinleştir",
"settings.auto_collapse": "Otomatik küçülme",
"settings.auto_collapse_all": "Her şey",
"settings.auto_collapse_height": "Yükseklik olarak değerlendirilmesi için gönderi genişliği (piksel türünde)",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
}

View file

@ -1,8 +1,5 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Булды",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,33 +1,104 @@
{
"about.fork_disclaimer": "Glitch-soc - це вільне програмне забезпечення з відкритим вихідним кодом, форк від Mastodon.",
"account.add_account_note": "Додати нотатку для @{name}",
"account.disclaimer_full": "Наведена нижче інформація може не повністю відображати профіль користувача.",
"account.follows": "Підписки",
"account.joined": "Приєднався {date}",
"account.suspended_disclaimer_full": "Цей користувач був призупинений модератором.",
"account.view_full_profile": "Переглянути повний профіль",
"account_note.cancel": "Скасувати",
"account_note.edit": "Змінити",
"account_note.glitch_placeholder": "Коментарі відсутні",
"account_note.save": "Зберегти",
"advanced_options.icon_title": "Додаткові налаштування",
"advanced_options.local-only.long": "Не дмухати це на інші сервери",
"advanced_options.local-only.short": "Лише локальне",
"advanced_options.local-only.tooltip": "Цей дмух лише локальний",
"advanced_options.threaded_mode.long": "Автоматично відкриває відповідь на публікацію",
"advanced_options.threaded_mode.short": "Потоковий режим",
"advanced_options.threaded_mode.tooltip": "Потоковий режим увімкнуто",
"boost_modal.missing_description": "Цей дмух містить деякі медіа без опису",
"column.favourited_by": "Уподобані",
"column.heading": "Різне",
"column.reblogged_by": "Поширено",
"column.subheading": "Інші параметри",
"column_header.profile": "Профіль",
"column_subheading.lists": "Списки",
"column_subheading.navigation": "Навігація",
"community.column_settings.allow_local_only": "Показувати тільки локальні дмухи",
"compose.attach": "Вкласти...",
"compose.attach.doodle": "Помалювати",
"compose.attach.upload": "Завантажити сюди файл",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
"compose.content-type.plain": "Звичайний текст",
"compose_form.poll.multiple_choices": "Дозволити кілька варіантів",
"compose_form.poll.single_choice": "Дозволити один вибір",
"compose_form.spoiler": "Приховати текст позаду попередження",
"confirmation_modal.do_not_ask_again": "Більше не запитувати підтвердження",
"confirmations.deprecated_settings.confirm": "Використовуйте налаштування Mastodon",
"confirmations.deprecated_settings.message": "Деякі з специфічних для пристрою glitch-soc {app_settings}, які ви використовуєте, було замінено на Mastodon {preferences}, і їх буде перевизначено:",
"confirmations.missing_media_description.confirm": "Все одно надіслати",
"confirmations.missing_media_description.edit": "Редагувати медіа",
"confirmations.missing_media_description.message": "Принаймні одна медіа-прикріплення не має опису. Подумайте про описання всіх медіавкладень для людей з порушеннями зору перед відправкою дмуху.",
"confirmations.unfilter.author": "Автор",
"confirmations.unfilter.confirm": "Показати",
"confirmations.unfilter.edit_filter": "Редагувати фільтр",
"confirmations.unfilter.filters": "Відповідність {count, plural, one {filter} other {filters}}",
"content-type.change": "Тип вмісту",
"direct.group_by_conversations": "Групування за розмовами",
"empty_column.follow_recommendations": "Схоже, для вас не було створено жодної пропозиції. Ви можете спробувати скористатися пошуком людей, яких ви можете знати, або переглянути популярні гештеґи.",
"endorsed_accounts_editor.endorsed_accounts": "Рекомендовані облікові записи",
"favourite_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
"follow_recommendations.done": "Готово",
"follow_recommendations.heading": "Підпишіться на людей, чиї дописи ви хочете бачити! Ось деякі пропозиції.",
"follow_recommendations.lead": "Дописи від людей, за якими ви стежите, з'являться в хронологічному порядку у вашій домашній стрічці. Не бійся помилятися, ви можете відписатися від людей так само легко в будь-який час!",
"getting_started.onboarding": "Шо тут",
"home.column_settings.advanced": "Додатково",
"home.column_settings.filter_regex": "Відфільтрувати за допомогою регулярних виразів",
"home.column_settings.show_direct": "Показати прямі повідомлення",
"layout.auto": "Автоматичний",
"layout.desktop": "Настільний",
"home.settings": "Налаштування стовпців",
"keyboard_shortcuts.bookmark": "В закладки",
"keyboard_shortcuts.secondary_toot": "надсилати повідомлення з використанням вторинних налаштувань конфіденційності",
"keyboard_shortcuts.toggle_collapse": "згорнути/розгорнути дмухи",
"media_gallery.sensitive": "Чутливі",
"moved_to_warning": "Цей обліковий запис позначено як переміщений до {moved_to_link} і тому не може прийняти нових підписок.",
"navigation_bar.app_settings": "Налаштування програми",
"navigation_bar.featured_users": "Рекомендовані користувачі",
"navigation_bar.keyboard_shortcuts": "Комбінації клавіш",
"navigation_bar.misc": "Різне",
"notification.markForDeletion": "Позначити для видалення",
"notification_purge.btn_all": "Вибрати\nвсе",
"notification_purge.btn_apply": "Очистити\nвибір",
"notification_purge.btn_invert": "Інвертувати\nвибір",
"notification_purge.btn_none": "Вибрати\nнічого",
"notification_purge.start": "Введіть режим очищення сповіщень",
"notifications.marked_clear": "Очистити вибрані сповіщення",
"notifications.marked_clear_confirmation": "Ви впевнені, що хочете незворотньо очистити всі вибрані сповіщення?",
"onboarding.done": "Готово",
"onboarding.next": "Далі",
"onboarding.page_five.public_timelines": "Місцева стрічка показує публічні публікації від усіх на {domain}. Об'єднана часова шкала показує публічні дописи від усіх, за ким стежать користувачі {domain}. Ці публічні хронології, чудовий спосіб відкрити для себе нових людей.",
"onboarding.page_four.home": "Домашня стрічка показує статті тих людей, за якими ви слідкуєте.",
"onboarding.page_four.notifications": "Стовпець сповіщень показує, коли хтось взаємодіє з вами.",
"onboarding.page_one.federation": "{domain} є сервером of Mastodon. Mastodon — мережа незалежних серверів, які працюють разом великою соціяльною мережою. Сервери Mastodon також називають „інстансами“.",
"onboarding.page_one.handle": "Ви знаходитесь на {domain}, так що ваш повний псевдонім {handle}",
"onboarding.page_one.welcome": "Ласкаво просимо до {domain}!",
"onboarding.page_six.admin": "Адміністратор вашого інстансу {admin}.",
"onboarding.page_six.almost_done": "Майже готово...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "{apps} доступні для iOS, Android та інших платформ.",
"onboarding.page_six.github": "{domain} використовує Glitchsoc. Glitchsoc — дружній {fork} {Mastodon}, сумісний з будь-яким сервером Mastodon або програмою для нього. Glitchsoc повністю вільний та відкритий. Повідомляти про баги, просити фічі, або працювати з кодом можна на {github}.",
"onboarding.page_six.guidelines": "правила спільноти",
"onboarding.page_six.read_guidelines": "Прочитайте {domain} в {guidelines}!",
"onboarding.page_six.various_app": "мобільні застосунки",
"onboarding.page_three.profile": "Відредагуйте свій профіль, щоб змінити аватарку, біографію та ім'я користувача. Там же ви знайдете інші налаштування.",
"onboarding.page_three.search": "Використовуйте рядок пошуку, щоб знайти людей, і шукайте за хештегами, такими як {illustration} та {introductions}. Щоб знайти людину, якої немає в цьому інстансі, використовуйте її повне ім'я.",
"onboarding.page_two.compose": "Писати дописи зі стовпчика \"Створити\". Ви можете завантажувати зображення, змінювати налаштування конфіденційності та додавати попередження про вміст за допомогою іконок нижче.",
"onboarding.skip": "Пропустити",
"settings.always_show_spoilers_field": "Завжди вмикати попередження про вміст",
"settings.auto_collapse": "Автоматичне згортання",
"settings.auto_collapse_all": "Все",
"settings.auto_collapse_height": "Висота (у пікселях), за якої дмух вважається довгим",
"settings.auto_collapse_lengthy": "Довгі дмухи",
"settings.auto_collapse_media": "Дмухи з медіафайлами",
"settings.auto_collapse_notifications": "Сповіщення",
@ -35,18 +106,92 @@
"settings.auto_collapse_replies": "Відповіді",
"settings.close": "Закрити",
"settings.collapsed_statuses": "Згорнуті дмухи",
"settings.compose_box_opts": "Compose box",
"settings.confirm_before_clearing_draft": "Показувати діалог підтвердження перед перезаписом повідомлення, що створюється",
"settings.confirm_boost_missing_media_description": "Показувати діалогове вікно підтвердження перед поширенням дмуху, у яких відсутні описи медіафайлів",
"settings.confirm_missing_media_description": "Показувати діалогове вікно підтвердження перед надсиланням дмуху без опису медіафайлів",
"settings.content_warnings": "Content warnings",
"settings.content_warnings.regexp": "Регулярний вираз",
"settings.content_warnings_filter": "Застереження щодо автоматичного розгортання вмісту:",
"settings.content_warnings_media_outside": "Відображати вкладені медіафайли поза попередженнями про вміст",
"settings.content_warnings_media_outside_hint": "Відтворити поведінку Mastodon перед початком роботи, встановивши перемикач попередження про вміст, щоб не впливати на вкладені файли мультимедіа",
"settings.content_warnings_shared_state": "Показати/приховати вміст усіх копій одночасно",
"settings.content_warnings_shared_state_hint": "Відтворити поведінку Mastodon, що передує, за допомогою кнопки попередження про вміст, яка впливає на всі копії допису одразу. Це запобігатиме автоматичному згортанню будь-якої копії допису з розгорнутим CW",
"settings.content_warnings_unfold_opts": "Параметри автоматичного розгортання",
"settings.deprecated_setting": "Цим параметром тепер можна керувати зі сторінки {settings_page_link} на сайті Mastodon",
"settings.enable_collapsed": "Увімкути згорнутання дмухів",
"settings.enable_collapsed_hint": "У згорнутих дописах частина їхнього вмісту прихована, щоб займати менше місця на екрані. Це відрізняється від функції попередження про вміст",
"settings.enable_content_warnings_auto_unfold": "Автоматично розгортати контент-попередження",
"settings.general": "Основне",
"settings.hicolor_privacy_icons": "Кольорові піктограми конфіденційності",
"settings.hicolor_privacy_icons.hint": "Відображати піктограми конфіденційності яскравими та легко помітними кольорами",
"settings.image_backgrounds": "Картинки на тлі",
"settings.image_backgrounds_media": "Підглядати медіа зі схованих дмухів",
"settings.image_backgrounds_media_hint": "Якщо допис має медіа-вкладення, використовуйте перше з них як фонове зображення",
"settings.image_backgrounds_users": "Давати схованим дмухам тло-картинку",
"settings.inline_preview_cards": "Вбудовані картки попереднього перегляду для зовнішніх посилань",
"settings.layout_opts": "Налаштування макету",
"settings.media": "Медіа",
"settings.media_fullwidth": "Показувати медіа повною шириною",
"settings.media_letterbox": "Обрізати медіа",
"settings.media_letterbox_hint": "Масштабувати медіа і букви замість заповнення контейнерів зображення розтягування та їх обрізання",
"settings.media_reveal_behind_cw": "Показувати чутливі медіа за замовчуванням",
"settings.notifications.favicon_badge": "Значок непрочитаних сповіщень",
"settings.notifications.favicon_badge.hint": "Додайте значок непрочитаних сповіщень на іконку",
"settings.notifications.tab_badge": "Значок непрочитаних сповіщень",
"settings.notifications.tab_badge.hint": "Відображати значок непрочитаних сповіщень в іконках стовпців, коли стовпець сповіщень не відкрито",
"settings.notifications_opts": "Параметри сповіщень",
"settings.pop_in_left": "Ліворуч",
"settings.pop_in_player": "Увімкнути спливаючий плеєр",
"settings.pop_in_position": "Позиція контекстного плеєра:",
"settings.pop_in_right": "Праворуч",
"settings.preferences": "Користувацькі налаштування",
"settings.prepend_cw_re": "Додавати \"re: \" до попереджень про вміст під час відповіді",
"settings.preselect_on_reply": "Попередньо вибирати імена користувачів для відповіді",
"settings.preselect_on_reply_hint": "Відповідаючи на розмову з кількома учасниками, попередньо вибирати імена користувачів після першого",
"settings.rewrite_mentions": "Переписувати згадки у відображуваних статусах",
"settings.rewrite_mentions_acct": "Переписати з ім'ям користувача та доменом (якщо обліковий запис віддалений)",
"settings.rewrite_mentions_no": "Не перезаписувати згадки",
"settings.rewrite_mentions_username": "Переписати за допомогою імені користувача",
"settings.shared_settings_link": "користувацькі налаштування",
"settings.show_action_bar": "Показувати кнопки у згорнутих дмухах",
"settings.show_content_type_choice": "Показувати вибір типу вмісту при авторизації дмухів",
"settings.show_reply_counter": "Відображати оцінку кількості відповідей",
"settings.side_arm": "Додаткова кнопка дмуху:",
"settings.side_arm.none": "Відсутня",
"settings.side_arm_reply_mode": "При відповіді на дмух, другорядна кнопка дмух повинна:",
"settings.side_arm_reply_mode.copy": "Копіювати налаштування конфіденційності дмуху до",
"settings.side_arm_reply_mode.keep": "Зберегати налаштування конфіденційності",
"settings.side_arm_reply_mode.restrict": "Обмежувати налаштування конфіденційності відповідно до дмуху",
"settings.status_icons": "Іконка дмуха",
"settings.status_icons_language": "Індикатор вибору мови",
"settings.status_icons_local_only": "Тільки локальний індикатор",
"settings.status_icons_media": "Індикатори медіа та опитування",
"settings.status_icons_reply": "Індикатор відповіді",
"settings.status_icons_visibility": "Індикатор конфіденційності дмуху",
"settings.swipe_to_change_columns": "Дозволити перегортання для зміни стовпців (лише для мобільних пристроїв)",
"settings.tag_misleading_links": "Позначати оманливі посилання",
"settings.tag_misleading_links.hint": "Додайте візуальну індикацію з цільовим хостом до кожного посилання, не згадуючи його явно",
"settings.wide_view": "Широкий вид (тільки в режимі для комп'ютерів)",
"settings.wide_view_hint": "Розтягує колони, щоб краще заповнити наявний простір.",
"status.collapse": "Згорнути",
"status.uncollapse": "Розгорнути"
"status.has_audio": "Особливості прикріплених аудіофайлів",
"status.has_pictures": "Особливості прикріплених зображень",
"status.has_preview_card": "Прикріплені функції попереднього перегляду",
"status.has_video": "Особливості прикріплених відео",
"status.in_reply_to": "Цей дмух є відповіддю",
"status.is_poll": "Цей дмух є опитуванням",
"status.local_only": "Видимий лише з вашого інстансу",
"status.sensitive_toggle": "Натисніть для перегляду",
"status.uncollapse": "Розгорнути",
"web_app_crash.change_your_settings": "Змініть свої {settings}",
"web_app_crash.content": "Ви можете спробувати одну з наступних дій:",
"web_app_crash.debug_info": "Debug-інформація",
"web_app_crash.disable_addons": "Вимкнути додатки для браузера або вбудовані інструменти перекладу",
"web_app_crash.issue_tracker": "баг-трекер",
"web_app_crash.reload": "Оновити",
"web_app_crash.reload_page": "{reload} поточну сторінку",
"web_app_crash.report_issue": "Повідомити про помилку у {issuetracker}",
"web_app_crash.settings": "налаштування",
"web_app_crash.title": "Нам шкода що так вийшло, але Mastodon чомусь не працює.."
}

View file

@ -1,8 +1,4 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

View file

@ -1,10 +1,12 @@
{
"about.fork_disclaimer": "Glitch-soc是从Mastodon派生的免费开源软件。",
"about.fork_disclaimer": "Glitch-soc是从Mastodon派生的自由开源软件。",
"account.add_account_note": "为 @{name} 添加备注",
"account.disclaimer_full": "以下信息可能无法完整代表你的个人资料。",
"account.follows": "正在关注",
"account.joined": "在 {date} 加入",
"account.suspended_disclaimer_full": "该用户已被封禁。",
"account.joined": "加入于 {date}",
"account.mute_notifications": "隐藏来自 @{name} 的通知",
"account.suspended_disclaimer_full": "该用户已被管理员封禁。",
"account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
"account.view_full_profile": "查看完整资料",
"account_note.cancel": "取消",
"account_note.edit": "编辑",
@ -12,20 +14,20 @@
"account_note.save": "保存",
"advanced_options.icon_title": "高级选项",
"advanced_options.local-only.long": "不要发布嘟文到其他实例",
"advanced_options.local-only.short": "本地模式",
"advanced_options.local-only.tooltip": "这条嘟文仅限于本实例",
"advanced_options.local-only.short": "仅本站",
"advanced_options.local-only.tooltip": "这条嘟文仅本站可见",
"advanced_options.threaded_mode.long": "发嘟时自动打开回复",
"advanced_options.threaded_mode.short": "线程模式",
"advanced_options.threaded_mode.tooltip": "线程模式已启用",
"advanced_options.threaded_mode.short": "话题模式",
"advanced_options.threaded_mode.tooltip": "话题模式已启用",
"boost_modal.missing_description": "这条嘟文未包含媒体描述",
"column.favourited_by": "喜欢",
"column.heading": "标题",
"column.favourited_by": "点赞",
"column.heading": "杂项",
"column.reblogged_by": "转嘟",
"column.subheading": "其他选项",
"column_header.profile": "个人资料",
"column_subheading.lists": "列表",
"column_subheading.navigation": "导航",
"community.column_settings.allow_local_only": "只显示本地模式嘟文",
"community.column_settings.allow_local_only": "只显示仅本站可见的嘟文",
"compose.attach": "附上...",
"compose.attach.doodle": "画点什么",
"compose.attach.upload": "上传文件",
@ -34,56 +36,51 @@
"compose.content-type.plain": "纯文本",
"compose_form.poll.multiple_choices": "允许多选",
"compose_form.poll.single_choice": "允许单选",
"compose_form.spoiler": "隐藏内容警告",
"confirmation_modal.do_not_ask_again": "下次不显示确认窗口",
"compose_form.spoiler": "隐藏并添加内容警告",
"confirmation_modal.do_not_ask_again": "不再要求确认",
"confirmations.deprecated_settings.confirm": "使用 Mastodon 偏好设置",
"confirmations.deprecated_settings.message": "您正使用的glitch-soc的特定于此设备的 {app_settings} 已被Mastodon {preferences} 替换,并将被覆盖:",
"confirmations.missing_media_description.confirm": "确认",
"confirmations.missing_media_description.edit": "编辑",
"confirmations.missing_media_description.message": "你没有为一种或多种媒体撰写描述。请考虑为视障人士添加描述。",
"confirmations.missing_media_description.confirm": "仍然发送",
"confirmations.missing_media_description.edit": "编辑媒体",
"confirmations.missing_media_description.message": "你没有为一个或多个媒体撰写描述。请考虑为视障人士添加描述。",
"confirmations.unfilter.author": "作者",
"confirmations.unfilter.confirm": "查看",
"confirmations.unfilter.edit_filter": "编辑过滤器",
"confirmations.unfilter.filters": "应用 {count, plural, one {过滤器} other {过滤器}}",
"confirmations.unfilter.confirm": "显示",
"confirmations.unfilter.edit_filter": "编辑筛选器",
"confirmations.unfilter.filters": "应用{count, plural, other {筛选器}}",
"content-type.change": "内容类型 ",
"direct.group_by_conversations": "对话分组",
"direct.group_by_conversations": "对话分组",
"empty_column.follow_recommendations": "似乎无法为你生成任何建议。你可以尝试使用搜索寻找你可能知道的人或探索热门标签。",
"endorsed_accounts_editor.endorsed_accounts": "推荐用户",
"endorsed_accounts_editor.endorsed_accounts": "精选账户",
"favourite_modal.combo": "下次你可以按 {combo} 跳过这个",
"firehose.column_settings.allow_local_only": "在“全部”中显示仅本站可见的嘟文",
"follow_recommendations.done": "完成",
"follow_recommendations.heading": "关注你感兴趣的用户!这里有一些推荐。",
"follow_recommendations.lead": "你关注的人的嘟文将按时间顺序显示在你的主页上。别担心,你可以在任何时候取消对别人的关注!",
"getting_started.onboarding": "参观一下",
"getting_started.onboarding": "带我参观一下",
"home.column_settings.advanced": "高级",
"home.column_settings.filter_regex": "按正则表达式过滤",
"home.column_settings.filter_regex": "按正则表达式筛选",
"home.column_settings.show_direct": "显示私信",
"home.settings": "列表设置",
"keyboard_shortcuts.bookmark": "书签",
"keyboard_shortcuts.secondary_toot": "使用二级隐私设置发送嘟文",
"keyboard_shortcuts.bookmark": "到收藏夹",
"keyboard_shortcuts.secondary_toot": "使用另一隐私设置发送嘟文",
"keyboard_shortcuts.toggle_collapse": "折叠或展开嘟文",
"layout.auto": "自动模式",
"layout.desktop": "桌面模式",
"layout.hint.auto": "根据“启用高级 Web 界面”设置和屏幕大小自动选择布局。",
"layout.hint.desktop": "“使用多列布局,无论“启用高级 Web 界面”设置和屏幕大小如何。",
"layout.hint.single": "使用单列布局,无论“启用高级 Web 界面”设置和屏幕大小如何。",
"layout.single": "移动模式",
"media_gallery.sensitive": "敏感内容",
"moved_to_warning": "此帐户已被标记为移至 {moved_to_link},并且似乎没有收到新关注者。",
"navigation_bar.app_settings": "应用选项",
"moved_to_warning": "此账户已被标记为移至 {moved_to_link},并且似乎没有收到新粉丝。",
"navigation_bar.app_settings": "应用设置",
"navigation_bar.featured_users": "推荐用户",
"navigation_bar.keyboard_shortcuts": "键盘快捷键",
"navigation_bar.misc": "杂项",
"notification.markForDeletion": "标记删除",
"notification.markForDeletion": "标记删除",
"notification_purge.btn_all": "全选",
"notification_purge.btn_apply": "清除已选",
"notification_purge.btn_invert": "反向选择",
"notification_purge.btn_invert": "反",
"notification_purge.btn_none": "取消全选",
"notification_purge.start": "进入通知清模式",
"notification_purge.start": "进入通知清模式",
"notifications.marked_clear": "清除选择的通知",
"notifications.marked_clear_confirmation": "你确定要永久清除所有选择的通知吗?",
"onboarding.done": "完成",
"onboarding.next": "下一",
"onboarding.page_five.public_timelines": "本时间线显示来自 {domain} 中所有人的公开嘟文。跨站时间线显示了 {domain} 用户关注的每个人的公开嘟文。这被称为公共时间线,是发现新朋友的好方法。",
"onboarding.next": "下一",
"onboarding.page_five.public_timelines": "本时间线显示来自 {domain} 中所有人的公开嘟文。跨站时间线显示了 {domain} 用户关注的每个人的公开嘟文。这被称为公共时间线,是发现新朋友的好方法。",
"onboarding.page_four.home": "你的主页时间线会显示你关注的人的嘟文。",
"onboarding.page_four.notifications": "通知栏显示某人与你互动的内容。",
"onboarding.page_one.federation": "{domain} 是 Mastodon 的一个“实例”。Mastodon 是一个由独立服务器组成的,通过不断联合形成的社交网络。我们称这些服务器为实例。",
@ -93,20 +90,14 @@
"onboarding.page_six.almost_done": "就快完成了...",
"onboarding.page_six.appetoot": "尽情享用吧!",
"onboarding.page_six.apps_available": "有适用于 iOS、Android 和其他平台的应用程序。",
"onboarding.page_six.github": "{domain} 在 Glitchsoc 上运行。Glitchsoc 是 {Mastodon} 的一个友好 {fork},与任何 Mastodon 实例或应用兼容。Glitchsoc 是完全免费和开源的。你可以在 {github} 上报告错误、请求功能或贡献代码。",
"onboarding.page_six.github": "{domain} 在 Glitch-soc 上运行。Glitch-soc 是 {Mastodon} 的一个友好 {fork},与任何 Mastodon 实例或应用兼容。Glitchsoc 是完全自由和开源的。你可以在 {github} 上报告错误、请求功能或贡献代码。",
"onboarding.page_six.guidelines": "社区准则",
"onboarding.page_six.read_guidelines": "请阅读 {domain} 的 {guidelines}",
"onboarding.page_six.various_app": "应用程序",
"onboarding.page_six.various_app": "移动应用",
"onboarding.page_three.profile": "编辑你的个人资料,更改你的头像、个人简介和昵称。在那里,你还会发现其他设置。",
"onboarding.page_three.search": "使用搜索栏查找用户并查看标签,例如 #illustration 和 #introductions。要查找不在此实例中的用户请使用他们的完整用户名。",
"onboarding.page_two.compose": "在撰写框中撰写嘟文。你可以使用下方图标上传图、更改隐私设置和添加内容警告。",
"onboarding.page_two.compose": "在撰写框中撰写嘟文。你可以使用下方图标上传图、更改隐私设置和添加内容警告。",
"onboarding.skip": "跳过",
"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": "用户",
"settings.always_show_spoilers_field": "始终显示内容警告框",
"settings.auto_collapse": "自动折叠",
"settings.auto_collapse_all": "所有",
@ -142,14 +133,13 @@
"settings.image_backgrounds_media_hint": "如果帖子有任何媒体附件,则使用第一个作为背景",
"settings.image_backgrounds_users": "为折叠嘟文附加图片背景",
"settings.inline_preview_cards": "外部链接的内嵌预览卡片",
"settings.layout": "布局:",
"settings.layout_opts": "布局选项",
"settings.media": "媒体",
"settings.media_fullwidth": "全宽媒体预览",
"settings.media_letterbox": "信箱媒体",
"settings.media_letterbox_hint": "缩小媒体以填充图像容器而不是拉伸和裁剪它们",
"settings.media_reveal_behind_cw": "默认显示内容警告的敏感媒体",
"settings.notifications.favicon_badge": "未读通知网站图标",
"settings.media_letterbox_hint": "缩小媒体以填充图像容器而不是拉伸和裁剪它们",
"settings.media_reveal_behind_cw": "默认显示内容警告的敏感媒体",
"settings.notifications.favicon_badge": "在网站图标显示未读通知",
"settings.notifications.favicon_badge.hint": "将未读通知添加到网站图标",
"settings.notifications.tab_badge": "未读通知图标",
"settings.notifications.tab_badge.hint": "当通知栏未打开时,显示未读通知图标",
@ -158,49 +148,49 @@
"settings.pop_in_player": "启用悬浮播放器",
"settings.pop_in_position": "悬浮播放器位置:",
"settings.pop_in_right": "右边",
"settings.preferences": "用户选项",
"settings.preferences": "用户偏好设置",
"settings.prepend_cw_re": "回复时在内容警告前加上“re:”",
"settings.preselect_on_reply": "回复时预先选择用户名",
"settings.preselect_on_reply_hint": "回复与多个参与者的对话时,预先选择第一个用户名",
"settings.rewrite_mentions": "重写嘟文中的提及",
"settings.rewrite_mentions_acct": "重写为用户名和域名(当帐户为远程时)",
"settings.rewrite_mentions_acct": "重写为用户名和域名(当账户为外站用户时)",
"settings.rewrite_mentions_no": "不要重写",
"settings.rewrite_mentions_username": "重写为用户名",
"settings.shared_settings_link": "用户偏好设置",
"settings.show_action_bar": "在折叠的嘟文中显示操作按钮",
"settings.show_content_type_choice": "允许你在撰写嘟文时选择格式类型",
"settings.show_content_type_choice": "允许在发嘟时选择格式类型",
"settings.show_reply_counter": "显示回复的大致数量",
"settings.side_arm": "辅助发嘟按钮:",
"settings.side_arm.none": "无",
"settings.side_arm_reply_mode": "当回复嘟文时",
"settings.side_arm_reply_mode": "当回复嘟文时,另一发嘟按钮会",
"settings.side_arm_reply_mode.copy": "复制被回复嘟文的隐私设置",
"settings.side_arm_reply_mode.keep": "保留辅助发嘟按钮以设置隐私",
"settings.side_arm_reply_mode.restrict": "将隐私设置限制为正在回复的那条嘟文",
"settings.status_icons": "嘟文图标",
"settings.status_icons_language": "语言指示器",
"settings.status_icons_local_only": "仅本指示器",
"settings.status_icons_local_only": "仅本指示器",
"settings.status_icons_media": "媒体和投票指示器",
"settings.status_icons_reply": "回复指示器",
"settings.status_icons_visibility": "嘟文隐私状态指示器",
"settings.swipe_to_change_columns": "允许滑动以在列之间切换(仅限移动模式)",
"settings.tag_misleading_links": "标记误导性链接",
"settings.tag_misleading_links.hint": "将带有目标网页链接的视觉指示添加到每个未明确的链接",
"settings.tag_misleading_links.hint": "在每个未明确展示目标网页的超链接上添加可见指示",
"settings.wide_view": "宽视图(仅限于桌面模式)",
"settings.wide_view_hint": "拉伸列宽以更好地填充可用空间。",
"status.collapse": "折叠",
"status.has_audio": "附带音频文件",
"status.has_pictures": "附带图片文件",
"status.has_audio": "附带音频",
"status.has_pictures": "附带图片",
"status.has_preview_card": "附带预览卡片",
"status.has_video": "附带视频文件",
"status.has_video": "附带视频",
"status.in_reply_to": "此嘟文是回复",
"status.is_poll": "此嘟文是投票",
"status.local_only": "此嘟文仅本实例可见",
"status.local_only": "此嘟文仅本可见",
"status.sensitive_toggle": "点击查看",
"status.uncollapse": "不折叠",
"status.uncollapse": "展开",
"web_app_crash.change_your_settings": "更改 {settings}",
"web_app_crash.content": "你可以尝试这些",
"web_app_crash.content": "你可以尝试以下任何一种",
"web_app_crash.debug_info": "调试信息",
"web_app_crash.disable_addons": "禁用浏览器插件或本地翻译工具",
"web_app_crash.disable_addons": "禁用浏览器插件或内置翻译工具",
"web_app_crash.issue_tracker": "问题追踪器",
"web_app_crash.reload": "刷新",
"web_app_crash.reload_page": "{reload} 此页面",

View file

@ -1,10 +1,134 @@
{
"about.fork_disclaimer": "Glitch-soc 是從 Mastodon 分支出來的自由開源軟體。",
"account.add_account_note": "為 @{name} 加入備註",
"account.disclaimer_full": "下面的資訊可能不完全反映使用者的個人資料。",
"account.follows": "跟隨",
"account.joined": "加入於 {date}",
"account.mute_notifications": "靜音來自 @{name} 的通知",
"account.suspended_disclaimer_full": "使用者已被管理者停權。",
"account.unmute_notifications": "重新接收來自 @{name} 的通知",
"account.view_full_profile": "查看完整個人資料",
"account_note.cancel": "取消",
"account_note.edit": "編輯",
"account_note.glitch_placeholder": "沒有註解",
"account_note.save": "儲存",
"advanced_options.icon_title": "進階選項",
"advanced_options.local-only.long": "不要傳遞給其他實例",
"advanced_options.local-only.short": "僅限本地",
"advanced_options.local-only.tooltip": "此嘟文僅限本地",
"advanced_options.threaded_mode.short": "討論串模式",
"advanced_options.threaded_mode.tooltip": "已啟用討論串模式",
"boost_modal.missing_description": "此嘟文包含未加說明的媒體檔案",
"column_header.profile": "個人檔案",
"column_subheading.lists": "列表",
"community.column_settings.allow_local_only": "顯示僅限本地的嘟文",
"compose.attach": "附加...",
"compose.attach.doodle": "塗鴉",
"compose.attach.upload": "上傳檔案",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "Markdown",
"compose.content-type.plain": "純文字",
"compose_form.poll.multiple_choices": "允許多重選擇",
"compose_form.poll.single_choice": "允許單一選擇",
"confirmation_modal.do_not_ask_again": "不要再顯示確認訊息",
"confirmations.deprecated_settings.confirm": "使用 Mastodon 偏好",
"confirmations.missing_media_description.edit": "編輯媒體",
"confirmations.missing_media_description.message": "至少有一個媒體附件缺少說明。 在發送嘟文之前,請考慮為視障人士在所有媒體附件加上說明。",
"confirmations.unfilter.author": "作者",
"confirmations.unfilter.confirm": "顯示",
"content-type.change": "內容類型",
"direct.group_by_conversations": "以對話分組",
"empty_column.follow_recommendations": "似乎未能為您產生任何建議。您可以嘗試使用搜尋來尋找您可能認識的人,或是探索熱門主題標籤。",
"follow_recommendations.done": "完成",
"follow_recommendations.heading": "跟隨您想檢視其嘟文的人!這裡有一些建議。",
"follow_recommendations.lead": "來自您跟隨的人之嘟文將會按時間順序顯示在您的首頁時間軸上。不要害怕犯錯,您隨時都可以取消跟隨其他人!",
"home.column_settings.advanced": "進階設定",
"home.column_settings.filter_regex": "以正規表達式進行過濾",
"media_gallery.sensitive": "敏感",
"notification_purge.btn_all": "選取全部",
"notification_purge.btn_apply": "清除所選項目",
"notification_purge.btn_invert": "反向選擇",
"notification_purge.btn_none": "取消選取",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
"settings.always_show_spoilers_field": "永遠啟用內容警告欄位",
"settings.auto_collapse": "自動折疊",
"settings.auto_collapse_all": "全部",
"settings.auto_collapse_height": "高度超過多少像素會被視為較長的嘟文",
"settings.auto_collapse_lengthy": "較長的嘟文",
"settings.auto_collapse_media": "包含媒體檔案的嘟文",
"settings.auto_collapse_notifications": "通知",
"settings.auto_collapse_reblogs": "轉嘟",
"settings.auto_collapse_replies": "回覆",
"settings.close": "關閉",
"settings.collapsed_statuses": "折疊的嘟文",
"settings.compose_box_opts": "嘟文撰寫框",
"settings.confirm_before_clearing_draft": "在覆蓋編輯中的嘟文前顯示確認對話框",
"settings.confirm_boost_missing_media_description": "在轉嘟包含缺少說明的媒體檔案的嘟文前顯示確認對話框",
"settings.confirm_missing_media_description": "在發出包含缺少說明的媒體檔案的嘟文前顯示確認對話框",
"settings.content_warnings": "內容警告",
"settings.content_warnings.regexp": "正規表達式",
"settings.content_warnings_filter": "不要自動展開內容警告:",
"settings.content_warnings_media_outside": "在內容警告外顯示媒體檔案",
"settings.content_warnings_media_outside_hint": "透過內容警告切換不影響媒體檔案來重現上游 Mastodon 行為",
"settings.content_warnings_shared_state": "一次顯示/隱藏所有副本的內容",
"settings.content_warnings_shared_state_hint": "透過內容警告按鈕同時影響嘟文的所有副本來重現上游 Mastodon 行為。 這將防止任何帶有展開的內容警告的嘟文副本自動折疊",
"settings.content_warnings_unfold_opts": "自動展開選項",
"settings.deprecated_setting": "此設定現在已由 Mastodon 的 {settings_page_link} 控制。",
"settings.enable_collapsed": "啟用折疊的嘟文",
"settings.enable_collapsed_hint": "折疊的嘟文隱藏了部分內容,以佔用更少的屏幕空間。這與內容警告功能不同",
"settings.enable_content_warnings_auto_unfold": "自動展開內容警告",
"settings.general": "一般設定",
"settings.hicolor_privacy_icons": "隱私圖示使用對比色",
"settings.hicolor_privacy_icons.hint": "用明亮且易於區分的顏色顯示隱私圖示",
"settings.image_backgrounds": "圖片背景",
"settings.image_backgrounds_media": "預覽折疊嘟文的媒體檔案",
"settings.image_backgrounds_media_hint": "如果嘟文包含媒體檔案,使用的一個作為圖片背景",
"settings.image_backgrounds_users": "為折疊的嘟文加上圖片背景",
"settings.layout_opts": "版面選項",
"settings.media": "媒體",
"settings.media_fullwidth": "在媒體預覽中使用完整寬度",
"settings.media_letterbox": "在媒體預覽加上黑邊",
"settings.media_letterbox_hint": "在媒體預覽中縮小並加上黑邊以取代延展與裁切",
"settings.media_reveal_behind_cw": "預設顯示隱藏在內容警告的敏感媒體檔案",
"settings.notifications.tab_badge": "未讀通知徽章",
"settings.notifications.tab_badge.hint": "當通知列未打開時,在導引圖示中顯示未讀通知的徽章",
"settings.notifications_opts": "通知選項",
"settings.pop_in_left": "左邊",
"settings.pop_in_player": "啟用彈出播放器",
"settings.pop_in_right": "右邊",
"settings.preferences": "使用者偏好設定",
"settings.prepend_cw_re": "回覆時在內容警告前添加 \"re:\"",
"settings.preselect_on_reply": "回覆時預先選擇用戶名稱",
"settings.preselect_on_reply_hint": "回覆與多個參與者的對話時,預先選擇第一個參與者之後的用戶名稱",
"settings.rewrite_mentions": "改寫已顯示嘟文中的提及",
"settings.rewrite_mentions_acct": "改寫為使用者名稱與網域(當使用者來自外部)",
"settings.rewrite_mentions_no": "不要改寫提及",
"settings.rewrite_mentions_username": "改寫為使用者名稱",
"settings.show_action_bar": "在折疊的嘟文顯示操作按鈕",
"settings.show_content_type_choice": "在編寫嘟文時顯示內容類型選擇",
"settings.show_reply_counter": "顯示回覆數量的估計值",
"settings.side_arm": "次要發出嘟文按鈕",
"settings.side_arm.none": "無",
"settings.side_arm_reply_mode": "當回覆一篇嘟文時,次要發出嘟文按鈕應該設為:",
"settings.side_arm_reply_mode.copy": "複製回覆嘟文的隱私設置",
"settings.side_arm_reply_mode.keep": "保持原本的隱私設定",
"settings.status_icons": "嘟文圖示",
"settings.status_icons_language": "語言指示器",
"settings.status_icons_local_only": "僅限本地指示器",
"settings.status_icons_media": "媒體與投票指示器",
"settings.status_icons_reply": "回覆指示器",
"settings.status_icons_visibility": "嘟文隱私指示器",
"settings.tag_misleading_links": "標記誤導性的連結",
"settings.tag_misleading_links.hint": "在每個未明確提及的連結添加帶有連結目標主機的視覺指示",
"settings.wide_view": "寬廣模式(僅限桌面模式)",
"settings.wide_view_hint": "延伸欄以更好地填充可用空間。",
"status.collapse": "折疊",
"status.has_audio": "包含音訊檔案",
"status.has_pictures": "包含圖片",
"status.has_preview_card": "包含預覽卡",
"status.has_video": "包含視訊檔案",
"status.in_reply_to": "嘟文有回覆",
"status.is_poll": "嘟文有投票",
"status.local_only": "只在此實例可見"
}

View file

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import {
COMPOSE_MENTION,
@ -13,6 +13,8 @@ import {
SEARCH_FETCH_SUCCESS,
SEARCH_SHOW,
SEARCH_EXPAND_SUCCESS,
SEARCH_RESULT_CLICK,
SEARCH_RESULT_FORGET,
} from 'flavours/glitch/actions/search';
const initialState = ImmutableMap({
@ -22,6 +24,7 @@ const initialState = ImmutableMap({
results: ImmutableMap(),
isLoading: false,
searchTerm: '',
recent: ImmutableOrderedSet(),
});
export default function search(state = initialState, action) {
@ -62,6 +65,10 @@ export default function search(state = initialState, action) {
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));
case SEARCH_RESULT_CLICK:
return state.update('recent', set => set.add(fromJS(action.result)));
case SEARCH_RESULT_FORGET:
return state.update('recent', set => set.filterNot(result => result.get('q') === action.q));
default:
return state;
}

View file

@ -3,9 +3,12 @@ import { Map as ImmutableMap, fromJS } from 'immutable';
import {
REBLOG_REQUEST,
REBLOG_FAIL,
UNREBLOG_REQUEST,
UNREBLOG_FAIL,
FAVOURITE_REQUEST,
FAVOURITE_FAIL,
UNFAVOURITE_SUCCESS,
UNFAVOURITE_REQUEST,
UNFAVOURITE_FAIL,
BOOKMARK_REQUEST,
BOOKMARK_FAIL,
REACTION_UPDATE,
@ -13,6 +16,8 @@ import {
REACTION_REMOVE_FAIL,
REACTION_ADD_REQUEST,
REACTION_REMOVE_REQUEST,
UNBOOKMARK_REQUEST,
UNBOOKMARK_FAIL,
} from 'flavours/glitch/actions/interactions';
import {
STATUS_MUTE_SUCCESS,
@ -117,14 +122,20 @@ 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:
return state.updateIn([action.status.get('id'), 'favourites_count'], x => Math.max(0, x - 1));
case FAVOURITE_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
case UNFAVOURITE_REQUEST:
return state.setIn([action.status.get('id'), 'favourited'], false);
case UNFAVOURITE_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], true);
case BOOKMARK_REQUEST:
return state.setIn([action.status.get('id'), 'bookmarked'], true);
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 UNBOOKMARK_REQUEST:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false);
case UNBOOKMARK_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], true);
case REBLOG_REQUEST:
return state.setIn([action.status.get('id'), 'reblogged'], true);
case REBLOG_FAIL:
@ -137,6 +148,10 @@ export default function statuses(state = initialState, action) {
case REACTION_REMOVE_REQUEST:
case REACTION_ADD_FAIL:
return removeReaction(state, action.id, action.name);
case UNREBLOG_REQUEST:
return state.setIn([action.status.get('id'), 'reblogged'], false);
case UNREBLOG_FAIL:
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], true);
case STATUS_MUTE_SUCCESS:
return state.setIn([action.id, 'muted'], true);
case STATUS_UNMUTE_SUCCESS:

View file

@ -200,6 +200,7 @@ button {
}
}
.error-boundary,
.app-holder noscript {
flex-direction: column;
font-size: 16px;

View file

@ -320,10 +320,10 @@
}
.relationship-tag {
color: $primary-text-color;
color: $white;
margin-bottom: 4px;
display: block;
background-color: $base-overlay-background;
background-color: rgba($black, 0.45);
text-transform: uppercase;
font-size: 11px;
font-weight: 500;

View file

@ -99,10 +99,6 @@
}
}
.search-popout {
@include search-popout;
}
.navigation-bar {
padding: 10px;
color: $darker-text-color;

View file

@ -1,30 +0,0 @@
.error-boundary {
color: $primary-text-color;
font-size: 15px;
line-height: 20px;
h1 {
font-size: 26px;
line-height: 36px;
font-weight: 400;
margin-bottom: 8px;
}
a {
color: $primary-text-color;
text-decoration: underline;
}
ul {
list-style: disc;
margin-inline-start: 0;
padding-inline-start: 1em;
}
textarea.web_app_crash-stacktrace {
width: 100%;
resize: none;
white-space: pre;
font-family: $font-monospace, monospace;
}
}

View file

@ -18,6 +18,10 @@
padding: 10px;
}
.search__popout {
border: 1px solid lighten($ui-base-color, 8%);
}
.search .fa {
top: 10px;
inset-inline-end: 10px;
@ -40,8 +44,9 @@
align-items: center;
color: $primary-text-color;
text-decoration: none;
padding: 15px 0;
padding: 15px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
gap: 15px;
&:last-child {
border-bottom: 0;
@ -50,33 +55,40 @@
&:hover,
&:active,
&:focus {
background-color: lighten($ui-base-color, 4%);
color: $highlight-text-color;
.story__details__publisher,
.story__details__shared {
color: $highlight-text-color;
}
}
&__details {
padding: 0 15px;
flex: 1 1 auto;
&__publisher {
color: $darker-text-color;
margin-bottom: 4px;
margin-bottom: 8px;
}
&__title {
font-size: 19px;
line-height: 24px;
font-weight: 500;
margin-bottom: 4px;
margin-bottom: 8px;
}
&__shared {
color: $darker-text-color;
}
strong {
font-weight: 500;
}
}
&__thumbnail {
flex: 0 0 auto;
margin: 0 15px;
position: relative;
width: 120px;
height: 120px;
@ -87,7 +99,7 @@
}
img {
border-radius: 4px;
border-radius: 8px;
display: block;
margin: 0;
width: 100%;
@ -96,7 +108,7 @@
}
&__preview {
border-radius: 4px;
border-radius: 8px;
display: block;
margin: 0;
width: 100%;
@ -112,4 +124,21 @@
}
}
}
&.expanded {
flex-direction: column;
.story__thumbnail {
order: 1;
width: 100%;
height: auto;
aspect-ratio: 1.91 / 1;
}
.story__details {
order: 2;
width: 100%;
flex: 0 0 auto;
}
}
}

View file

@ -18,7 +18,6 @@
@import 'lists';
@import 'emoji_picker';
@import 'local_settings';
@import 'error_boundary';
@import 'single_column';
@import 'announcements';
@import 'explore';

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