Merge tag 'v3.0.0' into hometown-dev
This commit is contained in:
@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Audio from 'mastodon/features/audio';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { previewState } from './video_modal';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
export default class AudioModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
if (this.context.router) {
|
||||
const history = this.context.router.history;
|
||||
|
||||
history.push(history.location.pathname, previewState);
|
||||
|
||||
this.unlistenHistory = history.listen(() => {
|
||||
this.props.onClose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.context.router) {
|
||||
this.unlistenHistory();
|
||||
|
||||
if (this.context.router.history.location.state === previewState) {
|
||||
this.context.router.history.goBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleStatusClick = e => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, status } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal audio-modal'>
|
||||
<div className='audio-modal__container'>
|
||||
<Audio
|
||||
src={media.get('url')}
|
||||
alt={media.get('description')}
|
||||
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
||||
height={135}
|
||||
preload
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status && (
|
||||
<div className={classNames('media-modal__meta')}>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
103
app/javascript/mastodon/features/ui/components/block_modal.js
Normal file
103
app/javascript/mastodon/features/ui/components/block_modal.js
Normal file
@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
import Button from '../../../components/button';
|
||||
import { closeModal } from '../../../actions/modal';
|
||||
import { blockAccount } from '../../../actions/accounts';
|
||||
import { initReport } from '../../../actions/reports';
|
||||
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onConfirm(account) {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
},
|
||||
|
||||
onBlockAndReport(account) {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
dispatch(initReport(account));
|
||||
},
|
||||
|
||||
onClose() {
|
||||
dispatch(closeModal());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(makeMapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class BlockModal extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onBlockAndReport: PropTypes.func.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.button.focus();
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClose();
|
||||
this.props.onConfirm(this.props.account);
|
||||
}
|
||||
|
||||
handleSecondary = () => {
|
||||
this.props.onClose();
|
||||
this.props.onBlockAndReport(this.props.account);
|
||||
}
|
||||
|
||||
handleCancel = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.button = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal block-modal'>
|
||||
<div className='block-modal__container'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='confirmations.block.message'
|
||||
defaultMessage='Are you sure you want to block {name}?'
|
||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='block-modal__action-bar'>
|
||||
<Button onClick={this.handleCancel} className='block-modal__cancel-button'>
|
||||
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
<Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
|
||||
<FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' />
|
||||
</Button>
|
||||
<Button onClick={this.handleClick} ref={this.setRef}>
|
||||
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -21,7 +21,7 @@ export default class ColumnLoading extends ImmutablePureComponent {
|
||||
let { title, icon } = this.props;
|
||||
return (
|
||||
<Column>
|
||||
<ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} />
|
||||
<ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder />
|
||||
<div className='scrollable' />
|
||||
</Column>
|
||||
);
|
||||
|
@ -12,7 +12,18 @@ import BundleContainer from '../containers/bundle_container';
|
||||
import ColumnLoading from './column_loading';
|
||||
import DrawerLoading from './drawer_loading';
|
||||
import BundleColumnError from './bundle_column_error';
|
||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
|
||||
import {
|
||||
Compose,
|
||||
Notifications,
|
||||
HomeTimeline,
|
||||
CommunityTimeline,
|
||||
PublicTimeline,
|
||||
HashtagTimeline,
|
||||
DirectTimeline,
|
||||
FavouritedStatuses,
|
||||
ListTimeline,
|
||||
Directory,
|
||||
} from '../../ui/util/async-components';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import ComposePanel from './compose_panel';
|
||||
import NavigationPanel from './navigation_panel';
|
||||
@ -30,6 +41,7 @@ const componentMap = {
|
||||
'DIRECT': DirectTimeline,
|
||||
'FAVOURITES': FavouritedStatuses,
|
||||
'LIST': ListTimeline,
|
||||
'DIRECTORY': Directory,
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { title } from 'mastodon/initial_state';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
unread: state.getIn(['missed_updates', 'unread']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class DocumentTitle extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
unread: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this._sideEffects();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._sideEffects();
|
||||
}
|
||||
|
||||
_sideEffects () {
|
||||
const { unread } = this.props;
|
||||
|
||||
if (unread > 99) {
|
||||
document.title = `(*) ${title}`;
|
||||
} else if (unread > 0) {
|
||||
document.title = `(${unread}) ${title}`;
|
||||
} else {
|
||||
document.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import api from '../../../api';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import api from 'mastodon/api';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class EmbedModal extends ImmutablePureComponent {
|
||||
@ -50,13 +55,17 @@ class EmbedModal extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, onClose } = this.props;
|
||||
const { oembed } = this.state;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal embed-modal'>
|
||||
<h4><FormattedMessage id='status.embed' defaultMessage='Embed' /></h4>
|
||||
<div className='modal-root__modal report-modal embed-modal'>
|
||||
<div className='report-modal__target'>
|
||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
|
||||
<FormattedMessage id='status.embed' defaultMessage='Embed' />
|
||||
</div>
|
||||
|
||||
<div className='embed-modal__container'>
|
||||
<div className='report-modal__container embed-modal__container' style={{ display: 'block' }}>
|
||||
<p className='hint'>
|
||||
<FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
|
||||
</p>
|
||||
|
@ -1,11 +1,27 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
import ImageLoader from './image_loader';
|
||||
import classNames from 'classnames';
|
||||
import { changeUploadCompose } from '../../../actions/compose';
|
||||
import { getPointerPosition } from '../../video';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import Button from 'mastodon/components/button';
|
||||
import Video from 'mastodon/features/video';
|
||||
import Audio from 'mastodon/features/audio';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
|
||||
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
|
||||
import { length } from 'stringz';
|
||||
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
|
||||
placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||
@ -13,17 +29,26 @@ const mapStateToProps = (state, { id }) => ({
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
|
||||
onSave: (x, y) => {
|
||||
dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
|
||||
onSave: (description, x, y) => {
|
||||
dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/\*\*\*\*\*\*/g, '\n\n');
|
||||
|
||||
const assetHost = process.env.CDN_HOST || '';
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class FocalPointModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -32,6 +57,9 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||
focusX: 0,
|
||||
focusY: 0,
|
||||
dragging: false,
|
||||
description: '',
|
||||
dirty: false,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
@ -57,6 +85,14 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||
this.setState({ dragging: true });
|
||||
}
|
||||
|
||||
handleTouchStart = e => {
|
||||
document.addEventListener('touchmove', this.handleMouseMove);
|
||||
document.addEventListener('touchend', this.handleTouchEnd);
|
||||
|
||||
this.updatePosition(e);
|
||||
this.setState({ dragging: true });
|
||||
}
|
||||
|
||||
handleMouseMove = e => {
|
||||
this.updatePosition(e);
|
||||
}
|
||||
@ -66,7 +102,13 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||
|
||||
this.setState({ dragging: false });
|
||||
this.props.onSave(this.state.focusX, this.state.focusY);
|
||||
}
|
||||
|
||||
handleTouchEnd = () => {
|
||||
document.removeEventListener('touchmove', this.handleMouseMove);
|
||||
document.removeEventListener('touchend', this.handleTouchEnd);
|
||||
|
||||
this.setState({ dragging: false });
|
||||
}
|
||||
|
||||
updatePosition = e => {
|
||||
@ -74,46 +116,166 @@ class FocalPointModal extends ImmutablePureComponent {
|
||||
const focusX = (x - .5) * 2;
|
||||
const focusY = (y - .5) * -2;
|
||||
|
||||
this.setState({ x, y, focusX, focusY });
|
||||
this.setState({ x, y, focusX, focusY, dirty: true });
|
||||
}
|
||||
|
||||
updatePositionFromMedia = media => {
|
||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||
const focusX = media.getIn(['meta', 'focus', 'x']);
|
||||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||
const description = media.get('description') || '';
|
||||
|
||||
if (focusX && focusY) {
|
||||
const x = (focusX / 2) + .5;
|
||||
const y = (focusY / -2) + .5;
|
||||
|
||||
this.setState({ x, y, focusX, focusY });
|
||||
this.setState({
|
||||
x,
|
||||
y,
|
||||
focusX,
|
||||
focusY,
|
||||
description,
|
||||
dirty: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 });
|
||||
this.setState({
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
focusX: 0,
|
||||
focusY: 0,
|
||||
description,
|
||||
dirty: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
this.setState({ description: e.target.value, dirty: true });
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.onSave(this.state.description, this.state.focusX, this.state.focusY);
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
handleTextDetection = () => {
|
||||
const { media } = this.props;
|
||||
const { x, y, dragging } = this.state;
|
||||
|
||||
this.setState({ detecting: true });
|
||||
|
||||
fetchTesseract().then(({ TesseractWorker }) => {
|
||||
const worker = new TesseractWorker({
|
||||
workerPath: `${assetHost}/packs/ocr/worker.min.js`,
|
||||
corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`,
|
||||
langPath: `${assetHost}/ocr/lang-data`,
|
||||
});
|
||||
|
||||
let media_url = media.get('file');
|
||||
|
||||
if (window.URL && URL.createObjectURL) {
|
||||
try {
|
||||
media_url = URL.createObjectURL(media.get('file'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
worker.recognize(media_url)
|
||||
.progress(({ progress }) => this.setState({ progress }))
|
||||
.finally(() => worker.terminate())
|
||||
.then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
|
||||
.catch(() => this.setState({ detecting: false }));
|
||||
}).catch(() => this.setState({ detecting: false }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, onClose } = this.props;
|
||||
const { x, y, dragging, description, dirty, detecting, progress } = this.state;
|
||||
|
||||
const width = media.getIn(['meta', 'original', 'width']) || null;
|
||||
const height = media.getIn(['meta', 'original', 'height']) || null;
|
||||
const focals = ['image', 'gifv'].includes(media.get('type'));
|
||||
|
||||
const previewRatio = 16/9;
|
||||
const previewWidth = 200;
|
||||
const previewHeight = previewWidth / previewRatio;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal video-modal focal-point-modal'>
|
||||
<div className={classNames('focal-point', { dragging })} ref={this.setRef}>
|
||||
<ImageLoader
|
||||
previewSrc={media.get('preview_url')}
|
||||
src={media.get('url')}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
<div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
|
||||
<div className='report-modal__target'>
|
||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
|
||||
<FormattedMessage id='upload_modal.edit_media' defaultMessage='Edit media' />
|
||||
</div>
|
||||
|
||||
<div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
|
||||
<div className='focal-point__overlay' onMouseDown={this.handleMouseDown} />
|
||||
<div className='report-modal__container'>
|
||||
<div className='report-modal__comment'>
|
||||
{focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
|
||||
|
||||
<label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label>
|
||||
|
||||
<div className='setting-text__wrapper'>
|
||||
<Textarea
|
||||
id='upload-modal__description'
|
||||
className='setting-text light'
|
||||
value={detecting ? '…' : description}
|
||||
onChange={this.handleChange}
|
||||
disabled={detecting}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className='setting-text__modifiers'>
|
||||
<UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={<FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='setting-text__toolbar'>
|
||||
<button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button>
|
||||
<CharacterCounter max={1500} text={detecting ? '' : description} />
|
||||
</div>
|
||||
|
||||
<Button disabled={!dirty || detecting || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
|
||||
</div>
|
||||
|
||||
<div className='focal-point-modal__content'>
|
||||
{focals && (
|
||||
<div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
|
||||
{media.get('type') === 'image' && <img src={media.get('url')} width={width} height={height} alt='' />}
|
||||
{media.get('type') === 'gifv' && <video src={media.get('url')} width={width} height={height} loop muted autoPlay />}
|
||||
|
||||
<div className='focal-point__preview'>
|
||||
<strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>
|
||||
<div style={{ width: previewWidth, height: previewHeight, backgroundImage: `url(${media.get('preview_url')})`, backgroundSize: 'cover', backgroundPosition: `${x * 100}% ${y * 100}%` }} />
|
||||
</div>
|
||||
|
||||
<div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} />
|
||||
<div className='focal-point__overlay' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{media.get('type') === 'video' && (
|
||||
<Video
|
||||
preview={media.get('preview_url')}
|
||||
blurhash={media.get('blurhash')}
|
||||
src={media.get('url')}
|
||||
detailed
|
||||
inline
|
||||
editable
|
||||
/>
|
||||
)}
|
||||
|
||||
{media.get('type') === 'audio' && (
|
||||
<Audio
|
||||
src={media.get('url')}
|
||||
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
||||
height={150}
|
||||
preload
|
||||
editable
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,39 +1,77 @@
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
|
||||
import { logOut } from 'mastodon/utils/log_out';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
|
||||
const LinkFooter = ({ withHotkeys }) => (
|
||||
<div className='getting-started__footer'>
|
||||
<ul>
|
||||
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
|
||||
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
|
||||
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
|
||||
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
|
||||
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
|
||||
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
|
||||
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
|
||||
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
|
||||
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
||||
</ul>
|
||||
const messages = defineMessages({
|
||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||
});
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
/> <FormattedMessage
|
||||
id='getting_started.hometown_open_source_notice'
|
||||
defaultMessage='Hometown is also open source, at {hometown} (v1.0.2).'
|
||||
values={{ hometown: <span><a href='https://github.com/hometown-fork/hometown' rel='noopener' target='_blank'>hometown-fork/hometown</a></span> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onLogout () {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
onConfirm: () => logOut(),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@connect(null, mapDispatchToProps)
|
||||
class LinkFooter extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
withHotkeys: PropTypes.bool,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleLogoutClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onLogout();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { withHotkeys } = this.props;
|
||||
|
||||
return (
|
||||
<div className='getting-started__footer'>
|
||||
<ul>
|
||||
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
|
||||
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
|
||||
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
|
||||
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
|
||||
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
|
||||
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
|
||||
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
|
||||
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
|
||||
<li><a href='/auth/sign_out' onClick={this.handleLogoutClick}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
|
||||
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='getting_started.hometown_open_source_notice'
|
||||
defaultMessage='Hometown is also open source, at {hometown} (v1.0.3).'
|
||||
values={{ hometown: <span><a href='https://github.com/hometown-fork/hometown' rel='noopener' target='_blank'>hometown-fork/hometown</a></span> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LinkFooter.propTypes = {
|
||||
withHotkeys: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default LinkFooter;
|
||||
|
@ -228,7 +228,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
{status && (
|
||||
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Base from '../../../components/modal_root';
|
||||
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
||||
import Base from 'mastodon/components/modal_root';
|
||||
import BundleContainer from '../containers/bundle_container';
|
||||
import BundleModalError from './bundle_modal_error';
|
||||
import ModalLoading from './modal_loading';
|
||||
@ -8,10 +9,12 @@ import ActionsModal from './actions_modal';
|
||||
import MediaModal from './media_modal';
|
||||
import VideoModal from './video_modal';
|
||||
import BoostModal from './boost_modal';
|
||||
import AudioModal from './audio_modal';
|
||||
import ConfirmationModal from './confirmation_modal';
|
||||
import FocalPointModal from './focal_point_modal';
|
||||
import {
|
||||
MuteModal,
|
||||
BlockModal,
|
||||
ReportModal,
|
||||
EmbedModal,
|
||||
ListEditor,
|
||||
@ -21,9 +24,11 @@ import {
|
||||
const MODAL_COMPONENTS = {
|
||||
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
||||
'VIDEO': () => Promise.resolve({ default: VideoModal }),
|
||||
'AUDIO': () => Promise.resolve({ default: AudioModal }),
|
||||
'BOOST': () => Promise.resolve({ default: BoostModal }),
|
||||
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
|
||||
'MUTE': MuteModal,
|
||||
'BLOCK': BlockModal,
|
||||
'REPORT': ReportModal,
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
@ -47,8 +52,10 @@ export default class ModalRoot extends React.PureComponent {
|
||||
componentDidUpdate (prevProps, prevState, { visible }) {
|
||||
if (visible) {
|
||||
document.body.classList.add('with-modals--active');
|
||||
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
|
||||
} else {
|
||||
document.body.classList.remove('with-modals--active');
|
||||
document.documentElement.style.marginRight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ import { toggleHideNotifications } from '../../../actions/mutes';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
|
||||
account: state.getIn(['mutes', 'new', 'account']),
|
||||
notifications: state.getIn(['mutes', 'new', 'notifications']),
|
||||
};
|
||||
@ -38,7 +37,6 @@ export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
class MuteModal extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isSubmitting: PropTypes.bool.isRequired,
|
||||
account: PropTypes.object.isRequired,
|
||||
notifications: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
@ -81,11 +79,16 @@ class MuteModal extends React.PureComponent {
|
||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||
/>
|
||||
</p>
|
||||
<div>
|
||||
<label htmlFor='mute-modal__hide-notifications-checkbox'>
|
||||
<p className='mute-modal__explanation'>
|
||||
<FormattedMessage
|
||||
id='confirmations.mute.explanation'
|
||||
defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.'
|
||||
/>
|
||||
</p>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
|
||||
<label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'>
|
||||
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||
{' '}
|
||||
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,10 +2,11 @@ import React from 'react';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { profile_directory } from 'mastodon/initial_state';
|
||||
import { profile_directory, showTrends } from 'mastodon/initial_state';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import FollowRequestsNavLink from './follow_requests_nav_link';
|
||||
import ListPanel from './list_panel';
|
||||
import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
|
||||
|
||||
const NavigationPanel = () => (
|
||||
<div className='navigation-panel'>
|
||||
@ -17,6 +18,7 @@ const NavigationPanel = () => (
|
||||
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
|
||||
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
|
||||
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
|
||||
|
||||
<ListPanel />
|
||||
|
||||
@ -24,7 +26,9 @@ const NavigationPanel = () => (
|
||||
|
||||
<a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
|
||||
<a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
|
||||
{!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>}
|
||||
|
||||
{showTrends && <div className='flex-spacer' />}
|
||||
{showTrends && <TrendsContainer />}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -73,9 +73,13 @@ class TabsBar extends React.PureComponent {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
return (
|
||||
<nav className='tabs-bar' ref={this.setRef}>
|
||||
{links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
|
||||
</nav>
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<nav className='tabs-bar' ref={this.setRef}>
|
||||
{links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
|
||||
</nav>
|
||||
|
||||
<div id='tabs-bar__portal' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import Video from 'mastodon/features/video';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
export const previewState = 'previewVideoModal';
|
||||
|
||||
@ -52,22 +54,25 @@ export default class VideoModal extends ImmutablePureComponent {
|
||||
render () {
|
||||
const { media, status, time, onClose } = this.props;
|
||||
|
||||
const link = status && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal video-modal'>
|
||||
<div>
|
||||
<div className='video-modal__container'>
|
||||
<Video
|
||||
preview={media.get('preview_url')}
|
||||
blurhash={media.get('blurhash')}
|
||||
src={media.get('url')}
|
||||
startTime={time}
|
||||
onCloseVideo={onClose}
|
||||
link={link}
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status && (
|
||||
<div className={classNames('media-modal__meta')}>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ const mapStateToProps = (state, { intl }) => {
|
||||
const value = notification[key];
|
||||
|
||||
if (typeof value === 'object') {
|
||||
notification[key] = intl.formatMessage(value);
|
||||
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import StatusList from '../../../components/status_list';
|
||||
import { scrollTopTimeline } from '../../../actions/timelines';
|
||||
import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import { debounce } from 'lodash';
|
||||
@ -37,6 +37,7 @@ const makeMapStateToProps = () => {
|
||||
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
|
||||
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
|
||||
hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
|
||||
numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
@ -52,6 +53,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({
|
||||
dispatch(scrollTopTimeline(timelineId, false));
|
||||
}, 100),
|
||||
|
||||
onLoadPending: () => dispatch(loadPending(timelineId)),
|
||||
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);
|
||||
|
@ -15,9 +15,12 @@ import { expandHomeTimeline } from '../../actions/timelines';
|
||||
import { expandNotifications } from '../../actions/notifications';
|
||||
import { fetchFilters } from '../../actions/filters';
|
||||
import { clearHeight } from '../../actions/height_cache';
|
||||
import { focusApp, unfocusApp } from 'mastodon/actions/app';
|
||||
import { submitMarkers } from 'mastodon/actions/markers';
|
||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||
import UploadArea from './components/upload_area';
|
||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||
import DocumentTitle from './components/document_title';
|
||||
import {
|
||||
Compose,
|
||||
Status,
|
||||
@ -45,6 +48,7 @@ import {
|
||||
PinnedStatuses,
|
||||
Lists,
|
||||
Search,
|
||||
Directory,
|
||||
} from './util/async-components';
|
||||
import { me, forceSingleColumn } from '../../initial_state';
|
||||
import { previewState as previewMediaState } from './components/media_modal';
|
||||
@ -62,6 +66,7 @@ const mapStateToProps = state => ({
|
||||
isComposing: state.getIn(['compose', 'is_composing']),
|
||||
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||
});
|
||||
|
||||
@ -110,12 +115,25 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||
|
||||
componentWillMount () {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
|
||||
if (this.state.mobile || forceSingleColumn) {
|
||||
document.body.classList.toggle('layout-single-column', true);
|
||||
document.body.classList.toggle('layout-multiple-columns', false);
|
||||
} else {
|
||||
document.body.classList.toggle('layout-single-column', false);
|
||||
document.body.classList.toggle('layout-multiple-columns', true);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
|
||||
this.node.handleChildrenContentChange();
|
||||
}
|
||||
|
||||
if (prevState.mobile !== this.state.mobile && !forceSingleColumn) {
|
||||
document.body.classList.toggle('layout-single-column', this.state.mobile);
|
||||
document.body.classList.toggle('layout-multiple-columns', !this.state.mobile);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@ -126,14 +144,24 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||
return location.state !== previewMediaState && location.state !== previewVideoState;
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
handleLayoutChange = debounce(() => {
|
||||
// The cached heights are no longer accurate, invalidate
|
||||
this.props.onLayoutChange();
|
||||
|
||||
this.setState({ mobile: isMobile(window.innerWidth) });
|
||||
}, 500, {
|
||||
trailing: true,
|
||||
});
|
||||
})
|
||||
|
||||
handleResize = () => {
|
||||
const mobile = isMobile(window.innerWidth);
|
||||
|
||||
if (mobile !== this.state.mobile) {
|
||||
this.handleLayoutChange.cancel();
|
||||
this.props.onLayoutChange();
|
||||
this.setState({ mobile });
|
||||
} else {
|
||||
this.handleLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c.getWrappedInstance();
|
||||
@ -163,6 +191,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
|
||||
<WrappedRoute path='/search' component={Search} content={children} />
|
||||
<WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
|
||||
<WrappedRoute path='/statuses/new' component={Compose} content={children} />
|
||||
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
@ -204,6 +233,7 @@ class UI extends React.PureComponent {
|
||||
isComposing: PropTypes.bool,
|
||||
hasComposingText: PropTypes.bool,
|
||||
hasMediaAttachments: PropTypes.bool,
|
||||
canUploadMore: PropTypes.bool,
|
||||
location: PropTypes.object,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dropdownMenuIsOpen: PropTypes.bool,
|
||||
@ -213,8 +243,10 @@ class UI extends React.PureComponent {
|
||||
draggingOver: false,
|
||||
};
|
||||
|
||||
handleBeforeUnload = (e) => {
|
||||
const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props;
|
||||
handleBeforeUnload = e => {
|
||||
const { intl, dispatch, isComposing, hasComposingText, hasMediaAttachments } = this.props;
|
||||
|
||||
dispatch(submitMarkers());
|
||||
|
||||
if (isComposing && (hasComposingText || hasMediaAttachments)) {
|
||||
// Setting returnValue to any string causes confirmation dialog.
|
||||
@ -224,6 +256,14 @@ class UI extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowFocus = () => {
|
||||
this.props.dispatch(focusApp());
|
||||
}
|
||||
|
||||
handleWindowBlur = () => {
|
||||
this.props.dispatch(unfocusApp());
|
||||
}
|
||||
|
||||
handleLayoutChange = () => {
|
||||
// The cached heights are no longer accurate, invalidate
|
||||
this.props.dispatch(clearHeight());
|
||||
@ -240,13 +280,14 @@ class UI extends React.PureComponent {
|
||||
this.dragTargets.push(e.target);
|
||||
}
|
||||
|
||||
if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) {
|
||||
if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore) {
|
||||
this.setState({ draggingOver: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleDragOver = (e) => {
|
||||
if (this.dataTransferIsText(e.dataTransfer)) return false;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@ -261,12 +302,13 @@ class UI extends React.PureComponent {
|
||||
|
||||
handleDrop = (e) => {
|
||||
if (this.dataTransferIsText(e.dataTransfer)) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({ draggingOver: false });
|
||||
this.dragTargets = [];
|
||||
|
||||
if (e.dataTransfer && e.dataTransfer.files.length >= 1) {
|
||||
if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore) {
|
||||
this.props.dispatch(uploadCompose(e.dataTransfer.files));
|
||||
}
|
||||
}
|
||||
@ -285,7 +327,7 @@ class UI extends React.PureComponent {
|
||||
}
|
||||
|
||||
dataTransferIsText = (dataTransfer) => {
|
||||
return (dataTransfer && Array.from(dataTransfer.types).includes('text/plain') && dataTransfer.items.length === 1);
|
||||
return (dataTransfer && Array.from(dataTransfer.types).filter((type) => type === 'text/plain').length === 1);
|
||||
}
|
||||
|
||||
closeUploadModal = () => {
|
||||
@ -301,6 +343,8 @@ class UI extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
window.addEventListener('focus', this.handleWindowFocus, false);
|
||||
window.addEventListener('blur', this.handleWindowBlur, false);
|
||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||
|
||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||
@ -330,7 +374,10 @@ class UI extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('focus', this.handleWindowFocus);
|
||||
window.removeEventListener('blur', this.handleWindowBlur);
|
||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||
|
||||
document.removeEventListener('dragenter', this.handleDragEnter);
|
||||
document.removeEventListener('dragover', this.handleDragOver);
|
||||
document.removeEventListener('drop', this.handleDrop);
|
||||
@ -489,6 +536,7 @@ class UI extends React.PureComponent {
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
<ModalContainer />
|
||||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||
<DocumentTitle />
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
|
@ -106,6 +106,10 @@ export function MuteModal () {
|
||||
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
|
||||
}
|
||||
|
||||
export function BlockModal () {
|
||||
return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal');
|
||||
}
|
||||
|
||||
export function ReportModal () {
|
||||
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
||||
}
|
||||
@ -133,3 +137,15 @@ export function ListAdder () {
|
||||
export function Search () {
|
||||
return import(/*webpackChunkName: "features/search" */'../../search');
|
||||
}
|
||||
|
||||
export function Tesseract () {
|
||||
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
||||
}
|
||||
|
||||
export function Audio () {
|
||||
return import(/* webpackChunkName: "features/audio" */'../../audio');
|
||||
}
|
||||
|
||||
export function Directory () {
|
||||
return import(/* webpackChunkName: "features/directory" */'../../directory');
|
||||
}
|
||||
|
Reference in New Issue
Block a user