commit
3abb0f7bc7
@ -1,24 +1,31 @@ |
||||
FROM ruby:2.3.1 |
||||
FROM ruby:2.3.1-alpine |
||||
|
||||
ENV RAILS_ENV=production |
||||
ENV NODE_ENV=production |
||||
|
||||
RUN echo 'deb http://httpredir.debian.org/debian jessie-backports main contrib non-free' >> /etc/apt/sources.list |
||||
RUN curl -sL https://deb.nodesource.com/setup_4.x | bash - |
||||
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev libxml2-dev libxslt1-dev nodejs ffmpeg && rm -rf /var/lib/apt/lists/* |
||||
RUN npm install -g npm@3 && npm install -g yarn |
||||
RUN mkdir /mastodon |
||||
ENV RAILS_ENV=production \ |
||||
NODE_ENV=production |
||||
|
||||
WORKDIR /mastodon |
||||
|
||||
ADD Gemfile /mastodon/Gemfile |
||||
ADD Gemfile.lock /mastodon/Gemfile.lock |
||||
RUN bundle install --deployment --without test development |
||||
|
||||
ADD package.json /mastodon/package.json |
||||
ADD yarn.lock /mastodon/yarn.lock |
||||
RUN yarn |
||||
COPY . /mastodon |
||||
|
||||
ADD . /mastodon |
||||
RUN BUILD_DEPS=" \ |
||||
postgresql-dev \ |
||||
libxml2-dev \ |
||||
libxslt-dev \ |
||||
build-base" \ |
||||
&& apk -U upgrade && apk add \ |
||||
$BUILD_DEPS \ |
||||
nodejs \ |
||||
libpq \ |
||||
libxml2 \ |
||||
libxslt \ |
||||
ffmpeg \ |
||||
file \ |
||||
imagemagick \ |
||||
&& npm install -g npm@3 && npm install -g yarn \ |
||||
&& bundle install --deployment --without test development \ |
||||
&& yarn \ |
||||
&& npm cache clean \ |
||||
&& apk del $BUILD_DEPS \ |
||||
&& rm -rf /tmp/* /var/cache/apk/* |
||||
|
||||
VOLUME ["/mastodon/public/system", "/mastodon/public/assets"] |
||||
VOLUME /mastodon/public/system /mastodon/public/assets |
||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 59 KiB |
@ -1,82 +0,0 @@ |
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; |
||||
import IconButton from './icon_button'; |
||||
import { Motion, spring } from 'react-motion'; |
||||
import { injectIntl } from 'react-intl'; |
||||
|
||||
const overlayStyle = { |
||||
position: 'fixed', |
||||
top: '0', |
||||
left: '0', |
||||
width: '100%', |
||||
height: '100%', |
||||
background: 'rgba(0, 0, 0, 0.5)', |
||||
display: 'flex', |
||||
justifyContent: 'center', |
||||
alignContent: 'center', |
||||
flexDirection: 'row', |
||||
zIndex: '9999' |
||||
}; |
||||
|
||||
const dialogStyle = { |
||||
color: '#282c37', |
||||
boxShadow: '0 0 30px rgba(0, 0, 0, 0.8)', |
||||
margin: 'auto', |
||||
position: 'relative' |
||||
}; |
||||
|
||||
const closeStyle = { |
||||
position: 'absolute', |
||||
top: '4px', |
||||
right: '4px' |
||||
}; |
||||
|
||||
const Lightbox = React.createClass({ |
||||
|
||||
propTypes: { |
||||
isVisible: React.PropTypes.bool, |
||||
onOverlayClicked: React.PropTypes.func, |
||||
onCloseClicked: React.PropTypes.func, |
||||
intl: React.PropTypes.object.isRequired, |
||||
children: React.PropTypes.node |
||||
}, |
||||
|
||||
mixins: [PureRenderMixin], |
||||
|
||||
componentDidMount () { |
||||
this._listener = e => { |
||||
if (this.props.isVisible && e.key === 'Escape') { |
||||
this.props.onCloseClicked(); |
||||
} |
||||
}; |
||||
|
||||
window.addEventListener('keyup', this._listener); |
||||
}, |
||||
|
||||
componentWillUnmount () { |
||||
window.removeEventListener('keyup', this._listener); |
||||
}, |
||||
|
||||
stopPropagation (e) { |
||||
e.stopPropagation(); |
||||
}, |
||||
|
||||
render () { |
||||
const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props; |
||||
|
||||
return ( |
||||
<Motion defaultStyle={{ backgroundOpacity: 0, opacity: 0, y: -400 }} style={{ backgroundOpacity: spring(isVisible ? 50 : 0), opacity: isVisible ? spring(200) : 0, y: spring(isVisible ? 0 : -400, { stiffness: 150, damping: 12 }) }}> |
||||
{({ backgroundOpacity, opacity, y }) => |
||||
<div className='lightbox' style={{...overlayStyle, background: `rgba(0, 0, 0, ${backgroundOpacity / 100})`, display: Math.floor(backgroundOpacity) === 0 ? 'none' : 'flex', pointerEvents: !isVisible ? 'none' : 'auto'}} onClick={onOverlayClicked}> |
||||
<div style={{...dialogStyle, transform: `translateY(${y}px)`, opacity: opacity / 100 }} onClick={this.stopPropagation}> |
||||
<IconButton title={intl.formatMessage({ id: 'lightbox.close', defaultMessage: 'Close' })} icon='times' onClick={onCloseClicked} size={16} style={closeStyle} /> |
||||
{children} |
||||
</div> |
||||
</div> |
||||
} |
||||
</Motion> |
||||
); |
||||
} |
||||
|
||||
}); |
||||
|
||||
export default injectIntl(Lightbox); |
@ -1,44 +0,0 @@ |
||||
import { Link } from 'react-router'; |
||||
import { injectIntl, defineMessages } from 'react-intl'; |
||||
|
||||
const messages = defineMessages({ |
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, |
||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' }, |
||||
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, |
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, |
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' } |
||||
}); |
||||
|
||||
const Drawer = ({ children, withHeader, intl }) => { |
||||
let header = ''; |
||||
|
||||
if (withHeader) { |
||||
header = ( |
||||
<div className='drawer__header'> |
||||
<Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link> |
||||
<Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link> |
||||
<Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link> |
||||
<a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a> |
||||
<a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className='drawer'> |
||||
{header} |
||||
|
||||
<div className='drawer__inner'> |
||||
{children} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
Drawer.propTypes = { |
||||
withHeader: React.PropTypes.bool, |
||||
children: React.PropTypes.node, |
||||
intl: React.PropTypes.object |
||||
}; |
||||
|
||||
export default injectIntl(Drawer); |
@ -0,0 +1,68 @@ |
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; |
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; |
||||
import AccountContainer from '../../../containers/account_container'; |
||||
import StatusContainer from '../../../containers/status_container'; |
||||
import { Link } from 'react-router'; |
||||
|
||||
const SearchResults = React.createClass({ |
||||
|
||||
propTypes: { |
||||
results: ImmutablePropTypes.map.isRequired |
||||
}, |
||||
|
||||
mixins: [PureRenderMixin], |
||||
|
||||
render () { |
||||
const { results } = this.props; |
||||
|
||||
let accounts, statuses, hashtags; |
||||
let count = 0; |
||||
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) { |
||||
count += results.get('accounts').size; |
||||
accounts = ( |
||||
<div className='search-results__section'> |
||||
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) { |
||||
count += results.get('statuses').size; |
||||
statuses = ( |
||||
<div className='search-results__section'> |
||||
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) { |
||||
count += results.get('hashtags').size; |
||||
hashtags = ( |
||||
<div className='search-results__section'> |
||||
{results.get('hashtags').map(hashtag => |
||||
<Link className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}> |
||||
#{hashtag} |
||||
</Link> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className='search-results'> |
||||
<div className='search-results__header'> |
||||
<FormattedMessage id='search_results.total' defaultMessage='{count} {count, plural, one {result} other {results}}' values={{ count }} /> |
||||
</div> |
||||
|
||||
{accounts} |
||||
{statuses} |
||||
{hashtags} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
}); |
||||
|
||||
export default SearchResults; |
@ -1,31 +0,0 @@ |
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; |
||||
import { FormattedMessage } from 'react-intl'; |
||||
import Toggle from 'react-toggle'; |
||||
import Collapsable from '../../../components/collapsable'; |
||||
|
||||
const SensitiveToggle = React.createClass({ |
||||
|
||||
propTypes: { |
||||
hasMedia: React.PropTypes.bool, |
||||
isSensitive: React.PropTypes.bool, |
||||
onChange: React.PropTypes.func.isRequired |
||||
}, |
||||
|
||||
mixins: [PureRenderMixin], |
||||
|
||||
render () { |
||||
const { hasMedia, isSensitive, onChange } = this.props; |
||||
|
||||
return ( |
||||
<Collapsable isVisible={hasMedia} fullHeight={39.5}> |
||||
<label className='compose-form__label'> |
||||
<Toggle checked={isSensitive} onChange={onChange} /> |
||||
<span className='compose-form__label__text'><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark media as sensitive' /></span> |
||||
</label> |
||||
</Collapsable> |
||||
); |
||||
} |
||||
|
||||
}); |
||||
|
||||
export default SensitiveToggle; |
@ -1,27 +0,0 @@ |
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; |
||||
import { FormattedMessage } from 'react-intl'; |
||||
import Toggle from 'react-toggle'; |
||||
|
||||
const SpoilerToggle = React.createClass({ |
||||
|
||||
propTypes: { |
||||
isSpoiler: React.PropTypes.bool, |
||||
onChange: React.PropTypes.func.isRequired |
||||
}, |
||||
|
||||
mixins: [PureRenderMixin], |
||||
|
||||
render () { |
||||
const { isSpoiler, onChange } = this.props; |
||||
|
||||
return ( |
||||
<label className='compose-form__label with-border' style={{ marginTop: '10px' }}> |
||||
<Toggle checked={isSpoiler} onChange={onChange} /> |
||||
<span className='compose-form__label__text'><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span> |
||||
</label> |
||||
); |
||||
} |
||||
|
||||
}); |
||||
|
||||
export default SpoilerToggle; |
@ -0,0 +1,8 @@ |
||||
import { connect } from 'react-redux'; |
||||
import SearchResults from '../components/search_results'; |
||||
|
||||
const mapStateToProps = state => ({ |
||||
results: state.getIn(['search', 'results']) |
||||
}); |
||||
|
||||
export default connect(mapStateToProps)(SearchResults); |
@ -0,0 +1,133 @@ |
||||
import LoadingIndicator from '../../../components/loading_indicator'; |
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; |
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import ExtendedVideoPlayer from '../../../components/extended_video_player'; |
||||
import ImageLoader from 'react-imageloader'; |
||||
import { defineMessages, injectIntl } from 'react-intl'; |
||||
import IconButton from '../../../components/icon_button'; |
||||
|
||||
const messages = defineMessages({ |
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' } |
||||
}); |
||||
|
||||
const leftNavStyle = { |
||||
position: 'absolute', |
||||
background: 'rgba(0, 0, 0, 0.5)', |
||||
padding: '30px 15px', |
||||
cursor: 'pointer', |
||||
fontSize: '24px', |
||||
top: '0', |
||||
left: '-61px', |
||||
boxSizing: 'border-box', |
||||
height: '100%', |
||||
display: 'flex', |
||||
alignItems: 'center' |
||||
}; |
||||
|
||||
const rightNavStyle = { |
||||
position: 'absolute', |
||||
background: 'rgba(0, 0, 0, 0.5)', |
||||
padding: '30px 15px', |
||||
cursor: 'pointer', |
||||
fontSize: '24px', |
||||
top: '0', |
||||
right: '-61px', |
||||
boxSizing: 'border-box', |
||||
height: '100%', |
||||
display: 'flex', |
||||
alignItems: 'center' |
||||
}; |
||||
|
||||
const closeStyle = { |
||||
position: 'absolute', |
||||
top: '4px', |
||||
right: '4px' |
||||
}; |
||||
|
||||
const MediaModal = React.createClass({ |
||||
|
||||
propTypes: { |
||||
media: ImmutablePropTypes.list.isRequired, |
||||
index: React.PropTypes.number.isRequired, |
||||
onClose: React.PropTypes.func.isRequired, |
||||
intl: React.PropTypes.object.isRequired |
||||
}, |
||||
|
||||
getInitialState () { |
||||
return { |
||||
index: null |
||||
}; |
||||
}, |
||||
|
||||
mixins: [PureRenderMixin], |
||||
|
||||
handleNextClick () { |
||||
this.setState({ index: (this.getIndex() + 1) % this.props.media.size}); |
||||
}, |
||||
|
||||
handlePrevClick () { |
||||
this.setState({ index: (this.getIndex() - 1) % this.props.media.size}); |
||||
}, |
||||
|
||||
handleKeyUp (e) { |
||||
switch(e.key) { |
||||
case 'ArrowLeft': |
||||
this.handlePrevClick(); |
||||
break; |
||||
case 'ArrowRight': |
||||
this.handleNextClick(); |
||||
break; |
||||
} |
||||
}, |
||||
|
||||
componentDidMount () { |
||||
window.addEventListener('keyup', this.handleKeyUp, false); |
||||
}, |
||||
|
||||
componentWillUnmount () { |
||||
window.removeEventListener('keyup', this.handleKeyUp); |
||||
}, |
||||
|
||||
getIndex () { |
||||
return this.state.index !== null ? this.state.index : this.props.index; |
||||
}, |
||||
|
||||
render () { |
||||
const { media, intl, onClose } = this.props; |
||||
|
||||
const index = this.getIndex(); |
||||
const attachment = media.get(index); |
||||
const url = attachment.get('url'); |
||||
|
||||
let leftNav, rightNav, content; |
||||
|
||||
leftNav = rightNav = content = ''; |
||||
|
||||
if (media.size > 1) { |
||||
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; |
||||
rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; |
||||
} |
||||
|
||||
if (attachment.get('type') === 'image') { |
||||
content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />; |
||||
} else if (attachment.get('type') === 'gifv') { |
||||
content = <ExtendedVideoPlayer src={url} />; |
||||
} |
||||
|
||||
return ( |
||||
<div className='modal-root__modal media-modal'> |
||||
{leftNav} |
||||
|
||||
<div> |
||||
<IconButton title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} style={closeStyle} /> |
||||
{content} |
||||
</div> |
||||
|
||||
{rightNav} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
}); |
||||
|
||||
export default injectIntl(MediaModal); |
@ -0,0 +1,80 @@ |
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; |
||||
import MediaModal from './media_modal'; |
||||
import { TransitionMotion, spring } from 'react-motion'; |
||||
|
||||
const MODAL_COMPONENTS = { |
||||
'MEDIA': MediaModal |
||||
}; |
||||
|
||||
const ModalRoot = React.createClass({ |
||||
|
||||
propTypes: { |
||||
type: React.PropTypes.string, |
||||
props: React.PropTypes.object, |
||||
onClose: React.PropTypes.func.isRequired |
||||
}, |
||||
|
||||
mixins: [PureRenderMixin], |
||||
|
||||
handleKeyUp (e) { |
||||
if (e.key === 'Escape' && !!this.props.type) { |
||||
this.props.onClose(); |
||||
} |
||||
}, |
||||
|
||||
componentDidMount () { |
||||
window.addEventListener('keyup', this.handleKeyUp, false); |
||||
}, |
||||
|
||||
componentWillUnmount () { |
||||
window.removeEventListener('keyup', this.handleKeyUp); |
||||
}, |
||||
|
||||
willEnter () { |
||||
return { opacity: 0, scale: 0.98 }; |
||||
}, |
||||
|
||||
willLeave () { |
||||
return { opacity: spring(0), scale: spring(0.98) }; |
||||
}, |
||||
|
||||
render () { |
||||
const { type, props, onClose } = this.props; |
||||
const items = []; |
||||
|
||||
if (!!type) { |
||||
items.push({ |
||||
key: type, |
||||
data: { type, props }, |
||||
style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) } |
||||
}); |
||||
} |
||||
|
||||
return ( |
||||
<TransitionMotion |
||||
styles={items} |
||||
willEnter={this.willEnter} |
||||
willLeave={this.willLeave}> |
||||
{interpolatedStyles => |
||||
<div className='modal-root'> |
||||
{interpolatedStyles.map(({ key, data: { type, props }, style }) => { |
||||
const SpecificComponent = MODAL_COMPONENTS[type]; |
||||
|
||||
return ( |
||||
<div key={key}> |
||||
<div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} /> |
||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}> |
||||
<SpecificComponent {...props} onClose={onClose} /> |
||||
</div> |
||||
</div> |
||||
); |
||||
})} |
||||
</div> |
||||
} |
||||
</TransitionMotion> |
||||
); |
||||
} |
||||
|
||||
}); |
||||
|
||||
export default ModalRoot; |
@ -1,15 +1,23 @@ |
||||
import { Link } from 'react-router'; |
||||
import { FormattedMessage } from 'react-intl'; |
||||
|
||||
const TabsBar = () => { |
||||
return ( |
||||
<div className='tabs-bar'> |
||||
<Link className='tabs-bar__link' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /> <FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link> |
||||
<Link className='tabs-bar__link' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /> <FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link> |
||||
<Link className='tabs-bar__link' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /> <FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link> |
||||
<Link className='tabs-bar__link' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link> |
||||
</div> |
||||
); |
||||
}; |
||||
const TabsBar = React.createClass({ |
||||
|
||||
render () { |
||||
return ( |
||||
<div className='tabs-bar'> |
||||
<Link className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link> |
||||
<Link className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link> |
||||
<Link className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link> |
||||
|
||||
<Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local'><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></Link> |
||||
<Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public'><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></Link> |
||||
|
||||
<Link className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
}); |
||||
|
||||
export default TabsBar; |
||||
|
@ -1,170 +1,16 @@ |
||||
import { connect } from 'react-redux'; |
||||
import { |
||||
closeModal, |
||||
decreaseIndexInModal, |
||||
increaseIndexInModal |
||||
} from '../../../actions/modal'; |
||||
import Lightbox from '../../../components/lightbox'; |
||||
import ImageLoader from 'react-imageloader'; |
||||
import LoadingIndicator from '../../../components/loading_indicator'; |
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; |
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import ExtendedVideoPlayer from '../../../components/extended_video_player'; |
||||
import { closeModal } from '../../../actions/modal'; |
||||
import ModalRoot from '../components/modal_root'; |
||||
|
||||
const mapStateToProps = state => ({ |
||||
media: state.getIn(['modal', 'media']), |
||||
index: state.getIn(['modal', 'index']), |
||||
isVisible: state.getIn(['modal', 'open']) |
||||
type: state.get('modal').modalType, |
||||
props: state.get('modal').modalProps |
||||
}); |
||||
|
||||
const mapDispatchToProps = dispatch => ({ |
||||
onCloseClicked () { |
||||
onClose () { |
||||
dispatch(closeModal()); |
||||
}, |
||||
|
||||
onOverlayClicked () { |
||||
dispatch(closeModal()); |
||||
}, |
||||
|
||||
onNextClicked () { |
||||
dispatch(increaseIndexInModal()); |
||||
}, |
||||
|
||||
onPrevClicked () { |
||||
dispatch(decreaseIndexInModal()); |
||||
} |
||||
}); |
||||
|
||||
const imageStyle = { |
||||
display: 'block', |
||||
maxWidth: '80vw', |
||||
maxHeight: '80vh' |
||||
}; |
||||
|
||||
const loadingStyle = { |
||||
width: '400px', |
||||
paddingBottom: '120px' |
||||
}; |
||||
|
||||
const preloader = () => ( |
||||
<div className='modal-container--preloader' style={loadingStyle}> |
||||
<LoadingIndicator /> |
||||
</div> |
||||
); |
||||
|
||||
const leftNavStyle = { |
||||
position: 'absolute', |
||||
background: 'rgba(0, 0, 0, 0.5)', |
||||
padding: '30px 15px', |
||||
cursor: 'pointer', |
||||
fontSize: '24px', |
||||
top: '0', |
||||
left: '-61px', |
||||
boxSizing: 'border-box', |
||||
height: '100%', |
||||
display: 'flex', |
||||
alignItems: 'center' |
||||
}; |
||||
|
||||
const rightNavStyle = { |
||||
position: 'absolute', |
||||
background: 'rgba(0, 0, 0, 0.5)', |
||||
padding: '30px 15px', |
||||
cursor: 'pointer', |
||||
fontSize: '24px', |
||||
top: '0', |
||||
right: '-61px', |
||||
boxSizing: 'border-box', |
||||
height: '100%', |
||||
display: 'flex', |
||||
alignItems: 'center' |
||||
}; |
||||
|
||||
const Modal = React.createClass({ |
||||
|
||||
propTypes: { |
||||
media: ImmutablePropTypes.list, |
||||
index: React.PropTypes.number.isRequired, |
||||
isVisible: React.PropTypes.bool, |
||||
onCloseClicked: React.PropTypes.func, |
||||
onOverlayClicked: React.PropTypes.func, |
||||
onNextClicked: React.PropTypes.func, |
||||
onPrevClicked: React.PropTypes.func |
||||
}, |
||||
|
||||
mixins: [PureRenderMixin], |
||||
|
||||
handleNextClick () { |
||||
this.props.onNextClicked(); |
||||
}, |
||||
|
||||
handlePrevClick () { |
||||
this.props.onPrevClicked(); |
||||
}, |
||||
|
||||
componentDidMount () { |
||||
this._listener = e => { |
||||
if (!this.props.isVisible) { |
||||
return; |
||||
} |
||||
|
||||
switch(e.key) { |
||||
case 'ArrowLeft': |
||||
this.props.onPrevClicked(); |
||||
break; |
||||
case 'ArrowRight': |
||||
this.props.onNextClicked(); |
||||
break; |
||||
} |
||||
}; |
||||
|
||||
window.addEventListener('keyup', this._listener); |
||||
}, |
||||
|
||||
componentWillUnmount () { |
||||
window.removeEventListener('keyup', this._listener); |
||||
}, |
||||
|
||||
render () { |
||||
const { media, index, ...other } = this.props; |
||||
|
||||
if (!media) { |
||||
return null; |
||||
} |
||||
|
||||
const attachment = media.get(index); |
||||
const url = attachment.get('url'); |
||||
|
||||
let leftNav, rightNav, content; |
||||
|
||||
leftNav = rightNav = content = ''; |
||||
|
||||
if (media.size > 1) { |
||||
leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; |
||||
rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; |
||||
} |
||||
|
||||
if (attachment.get('type') === 'image') { |
||||
content = ( |
||||
<ImageLoader |
||||
src={url} |
||||
preloader={preloader} |
||||
imgProps={{ style: imageStyle }} |
||||
/> |
||||
); |
||||
} else if (attachment.get('type') === 'gifv') { |
||||
content = <ExtendedVideoPlayer src={url} />; |
||||
} |
||||
|
||||
return ( |
||||
<Lightbox {...other}> |
||||
{leftNav} |
||||
{content} |
||||
{rightNav} |
||||
</Lightbox> |
||||
); |
||||
} |
||||
|
||||
}); |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Modal); |
||||
export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot); |
||||
|
@ -0,0 +1,68 @@ |
||||
const fi = { |
||||
"column_back_button.label": "Takaisin", |
||||
"lightbox.close": "Sulje", |
||||
"loading_indicator.label": "Ladataan...", |
||||
"status.mention": "Mainitse @{name}", |
||||
"status.delete": "Poista", |
||||
"status.reply": "Vastaa", |
||||
"status.reblog": "Boostaa", |
||||
"status.favourite": "Tykkää", |
||||
"status.reblogged_by": "{name} boostattu", |
||||
"status.sensitive_warning": "Arkaluontoista sisältöä", |
||||
"status.sensitive_toggle": "Klikkaa nähdäksesi", |
||||
"video_player.toggle_sound": "Äänet päälle/pois", |
||||
"account.mention": "Mainitse @{name}", |
||||
"account.edit_profile": "Muokkaa", |
||||
"account.unblock": "Salli @{name}", |
||||
"account.unfollow": "Lopeta seuraaminen", |
||||
"account.block": "Estä @{name}", |
||||
"account.follow": "Seuraa", |
||||
"account.posts": "Postit", |
||||
"account.follows": "Seuraa", |
||||
"account.followers": "Seuraajia", |
||||
"account.follows_you": "Seuraa sinua", |
||||
"account.requested": "Odottaa hyväksyntää", |
||||
"getting_started.heading": "Päästä alkuun", |
||||
"getting_started.about_addressing": "Voit seurata ihmisiä jos tiedät heidän käyttäjänimensä ja domainin missä he ovat syöttämällä e-mail-esque osoitteen Etsi kenttään.", |
||||
"getting_started.about_shortcuts": "Jos etsimäsi henkilö on samassa domainissa kuin sinä, pelkkä käyttäjänimi kelpaa. Sama pätee kun mainitset ihmisiä statuksessasi", |
||||
"getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia githubissa {github}. {apps}.", |
||||
"column.home": "Koti", |
||||
"column.community": "Paikallinen aikajana", |
||||
"column.public": "Yhdistetty aikajana", |
||||
"column.notifications": "Ilmoitukset", |
||||
"tabs_bar.compose": "Luo", |
||||
"tabs_bar.home": "Koti", |
||||
"tabs_bar.mentions": "Maininnat", |
||||
"tabs_bar.public": "Yleinen aikajana", |
||||
"tabs_bar.notifications": "Ilmoitukset", |
||||
"compose_form.placeholder": "Mitä sinulla on mielessä?", |
||||
"compose_form.publish": "Toot", |
||||
"compose_form.sensitive": "Merkitse media herkäksi", |
||||
"compose_form.spoiler": "Piiloita teksti varoituksen taakse", |
||||
"compose_form.private": "Merkitse yksityiseksi", |
||||
"compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.", |
||||
"compose_form.unlisted": "Älä näytä julkisilla aikajanoilla", |
||||
"navigation_bar.edit_profile": "Muokkaa profiilia", |
||||
"navigation_bar.preferences": "Ominaisuudet", |
||||
"navigation_bar.community_timeline": "Paikallinen aikajana", |
||||
"navigation_bar.public_timeline": "Yleinen aikajana", |
||||
"navigation_bar.logout": "Kirjaudu ulos", |
||||
"reply_indicator.cancel": "Peruuta", |
||||
"search.placeholder": "Hae", |
||||
"search.account": "Tili", |
||||
"search.hashtag": "Hashtag", |
||||
"upload_button.label": "Lisää mediaa", |
||||
"upload_form.undo": "Peru", |
||||
"notification.follow": "{name} seurasi sinua", |
||||
"notification.favourite": "{name} tykkäsi statuksestasi", |
||||
"notification.reblog": "{name} boostasi statustasi", |
||||
"notification.mention": "{name} mainitsi sinut", |
||||
"notifications.column_settings.alert": "Työpöytä ilmoitukset", |
||||
"notifications.column_settings.show": "Näytä sarakkeessa", |
||||
"notifications.column_settings.follow": "Uusia seuraajia:", |
||||
"notifications.column_settings.favourite": "Tykkäyksiä:", |
||||
"notifications.column_settings.mention": "Mainintoja:", |
||||
"notifications.column_settings.reblog": "Boosteja:", |
||||
}; |
||||
|
||||
export default fi; |
@ -1,68 +1,91 @@ |
||||
const fr = { |
||||
"account.block": "Bloquer", |
||||
"column_back_button.label": "Retour", |
||||
"lightbox.close": "Fermer", |
||||
"loading_indicator.label": "Chargement…", |
||||
"status.mention": "Mentionner", |
||||
"status.delete": "Effacer", |
||||
"status.reply": "Répondre", |
||||
"status.reblog": "Partager", |
||||
"status.favourite": "Ajouter aux favoris", |
||||
"status.reblogged_by": "{name} a partagé :", |
||||
"status.sensitive_warning": "Contenu délicat", |
||||
"status.sensitive_toggle": "Cliquer pour dévoiler", |
||||
"video_player.toggle_sound": "Mettre/Couper le son", |
||||
"account.mention": "Mentionner", |
||||
"account.edit_profile": "Modifier le profil", |
||||
"account.followers": "Abonnés", |
||||
"account.follows": "Abonnements", |
||||
"account.unblock": "Débloquer", |
||||
"account.unfollow": "Ne plus suivre", |
||||
"account.block": "Bloquer", |
||||
"account.mute": "Masquer", |
||||
"account.unmute": "Ne plus masquer", |
||||
"account.follow": "Suivre", |
||||
"account.follows_you": "Vous suit", |
||||
"account.mention": "Mentionner", |
||||
"account.posts": "Statuts", |
||||
"account.follows": "Abonnements", |
||||
"account.followers": "Abonnés", |
||||
"account.follows_you": "Vous suit", |
||||
"account.requested": "Invitation envoyée", |
||||
"account.unblock": "Débloquer", |
||||
"account.unfollow": "Ne plus suivre", |
||||
"column_back_button.label": "Retour", |
||||
"account.report": "Signaler", |
||||
"account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.", |
||||
"getting_started.heading": "Pour commencer", |
||||
"getting_started.about_addressing": "Vous pouvez suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.", |
||||
"getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.", |
||||
"getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social", |
||||
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", |
||||
"column.home": "Accueil", |
||||
"column.mentions": "Mentions", |
||||
"column.community": "Fil public local", |
||||
"column.public": "Fil public global", |
||||
"column.notifications": "Notifications", |
||||
"column.public": "Fil public", |
||||
"column.blocks": "Utilisateurs bloqués", |
||||
"column.favourites": "Favoris", |
||||
"tabs_bar.compose": "Composer", |
||||
"tabs_bar.home": "Accueil", |
||||
"tabs_bar.mentions": "Mentions", |
||||
"tabs_bar.public": "Fil public global", |
||||
"tabs_bar.notifications": "Notifications", |
||||
"compose_form.placeholder": "Qu’avez-vous en tête ?", |
||||
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ?", |
||||
"compose_form.private": "Rendre privé", |
||||
"compose_form.publish": "Pouet ", |
||||
"compose_form.sensitive": "Marquer le média comme délicat", |
||||
"compose_form.spoiler": "Masque le texte par un avertissement", |
||||
"compose_form.unlisted": "Ne pas afficher dans le fil public", |
||||
"getting_started.about_addressing": "Vous pouvez vous suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.", |
||||
"getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social", |
||||
"getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.", |
||||
"getting_started.heading": "Pour commencer", |
||||
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", |
||||
"lightbox.close": "Fermer", |
||||
"loading_indicator.label": "Chargement…", |
||||
"compose_form.spoiler": "Masquer le texte par un avertissement", |
||||
"compose_form.private": "Rendre privé", |
||||
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues", |
||||
"compose_form.unlisted": "Ne pas afficher dans les fils publics", |
||||
"emoji_button.label": "Insérer un emoji", |
||||
"navigation_bar.edit_profile": "Modifier le profil", |
||||
"navigation_bar.logout": "Déconnexion", |
||||
"navigation_bar.preferences": "Préférences", |
||||
"navigation_bar.public_timeline": "Fil public", |
||||
"navigation_bar.community_timeline": "Fil public local", |
||||
"navigation_bar.public_timeline": "Fil public global", |
||||
"navigation_bar.blocks": "Utilisateurs bloqués", |
||||
"navigation_bar.favourites": "Favoris", |
||||
"navigation_bar.info": "Plus d'informations", |
||||
"notification.favourite": "{name} a ajouté à ses favoris :", |
||||
"navigation_bar.logout": "Déconnexion", |
||||
"reply_indicator.cancel": "Annuler", |
||||
"search.placeholder": "Chercher", |
||||
"search.account": "Compte", |
||||
"search.hashtag": "Mot-clé", |
||||
"search_results.total": "{count} {count, plural, one {résultat} other {résultats}}", |
||||
"upload_button.label": "Joindre un média", |
||||
"upload_form.undo": "Annuler", |
||||
"notification.follow": "{name} vous suit.", |
||||
"notification.mention": "{name} vous a mentionné⋅e :", |
||||
"notification.favourite": "{name} a ajouté à ses favoris :", |
||||
"notification.reblog": "{name} a partagé votre statut :", |
||||
"notification.mention": "{name} vous a mentionné⋅e :", |
||||
"notifications.column_settings.alert": "Notifications locales", |
||||
"notifications.column_settings.favourite": "Favoris :", |
||||
"notifications.column_settings.show": "Afficher dans la colonne", |
||||
"notifications.column_settings.follow": "Nouveaux abonnés :", |
||||
"notifications.column_settings.favourite": "Favoris :", |
||||
"notifications.column_settings.mention": "Mentions :", |
||||
"notifications.column_settings.reblog": "Partages :", |
||||
"notifications.column_settings.show": "Afficher dans la colonne", |
||||
"reply_indicator.cancel": "Annuler", |
||||
"search.account": "Compte", |
||||
"search.hashtag": "Mot-clé", |
||||
"search.placeholder": "Chercher", |
||||
"status.delete": "Effacer", |
||||
"status.favourite": "Ajouter aux favoris", |
||||
"status.mention": "Mentionner", |
||||
"status.reblogged_by": "{name} a partagé :", |
||||
"status.reblog": "Partager", |
||||
"status.reply": "Répondre", |
||||
"status.sensitive_toggle": "Cliquer pour dévoiler", |
||||
"status.sensitive_warning": "Contenu délicat", |
||||
"tabs_bar.compose": "Composer", |
||||
"tabs_bar.home": "Accueil", |
||||
"tabs_bar.mentions": "Mentions", |
||||
"tabs_bar.notifications": "Notifications", |
||||
"tabs_bar.public": "Public", |
||||
"upload_button.label": "Joindre un média", |
||||
"upload_form.undo": "Annuler", |
||||
"video_player.toggle_sound": "Mettre/Couper le son", |
||||
"privacy.public.short": "Public", |
||||
"privacy.public.long": "Afficher dans les fils publics", |
||||
"privacy.unlisted.short": "Non-listé", |
||||
"privacy.unlisted.long": "Ne pas afficher dans les fils publics", |
||||
"privacy.private.short": "Privé", |
||||
"privacy.private.long": "N’afficher que pour vos abonné⋅e⋅s", |
||||
"privacy.direct.short": "Direct", |
||||
"privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s", |
||||
"privacy.change": "Ajuster la confidentialité du message", |
||||
}; |
||||
|
||||
export default fr; |
||||
|
@ -0,0 +1,34 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class Settings::ImportsController < ApplicationController |
||||
layout 'admin' |
||||
|
||||
before_action :authenticate_user! |
||||
before_action :set_account |
||||
|
||||
def show |
||||
@import = Import.new |
||||
end |
||||
|
||||
def create |
||||
@import = Import.new(import_params) |
||||
@import.account = @account |
||||
|
||||
if @import.save |
||||
ImportWorker.perform_async(@import.id) |
||||
redirect_to settings_import_path, notice: I18n.t('imports.success') |
||||
else |
||||
render action: :show |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def set_account |
||||
@account = current_user.account |
||||
end |
||||
|
||||
def import_params |
||||
params.require(:import).permit(:data, :type) |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class Import < ApplicationRecord |
||||
self.inheritance_column = false |
||||
|
||||
enum type: [:following, :blocking] |
||||
|
||||
belongs_to :account |
||||
|
||||
FILE_TYPES = ['text/plain', 'text/csv'].freeze |
||||
|
||||
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET'] |
||||
validates_attachment_content_type :data, content_type: FILE_TYPES |
||||
end |
@ -0,0 +1,18 @@ |
||||
- content_for :page_title do |
||||
New domain block |
||||
|
||||
= simple_form_for @domain_block, url: admin_domain_blocks_path do |f| |
||||
= render 'shared/error_messages', object: @domain_block |
||||
|
||||
%p.hint The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts. |
||||
|
||||
= f.input :domain, placeholder: 'Domain' |
||||
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false |
||||
|
||||
%p.hint |
||||
%strong Silence |
||||
will make the account's posts invisible to anyone who isn't following them. |
||||
%strong Suspend |
||||
will remove all of the account's content, media, and profile data. |
||||
.actions |
||||
= f.button :button, 'Create block', type: :submit |
@ -0,0 +1,11 @@ |
||||
- content_for :page_title do |
||||
= t('settings.import') |
||||
|
||||
%p.hint= t('imports.preface') |
||||
|
||||
= simple_form_for @import, url: settings_import_path do |f| |
||||
= f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") } |
||||
= f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data') |
||||
|
||||
.actions |
||||
= f.button :button, t('imports.upload'), type: :submit |
@ -0,0 +1,11 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class DomainBlockWorker |
||||
include Sidekiq::Worker |
||||
|
||||
def perform(domain_block_id) |
||||
BlockDomainService.new.call(DomainBlock.find(domain_block_id)) |
||||
rescue ActiveRecord::RecordNotFound |
||||
true |
||||
end |
||||
end |
@ -0,0 +1,54 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
require 'csv' |
||||
|
||||
class ImportWorker |
||||
include Sidekiq::Worker |
||||
|
||||
sidekiq_options queue: 'pull', retry: false |
||||
|
||||
def perform(import_id) |
||||
import = Import.find(import_id) |
||||
|
||||
case import.type |
||||
when 'blocking' |
||||
process_blocks(import) |
||||
when 'following' |
||||
process_follows(import) |
||||
end |
||||
|
||||
import.destroy |
||||
end |
||||
|
||||
private |
||||
|
||||
def process_blocks(import) |
||||
from_account = import.account |
||||
|
||||
CSV.foreach(import.data.path) do |row| |
||||
next if row.size != 1 |
||||
|
||||
begin |
||||
target_account = FollowRemoteAccountService.new.call(row[0]) |
||||
next if target_account.nil? |
||||
BlockService.new.call(from_account, target_account) |
||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError |
||||
next |
||||
end |
||||
end |
||||
end |
||||
|
||||
def process_follows(import) |
||||
from_account = import.account |
||||
|
||||
CSV.foreach(import.data.path) do |row| |
||||
next if row.size != 1 |
||||
|
||||
begin |
||||
FollowService.new.call(from_account, row[0]) |
||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError |
||||
next |
||||
end |
||||
end |
||||
end |
||||
end |
@ -1,4 +1,6 @@ |
||||
Rack::Timeout::Logger.disable |
||||
Rack::Timeout.service_timeout = false |
||||
|
||||
if Rails.env.production? |
||||
Rack::Timeout.service_timeout = 90 |
||||
Rack::Timeout::Logger.disable |
||||
end |
||||
|
@ -0,0 +1,61 @@ |
||||
--- |
||||
fi: |
||||
devise: |
||||
confirmations: |
||||
confirmed: Sähköpostisi on onnistuneesti vahvistettu. |
||||
send_instructions: Saat kohta sähköpostiisi ohjeet kuinka voit aktivoida tilisi. |
||||
send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet sen varmentamiseen. |
||||
failure: |
||||
already_authenticated: Olet jo kirjautunut sisään. |
||||
inactive: Tiliäsi ei ole viellä aktivoitu. |
||||
invalid: Virheellinen %{authentication_keys} tai salasana. |
||||
last_attempt: Sinulla on yksi yritys jäljellä tai tili lukitaan. |
||||
locked: Tili on lukittu. |
||||
not_found_in_database: Virheellinen %{authentication_keys} tai salasana. |
||||
timeout: Sessiosi on umpeutunut. Kirjaudu sisään jatkaaksesi. |
||||
unauthenticated: Sinun tarvitsee kirjautua sisään tai rekisteröityä jatkaaksesi. |
||||
unconfirmed: Sinun tarvitsee varmentaa sähköpostisi jatkaaksesi. |
||||
mailer: |
||||
confirmation_instructions: |
||||
subject: 'Mastodon: Varmistus ohjeet' |
||||
password_change: |
||||
subject: 'Mastodon: Salasana vaihdettu' |
||||
reset_password_instructions: |
||||
subject: 'Mastodon: Salasanan vaihto ohjeet' |
||||
unlock_instructions: |
||||
subject: 'Mastodon: Avauksen ohjeet' |
||||
omniauth_callbacks: |
||||
failure: Varmennus %{kind} epäonnistui koska "%{reason}". |
||||
success: Onnistuneesti varmennettu %{kind} tilillä. |
||||
passwords: |
||||
no_token: Et pääse tälle sivulle ilman salasanan vaihto sähköpostia. Jos tulet tämmöisestä postista, varmista että sinulla on täydellinen URL. |
||||
send_instructions: Saat sähköpostitse ohjeet salasanan palautukseen muutaman minuutin kuluessa. |
||||
send_paranoid_instructions: Jos sähköpostisi on meidän tietokannassa, saat pian ohjeet salasanan palautukseen. |
||||
updated: Salasanasi vaihdettu onnistuneesti. Olet nyt kirjautunut sisään. |
||||
updated_not_active: Salasanasi vaihdettu onnistuneesti. |
||||
registrations: |
||||
destroyed: Näkemiin! Tilisi on onnistuneesti peruttu. Toivottavasti näemme joskus uudestaan. |
||||
signed_up: Tervetuloa! Rekisteröitymisesi onnistu. |
||||
signed_up_but_inactive: Olet onnistuneesti rekisteröitynyt, mutta emme voi kirjata sinua sisään koska tiliäsi ei ole viellä aktivoitu. |
||||
signed_up_but_locked: Olet onnistuneesti rekisteröitynyt, mutta emme voi kirjata sinua sisään koska tilisi on lukittu. |
||||
signed_up_but_unconfirmed: Varmistuslinkki on lähetty sähköpostiisi. Seuraa sitä jotta tilisi voidaan aktivoida. |
||||
update_needs_confirmation: Tilisi on onnistuneesti päivitetty, mutta meidän tarvitsee vahvistaa sinun uusi sähköpostisi. Tarkista sähköpostisi ja seuraa viestissä tullutta linkkiä varmistaaksesi uuden osoitteen.. |
||||
updated: Tilisi on onnistuneesti päivitetty. |
||||
sessions: |
||||
already_signed_out: Ulos kirjautuminen onnistui. |
||||
signed_in: Sisäänkirjautuminen onnistui. |
||||
signed_out: Ulos kirjautuminen onnistui. |
||||
unlocks: |
||||
send_instructions: Saat sähköpostiisi pian ohjeet, jolla voit avata tilisi uudestaan. |
||||
send_paranoid_instructions: Jos tilisi on olemassa, saat sähköpostiisi pian ohjeet tilisi avaamiseen. |
||||
unlocked: Tilisi on avattu onnistuneesti. Kirjaudu normaalisti sisään. |
||||
errors: |
||||
messages: |
||||
already_confirmed: on jo varmistettu. Yritä kirjautua sisään |
||||
confirmation_period_expired: pitää varmistaa %{period} sisällä, ole hyvä ja pyydä uusi |
||||
expired: on erääntynyt, ole hyvä ja pyydä uusi |
||||
not_found: ei löydy |
||||
not_locked: ei ollut lukittu |
||||
not_saved: |
||||
one: '1 virhe esti %{resource} tallennuksen:' |
||||
other: "%{count} virhettä esti %{resource} tallennuksen:" |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue