about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2019-08-15 15:13:26 +0200
committerGitHub <noreply@github.com>2019-08-15 15:13:26 +0200
commit28636f43e4b0c04befa243b847c38e81c90f1289 (patch)
treec0c2210eb09026e02dde629cdb6899893c360437 /app/javascript
parentf178a01c11a4af077926dd035a0c4c44f6b3985c (diff)
Add OCR tool to media editing modal (#11566)
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/mastodon/features/compose/components/upload_form.js3
-rw-r--r--app/javascript/mastodon/features/compose/components/upload_progress.js9
-rw-r--r--app/javascript/mastodon/features/ui/components/focal_point_modal.js60
-rw-r--r--app/javascript/styles/mastodon/components.scss81
4 files changed, 125 insertions, 28 deletions
diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js
index 9ff2aa0fa..c6eac554e 100644
--- a/app/javascript/mastodon/features/compose/components/upload_form.js
+++ b/app/javascript/mastodon/features/compose/components/upload_form.js
@@ -4,6 +4,7 @@ import UploadProgressContainer from '../containers/upload_progress_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import UploadContainer from '../containers/upload_container';
 import SensitiveButtonContainer from '../containers/sensitive_button_container';
+import { FormattedMessage } from 'react-intl';
 
 export default class UploadForm extends ImmutablePureComponent {
 
@@ -16,7 +17,7 @@ export default class UploadForm extends ImmutablePureComponent {
 
     return (
       <div className='compose-form__upload-wrapper'>
-        <UploadProgressContainer />
+        <UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} />
 
         <div className='compose-form__uploads-wrapper'>
           {mediaIds.map(id => (
diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js
index cbe58f573..b0bfe0c9a 100644
--- a/app/javascript/mastodon/features/compose/components/upload_progress.js
+++ b/app/javascript/mastodon/features/compose/components/upload_progress.js
@@ -2,7 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import Motion from '../../ui/util/optional_motion';
 import spring from 'react-motion/lib/spring';
-import { FormattedMessage } from 'react-intl';
 import Icon from 'mastodon/components/icon';
 
 export default class UploadProgress extends React.PureComponent {
@@ -10,10 +9,12 @@ export default class UploadProgress extends React.PureComponent {
   static propTypes = {
     active: PropTypes.bool,
     progress: PropTypes.number,
+    icon: PropTypes.string.isRequired,
+    message: PropTypes.node.isRequired,
   };
 
   render () {
-    const { active, progress } = this.props;
+    const { active, progress, icon, message } = this.props;
 
     if (!active) {
       return null;
@@ -22,11 +23,11 @@ export default class UploadProgress extends React.PureComponent {
     return (
       <div className='upload-progress'>
         <div className='upload-progress__icon'>
-          <Icon id='upload' />
+          <Icon id={icon} />
         </div>
 
         <div className='upload-progress__message'>
-          <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
+          {message}
 
           <div className='upload-progress__backdrop'>
             <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index e15bf69d6..b21d5c9d4 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
@@ -10,6 +10,11 @@ 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 { TesseractWorker } from 'tesseract.js';
+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';
 
 const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
@@ -29,6 +34,12 @@ const mapDispatchToProps = (dispatch, { id }) => ({
 
 });
 
+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 {
@@ -47,6 +58,7 @@ class FocalPointModal extends ImmutablePureComponent {
     dragging: false,
     description: '',
     dirty: false,
+    progress: 0,
   };
 
   componentWillMount () {
@@ -133,9 +145,27 @@ class FocalPointModal extends ImmutablePureComponent {
     this.node = c;
   }
 
+  handleTextDetection = () => {
+    const { media } = this.props;
+
+    const worker = new TesseractWorker({
+      workerPath: `${assetHost}/packs/ocr/worker.min.js`,
+      corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`,
+      langPath: `${assetHost}/ocr/lang-data`,
+    });
+
+    this.setState({ detecting: true });
+
+    worker.recognize(media.get('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 }));
+  }
+
   render () {
     const { media, intl, onClose } = this.props;
-    const { x, y, dragging, description, dirty } = this.state;
+    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;
@@ -158,15 +188,27 @@ class FocalPointModal extends ImmutablePureComponent {
 
             <label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label>
 
-            <textarea
-              id='upload-modal__description'
-              className='setting-text light'
-              value={description}
-              onChange={this.handleChange}
-              autoFocus
-            />
+            <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={420} text={detecting ? '' : description} />
+            </div>
 
-            <Button disabled={!dirty} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
+            <Button disabled={!dirty || detecting || length(description) > 420} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
           </div>
 
           <div className='report-modal__statuses'>
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index f2967a398..893be9095 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3,6 +3,27 @@
   -ms-overflow-style: -ms-autohiding-scrollbar;
 }
 
+.link-button {
+  display: block;
+  font-size: 15px;
+  line-height: 20px;
+  color: $ui-highlight-color;
+  border: 0;
+  background: transparent;
+  padding: 0;
+  cursor: pointer;
+
+  &:hover,
+  &:active {
+    text-decoration: underline;
+  }
+
+  &:disabled {
+    color: $ui-primary-color;
+    cursor: default;
+  }
+}
+
 .button {
   background-color: $ui-highlight-color;
   border: 10px none;
@@ -637,18 +658,6 @@
     .character-counter__wrapper {
       align-self: center;
       margin-right: 4px;
-
-      .character-counter {
-        cursor: default;
-        font-family: $font-sans-serif, sans-serif;
-        font-size: 14px;
-        font-weight: 600;
-        color: $lighter-text-color;
-
-        &.character-counter--over {
-          color: $warning-red;
-        }
-      }
     }
   }
 
@@ -665,6 +674,18 @@
   }
 }
 
+.character-counter {
+  cursor: default;
+  font-family: $font-sans-serif, sans-serif;
+  font-size: 14px;
+  font-weight: 600;
+  color: $lighter-text-color;
+
+  &.character-counter--over {
+    color: $warning-red;
+  }
+}
+
 .no-reduce-motion .spoiler-input {
   transition: height 0.4s ease, opacity 0.4s ease;
 }
@@ -4555,16 +4576,48 @@ a.status-card.compact:hover {
     padding: 10px;
     font-family: inherit;
     font-size: 14px;
-    resize: vertical;
+    resize: none;
     border: 0;
     outline: 0;
     border-radius: 4px;
     border: 1px solid $ui-secondary-color;
-    margin-bottom: 20px;
+    min-height: 100px;
+    max-height: 50vh;
+    margin-bottom: 10px;
 
     &:focus {
       border: 1px solid darken($ui-secondary-color, 8%);
     }
+
+    &__wrapper {
+      background: $white;
+      border: 1px solid $ui-secondary-color;
+      margin-bottom: 10px;
+      border-radius: 4px;
+
+      .setting-text {
+        border: 0;
+        margin-bottom: 0;
+        border-radius: 0;
+
+        &:focus {
+          border: 0;
+        }
+      }
+
+      &__modifiers {
+        color: $inverted-text-color;
+        font-family: inherit;
+        font-size: 14px;
+        background: $white;
+      }
+    }
+
+    &__toolbar {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 20px;
+    }
   }
 
   .setting-text-label {