From 04c5f29f58ae0100a92259c94d0102c6bc03d168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Fri, 13 Oct 2017 18:01:14 +0200 Subject: [PATCH] Initial doodle support --- .../compose/components/compose_form.js | 2 + .../compose/components/doodle_button.js | 41 ++++++++++++ .../containers/doodle_button_container.js | 33 ++++++++++ .../features/ui/components/doodle_modal.js | 65 +++++++++++++++++++ .../features/ui/components/modal_root.js | 4 +- .../styles/mastodon/components.scss | 6 ++ package.json | 1 + yarn.lock | 4 ++ 8 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 app/javascript/mastodon/features/compose/components/doodle_button.js create mode 100644 app/javascript/mastodon/features/compose/containers/doodle_button_container.js create mode 100644 app/javascript/mastodon/features/ui/components/doodle_modal.js diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 6eb01123e..ee96dd58a 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 SpoilerButtonContainer from '../containers/spoiler_button_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; @@ -204,6 +205,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 000000000..0af02458f --- /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 000000000..e1fc894f9 --- /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 000000000..7f91b848d --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/doodle_modal.js @@ -0,0 +1,65 @@ +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.sketcher.clear(); + } + } + + 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'); + + // pre-fill with white + this.sketcher.context.fillStyle = 'white'; + this.sketcher.context.fillRect(0, 0, elem.width, elem.height); + + // .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 d8e034554..a45ec63f6 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -8,6 +8,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 FocalPointModal from './focal_point_modal'; import { @@ -23,6 +24,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, @@ -53,7 +55,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/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index c2965a5d7..9075c6588 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4065,6 +4065,7 @@ a.status-card { } .boost-modal, +.doodle-modal, .confirmation-modal, .report-modal, .actions-modal, @@ -4097,6 +4098,10 @@ a.status-card { } } +.doodle-modal { + width: unset; +} + .actions-modal { .status { background: $white; @@ -4120,6 +4125,7 @@ a.status-card { } } +.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 b4d81d603..3f6f4c7fd 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "private": true, "dependencies": { "array-includes": "^3.0.3", + "atrament": "^0.2.3", "autoprefixer": "^8.6.5", "axios": "~0.16.2", "babel-core": "^6.26.3", diff --git a/yarn.lock b/yarn.lock index e558ea664..fe5c197d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -574,6 +574,10 @@ atob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a" +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"