250 lines
7.5 KiB
JavaScript
Executable file
250 lines
7.5 KiB
JavaScript
Executable file
import React from 'react';
|
|
import ReactDOM from 'react-dom';
|
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
import Flux from '../lib/flux';
|
|
import classNames from 'classnames';
|
|
import {Permissions} from '../Constants';
|
|
import GuildVerificationStore from '../stores/GuildVerificationStore';
|
|
import SelectedChannelStore from '../stores/SelectedChannelStore';
|
|
import SelectedGuildStore from '../stores/SelectedGuildStore';
|
|
import PermissionMixin from '../mixins/PermissionMixin';
|
|
import ChannelStore from '../stores/ChannelStore';
|
|
import LayerStore from '../stores/LayerStore';
|
|
import ModalActionCreators from '../actions/ModalActionCreators';
|
|
import ModalStore from '../stores/ModalStore';
|
|
import i18n from '../i18n';
|
|
import UploadMixin from '../mixins/web/UploadMixin';
|
|
import TimerMixin from 'react-timer-mixin';
|
|
import UploadError from './warnings/UploadError';
|
|
import '../styles/common/sprites.styl';
|
|
import '../styles/upload_modal.styl';
|
|
|
|
const REMOVE_AFTER_TIME = 1000;
|
|
|
|
const UploadArea = React.createClass({
|
|
mixins: [
|
|
Flux.StoreListenerMixin(ChannelStore, LayerStore, SelectedChannelStore, SelectedGuildStore),
|
|
PermissionMixin,
|
|
PureRenderMixin,
|
|
UploadMixin,
|
|
TimerMixin,
|
|
],
|
|
|
|
getInitialState() {
|
|
return {
|
|
dragging: false,
|
|
requireConfirm: true,
|
|
};
|
|
},
|
|
|
|
getStateFromStores() {
|
|
return {
|
|
hasLayers: LayerStore.hasLayers(),
|
|
channel: SelectedChannelStore.getChannelId() && ChannelStore.getChannel(SelectedChannelStore.getChannelId()),
|
|
};
|
|
},
|
|
|
|
componentDidMount() {
|
|
document.body.addEventListener('dragover', this.handleDragOver, false);
|
|
document.body.addEventListener('drop', this.handleDragLeave, false);
|
|
ReactDOM.findDOMNode(this).addEventListener('dragleave', this.handleDragLeave, false);
|
|
ReactDOM.findDOMNode(this).addEventListener('drop', this.handleDrop, false);
|
|
},
|
|
|
|
componentWillUnmount() {
|
|
document.body.removeEventListener('dragover', this.handleDragOver, false);
|
|
document.body.removeEventListener('drop', this.handleDragLeave, false);
|
|
ReactDOM.findDOMNode(this).removeEventListener('dragleave', this.handleDragLeave, false);
|
|
ReactDOM.findDOMNode(this).removeEventListener('drop', this.handleDrop, false);
|
|
},
|
|
|
|
render() {
|
|
const showDragging = this.state.dragging && this.canDropFile();
|
|
|
|
let title = i18n.Messages.UPLOAD_AREA_TITLE;
|
|
if (!this.state.requireConfirm) {
|
|
title = i18n.Messages.UPLOAD_AREA_TITLE_NO_CONFIRMATION;
|
|
}
|
|
|
|
let sprites = null;
|
|
if (showDragging) {
|
|
sprites = (
|
|
<div>
|
|
<div className="sprite sparkle one" />
|
|
<div className="sprite sparkle two" />
|
|
<div className="sprite light one" />
|
|
<div className="sprite light two" />
|
|
<div className="sprite cross one" />
|
|
<div className="sprite cross two" />
|
|
<div className="sprite pop one" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={classNames('upload-area', {'upload-modal-in': showDragging})}>
|
|
<div className="upload-drop-modal">
|
|
{sprites}
|
|
|
|
<div className="bgScale" />
|
|
<div className="inner">
|
|
<div className="icons">
|
|
<div className="wrap-one"><div className="icon one" /></div>
|
|
<div className="wrap-three"><div className="icon three" /></div>
|
|
<div className="wrap-two"><div className="icon two" /></div>
|
|
</div>
|
|
<div className="title">
|
|
{title}
|
|
</div>
|
|
<div className="instructions">
|
|
<pre>
|
|
{i18n.Messages.UPLOAD_AREA_HELP}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
|
|
// DND
|
|
|
|
canDropFile() {
|
|
const {channel, hasLayers} = this.state;
|
|
return (
|
|
channel &&
|
|
!hasLayers &&
|
|
(channel.isPrivate() ||
|
|
(this.can(Permissions.ATTACH_FILES, this.state.channel) &&
|
|
GuildVerificationStore.canChatInGuild(SelectedGuildStore.getGuildId())))
|
|
);
|
|
},
|
|
|
|
isAllDropFiles(items) {
|
|
for (let i = 0; i < items.length; i++) {
|
|
try {
|
|
const item = items[i].webkitGetAsEntry() || items[i].getAsEntry();
|
|
if (item && !item.isFile) {
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
// For DnD glitching and not vanishing within an interval
|
|
dragOverTimeout: null,
|
|
|
|
// Explicitly prevent using Discord as a webpage by dropping links into it
|
|
preventUnwantedDrop(evt, errorModal = false) {
|
|
const dataTransfer = evt.dataTransfer;
|
|
|
|
const hasLinkOrTextChannels =
|
|
dataTransfer.types instanceof Array &&
|
|
dataTransfer.types.indexOf('text/uri-list') != -1 &&
|
|
dataTransfer.types.indexOf('application/json') == -1;
|
|
|
|
const hasNonFile = dataTransfer.items != null && !this.isAllDropFiles(dataTransfer.items);
|
|
|
|
if (hasLinkOrTextChannels || hasNonFile) {
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
dataTransfer.effectAllowed = 'none';
|
|
dataTransfer.dropEffect = 'none';
|
|
|
|
if (errorModal) {
|
|
this.setState({
|
|
dragging: false,
|
|
});
|
|
ModalActionCreators.push(UploadError, {
|
|
title: i18n.Messages.UPLOAD_AREA_INVALID_FILE_TYPE_TITLE,
|
|
help: i18n.Messages.UPLOAD_AREA_INVALID_FILE_TYPE_HELP,
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
handleDragOver(e) {
|
|
if (!this.preventUnwantedDrop(e)) return false;
|
|
|
|
// Just use 'move' for everything in the app.
|
|
e.dataTransfer.dropEffect = 'move';
|
|
|
|
const openModal = ModalStore.getModal();
|
|
if (openModal && openModal.modal === UploadError) {
|
|
ModalActionCreators.pop();
|
|
}
|
|
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
if (!this.state.dragging) {
|
|
// The structure of data transfer types varies on webkit/firefox.
|
|
if (
|
|
(e.dataTransfer.types instanceof DOMStringList && e.dataTransfer.types.contains('application/x-moz-file')) ||
|
|
(e.dataTransfer.types instanceof Array && e.dataTransfer.types.indexOf('Files') != -1)
|
|
) {
|
|
// Use transactional setState, as it seems like sometimes on windows the events fire in such a way
|
|
// that we end up transitioning back to dragging: true when they leave.
|
|
this.setState(prevState => {
|
|
if (!prevState.dragging) {
|
|
return {dragging: true, requireConfirm: !e.shiftKey};
|
|
}
|
|
|
|
return {};
|
|
});
|
|
}
|
|
} else if (this.state.dragging) {
|
|
if (e.shiftKey !== !this.state.requireConfirm) {
|
|
this.setState({requireConfirm: !e.shiftKey});
|
|
}
|
|
}
|
|
|
|
// In case the DnD overlay gets 'frozen', remove it after a second of no dragOver event firing.
|
|
this.clearTimeout(this.dragOverTimeout);
|
|
this.dragOverTimeout = this.setTimeout(() => {
|
|
this.setState({dragging: false, requireConfirm: true});
|
|
}, REMOVE_AFTER_TIME);
|
|
},
|
|
|
|
handleDragLeave(e) {
|
|
if (this.state.dragging) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
this.clearDragging();
|
|
}
|
|
},
|
|
|
|
clearDragging() {
|
|
this.setState({dragging: false, requireConfirm: true});
|
|
},
|
|
|
|
handleDrop(e) {
|
|
if (!this.preventUnwantedDrop(e, true)) return false;
|
|
|
|
if (this.state.dragging) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (this.canDropFile()) {
|
|
this.promptToUpload(e.dataTransfer.files, this.state.channel.id, true, this.state.requireConfirm);
|
|
}
|
|
|
|
this.clearDragging();
|
|
}
|
|
},
|
|
});
|
|
|
|
export default UploadArea;
|
|
|
|
|
|
|
|
// WEBPACK FOOTER //
|
|
// ./discord_app/components/UploadArea.js
|