diff options
Diffstat (limited to 'app')
38 files changed, 342 insertions, 113 deletions
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx index 1b3cc60dc..88e91c356 100644 --- a/app/assets/javascripts/components/actions/compose.jsx +++ b/app/assets/javascripts/components/actions/compose.jsx @@ -2,6 +2,8 @@ import api from '../api'; import { updateTimeline } from './timelines'; +import * as emojione from 'emojione'; + export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; @@ -72,9 +74,8 @@ export function mentionCompose(account, router) { export function submitCompose() { return function (dispatch, getState) { dispatch(submitComposeRequest()); - api(getState).post('/api/v1/statuses', { - status: getState().getIn(['compose', 'text'], ''), + status: emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']), diff --git a/app/assets/javascripts/components/actions/reports.jsx b/app/assets/javascripts/components/actions/reports.jsx index 2c1245dc4..094670d62 100644 --- a/app/assets/javascripts/components/actions/reports.jsx +++ b/app/assets/javascripts/components/actions/reports.jsx @@ -7,7 +7,8 @@ export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; -export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; +export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; +export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; export function initReport(account, status) { return { @@ -62,3 +63,10 @@ export function submitReportFail(error) { error }; }; + +export function changeReportComment(comment) { + return { + type: REPORT_COMMENT_CHANGE, + comment + }; +}; diff --git a/app/assets/javascripts/components/components/extended_video_player.jsx b/app/assets/javascripts/components/components/extended_video_player.jsx index 66e5dee16..a64515583 100644 --- a/app/assets/javascripts/components/components/extended_video_player.jsx +++ b/app/assets/javascripts/components/components/extended_video_player.jsx @@ -3,15 +3,43 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; const ExtendedVideoPlayer = React.createClass({ propTypes: { - src: React.PropTypes.string.isRequired + src: React.PropTypes.string.isRequired, + time: React.PropTypes.number, + controls: React.PropTypes.bool.isRequired, + muted: React.PropTypes.bool.isRequired }, mixins: [PureRenderMixin], + handleLoadedData () { + if (this.props.time) { + this.video.currentTime = this.props.time; + } + }, + + componentDidMount () { + this.video.addEventListener('loadeddata', this.handleLoadedData); + }, + + componentWillUnmount () { + this.video.removeEventListener('loadeddata', this.handleLoadedData); + }, + + setRef (c) { + this.video = c; + }, + render () { return ( - <div> - <video src={this.props.src} autoPlay muted loop /> + <div className='extended-video-player'> + <video + ref={this.setRef} + src={this.props.src} + autoPlay + muted={this.props.muted} + controls={this.props.controls} + loop={!this.props.controls} + /> </div> ); }, diff --git a/app/assets/javascripts/components/components/icon_button.jsx b/app/assets/javascripts/components/components/icon_button.jsx index 33835f9a0..0c683db5d 100644 --- a/app/assets/javascripts/components/components/icon_button.jsx +++ b/app/assets/javascripts/components/components/icon_button.jsx @@ -13,7 +13,8 @@ const IconButton = React.createClass({ activeStyle: React.PropTypes.object, disabled: React.PropTypes.bool, inverted: React.PropTypes.bool, - animate: React.PropTypes.bool + animate: React.PropTypes.bool, + overlay: React.PropTypes.bool }, getDefaultProps () { @@ -21,7 +22,8 @@ const IconButton = React.createClass({ size: 18, active: false, disabled: false, - animate: false + animate: false, + overlay: false }; }, @@ -39,7 +41,7 @@ const IconButton = React.createClass({ let style = { fontSize: `${this.props.size}px`, width: `${this.props.size * 1.28571429}px`, - height: `${this.props.size}px`, + height: `${this.props.size * 1.28571429}px`, lineHeight: `${this.props.size}px`, ...this.props.style }; @@ -48,13 +50,31 @@ const IconButton = React.createClass({ style = { ...style, ...this.props.activeStyle }; } + const classes = ['icon-button']; + + if (this.props.active) { + classes.push('active'); + } + + if (this.props.disabled) { + classes.push('disabled'); + } + + if (this.props.inverted) { + classes.push('inverted'); + } + + if (this.props.overlay) { + classes.push('overlayed'); + } + return ( <Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}> {({ rotate }) => <button aria-label={this.props.title} title={this.props.title} - className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''} ${this.props.inverted ? 'inverted' : ''}`} + className={classes.join(' ')} onClick={this.handleClick} style={style}> <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx index 72b5e977f..10b7d525b 100644 --- a/app/assets/javascripts/components/components/media_gallery.jsx +++ b/app/assets/javascripts/components/components/media_gallery.jsx @@ -39,8 +39,8 @@ const spoilerSubSpanStyle = { const spoilerButtonStyle = { position: 'absolute', - top: '6px', - left: '8px', + top: '4px', + left: '4px', zIndex: '100' }; @@ -232,8 +232,8 @@ const MediaGallery = React.createClass({ return ( <div style={{ ...outerStyle, height: `${this.props.height}px` }}> - <div style={spoilerButtonStyle}> - <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} /> + <div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}> + <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> </div> {children} diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx index c4d5f829b..d2d2aaf20 100644 --- a/app/assets/javascripts/components/components/status.jsx +++ b/app/assets/javascripts/components/components/status.jsx @@ -25,6 +25,7 @@ const Status = React.createClass({ onReblog: React.PropTypes.func, onDelete: React.PropTypes.func, onOpenMedia: React.PropTypes.func, + onOpenVideo: React.PropTypes.func, onBlock: React.PropTypes.func, me: React.PropTypes.number, boostModal: React.PropTypes.bool, @@ -76,7 +77,7 @@ const Status = React.createClass({ if (status.get('media_attachments').size > 0 && !this.props.muted) { if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />; + media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />; } else { media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />; } diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 9cf03bb32..c7eefcaf5 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -92,7 +92,7 @@ const StatusContent = React.createClass({ const { status } = this.props; const { hidden } = this.state; - const content = { __html: emojify(status.get('content')).replace(/\n/g, '') }; + const content = { __html: emojify(status.get('content')) }; const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; const directionStyle = { direction: 'ltr' }; diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx index ab21ca9cd..dce276c75 100644 --- a/app/assets/javascripts/components/components/video_player.jsx +++ b/app/assets/javascripts/components/components/video_player.jsx @@ -6,7 +6,8 @@ import { isIOS } from '../is_mobile'; const messages = defineMessages({ toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' }, - toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' } + toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }, + expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' } }); const videoStyle = { @@ -21,8 +22,8 @@ const videoStyle = { const muteStyle = { position: 'absolute', - top: '10px', - right: '10px', + top: '4px', + right: '4px', color: 'white', textShadow: "0px 1px 1px black, 1px 0px 1px black", opacity: '0.8', @@ -54,8 +55,17 @@ const spoilerSubSpanStyle = { const spoilerButtonStyle = { position: 'absolute', - top: '6px', - left: '8px', + top: '4px', + left: '4px', + color: 'white', + textShadow: "0px 1px 1px black, 1px 0px 1px black", + zIndex: '100' +}; + +const expandButtonStyle = { + position: 'absolute', + bottom: '4px', + right: '4px', color: 'white', textShadow: "0px 1px 1px black, 1px 0px 1px black", zIndex: '100' @@ -68,7 +78,8 @@ const VideoPlayer = React.createClass({ height: React.PropTypes.number, sensitive: React.PropTypes.bool, intl: React.PropTypes.object.isRequired, - autoplay: React.PropTypes.bool + autoplay: React.PropTypes.bool, + onOpenVideo: React.PropTypes.func.isRequired }, getDefaultProps () { @@ -116,6 +127,11 @@ const VideoPlayer = React.createClass({ }); }, + handleExpand () { + this.video.pause(); + this.props.onOpenVideo(this.props.media, this.video.currentTime); + }, + setRef (c) { this.video = c; }, @@ -154,8 +170,14 @@ const VideoPlayer = React.createClass({ const { media, intl, width, height, sensitive, autoplay } = this.props; let spoilerButton = ( - <div style={spoilerButtonStyle} > - <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} /> + <div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} > + <IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} /> + </div> + ); + + let expandButton = ( + <div style={expandButtonStyle} > + <IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} /> </div> ); @@ -164,7 +186,7 @@ const VideoPlayer = React.createClass({ if (this.state.hasAudio) { muteButton = ( <div style={muteStyle}> - <IconButton title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} /> + <IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} /> </div> ); } @@ -202,6 +224,7 @@ const VideoPlayer = React.createClass({ <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}> {spoilerButton} {muteButton} + {expandButton} <video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} /> </div> ); diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index b9086de42..5cd727822 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -48,6 +48,7 @@ import fr from 'react-intl/locale-data/fr'; import hu from 'react-intl/locale-data/hu'; import ja from 'react-intl/locale-data/ja'; import pt from 'react-intl/locale-data/pt'; +import nl from 'react-intl/locale-data/nl'; import no from 'react-intl/locale-data/no'; import ru from 'react-intl/locale-data/ru'; import uk from 'react-intl/locale-data/uk'; @@ -76,6 +77,7 @@ addLocaleData([ ...hu, ...ja, ...pt, + ...nl, ...no, ...ru, ...uk, diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx index fedf80fbf..f704ac722 100644 --- a/app/assets/javascripts/components/containers/status_container.jsx +++ b/app/assets/javascripts/components/containers/status_container.jsx @@ -75,6 +75,10 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(openModal('MEDIA', { media, index })); }, + onOpenVideo (media, time) { + dispatch(openModal('VIDEO', { media, time })); + }, + onBlock (account) { dispatch(blockAccount(account.get('id'))); }, diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx index 4e5fe1263..2edf98292 100644 --- a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx +++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx @@ -27,8 +27,9 @@ const ColumnSettings = React.createClass({ propTypes: { settings: ImmutablePropTypes.map.isRequired, + intl: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired, - onSave: React.PropTypes.func.isRequired + onSave: React.PropTypes.func.isRequired, }, mixins: [PureRenderMixin], diff --git a/app/assets/javascripts/components/features/report/index.jsx b/app/assets/javascripts/components/features/report/index.jsx index 3177d28b1..fc8e543c5 100644 --- a/app/assets/javascripts/components/features/report/index.jsx +++ b/app/assets/javascripts/components/features/report/index.jsx @@ -47,7 +47,7 @@ const Report = React.createClass({ propTypes: { isSubmitting: React.PropTypes.bool, account: ImmutablePropTypes.map, - statusIds: ImmutablePropTypes.list.isRequired, + statusIds: ImmutablePropTypes.orderedSet.isRequired, comment: React.PropTypes.string.isRequired, dispatch: React.PropTypes.func.isRequired, intl: React.PropTypes.object.isRequired @@ -94,7 +94,8 @@ const Report = React.createClass({ return ( <Column heading={intl.formatMessage(messages.heading)} icon='flag'> <ColumnBackButtonSlim /> - <div className='report' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}> + + <div className='report scrollable' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}> <div className='report__target' style={{ flex: '0 0 auto', padding: '10px' }}> <FormattedMessage id='report.target' defaultMessage='Reporting' /> <strong>{account.get('acct')}</strong> @@ -106,7 +107,7 @@ const Report = React.createClass({ </div> </div> - <div style={{ flex: '0 0 160px', padding: '10px' }}> + <div style={{ flex: '0 0 100px', padding: '10px' }}> <textarea className='report__textarea' placeholder={intl.formatMessage(messages.placeholder)} diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx index 2da57252e..ceafc1a32 100644 --- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx +++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx @@ -17,7 +17,8 @@ const DetailedStatus = React.createClass({ propTypes: { status: ImmutablePropTypes.map.isRequired, - onOpenMedia: React.PropTypes.func.isRequired + onOpenMedia: React.PropTypes.func.isRequired, + onOpenVideo: React.PropTypes.func.isRequired, }, mixins: [PureRenderMixin], @@ -39,7 +40,7 @@ const DetailedStatus = React.createClass({ if (status.get('media_attachments').size > 0) { if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />; + media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />; } else { media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />; } diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index 48fea658d..7ead68807 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -112,6 +112,10 @@ const Status = React.createClass({ this.props.dispatch(openModal('MEDIA', { media, index })); }, + handleOpenVideo (media, time) { + this.props.dispatch(openModal('VIDEO', { media, time })); + }, + handleReport (status) { this.props.dispatch(initReport(status.get('account'), status)); }, @@ -151,7 +155,7 @@ const Status = React.createClass({ <div className='scrollable'> {ancestors} - <DetailedStatus status={status} me={me} onOpenMedia={this.handleOpenMedia} /> + <DetailedStatus status={status} me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} /> <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} /> {descendants} diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx index 35eb2cb0c..130f48b46 100644 --- a/app/assets/javascripts/components/features/ui/components/media_modal.jsx +++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx @@ -111,7 +111,7 @@ const MediaModal = React.createClass({ if (attachment.get('type') === 'image') { content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />; } else if (attachment.get('type') === 'gifv') { - content = <ExtendedVideoPlayer src={url} />; + content = <ExtendedVideoPlayer src={url} muted={true} controls={false} />; } return ( diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx index e7ac02dde..74eb50039 100644 --- a/app/assets/javascripts/components/features/ui/components/modal_root.jsx +++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx @@ -1,10 +1,12 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import MediaModal from './media_modal'; +import VideoModal from './video_modal'; import BoostModal from './boost_modal'; import { TransitionMotion, spring } from 'react-motion'; const MODAL_COMPONENTS = { 'MEDIA': MediaModal, + 'VIDEO': VideoModal, 'BOOST': BoostModal }; diff --git a/app/assets/javascripts/components/features/ui/components/video_modal.jsx b/app/assets/javascripts/components/features/ui/components/video_modal.jsx new file mode 100644 index 000000000..1c3519bd3 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/video_modal.jsx @@ -0,0 +1,47 @@ +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 { defineMessages, injectIntl } from 'react-intl'; +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' } +}); + +const closeStyle = { + position: 'absolute', + zIndex: '100', + top: '4px', + right: '4px' +}; + +const VideoModal = React.createClass({ + + propTypes: { + media: ImmutablePropTypes.map.isRequired, + time: React.PropTypes.number, + onClose: React.PropTypes.func.isRequired, + intl: React.PropTypes.object.isRequired + }, + + mixins: [PureRenderMixin], + + render () { + const { media, intl, time, onClose } = this.props; + + const url = media.get('url'); + + return ( + <div className='modal-root__modal media-modal'> + <div> + <div style={closeStyle}><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div> + <ExtendedVideoPlayer src={url} muted={false} controls={true} time={time} /> + </div> + </div> + ); + } + +}); + +export default injectIntl(VideoModal); diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index 89fb82568..5c7cc6ef4 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -47,7 +47,7 @@ const UI = React.createClass({ this.dragTargets.push(e.target); } - if (e.dataTransfer && e.dataTransfer.files.length > 0) { + if (e.dataTransfer && e.dataTransfer.items.length > 0) { this.setState({ draggingOver: true }); } }, diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 740caef9d..47e23d983 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -5,7 +5,7 @@ * 1. to add your new string here; and * 2. to remove old strings that are no longer needed; and * 3. to sort the strings by the key. - # 4. To rename the `en` const name and export default name to match your locale. + * 4. To rename the `en` const name and export default name to match your locale. * Thanks! */ const en = { @@ -47,7 +47,7 @@ const en = { "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "follow_request.authorize": "Authorize", - "follow_request.reject": "Rejec", + "follow_request.reject": "Reject", "getting_started.apps": "Various apps are available", "getting_started.heading": "Getting started", "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.", @@ -125,6 +125,7 @@ const en = { "upload_progress.label": "Uploading...", "video_player.toggle_sound": "Toggle sound", "video_player.toggle_visible": "Toggle visibility", + "video_player.expand": "Expand video", }; export default en; diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index f14568a3d..7525022b1 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -3,6 +3,7 @@ import de from './de'; import es from './es'; import hu from './hu'; import fr from './fr'; +import nl from './nl'; import no from './no'; import pt from './pt'; import uk from './uk'; @@ -19,6 +20,7 @@ const locales = { es, hu, fr, + nl, no, pt, uk, diff --git a/app/assets/javascripts/components/locales/nl.jsx b/app/assets/javascripts/components/locales/nl.jsx new file mode 100644 index 000000000..cc80854fc --- /dev/null +++ b/app/assets/javascripts/components/locales/nl.jsx @@ -0,0 +1,68 @@ +const nl = { + "column_back_button.label": "terug", + "lightbox.close": "Sluiten", + "loading_indicator.label": "Laden...", + "status.mention": "Vermeld @{name}", + "status.delete": "Verwijder", + "status.reply": "Reageer", + "status.reblog": "Boost", + "status.favourite": "Favoriet", + "status.reblogged_by": "{name} boostte", + "status.sensitive_warning": "Gevoelige inhoud", + "status.sensitive_toggle": "Klik om te zien", + "video_player.toggle_sound": "Geluid omschakelen", + "account.mention": "Vermeld @{name}", + "account.edit_profile": "Bewerk profiel", + "account.unblock": "Deblokkeer @{name}", + "account.unfollow": "Ontvolg", + "account.block": "Blokkeer @{name}", + "account.follow": "Volg", + "account.posts": "Berichten", + "account.follows": "Volgt", + "account.followers": "Volgers", + "account.follows_you": "Volgt jou", + "account.requested": "Wacht op goedkeuring", + "getting_started.heading": "Beginnen", + "getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent, door het e-mailachtige adres in het zoekscherm in te voeren.", + "getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.", + "getting_started.open_source_notice": "Mastodon is open source software. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.", + "column.home": "Thuis", + "column.community": "Lokale tijdlijn", + "column.public": "Federatietijdlijn", + "column.notifications": "Meldingen", + "tabs_bar.compose": "Schrijven", + "tabs_bar.home": "Thuis", + "tabs_bar.mentions": "Vermeldingen", + "tabs_bar.public": "Federatietijdlijn", + "tabs_bar.notifications": "Meldingen", + "compose_form.placeholder": "Waar ben je mee bezig?", + "compose_form.publish": "Toot", + "compose_form.sensitive": "Markeer media als gevoelig", + "compose_form.spoiler": "Verberg tekst achter waarschuwing", + "compose_form.private": "Mark als privé", + "compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.", + "compose_form.unlisted": "Niet tonen op openbare tijdlijnen", + "navigation_bar.edit_profile": "Bewerk profiel", + "navigation_bar.preferences": "Voorkeuren", + "navigation_bar.community_timeline": "Lokale tijdlijn", + "navigation_bar.public_timeline": "Federatietijdlijn", + "navigation_bar.logout": "Uitloggen", + "reply_indicator.cancel": "Annuleren", + "search.placeholder": "Zoeken", + "search.account": "Account", + "search.hashtag": "Hashtag", + "upload_button.label": "Toevoegen media", + "upload_form.undo": "Ongedaan maken", + "notification.follow": "{name} volgde jou", + "notification.favourite": "{name} markeerde je status als favoriet", + "notification.reblog": "{name} boostte je status", + "notification.mention": "{name} vermeldde jou", + "notifications.column_settings.alert": "Desktopmeldingen", + "notifications.column_settings.show": "Tonen in kolom", + "notifications.column_settings.follow": "Nieuwe volgers:", + "notifications.column_settings.favourite": "Favoriten:", + "notifications.column_settings.mention": "Vermeldingen:", + "notifications.column_settings.reblog": "Boosts:", +}; + +export default nl; diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx index 86974239b..7349cc351 100644 --- a/app/assets/javascripts/components/reducers/compose.jsx +++ b/app/assets/javascripts/components/reducers/compose.jsx @@ -67,6 +67,7 @@ function clearAll(state) { map.set('is_submitting', false); map.set('in_reply_to', null); map.set('privacy', state.get('default_privacy')); + map.set('sensitive', false); map.update('media_attachments', list => list.clear()); }); }; diff --git a/app/assets/javascripts/components/reducers/reports.jsx b/app/assets/javascripts/components/reducers/reports.jsx index e1cce1c5f..eab004377 100644 --- a/app/assets/javascripts/components/reducers/reports.jsx +++ b/app/assets/javascripts/components/reducers/reports.jsx @@ -4,7 +4,8 @@ import { REPORT_SUBMIT_SUCCESS, REPORT_SUBMIT_FAIL, REPORT_CANCEL, - REPORT_STATUS_TOGGLE + REPORT_STATUS_TOGGLE, + REPORT_COMMENT_CHANGE } from '../actions/reports'; import Immutable from 'immutable'; @@ -39,6 +40,8 @@ export default function reports(state = initialState, action) { return set.remove(action.statusId); }); + case REPORT_COMMENT_CHANGE: + return state.setIn(['new', 'comment'], action.comment); case REPORT_SUBMIT_REQUEST: return state.setIn(['new', 'isSubmitting'], true); case REPORT_SUBMIT_FAIL: diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index b135d27c9..1c363ec15 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -112,6 +112,18 @@ color: $color3; } } + + &.overlayed { + box-sizing: content-box; + background: rgba($color8, 0.6); + color: rgba($color5, 0.7); + border-radius: 4px; + padding: 2px; + + &:hover { + background: rgba($color8, 0.9); + } + } } .text-icon-button { diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss index 4a6dc6aa4..7bd180c15 100644 --- a/app/assets/stylesheets/stream_entries.scss +++ b/app/assets/stylesheets/stream_entries.scss @@ -218,6 +218,7 @@ margin-top: 8px; height: 300px; overflow: hidden; + position: relative; video { position: relative; diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 3162e565f..cf7b9b381 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -9,6 +9,7 @@ module SettingsHelper fr: 'Français', it: 'Italiano', hu: 'Magyar', + nl: 'Nederlands', no: 'Norsk', pt: 'Português', fi: 'Suomi', diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index b6d371ed2..64b1f86d4 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -15,6 +15,7 @@ class Formatter html = status.text html = encode(html) html = simple_format(html, {}, sanitize: false) + html = html.gsub(/\n/, '') html = link_urls(html) html = link_mentions(html, status.mentions) html = link_hashtags(html) @@ -95,6 +96,6 @@ class Formatter end def mention_html(match, account) - "#{match.split('@').first}<span class=\"h-card\"><a href=\"#{TagManager.instance.url_for(account)}\" class=\"u-url mention\">@#{account.username}</a></span>" + "#{match.split('@').first}<span class=\"h-card\"><a href=\"#{TagManager.instance.url_for(account)}\" class=\"u-url mention\">@<span>#{account.username}</span></a></span>" end end diff --git a/app/lib/email_validator.rb b/app/validators/email_validator.rb index 06e9375f6..06e9375f6 100644 --- a/app/lib/email_validator.rb +++ b/app/validators/email_validator.rb diff --git a/app/lib/status_length_validator.rb b/app/validators/status_length_validator.rb index 55135a598..55135a598 100644 --- a/app/lib/status_length_validator.rb +++ b/app/validators/status_length_validator.rb diff --git a/app/lib/url_validator.rb b/app/validators/url_validator.rb index 4a5c4ef3f..4a5c4ef3f 100644 --- a/app/lib/url_validator.rb +++ b/app/validators/url_validator.rb diff --git a/app/views/about/terms.en.html.haml b/app/views/about/terms.en.html.haml index 9fb318053..e1766ca16 100644 --- a/app/views/about/terms.en.html.haml +++ b/app/views/about/terms.en.html.haml @@ -51,7 +51,7 @@ %h3#coppa Children's Online Privacy Protection Act Compliance %p - Our site, products and services are all directed to people who are at least 13 years old or older. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA + Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA = surround '(', '),' do = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' do not use this site. diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 4d636601e..bd1eb7ecd 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -1,30 +1,30 @@ - content_for :page_title do - Accounts + = t('admin.accounts.title') .filters .filter-subset - %strong Location + %strong= t('admin.accounts.location.title') %ul - %li= filter_link_to 'All', local: nil, remote: nil - %li= filter_link_to 'Local', local: '1', remote: nil - %li= filter_link_to 'Remote', remote: '1', local: nil + %li= filter_link_to t('admin.accounts.location.all'), local: nil, remote: nil + %li= filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil + %li= filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil .filter-subset - %strong Moderation + %strong= t('admin.accounts.moderation.title') %ul - %li= filter_link_to 'All', silenced: nil, suspended: nil - %li= filter_link_to 'Silenced', silenced: '1' - %li= filter_link_to 'Suspended', suspended: '1' + %li= filter_link_to t('admin.accounts.moderation.all'), silenced: nil, suspended: nil + %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1' + %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1' .filter-subset - %strong Order + %strong= t('admin.accounts.order.title') %ul - %li= filter_link_to 'Alphabetic', recent: nil - %li= filter_link_to 'Most recent', recent: '1' + %li= filter_link_to t('admin.accounts.order.alphabetic'), recent: nil + %li= filter_link_to t('admin.accounts.order.most_recent'), recent: '1' %table.table %thead %tr - %th Username - %th Domain + %th= t('admin.accounts.username') + %th= t('admin.accounts.domain') %th= fa_icon 'paper-plane-o' %th %tbody @@ -36,14 +36,14 @@ = link_to account.domain, admin_accounts_path(by_domain: account.domain) %td - if account.local? - Local + = t('admin.accounts.location.local') - elsif account.subscribed? %i.fa.fa-check - else %i.fa.fa-times %td - = table_link_to 'circle', 'Web', web_path("accounts/#{account.id}") - = table_link_to 'globe', 'Public', TagManager.instance.url_for(account) - = table_link_to 'pencil', 'Edit', admin_account_path(account.id) + = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}") + = table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account) + = table_link_to 'pencil', t('admin.accounts.edit'), admin_account_path(account.id) = paginate @accounts diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 22901aed1..6d2a4d123 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -4,24 +4,24 @@ %table.table %tbody %tr - %th Username + %th= t('admin.accounts.username') %td= @account.username %tr - %th Domain + %th= t('admin.accounts.domain') %td= @account.domain %tr - %th Display name + %th= t('admin.accounts.display_name') %td= @account.display_name - if @account.local? %tr - %th E-mail + %th= t('admin.accounts.email') %td= @account.user.email %tr - %th Most recent IP + %th= t('admin.accounts.most_recent_ip') %td= @account.user.current_sign_in_ip %tr - %th Most recent activity + %th= t('admin.accounts.most_recent_activity') %td - if @account.user.current_sign_in_at = l @account.user.current_sign_in_at @@ -29,44 +29,44 @@ Never - else %tr - %th Profile URL + %th= t('admin.accounts.profile_url') %td= link_to @account.url %tr - %th Feed URL + %th= t('admin.accounts.feed_url') %td= link_to @account.remote_url %tr - %th PuSH subscription expires + %th= t('admin.accounts.push_subscription_expires') %td - if @account.subscribed? = l @account.subscription_expires_at - else - Not subscribed + = t('admin.accounts.not_subscribed') %tr - %th Salmon URL + %th= t('admin.accounts.salmon_url') %td= link_to @account.salmon_url %tr - %th Follows + %th= t('admin.accounts.follows') %td= @account.following_count %tr - %th Followers + %th= t('admin.accounts.followers') %td= @account.followers_count %tr - %th Statuses + %th= t('admin.accounts.statuses') %td= @account.statuses_count %tr - %th Media attachments + %th= t('admin.accounts.media_attachments') %td = @account.media_attachments.count = surround '(', ')' do = number_to_human_size @account.media_attachments.sum('file_file_size') - if @account.silenced? - = link_to 'Undo silence', admin_account_silence_path(@account.id), method: :delete, class: 'button' + = link_to t('admin.accounts.undo_silenced'), admin_account_silence_path(@account.id), method: :delete, class: 'button' - else - = link_to 'Silence', admin_account_silence_path(@account.id), method: :post, class: 'button' + = link_to t('admin.accounts.silence'), admin_account_silence_path(@account.id), method: :post, class: 'button' - if @account.suspended? - = link_to 'Undo suspension', admin_account_suspension_path(@account.id), method: :delete, class: 'button' + = link_to t('admin.accounts.undo_suspension'), admin_account_suspension_path(@account.id), method: :delete, class: 'button' - else - = link_to 'Perform full suspension', admin_account_suspension_path(@account.id), method: :post, data: { confirm: 'Are you sure?' }, class: 'button' + = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml index fe6ff683f..6f4ba9b57 100644 --- a/app/views/admin/domain_blocks/index.html.haml +++ b/app/views/admin/domain_blocks/index.html.haml @@ -1,11 +1,11 @@ - content_for :page_title do - Domain Blocks + = t('admin.domain_block.title') %table.table %thead %tr - %th Domain - %th Severity + %th= t('admin.domain_block.domain') + %th= t('admin.domain_block.severity') %tbody - @blocks.each do |block| %tr @@ -14,4 +14,4 @@ %td= block.severity = paginate @blocks -= link_to 'Add new', new_admin_domain_block_path, class: 'button' += link_to t('admin.domain_block.add_new'), new_admin_domain_block_path, class: 'button' diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml index fbd39d6cf..53aab21ff 100644 --- a/app/views/admin/domain_blocks/new.html.haml +++ b/app/views/admin/domain_blocks/new.html.haml @@ -1,18 +1,14 @@ - content_for :page_title do - New domain block + = t('admin.domain_block.new.title') = 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. + %p.hint= t('admin.domain_block.new.hint') - = f.input :domain, placeholder: 'Domain' - = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false + = f.input :domain, placeholder: t('admin.domain_block.domain') + = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("admin.domain_block.new.severity.#{type}") } - %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. + %p.hint= t('admin.domain_block.new.severity.desc_html') .actions - = f.button :button, 'Create block', type: :submit + = f.button :button, t('admin.domain_block.new.create'), type: :submit diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml index dcbb11c11..6b1d1ba4a 100644 --- a/app/views/admin/pubsubhubbub/index.html.haml +++ b/app/views/admin/pubsubhubbub/index.html.haml @@ -1,14 +1,14 @@ - content_for :page_title do - PubSubHubbub + = t('admin.pubsubhubbub.title') %table.table %thead %tr - %th Topic - %th Callback URL - %th Confirmed - %th Expires in - %th Last delivery + %th= t('admin.pubsubhubbub.topic') + %th= t('admin.pubsubhubbub.callback_url') + %th= t('admin.pubsubhubbub.confirmed') + %th= t('admin.pubsubhubbub.expires_in') + %th= t('admin.pubsubhubbub.last_delivery') %tbody - @subscriptions.each do |subscription| %tr diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 68dc07016..d5deec8f6 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -1,12 +1,12 @@ - content_for :page_title do - = t('reports.reports') + = t('admin.reports.title') .filters .filter-subset - %strong= t('reports.status') + %strong= t('admin.reports.status') %ul - %li= filter_link_to t('reports.unresolved'), action_taken: nil - %li= filter_link_to t('reports.resolved'), action_taken: '1' + %li= filter_link_to t('admin.reports.unresolved'), action_taken: nil + %li= filter_link_to t('admin.reports.resolved'), action_taken: '1' = form_tag do @@ -14,10 +14,10 @@ %thead %tr %th - %th= t('reports.id') - %th= t('reports.target') - %th= t('reports.reported_by') - %th= t('reports.comment.label') + %th= t('admin.reports.id') + %th= t('admin.reports.target') + %th= t('admin.reports.reported_by') + %th= t('admin.reports.comment.label') %th %tbody - @reports.each do |report| @@ -27,6 +27,6 @@ %td= link_to report.target_account.acct, admin_account_path(report.target_account.id) %td= link_to report.account.acct, admin_account_path(report.account.id) %td= truncate(report.comment, length: 30, separator: ' ') - %td= table_link_to 'circle', t('reports.view'), admin_report_path(report) + %td= table_link_to 'circle', t('admin.reports.view'), admin_report_path(report) = paginate @reports diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index ecbb98482..a7430f396 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -1,16 +1,16 @@ - content_for :page_title do - = t('reports.report', id: @report.id) + = t('admin.reports.report', id: @report.id) .report-accounts .report-accounts__item - %strong= t('reports.reported_account') + %strong= t('admin.reports.reported_account') = render partial: 'authorize_follow/card', locals: { account: @report.target_account } .report-accounts__item - %strong= t('reports.reported_by') + %strong= t('admin.reports.reported_by') = render partial: 'authorize_follow/card', locals: { account: @report.account } %p - %strong= t('reports.comment.label') + %strong= t('admin.reports.comment.label') \: = @report.comment.presence || t('reports.comment.none') @@ -22,7 +22,7 @@ .activity-stream.activity-stream-headless .entry= render partial: 'stream_entries/simple_status', locals: { status: status } .report-status__actions - = link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: t('reports.delete') do + = link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: t('admin.reports.delete') do = fa_icon 'trash' - if !@report.action_taken? @@ -30,10 +30,10 @@ %div{ style: 'overflow: hidden' } %div{ style: 'float: right' } - = link_to t('reports.silence_account'), silence_admin_report_path(@report), method: :post, class: 'button' - = link_to t('reports.suspend_account'), suspend_admin_report_path(@report), method: :post, class: 'button' + = link_to t('admin.reports.silence_account'), silence_admin_report_path(@report), method: :post, class: 'button' + = link_to t('admin.reports.suspend_account'), suspend_admin_report_path(@report), method: :post, class: 'button' %div{ style: 'float: left' } - = link_to t('reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button' + = link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button' - elsif !@report.action_taken_by_account.nil? %hr/ |