diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 097dccfb4b..5b06cef7c5 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import UploadButtonContainer from '../containers/upload_button_container'; +import DoodleButtonContainer from '../containers/doodle_button_container'; import { defineMessages, injectIntl } from 'react-intl'; import Collapsable from '../../../components/collapsable'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; @@ -249,6 +250,7 @@ export default class ComposeForm extends ImmutablePureComponent {
+ diff --git a/app/javascript/mastodon/features/compose/components/doodle_button.js b/app/javascript/mastodon/features/compose/components/doodle_button.js new file mode 100644 index 0000000000..0af02458fc --- /dev/null +++ b/app/javascript/mastodon/features/compose/components/doodle_button.js @@ -0,0 +1,41 @@ +import React from 'react'; +import IconButton from '../../../components/icon_button'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + doodle: { id: 'doodle_button.label', defaultMessage: 'Add a drawing' }, +}); + +const iconStyle = { + height: null, + lineHeight: '27px', +}; + +@injectIntl +export default class UploadButton extends ImmutablePureComponent { + + static propTypes = { + disabled: PropTypes.bool, + onOpenCanvas: PropTypes.func.isRequired, + style: PropTypes.object, + intl: PropTypes.object.isRequired, + }; + + handleClick = () => { + this.props.onOpenCanvas(); + } + + render () { + + const { intl, disabled } = this.props; + + return ( +
+ +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/compose/containers/doodle_button_container.js b/app/javascript/mastodon/features/compose/containers/doodle_button_container.js new file mode 100644 index 0000000000..e1fc894f94 --- /dev/null +++ b/app/javascript/mastodon/features/compose/containers/doodle_button_container.js @@ -0,0 +1,33 @@ +import { connect } from 'react-redux'; +import DoodleButton from '../components/doodle_button'; +import { openModal } from '../../../actions/modal'; +import { uploadCompose } from '../../../actions/compose'; + +const mapStateToProps = state => ({ + disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), +}); + +//https://stackoverflow.com/questions/35940290/how-to-convert-base64-string-to-javascript-file-object-like-as-from-file-input-f +function dataURLtoFile(dataurl, filename) { + let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], + bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); + while(n--){ + u8arr[n] = bstr.charCodeAt(n); + } + return new File([u8arr], filename, { type: mime }); +} + +const mapDispatchToProps = dispatch => ({ + + onOpenCanvas () { + dispatch(openModal('DOODLE', { + status, + onDoodleSubmit: (b64data) => { + dispatch(uploadCompose([dataURLtoFile(b64data, 'doodle.png')])); + }, + })); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DoodleButton); diff --git a/app/javascript/mastodon/features/ui/components/doodle_modal.js b/app/javascript/mastodon/features/ui/components/doodle_modal.js new file mode 100644 index 0000000000..d13f9604a5 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/doodle_modal.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Button from '../../../components/button'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Atrament from 'atrament'; // the doodling library + +export default class DoodleModal extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + onDoodleSubmit: PropTypes.func.isRequired, // gets the base64 as argument + onClose: PropTypes.func.isRequired, + }; + + handleKeyUp = (e) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + this.clearScreen(); + } + } + + clearScreen () { + this.sketcher.context.fillStyle = 'white'; + this.sketcher.context.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + } + + handleDone = () => { + this.props.onDoodleSubmit(this.sketcher.toImage()); + this.sketcher.destroy(); + this.props.onClose(); + } + + setCanvasRef = (elem) => { + this.canvas = elem; + if (elem) { + this.sketcher = new Atrament(elem, 500, 500, 'black'); + + this.clearScreen(); + + // .smoothing looks good with mouse but works really poorly with a tablet + this.sketcher.smoothing = false; + + // There's a bunch of options we should add UI controls for later + // ref: https://github.com/jakubfiala/atrament.js + } + } + + render () { + return ( +
+
+ +
+ +
+
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 88a4d0a59d..b4a0b5d939 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -7,6 +7,7 @@ import ActionsModal from './actions_modal'; import MediaModal from './media_modal'; import VideoModal from './video_modal'; import BoostModal from './boost_modal'; +import DoodleModal from './doodle_modal'; import ConfirmationModal from './confirmation_modal'; import { OnboardingModal, @@ -21,6 +22,7 @@ const MODAL_COMPONENTS = { 'ONBOARDING': OnboardingModal, 'VIDEO': () => Promise.resolve({ default: VideoModal }), 'BOOST': () => Promise.resolve({ default: BoostModal }), + 'DOODLE': () => Promise.resolve({ default: DoodleModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), 'MUTE': MuteModal, 'REPORT': ReportModal, @@ -88,7 +90,7 @@ export default class ModalRoot extends React.PureComponent { } renderLoading = modalId => () => { - return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? : null; + return ['MEDIA', 'VIDEO', 'BOOST', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? : null; } renderError = (props) => { diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 8ecc0b91bb..7056e22088 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3874,6 +3874,7 @@ button.icon-button.active i.fa-retweet { } .boost-modal, +.doodle-modal, .confirmation-modal, .report-modal, .actions-modal, @@ -3892,6 +3893,10 @@ button.icon-button.active i.fa-retweet { } } +.doodle-modal { + width: unset; +} + .actions-modal { .status { background: $white; @@ -3915,6 +3920,7 @@ button.icon-button.active i.fa-retweet { } } +.doodle-modal__action-bar, .boost-modal__action-bar, .confirmation-modal__action-bar, .mute-modal__action-bar, diff --git a/package.json b/package.json index 93e254abc4..5dc2a9144d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "private": true, "dependencies": { "array-includes": "^3.0.3", + "atrament": "^0.2.3", "autoprefixer": "^7.1.2", "axios": "^0.16.2", "babel-core": "^6.25.0", diff --git a/yarn.lock b/yarn.lock index f32c9acebd..a1c29badd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -300,6 +300,10 @@ atob@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" +atrament@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/atrament/-/atrament-0.2.3.tgz#6ccbc0daa6d3f25e5aeaeb31befeb78e86980348" + autoprefixer@^6.3.1: version "6.7.7" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"