diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index 8a1992fca..9f330f0df 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::MediaController < Api::BaseController
respond_to :json
def create
- @media = current_account.media_attachments.create!(file: media_params[:file])
+ @media = current_account.media_attachments.create!(media_params)
render json: @media, serializer: REST::MediaAttachmentSerializer
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422
@@ -18,10 +18,16 @@ class Api::V1::MediaController < Api::BaseController
render json: processing_error, status: 500
end
+ def update
+ @media = current_account.media_attachments.where(status_id: nil).find(params[:id])
+ @media.update!(media_params)
+ render json: @media, serializer: REST::MediaAttachmentSerializer
+ end
+
private
def media_params
- params.permit(:file)
+ params.permit(:file, :description)
end
def file_type_error
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 9f10a8c15..8be5b939f 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -37,6 +37,10 @@ export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
+export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST';
+export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
+export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
+
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@@ -165,6 +169,40 @@ export function uploadCompose(files) {
};
};
+export function changeUploadCompose(id, description) {
+ return (dispatch, getState) => {
+ dispatch(changeUploadComposeRequest());
+
+ api(getState).put(`/api/v1/media/${id}`, { description }).then(response => {
+ dispatch(changeUploadComposeSuccess(response.data));
+ }).catch(error => {
+ dispatch(changeUploadComposeFail(id, error));
+ });
+ };
+};
+
+export function changeUploadComposeRequest() {
+ return {
+ type: COMPOSE_UPLOAD_CHANGE_REQUEST,
+ skipLoading: true,
+ };
+};
+export function changeUploadComposeSuccess(media) {
+ return {
+ type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
+ media: media,
+ skipLoading: true,
+ };
+};
+
+export function changeUploadComposeFail(error) {
+ return {
+ type: COMPOSE_UPLOAD_CHANGE_FAIL,
+ error: error,
+ skipLoading: true,
+ };
+};
+
export function uploadComposeRequest() {
return {
type: COMPOSE_UPLOAD_REQUEST,
diff --git a/app/javascript/mastodon/components/extended_video_player.js b/app/javascript/mastodon/components/extended_video_player.js
index 5ab5e9e58..f8bd067e8 100644
--- a/app/javascript/mastodon/components/extended_video_player.js
+++ b/app/javascript/mastodon/components/extended_video_player.js
@@ -5,6 +5,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
+ alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
time: PropTypes.number,
@@ -31,15 +32,20 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
}
render () {
+ const { src, muted, controls, alt } = this.props;
+
return (
);
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index a81409871..38b26b1fc 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -136,7 +136,7 @@ class Item extends React.PureComponent {
onClick={this.handleClick}
target='_blank'
>
-
+
);
} else if (attachment.get('type') === 'gifv') {
@@ -146,6 +146,7 @@ class Item extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index b8c5e885a..ad5493f8c 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -90,10 +90,6 @@ export function MediaGallery () {
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
}
-export function VideoPlayer () {
- return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
-}
-
export function Video () {
return import(/* webpackChunkName: "features/video" */'../../video');
}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index f228e434b..069264ef5 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -104,6 +104,7 @@ export default class Video extends React.PureComponent {
static propTypes = {
preview: PropTypes.string,
src: PropTypes.string.isRequired,
+ alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
sensitive: PropTypes.bool,
@@ -247,7 +248,7 @@ export default class Video extends React.PureComponent {
}
render () {
- const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl } = this.props;
+ const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt } = this.props;
const { progress, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
return (
@@ -260,6 +261,7 @@ export default class Video extends React.PureComponent {
loop
role='button'
tabIndex='0'
+ aria-label={alt}
width={width}
height={height}
onClick={this.togglePlay}
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 9d39584fc..082d4d370 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -22,6 +22,9 @@ import {
COMPOSE_VISIBILITY_CHANGE,
COMPOSE_COMPOSING_CHANGE,
COMPOSE_EMOJI_INSERT,
+ COMPOSE_UPLOAD_CHANGE_REQUEST,
+ COMPOSE_UPLOAD_CHANGE_SUCCESS,
+ COMPOSE_UPLOAD_CHANGE_FAIL,
} from '../actions/compose';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
@@ -220,15 +223,15 @@ export default function compose(state = initialState, action) {
map.set('idempotencyKey', uuid());
});
case COMPOSE_SUBMIT_REQUEST:
+ case COMPOSE_UPLOAD_CHANGE_REQUEST:
return state.set('is_submitting', true);
case COMPOSE_SUBMIT_SUCCESS:
return clearAll(state);
case COMPOSE_SUBMIT_FAIL:
+ case COMPOSE_UPLOAD_CHANGE_FAIL:
return state.set('is_submitting', false);
case COMPOSE_UPLOAD_REQUEST:
- return state.withMutations(map => {
- map.set('is_uploading', true);
- });
+ return state.set('is_uploading', true);
case COMPOSE_UPLOAD_SUCCESS:
return appendMedia(state, fromJS(action.media));
case COMPOSE_UPLOAD_FAIL:
@@ -256,6 +259,16 @@ export default function compose(state = initialState, action) {
}
case COMPOSE_EMOJI_INSERT:
return insertEmoji(state, action.position, action.emoji);
+ case COMPOSE_UPLOAD_CHANGE_SUCCESS:
+ return state
+ .set('is_submitting', false)
+ .update('media_attachments', list => list.map(item => {
+ if (item.get('id') === action.media.id) {
+ return item.set('description', action.media.description);
+ }
+
+ return item;
+ }));
default:
return state;
}
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index da479347b..631cd7a13 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -335,12 +335,52 @@
.compose-form__uploads-wrapper {
display: flex;
+ flex-direction: row;
padding: 5px;
+ flex-wrap: wrap;
}
.compose-form__upload {
flex: 1 1 0;
+ min-width: 40%;
margin: 5px;
+
+ &-description {
+ position: absolute;
+ z-index: 2;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ box-sizing: border-box;
+ background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
+ padding: 10px;
+ opacity: 0;
+ transition: opacity .1s ease;
+
+ input {
+ background: transparent;
+ color: $ui-secondary-color;
+ border: 0;
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: 500;
+
+ &:focus {
+ color: $white;
+ }
+ }
+
+ &.active {
+ opacity: 1;
+ }
+ }
+
+ .icon-button {
+ mix-blend-mode: difference;
+ }
}
.compose-form__upload-thumbnail {
@@ -352,13 +392,6 @@
width: 100%;
}
-.compose-form__upload-cancel {
- background-size: cover;
- border-radius: 4px;
- height: 100px;
- width: 100px;
-}
-
.compose-form__label {
display: block;
line-height: 24px;
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 4e19b3096..55addd66e 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -105,7 +105,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
href = Addressable::URI.parse(attachment['url']).normalize.to_s
- media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href)
+ media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href, description: attachment['name'].presence)
next if skip_download?
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index e4a974f96..25e41c209 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -16,6 +16,7 @@
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
+# description :text
#
require 'mime/types'
@@ -58,6 +59,7 @@ class MediaAttachment < ApplicationRecord
validates_attachment_size :file, less_than: 8.megabytes
validates :account, presence: true
+ validates :description, length: { maximum: 140 }, if: :local?
scope :attached, -> { where.not(status_id: nil) }
scope :unattached, -> { where(status_id: nil) }
@@ -78,6 +80,7 @@ class MediaAttachment < ApplicationRecord
shortcode
end
+ before_create :prepare_description, unless: :local?
before_create :set_shortcode
before_post_process :set_type_and_extension
before_save :set_meta
@@ -136,6 +139,10 @@ class MediaAttachment < ApplicationRecord
end
end
+ def prepare_description
+ self.description = description.strip[0...140] unless description.nil?
+ end
+
def set_type_and_extension
self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
extension = appropriate_extension
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index f94c3b9dc..4dbf6a444 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -89,12 +89,16 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer
class MediaAttachmentSerializer < ActiveModel::Serializer
include RoutingHelper
- attributes :type, :media_type, :url
+ attributes :type, :media_type, :url, :name
def type
'Document'
end
+ def name
+ object.description
+ end
+
def media_type
object.file_content_type
end
diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb
index f6e7c79d1..e6e9c8e82 100644
--- a/app/serializers/rest/media_attachment_serializer.rb
+++ b/app/serializers/rest/media_attachment_serializer.rb
@@ -4,7 +4,8 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :id, :type, :url, :preview_url,
- :remote_url, :text_url, :meta
+ :remote_url, :text_url, :meta,
+ :description
def id
object.id.to_s
diff --git a/config/routes.rb b/config/routes.rb
index cb7e84d7b..ad2d8fca2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -193,7 +193,7 @@ Rails.application.routes.draw do
get '/search', to: 'search#index', as: :search
resources :follows, only: [:create]
- resources :media, only: [:create]
+ resources :media, only: [:create, :update]
resources :apps, only: [:create]
resources :blocks, only: [:index]
resources :mutes, only: [:index]
diff --git a/db/migrate/20170927215609_add_description_to_media_attachments.rb b/db/migrate/20170927215609_add_description_to_media_attachments.rb
new file mode 100644
index 000000000..db8d76566
--- /dev/null
+++ b/db/migrate/20170927215609_add_description_to_media_attachments.rb
@@ -0,0 +1,5 @@
+class AddDescriptionToMediaAttachments < ActiveRecord::Migration[5.1]
+ def change
+ add_column :media_attachments, :description, :text
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index e16599d32..90f8a5683 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170924022025) do
+ActiveRecord::Schema.define(version: 20170927215609) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -161,6 +161,7 @@ ActiveRecord::Schema.define(version: 20170924022025) do
t.string "shortcode"
t.integer "type", default: 0, null: false
t.json "file_meta"
+ t.text "description"
t.index ["account_id"], name: "index_media_attachments_on_account_id"
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true
t.index ["status_id"], name: "index_media_attachments_on_status_id"
diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb
index baa22d7e4..0e494638f 100644
--- a/spec/controllers/api/v1/media_controller_spec.rb
+++ b/spec/controllers/api/v1/media_controller_spec.rb
@@ -101,4 +101,33 @@ RSpec.describe Api::V1::MediaController, type: :controller do
end
end
end
+
+ describe 'PUT #update' do
+ context 'when somebody else\'s' do
+ let(:media) { Fabricate(:media_attachment, status: nil) }
+
+ it 'returns http not found' do
+ put :update, params: { id: media.id, description: 'Lorem ipsum!!!' }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when not attached to a status' do
+ let(:media) { Fabricate(:media_attachment, status: nil, account: user.account) }
+
+ it 'updates the description' do
+ put :update, params: { id: media.id, description: 'Lorem ipsum!!!' }
+ expect(media.reload.description).to eq 'Lorem ipsum!!!'
+ end
+ end
+
+ context 'when attached to a status' do
+ let(:media) { Fabricate(:media_attachment, status: Fabricate(:status), account: user.account) }
+
+ it 'returns http not found' do
+ put :update, params: { id: media.id, description: 'Lorem ipsum!!!' }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
end
diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb
index f6717b7d5..f20698c45 100644
--- a/spec/models/media_attachment_spec.rb
+++ b/spec/models/media_attachment_spec.rb
@@ -17,7 +17,6 @@ RSpec.describe MediaAttachment, type: :model do
expect(media.file.meta["original"]["height"]).to eq 128
expect(media.file.meta["original"]["aspect"]).to eq 1.0
end
-
end
describe 'non-animated gif non-conversion' do
@@ -50,4 +49,12 @@ RSpec.describe MediaAttachment, type: :model do
expect(media.file.meta["small"]["aspect"]).to eq 400.0/267
end
end
+
+ describe 'descriptions for remote attachments' do
+ it 'are cut off at 140 characters' do
+ media = Fabricate(:media_attachment, description: 'foo' * 100, remote_url: 'http://example.com/blah.jpg')
+
+ expect(media.description.size).to be <= 140
+ end
+ end
end