diff options
Diffstat (limited to 'app/javascript/flavours')
25 files changed, 497 insertions, 44 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 54909b56e..7a4af4cda 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -93,12 +93,13 @@ export const ensureComposeIsVisible = (getState, routerHistory) => { } }; -export function setComposeToStatus(status, text, spoiler_text) { +export function setComposeToStatus(status, text, spoiler_text, content_type) { return{ type: COMPOSE_SET_STATUS, status, text, spoiler_text, + content_type, }; }; diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 5930b7a16..efb4cc33b 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -34,6 +34,11 @@ export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST'; export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS'; export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL'; +export const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST'; +export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS'; +export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; +export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; + export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -101,7 +106,7 @@ export const editStatus = (id, routerHistory) => (dispatch, getState) => { api(getState).get(`/api/v1/statuses/${id}/source`).then(response => { dispatch(fetchStatusSourceSuccess()); ensureComposeIsVisible(getState, routerHistory); - dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text)); + dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type)); }).catch(error => { dispatch(fetchStatusSourceFail(error)); }); @@ -310,4 +315,36 @@ export function toggleStatusCollapse(id, isCollapsed) { id, isCollapsed, }; -} +}; + +export const translateStatus = id => (dispatch, getState) => { + dispatch(translateStatusRequest(id)); + + api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => { + dispatch(translateStatusSuccess(id, response.data)); + }).catch(error => { + dispatch(translateStatusFail(id, error)); + }); +}; + +export const translateStatusRequest = id => ({ + type: STATUS_TRANSLATE_REQUEST, + id, +}); + +export const translateStatusSuccess = (id, translation) => ({ + type: STATUS_TRANSLATE_SUCCESS, + id, + translation, +}); + +export const translateStatusFail = (id, error) => ({ + type: STATUS_TRANSLATE_FAIL, + id, + error, +}); + +export const undoStatusTranslation = id => ({ + type: STATUS_TRANSLATE_UNDO, + id, +}); diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 800832dc8..4041b4819 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -83,6 +83,7 @@ class Status extends ImmutablePureComponent { onEmbed: PropTypes.func, onHeightChange: PropTypes.func, onToggleHidden: PropTypes.func, + onTranslate: PropTypes.func, onInteractionModal: PropTypes.func, muted: PropTypes.bool, hidden: PropTypes.bool, @@ -472,6 +473,10 @@ class Status extends ImmutablePureComponent { this.node = c; } + handleTranslate = () => { + this.props.onTranslate(this.props.status); + } + renderLoadingMediaGallery () { return <div className='media-gallery' style={{ height: '110px' }} />; } @@ -788,6 +793,7 @@ class Status extends ImmutablePureComponent { mediaIcons={contentMediaIcons} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} + onTranslate={this.handleTranslate} parseClick={parseClick} disabled={!router} tagLinks={settings.get('tag_misleading_links')} diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index c618cedca..c59f42220 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -1,11 +1,11 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, injectIntl } from 'react-intl'; import Permalink from './permalink'; import classnames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; +import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'flavours/glitch/initial_state'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; const textMatchesTarget = (text, origin, host) => { @@ -62,13 +62,56 @@ const isLinkMisleading = (link) => { return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host)); }; -export default class StatusContent extends React.PureComponent { +class TranslateButton extends React.PureComponent { + + static propTypes = { + translation: ImmutablePropTypes.map, + onClick: PropTypes.func, + }; + + render () { + const { translation, onClick } = this.props; + + if (translation) { + const language = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language')); + const languageName = language ? language[2] : translation.get('detected_source_language'); + const provider = translation.get('provider'); + + return ( + <div className='translate-button'> + <div className='translate-button__meta'> + <FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} /> + </div> + + <button className='link-button' onClick={onClick}> + <FormattedMessage id='status.show_original' defaultMessage='Show original' /> + </button> + </div> + ); + } + + return ( + <button className='status__content__read-more-button' onClick={onClick}> + <FormattedMessage id='status.translate' defaultMessage='Translate' /> + </button> + ); + } + +} + +export default @injectIntl +class StatusContent extends React.PureComponent { + + static contextTypes = { + identity: PropTypes.object, + }; static propTypes = { status: ImmutablePropTypes.map.isRequired, expanded: PropTypes.bool, collapsed: PropTypes.bool, onExpandedToggle: PropTypes.func, + onTranslate: PropTypes.func, media: PropTypes.node, extraMedia: PropTypes.node, mediaIcons: PropTypes.arrayOf(PropTypes.string), @@ -77,6 +120,7 @@ export default class StatusContent extends React.PureComponent { onUpdate: PropTypes.func, tagLinks: PropTypes.bool, rewriteMentions: PropTypes.string, + intl: PropTypes.object, }; static defaultProps = { @@ -249,6 +293,10 @@ export default class StatusContent extends React.PureComponent { } } + handleTranslate = () => { + this.props.onTranslate(); + } + setContentsRef = (c) => { this.contentsNode = c; } @@ -263,18 +311,24 @@ export default class StatusContent extends React.PureComponent { disabled, tagLinks, rewriteMentions, + intl, } = this.props; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; + const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language'); - const content = { __html: status.get('contentHtml') }; + const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') }; - const lang = status.get('language'); + const lang = status.get('translation') ? intl.locale : status.get('language'); const classNames = classnames('status__content', { 'status__content--with-action': parseClick && !disabled, 'status__content--with-spoiler': status.get('spoiler_text').length > 0, }); + const translateButton = renderTranslate && ( + <TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} /> + ); + if (status.get('spoiler_text').length > 0) { let mentionsPlaceholder = ''; @@ -350,11 +404,11 @@ export default class StatusContent extends React.PureComponent { onMouseLeave={this.handleMouseLeave} lang={lang} /> + {!hidden && translateButton} {media} </div> {extraMedia} - </div> ); } else if (parseClick) { @@ -375,6 +429,7 @@ export default class StatusContent extends React.PureComponent { onMouseLeave={this.handleMouseLeave} lang={lang} /> + {translateButton} {media} {extraMedia} </div> @@ -395,6 +450,7 @@ export default class StatusContent extends React.PureComponent { onMouseLeave={this.handleMouseLeave} lang={lang} /> + {translateButton} {media} {extraMedia} </div> diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index c12b2e614..947573fc7 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -23,7 +23,9 @@ import { deleteStatus, hideStatus, revealStatus, - editStatus + editStatus, + translateStatus, + undoStatusTranslation, } from 'flavours/glitch/actions/statuses'; import { initAddFilter, @@ -187,6 +189,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ dispatch(editStatus(status.get('id'), history)); }, + onTranslate (status) { + if (status.get('translation')) { + dispatch(undoStatusTranslation(status.get('id'))); + } else { + dispatch(translateStatus(status.get('id'))); + } + }, + onDirect (account, router) { dispatch(directCompose(account, router)); }, diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.js b/app/javascript/flavours/glitch/features/compose/components/action_bar.js new file mode 100644 index 000000000..267c0ba69 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.js @@ -0,0 +1,66 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; +import { defineMessages, injectIntl } from 'react-intl'; +import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links'; + +const messages = defineMessages({ + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, + 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' }, + lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, + filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, + bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, +}); + +export default @injectIntl +class ActionBar extends React.PureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onLogout: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleLogout = () => { + this.props.onLogout(); + } + + render () { + const { intl } = this.props; + + let menu = []; + + menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); + menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink }); + menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); + menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); + menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); + menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); + menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); + menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); + menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout }); + + return ( + <div className='compose__action-bar'> + <div className='compose__action-bar-dropdown'> + <DropdownMenuContainer items={menu} icon='ellipsis-v' size={18} direction='right' /> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js index ba73ed553..1a68f1e12 100644 --- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js +++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js @@ -1,5 +1,7 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import ActionBar from './action_bar'; import Avatar from 'flavours/glitch/components/avatar'; import Permalink from 'flavours/glitch/components/permalink'; import { FormattedMessage } from 'react-intl'; @@ -10,11 +12,12 @@ export default class NavigationBar extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, + onLogout: PropTypes.func.isRequired, }; render () { return ( - <div className='drawer--account'> + <div className='navigation-bar'> <Permalink className='avatar' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}> <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> <Avatar account={this.props.account} size={48} /> @@ -28,11 +31,16 @@ export default class NavigationBar extends ImmutablePureComponent { { profileLink !== undefined && ( <a className='edit' - href={ profileLink } + href={profileLink} ><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> )} </div> + + <div className='navigation-bar__actions'> + <ActionBar account={this.props.account} onLogout={this.props.onLogout} /> + </div> </div> ); - }; + } + } diff --git a/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js index 0e1400261..89036adcd 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js @@ -1,11 +1,30 @@ import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; import NavigationBar from '../components/navigation_bar'; +import { logOut } from 'flavours/glitch/utils/log_out'; +import { openModal } from 'flavours/glitch/actions/modal'; import { me } from 'flavours/glitch/initial_state'; +const messages = defineMessages({ + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, +}); + const mapStateToProps = state => { return { account: state.getIn(['accounts', me]), }; }; -export default connect(mapStateToProps)(NavigationBar); +const mapDispatchToProps = (dispatch, { intl }) => ({ + onLogout () { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, + onConfirm: () => logOut(), + })); + }, +}); + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar)); diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 46770930f..7d2c2aace 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -34,6 +34,7 @@ class DetailedStatus extends ImmutablePureComponent { onOpenMedia: PropTypes.func.isRequired, onOpenVideo: PropTypes.func.isRequired, onToggleHidden: PropTypes.func, + onTranslate: PropTypes.func.isRequired, expanded: PropTypes.bool, measureHeight: PropTypes.bool, onHeightChange: PropTypes.func, @@ -112,6 +113,11 @@ class DetailedStatus extends ImmutablePureComponent { window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); } + handleTranslate = () => { + const { onTranslate, status } = this.props; + onTranslate(status); + } + render () { const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props; @@ -305,6 +311,7 @@ class DetailedStatus extends ImmutablePureComponent { expanded={expanded} collapsed={false} onExpandedToggle={onToggleHidden} + onTranslate={this.handleTranslate} parseClick={this.parseClick} onUpdate={this.handleChildUpdate} tagLinks={settings.get('tag_misleading_links')} diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index aaa9c7928..e190652b0 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -33,7 +33,9 @@ import { deleteStatus, editStatus, hideStatus, - revealStatus + revealStatus, + translateStatus, + undoStatusTranslation, } from 'flavours/glitch/actions/statuses'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; @@ -437,6 +439,16 @@ class Status extends ImmutablePureComponent { this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded }); } + handleTranslate = status => { + const { dispatch } = this.props; + + if (status.get('translation')) { + dispatch(undoStatusTranslation(status.get('id'))); + } else { + dispatch(translateStatus(status.get('id'))); + } + } + handleBlockClick = (status) => { const { dispatch } = this.props; const account = status.get('account'); @@ -666,6 +678,7 @@ class Status extends ImmutablePureComponent { onOpenMedia={this.handleOpenMedia} expanded={isExpanded} onToggleHidden={this.handleToggleHidden} + onTranslate={this.handleTranslate} domain={domain} showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} diff --git a/app/javascript/flavours/glitch/features/ui/components/header.js b/app/javascript/flavours/glitch/features/ui/components/header.js index 891f7fc07..d9ad94961 100644 --- a/app/javascript/flavours/glitch/features/ui/components/header.js +++ b/app/javascript/flavours/glitch/features/ui/components/header.js @@ -7,6 +7,7 @@ import Avatar from 'flavours/glitch/components/avatar'; import Permalink from 'flavours/glitch/components/permalink'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { openModal } from 'flavours/glitch/actions/modal'; const Account = connect(state => ({ account: state.getIn(['accounts', me]), @@ -16,7 +17,14 @@ const Account = connect(state => ({ </Permalink> )); -export default @withRouter +const mapDispatchToProps = (dispatch) => ({ + openClosedRegistrationsModal() { + dispatch(openModal('CLOSED_REGISTRATIONS')); + }, +}); + +export default @connect(null, mapDispatchToProps) +@withRouter class Header extends React.PureComponent { static contextTypes = { @@ -24,12 +32,13 @@ class Header extends React.PureComponent { }; static propTypes = { + openClosedRegistrationsModal: PropTypes.func, location: PropTypes.object, }; render () { const { signedIn } = this.context.identity; - const { location } = this.props; + const { location, openClosedRegistrationsModal } = this.props; let content; @@ -41,10 +50,26 @@ class Header extends React.PureComponent { </> ); } else { + let signupButton; + + if (registrationsOpen) { + signupButton = ( + <a href='/auth/sign_up' className='button button-tertiary'> + <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> + </a> + ); + } else { + signupButton = ( + <button className='button button-tertiary' onClick={openClosedRegistrationsModal}> + <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> + </button> + ); + } + content = ( <> <a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> - <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> + {signupButton} </> ); } diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index 5be177ced..bbf25c8a8 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -79,6 +79,7 @@ * @property {boolean} use_blurhash * @property {boolean=} use_pending_items * @property {string} version + * @property {boolean} translation_enabled * @property {object} local_settings */ @@ -137,6 +138,7 @@ export const unfollowModal = getMeta('unfollow_modal'); export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); +export const translationEnabled = getMeta('translation_enabled'); export const languages = initialState?.languages; // Glitch-soc-specific settings diff --git a/app/javascript/flavours/glitch/locales/cs.js b/app/javascript/flavours/glitch/locales/cs.js index ac7db0327..e789facb0 100644 --- a/app/javascript/flavours/glitch/locales/cs.js +++ b/app/javascript/flavours/glitch/locales/cs.js @@ -1,7 +1,180 @@ import inherited from 'mastodon/locales/cs.json'; const messages = { - // No translations available. + 'about.fork_disclaimer': 'Glitch-soc je svobodný software s otevřeným zdrojovým kódem založený na Mastodonu.', + 'settings.layout_opts': 'Možnosti rozvržení', + 'settings.layout': 'Rozložení:', + 'layout.current_is': 'Nastavené rozložení je:', + 'layout.auto': 'Automatické', + 'layout.desktop': 'Desktop', + 'layout.mobile': 'Mobil', + '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.', + 'navigation_bar.app_settings': 'Nastavení aplikace', + 'navigation_bar.featured_users': 'Vybraní uživatelé', + 'endorsed_accounts_editor.endorsed_accounts': 'Vybrané účty', + 'navigation_bar.info': 'Rozšířené informace', + 'navigation_bar.misc': 'Různé', + 'navigation_bar.keyboard_shortcuts': 'Klávesové zkratky', + 'getting_started.onboarding': 'Ukaž mi to tu', + 'onboarding.skip': 'Přeskočit', + 'onboarding.next': 'Další', + 'onboarding.done': 'Hotovo', + 'onboarding.page_one.federation': '{domain} je \'instance\' Mastodonu. Mastodon je síť nezávislých serverů, které jsou spolu propojené do jedné velké sociální sítě. Těmto serverům říkáme instance.', + 'onboarding.page_one.handle': 'Jste na instanci {domain}, takže celá adresa vašeho profilu je {handle}', + 'onboarding.page_one.welcome': 'Vítá vás {domain}!', + 'onboarding.page_two.compose': 'Příspěvky se píší v levém sloupci. Pomocí ikon pod příspěvkem k němu můžete připojit obrázky, změnit úroveň soukromí nebo přidat varování o obsahu.', + 'onboarding.page_three.search': 'Pomocí vyhledávací lišty můžete hledat lidi nebo hashtagy. Pokud hledáte někoho z jiné instance, musíte použít celou adresu jeho profilu.', + 'onboarding.page_three.profile': 'Upravte si svůj profil a nastavte si profilový obrázek, jméno, a krátký text o sobě. Naleznete tam i další možnosti nastavení.', + 'onboarding.page_four.home': 'Domovská časová osa zobrazuje příspěvky od lidí, které sledujete.', + 'onboarding.page_four.notifications': 'Notifikace se zobrazí, když s vámi někdo interaguje.', + 'onboarding.page_five.public_timelines': 'Místní časová osa zobrazuje veřejné příspěvky všech uživatelů instance {domain}. Federovaná časová osa zobrazí příspěvky od všech, koho uživatelé instance {domain} sledují. Tyto veřejné časové osy jsou skvělý způsob, jak objevit nové lidi.', + 'onboarding.page_six.almost_done': 'Skoro hotovo...', + 'onboarding.page_six.github': 'Na serveru {domain} běží Glitchsoc. Glitchsoc je přátelský {fork} programu {Mastodon}, a je kompatibilní s jakoukoliv jinou mastodoní instancí nebo aplikací. Glitchsoc je zcela svobodný a má otevřený zdrojový kód. Na stránce {github} můžete hlásit chyby, žádat o nové funkce, nebo ke kódu vlastnoručně přispět.', + 'onboarding.page_six.apps_available': 'Jsou dostupné {apps} pro iOS, Android i jiné platformy.', + 'onboarding.page_six.various_app': 'mobilní aplikace', + 'onboarding.page_six.appetoot': 'Veselé mastodonění!', + 'settings.auto_collapse': 'Automaticky sbalit', + 'settings.auto_collapse_all': 'Všechno', + 'settings.auto_collapse_lengthy': 'Dlouhé příspěvky', + 'settings.auto_collapse_media': 'Příspěvky s přílohami', + 'settings.auto_collapse_notifications': 'Oznámení', + 'settings.auto_collapse_reblogs': 'Boosty', + 'settings.auto_collapse_replies': 'Odpovědi', + 'settings.show_action_bar': 'Zobrazit ve sbalených příspěvcích tlačítka s akcemi', + 'settings.close': 'Zavřít', + 'settings.collapsed_statuses': 'Sbalené příspěvky', + 'settings.confirm_boost_missing_media_description': 'Zobrazit potvrzovací dialog před boostnutím příspěvku s chybějícími popisky obrázků', + 'boost_modal.missing_description': 'Příspěvek obsahuje obrázky bez popisků', + 'settings.enable_collapsed': 'Povolit sbalené příspěvky', + 'settings.enable_collapsed_hint': 'U sbalených příspěvků je část jejich obsahu skrytá, aby zabraly méně místa na obrazovce. (Tohle není stejná funkce jako varování o obsahu.)', + 'settings.general': 'Obecné', + 'settings.hicolor_privacy_icons': 'Barevné ikony soukromí', + 'settings.hicolor_privacy_icons.hint': 'Zobrazit ikony úrovně soukromí příspěvků v jasných, snadno rozlišitelných barvách', + 'settings.image_backgrounds': 'Obrázkové pozadí', + 'settings.image_backgrounds_media': 'Náhled médií ve sbalených příspěvcích', + '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.media': 'Média', + 'settings.media_letterbox': 'Neořezávat obrázky', + 'settings.media_letterbox_hint': 'Místo výřezu obrázku zobrazit obrázek celý, doplněný podle potřeby o prázdné okraje', + 'settings.media_fullwidth': 'Zobrazit náhledy v plné šířce', + 'settings.notifications_opts': 'Možnosti oznámení', + 'settings.notifications.tab_badge': 'Zobrazit počet nepřečtených oznámení', + 'settings.notifications.tab_badge.hint': 'Počet nepřečtených oznámení se viditelně zobrazí na hlavní stránce (pokud není seznam oznámení viditelný)', + 'settings.notifications.favicon_badge': 'Zobrazit počet na ikoně serveru', + 'settings.notifications.favicon_badge.hint': 'Zobrazí počet nepřečtených oznámení na ikoně serveru', + 'settings.preferences': 'Předvolby', + 'settings.rewrite_mentions': 'Přepsat zmínky v zobrazených příspěvcích', + 'settings.rewrite_mentions_no': 'Nepřepisovat zmínky', + 'settings.rewrite_mentions_acct': 'Přepsat uživatelským jménem a doménou (pokud je účet na jiném serveru)', + 'settings.rewrite_mentions_username': 'Přepsat uživatelským jménem', + 'settings.show_reply_counter': 'Zobrazit odhad počtu odpovědí', + 'settings.status_icons': 'Ikony u příspěvků', + 'settings.status_icons_language': 'Indikace jazyk', + 'settings.status_icons_reply': 'Indikace odpovědi', + 'settings.status_icons_local_only': 'Indikace lokálního příspěvku', + 'settings.status_icons_media': 'Indikace obrázků a anket', + 'settings.status_icons_visibility': 'Indikace úrovně soukromí', + 'settings.tag_misleading_links': 'Označit zavádějící odkazy', + 'settings.tag_misleading_links.hint': 'Zobrazit skutečný cíl u každého odkazu, který ho explicitně nezmiňuje', + 'settings.wide_view': 'Široké sloupce (pouze v režimu Desktop)', + 'settings.wide_view_hint': 'Sloupce se roztáhnout, aby lépe vyplnily dostupný prostor.', + 'settings.navbar_under': 'Navigační lišta vespod (pouze v režimu Mobil)', + 'settings.compose_box_opts': 'Editační pole', + 'settings.always_show_spoilers_field': 'Vždy zobrazit pole pro varování o obsahu', + 'settings.prepend_cw_re': 'Při odpovídání přidat před varování o obsahu “re: ”', + 'settings.preselect_on_reply': 'Při odpovědi označit uživatelská jména', + 'settings.preselect_on_reply_hint': 'Při odpovídání na konverzaci s více účastníky se jména všech kromě prvního označí, aby šla jednoduše smazat', + 'settings.confirm_missing_media_description': 'Zobrazit potvrzovací dialog při odesílání příspěvku, ve kterém chybí popisky obrázků', + 'settings.confirm_before_clearing_draft': 'Zobrazit potvrzovací dialog před přepsáním právě vytvářené zprávy', + 'settings.show_content_type_choice': 'Zobrazit volbu formátu příspěvku', + 'settings.side_arm': 'Vedlejší odesílací tlačítko:', + 'settings.side_arm.none': 'Žádné', + 'settings.side_arm_reply_mode': 'Při odpovídání na příspěvek by vedlejší odesílací tlačítko mělo:', + 'settings.side_arm_reply_mode.keep': 'Použít svou nastavenou úroveň soukromí', + 'settings.side_arm_reply_mode.copy': 'Použít úroveň soukromí příspěvku, na který odpovídáte', + 'settings.side_arm_reply_mode.restrict': 'Zvýšit úroveň soukromí nejméně na úroveň příspěvku, na který odpovídáte', + 'settings.content_warnings': 'Varování o obsahu', + 'settings.content_warnings_shared_state': 'Zobrazit/schovat všechny kopie naráz', + 'settings.content_warnings_shared_state_hint': 'Tlačítko varování o obsahu bude mít efekt na všechny kopie příspěvku naráz, stejně jako na běžném Mastodonu. Nebude pak možné automaticky sbalit jakoukoliv kopii příspěvku, která má rozbalené varování o obsahu', + 'settings.content_warnings_media_outside': 'Zobrazit obrázky a videa mimo varování o obsahu', + 'settings.content_warnings_media_outside_hint': 'Obrázky a videa z příspěvku s varováním o obsahu se zobrazí se separátním přepínačem zobrazení, stejně jako na běžném Mastodonu.', + 'settings.content_warnings_unfold_opts': 'Možnosti automatického rozbalení', + 'settings.enable_content_warnings_auto_unfold': 'Vždy rozbalit příspěvky označené varováním o obsahu', + 'settings.deprecated_setting': 'Tato možnost se nyní nastavuje v {settings_page_link}', + 'settings.shared_settings_link': 'předvolbách Mastodonu', + 'settings.content_warnings_filter': 'Tato varování o obsahu automaticky nerozbalovat:', + 'settings.content_warnings.regexp': 'Regulární výraz', + 'settings.media_reveal_behind_cw': 'Automaticky zobrazit média označená varováním o obsahu', + 'settings.pop_in_player': 'Povolit plovoucí okno přehrávače', + 'settings.pop_in_position': 'Pozice plovoucího okna:', + 'settings.pop_in_left': 'Vlevo', + 'settings.pop_in_right': 'Vpravo', + + + 'status.collapse': 'Sbalit', + 'status.uncollapse': 'Rozbalit', + 'status.in_reply_to': 'Tento příspěvek je odpověď', + 'status.has_preview_card': 'Obsahuje náhled odkazu', + 'status.has_pictures': 'Obsahuje obrázky', + 'status.is_poll': 'Tento příspěvek je anketa', + 'status.has_video': 'Obsahuje video', + 'status.has_audio': 'Obsahuje audio', + 'status.local_only': 'Viditelné pouze z vaší instance', + + 'media_gallery.sensitive': 'Citlivý obsah', + + 'favourite_modal.combo': 'Příště můžete pro přeskočení stisknout {combo}', + + 'home.column_settings.show_direct': 'Zobrazit přímé zprávy', + + 'notification_purge.start': 'Čistící režim', + 'notifications.mark_as_read': 'Označit všechna oznámení jako přečtená', + + 'notification.markForDeletion': 'Označit pro smazání', + 'notifications.clear': 'Vymazat všechna oznámení', + 'notifications.marked_clear_confirmation': 'Určitě chcete trvale smazat všechna vybraná oznámení?', + 'notifications.marked_clear': 'Smazat vybraná oznámení', + + 'notification_purge.btn_all': 'Vybrat\nvše', + 'notification_purge.btn_none': 'Nevybrat\nnic', + 'notification_purge.btn_invert': 'Obrátit\nvýběr', + 'notification_purge.btn_apply': 'Smazat\nvybrané', + + 'compose.attach.upload': 'Nahrát soubor', + 'compose.attach.doodle': 'Něco namalovat', + 'compose.attach': 'Připojit...', + + 'advanced_options.local-only.short': 'Lokální příspěvek', + 'advanced_options.local-only.long': 'Neposílat na jiné servery', + 'advanced_options.local-only.tooltip': 'Tento příspěvek je pouze lokální', + 'advanced_options.icon_title': 'Pokročilá nastavení', + 'advanced_options.threaded_mode.short': 'Režim vlákna', + 'advanced_options.threaded_mode.long': 'Po odeslání automaticky otevře pole pro odpověď', + 'advanced_options.threaded_mode.tooltip': 'Režim vlákna je zapnutý', + + 'home.column_settings.advanced': 'Pokročilé', + 'home.column_settings.filter_regex': 'Filtrovat podle regulárních výrazů', + + 'compose_form.poll.single_choice': 'Povolit jednu odpověď', + 'compose_form.poll.multiple_choices': 'Povolit více odpovědí', + + 'compose.content-type.plain': 'Prostý text', + 'content-type.change': 'Formát příspěvku', + 'compose_form.spoiler': 'Přidat varování o obsahu', + + 'direct.group_by_conversations': 'Seskupit do konverzací', + 'column.toot': 'Příspěvky a odpovědi', + 'confirmation_modal.do_not_ask_again': 'Příště se už neptat', + + 'keyboard_shortcuts.bookmark': 'Přidat do záložek', + 'keyboard_shortcuts.toggle_collapse': 'Sbalit/rozbalit příspěvek', + 'keyboard_shortcuts.secondary_toot': 'Odeslat příspěvek s druhotným nastavením soukromí', + + 'column.subheading': 'Různé', }; export default Object.assign({}, inherited, messages); diff --git a/app/javascript/flavours/glitch/names.yml b/app/javascript/flavours/glitch/names.yml index 68db3a73e..f35b457e1 100644 --- a/app/javascript/flavours/glitch/names.yml +++ b/app/javascript/flavours/glitch/names.yml @@ -6,6 +6,14 @@ en: skins: glitch: default: Default +cs: + flavours: + glitch: + description: Výchozí rozhraní instancí GlitchSoc. + name: Glitch + skins: + glitch: + default: Výchozí pl: flavours: glitch: diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index b1c792406..9b50ec23a 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -599,6 +599,7 @@ export default function compose(state = initialState, action) { return state.withMutations(map => { map.set('id', action.status.get('id')); map.set('text', action.text); + map.set('content_type', action.content_type || 'text/plain'); map.set('in_reply_to', action.status.get('in_reply_to_id')); map.set('privacy', action.status.get('visibility')); map.set('media_attachments', action.status.get('media_attachments')); diff --git a/app/javascript/flavours/glitch/reducers/status_lists.js b/app/javascript/flavours/glitch/reducers/status_lists.js index ada0484f4..7ac0dab47 100644 --- a/app/javascript/flavours/glitch/reducers/status_lists.js +++ b/app/javascript/flavours/glitch/reducers/status_lists.js @@ -25,7 +25,7 @@ import { TRENDS_STATUSES_EXPAND_SUCCESS, TRENDS_STATUSES_EXPAND_FAIL, } from 'flavours/glitch/actions/trends'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; import { FAVOURITE_SUCCESS, UNFAVOURITE_SUCCESS, @@ -43,22 +43,22 @@ const initialState = ImmutableMap({ favourites: ImmutableMap({ next: null, loaded: false, - items: ImmutableList(), + items: ImmutableOrderedSet(), }), bookmarks: ImmutableMap({ next: null, loaded: false, - items: ImmutableList(), + items: ImmutableOrderedSet(), }), pins: ImmutableMap({ next: null, loaded: false, - items: ImmutableList(), + items: ImmutableOrderedSet(), }), trending: ImmutableMap({ next: null, loaded: false, - items: ImmutableList(), + items: ImmutableOrderedSet(), }), }); @@ -67,7 +67,7 @@ const normalizeList = (state, listType, statuses, next) => { map.set('next', next); map.set('loaded', true); map.set('isLoading', false); - map.set('items', ImmutableList(statuses.map(item => item.id))); + map.set('items', ImmutableOrderedSet(statuses.map(item => item.id))); })); }; @@ -75,20 +75,22 @@ const appendToList = (state, listType, statuses, next) => { return state.update(listType, listMap => listMap.withMutations(map => { map.set('next', next); map.set('isLoading', false); - map.set('items', map.get('items').concat(statuses.map(item => item.id))); + map.set('items', map.get('items').union(statuses.map(item => item.id))); })); }; const prependOneToList = (state, listType, status) => { - return state.update(listType, listMap => listMap.withMutations(map => { - map.set('items', map.get('items').unshift(status.get('id'))); - })); + return state.updateIn([listType, 'items'], (list) => { + if (list.includes(status.get('id'))) { + return list; + } else { + return ImmutableOrderedSet([status.get('id')]).union(list); + } + }); }; const removeOneFromList = (state, listType, status) => { - return state.update(listType, listMap => listMap.withMutations(map => { - map.set('items', map.get('items').filter(item => item !== status.get('id'))); - })); + return state.updateIn([listType, 'items'], (list) => list.delete(status.get('id'))); }; export default function statusLists(state = initialState, action) { @@ -139,7 +141,7 @@ export default function statusLists(state = initialState, action) { return removeOneFromList(state, 'pins', action.status); case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_MUTE_SUCCESS: - return state.updateIn(['trending', 'items'], ImmutableList(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id)); + return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id)); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index b47155c5f..f0c4c804b 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -13,6 +13,8 @@ import { STATUS_REVEAL, STATUS_HIDE, STATUS_COLLAPSE, + STATUS_TRANSLATE_SUCCESS, + STATUS_TRANSLATE_UNDO, STATUS_FETCH_REQUEST, STATUS_FETCH_FAIL, } from 'flavours/glitch/actions/statuses'; @@ -85,6 +87,10 @@ export default function statuses(state = initialState, action) { return state.setIn([action.id, 'collapsed'], action.isCollapsed); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); + case STATUS_TRANSLATE_SUCCESS: + return state.setIn([action.id, 'translation'], fromJS(action.translation)); + case STATUS_TRANSLATE_UNDO: + return state.deleteIn([action.id, 'translation']); default: return state; } diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 71edf7fb3..42a591931 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -65,6 +65,7 @@ $ui-header-height: 55px; z-index: 2; justify-content: space-between; align-items: center; + overflow: hidden; &__logo { display: inline-flex; @@ -81,10 +82,15 @@ $ui-header-height: 55px; align-items: center; gap: 10px; padding: 0 10px; + overflow: hidden; .button { flex: 0 0 auto; } + + .button-tertiary { + flex-shrink: 1; + } } } diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index dfb9dc595..3e2604d4d 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -92,7 +92,7 @@ @include search-popout(); } -.drawer--account { +.navigation-bar { padding: 10px; color: $darker-text-color; display: flex; diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 80b0598a5..84aca2ebc 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -15,7 +15,7 @@ display: block; font-size: 15px; line-height: 20px; - color: $ui-highlight-color; + color: $highlight-text-color; border: 0; background: transparent; padding: 0; diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index ab8609170..8ba8bec10 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -1306,7 +1306,8 @@ img.modal-warning { width: 600px; background: $ui-base-color; border-radius: 8px; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; position: relative; display: block; padding: 20px; diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index d91306151..45d57aedd 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -37,7 +37,7 @@ top: 15px; } - .drawer--account { + .navigation-bar { flex: 0 1 48px; } diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 5e4bddc67..64dbc3cf0 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -206,15 +206,13 @@ } } -.status__content__edited-label { - display: block; - cursor: default; +.translate-button { + margin-top: 16px; font-size: 15px; line-height: 20px; - padding: 0; - padding-top: 8px; + display: flex; + justify-content: space-between; color: $dark-text-color; - font-weight: 500; } .status__content__spoiler-link { diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss index 947a5d3ae..88fa3ffa0 100644 --- a/app/javascript/flavours/glitch/styles/statuses.scss +++ b/app/javascript/flavours/glitch/styles/statuses.scss @@ -268,7 +268,7 @@ a.button.logo-button { border: 0; background: transparent; padding: 0; - padding-top: 8px; + padding-top: 16px; text-decoration: none; &:hover, diff --git a/app/javascript/flavours/vanilla/names.yml b/app/javascript/flavours/vanilla/names.yml index 0e3f43ed0..9b7fc189d 100644 --- a/app/javascript/flavours/vanilla/names.yml +++ b/app/javascript/flavours/vanilla/names.yml @@ -6,6 +6,14 @@ en: skins: vanilla: default: Default +cs: + flavours: + vanilla: + description: Standardní rozhraní Mastodonu. Některé funkce GlitchSoc v něm nejsou podporované. + name: Standardní Mastodon + skins: + vanilla: + default: Výchozí pl: flavours: vanilla: |