about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch')
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js49
-rw-r--r--app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js27
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js55
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js22
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss9
5 files changed, 147 insertions, 15 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index f98cb7bf8..c00fb5c40 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -30,6 +30,11 @@ export const COMPOSE_UPLOAD_FAIL     = 'COMPOSE_UPLOAD_FAIL';
 export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
 export const COMPOSE_UPLOAD_UNDO     = 'COMPOSE_UPLOAD_UNDO';
 
+export const THUMBNAIL_UPLOAD_REQUEST  = 'THUMBNAIL_UPLOAD_REQUEST';
+export const THUMBNAIL_UPLOAD_SUCCESS  = 'THUMBNAIL_UPLOAD_SUCCESS';
+export const THUMBNAIL_UPLOAD_FAIL     = 'THUMBNAIL_UPLOAD_FAIL';
+export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
+
 export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
 export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
 export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
@@ -289,6 +294,49 @@ export function uploadCompose(files) {
   };
 };
 
+export const uploadThumbnail = (id, file) => (dispatch, getState) => {
+  dispatch(uploadThumbnailRequest());
+
+  const total = file.size;
+  const data = new FormData();
+
+  data.append('thumbnail', file);
+
+  api(getState).put(`/api/v1/media/${id}`, data, {
+    onUploadProgress: ({ loaded }) => {
+      dispatch(uploadThumbnailProgress(loaded, total));
+    },
+  }).then(({ data }) => {
+    dispatch(uploadThumbnailSuccess(data));
+  }).catch(error => {
+    dispatch(uploadThumbnailFail(id, error));
+  });
+};
+
+export const uploadThumbnailRequest = () => ({
+  type: THUMBNAIL_UPLOAD_REQUEST,
+  skipLoading: true,
+});
+
+export const uploadThumbnailProgress = (loaded, total) => ({
+  type: THUMBNAIL_UPLOAD_PROGRESS,
+  loaded,
+  total,
+  skipLoading: true,
+});
+
+export const uploadThumbnailSuccess = media => ({
+  type: THUMBNAIL_UPLOAD_SUCCESS,
+  media,
+  skipLoading: true,
+});
+
+export const uploadThumbnailFail = error => ({
+  type: THUMBNAIL_UPLOAD_FAIL,
+  error,
+  skipLoading: true,
+});
+
 export function changeUploadCompose(id, params) {
   return (dispatch, getState) => {
     dispatch(changeUploadComposeRequest());
@@ -307,6 +355,7 @@ export function changeUploadComposeRequest() {
     skipLoading: true,
   };
 };
+
 export function changeUploadComposeSuccess(media) {
   return {
     type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js
index 956f16734..27300f020 100644
--- a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js
@@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Toggle from 'react-toggle';
 import AsyncSelect from 'react-select/async';
+import { NonceProvider } from 'react-select';
 import SettingToggle from '../../notifications/components/setting_toggle';
 
 const messages = defineMessages({
@@ -58,18 +59,20 @@ class ColumnSettings extends React.PureComponent {
           {this.modeLabel(mode)}
         </span>
 
-        <AsyncSelect
-          isMulti
-          autoFocus
-          value={this.tags(mode)}
-          onChange={this.onSelect(mode)}
-          loadOptions={this.props.onLoad}
-          className='column-select__container'
-          classNamePrefix='column-select'
-          name='tags'
-          placeholder={this.props.intl.formatMessage(messages.placeholder)}
-          noOptionsMessage={this.noOptionsMessage}
-        />
+        <NonceProvider nonce={document.querySelector('meta[name=style-nonce]').content}>
+          <AsyncSelect
+            isMulti
+            autoFocus
+            value={this.tags(mode)}
+            onChange={this.onSelect(mode)}
+            loadOptions={this.props.onLoad}
+            className='column-select__container'
+            classNamePrefix='column-select'
+            name='tags'
+            placeholder={this.props.intl.formatMessage(messages.placeholder)}
+            noOptionsMessage={this.noOptionsMessage}
+          />
+        </NonceProvider>
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
index 2f4200e10..c8b0e4bd7 100644
--- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { connect } from 'react-redux';
 import classNames from 'classnames';
-import { changeUploadCompose } from 'flavours/glitch/actions/compose';
+import { changeUploadCompose, uploadThumbnail } from 'flavours/glitch/actions/compose';
 import { getPointerPosition } from 'flavours/glitch/features/video';
 import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import IconButton from 'flavours/glitch/components/icon_button';
@@ -23,11 +23,13 @@ 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' },
+  chooseImage: { id: 'upload_modal.choose_image', defaultMessage: 'Choose image' },
 });
 
 const mapStateToProps = (state, { id }) => ({
   media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
   account: state.getIn(['accounts', me]),
+  isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
 });
 
 const mapDispatchToProps = (dispatch, { id }) => ({
@@ -36,6 +38,10 @@ const mapDispatchToProps = (dispatch, { id }) => ({
     dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
   },
 
+  onSelectThumbnail: files => {
+    dispatch(uploadThumbnail(id, files[0]));
+  },
+
 });
 
 const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
@@ -81,6 +87,9 @@ class FocalPointModal extends ImmutablePureComponent {
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
     account: ImmutablePropTypes.map.isRequired,
+    isUploadingThumbnail: PropTypes.bool,
+    onSave: PropTypes.func.isRequired,
+    onSelectThumbnail: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
   };
@@ -235,13 +244,29 @@ class FocalPointModal extends ImmutablePureComponent {
     }).catch(() => this.setState({ detecting: false }));
   }
 
+  handleThumbnailChange = e => {
+    if (e.target.files.length > 0) {
+      this.setState({ dirty: true });
+      this.props.onSelectThumbnail(e.target.files);
+    }
+  }
+
+  setFileInputRef = c => {
+    this.fileInput = c;
+  }
+
+  handleFileInputClick = () => {
+    this.fileInput.click();
+  }
+
   render () {
-    const { media, intl, account, onClose } = this.props;
+    const { media, intl, account, onClose, isUploadingThumbnail } = 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 thumbnailable = ['audio', 'video'].includes(media.get('type'));
 
     const previewRatio  = 16/9;
     const previewWidth  = 200;
@@ -268,6 +293,30 @@ class FocalPointModal extends ImmutablePureComponent {
           <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>}
 
+            {thumbnailable && (
+              <React.Fragment>
+                <label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
+
+                <Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
+
+                <label>
+                  <span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
+
+                  <input
+                    id='upload-modal__thumbnail'
+                    ref={this.setFileInputRef}
+                    type='file'
+                    accept='image/png,image/jpeg'
+                    onChange={this.handleThumbnailChange}
+                    style={{ display: 'none' }}
+                    disabled={isUploadingThumbnail}
+                  />
+                </label>
+
+                <hr className='setting-divider' />
+              </React.Fragment>
+            )}
+
             <label className='setting-text-label' htmlFor='upload-modal__description'>
               {descriptionLabel}
             </label>
@@ -293,7 +342,7 @@ class FocalPointModal extends ImmutablePureComponent {
               <CharacterCounter max={1500} text={detecting ? '' : description} />
             </div>
 
-            <Button disabled={!dirty || detecting || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
+            <Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
           </div>
 
           <div className='focal-point-modal__content'>
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index f758d5c93..a2cac88ac 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -15,6 +15,10 @@ import {
   COMPOSE_UPLOAD_FAIL,
   COMPOSE_UPLOAD_UNDO,
   COMPOSE_UPLOAD_PROGRESS,
+  THUMBNAIL_UPLOAD_REQUEST,
+  THUMBNAIL_UPLOAD_SUCCESS,
+  THUMBNAIL_UPLOAD_FAIL,
+  THUMBNAIL_UPLOAD_PROGRESS,
   COMPOSE_SUGGESTIONS_CLEAR,
   COMPOSE_SUGGESTIONS_READY,
   COMPOSE_SUGGESTION_SELECT,
@@ -77,6 +81,8 @@ const initialState = ImmutableMap({
   is_uploading: false,
   is_changing_upload: false,
   progress: 0,
+  isUploadingThumbnail: false,
+  thumbnailProgress: 0,
   media_attachments: ImmutableList(),
   pending_media_attachments: 0,
   poll: null,
@@ -433,6 +439,22 @@ export default function compose(state = initialState, action) {
     return removeMedia(state, action.media_id);
   case COMPOSE_UPLOAD_PROGRESS:
     return state.set('progress', Math.round((action.loaded / action.total) * 100));
+  case THUMBNAIL_UPLOAD_REQUEST:
+    return state.set('isUploadingThumbnail', true);
+  case THUMBNAIL_UPLOAD_PROGRESS:
+    return state.set('thumbnailProgress', Math.round((action.loaded / action.total) * 100));
+  case THUMBNAIL_UPLOAD_FAIL:
+    return state.set('isUploadingThumbnail', false);
+  case THUMBNAIL_UPLOAD_SUCCESS:
+    return state
+      .set('isUploadingThumbnail', false)
+      .update('media_attachments', list => list.map(item => {
+        if (item.get('id') === action.media.id) {
+          return fromJS(action.media);
+        }
+
+        return item;
+      }));
   case COMPOSE_MENTION:
     return state.withMutations(map => {
       map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index 75bddeefc..4310f620a 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -555,6 +555,15 @@
   }
 }
 
+.setting-divider {
+  background: transparent;
+  border: 0;
+  margin: 0;
+  width: 100%;
+  height: 1px;
+  margin-bottom: 29px;
+}
+
 .report-modal__comment {
   padding: 20px;
   border-right: 1px solid $ui-secondary-color;