commit
c7e14e496b
@ -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 = () => {
|
||||
const TabsBar = React.createClass({
|
||||
|
||||
render () {
|
||||
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>
|
||||
<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;
|
@ -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