logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe
commit: ec26ff04ce541c3ba5a7f89d57cd04f74e79cfcc
parent: bbb977a04475a445fc0588af86b3c5eb33ec0577
Author: Shpuld Shpludson <shp@cock.li>
Date:   Tue,  7 Jul 2020 12:14:58 +0000

Merge branch 'xenofem/pleroma-fe-rebased-image-description-authoring' into 'develop'

media description authoring v3

See merge request pleroma/pleroma-fe!1161

Diffstat:

MCHANGELOG.md6++++++
Msrc/components/attachment/attachment.js32+++++++++++++++++++++++---------
Msrc/components/attachment/attachment.vue31++++++++++++++++++-------------
Msrc/components/gallery/gallery.vue4+---
Msrc/components/media_modal/media_modal.vue10++++++++++
Msrc/components/post_status_form/post_status_form.js73++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/components/post_status_form/post_status_form.vue68++++++++++++++++++++++++++++----------------------------------------
Msrc/components/status_content/status_content.js11++---------
Msrc/components/status_content/status_content.vue12++++++++++--
Msrc/components/still-image/still-image.js3++-
Msrc/components/still-image/still-image.vue2++
Msrc/components/video_attachment/video_attachment.vue2++
Msrc/i18n/en.json2++
Msrc/modules/media_viewer.js2+-
Msrc/services/api/api.service.js12++++++++++++
Msrc/services/status_poster/status_poster.service.js9+++++++--
Mstatic/fontello.json12++++++++++++
17 files changed, 186 insertions(+), 105 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Autocomplete domains from list of known instances - 'Bot' settings option and badge - Added profile meta data fields that can be set in profile settings +- Descriptions can be set on uploaded files before posting - Added status preview option to preview your statuses before posting - When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style @@ -28,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Close the media modal on navigation events - Add colons to the emoji alt text, to make them copyable - Add better visual indication for drag-and-drop for files +- When disabling attachments, the placeholder links now show an icon and the description instead of just IMAGE or VIDEO etc ### Fixed - Custom Emoji will display in poll options now. @@ -39,6 +41,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Subject field now appears disabled when posting - Fix status ellipsis menu being cut off in notifications column - Fixed autocomplete sometimes not returning the right user when there's already some results +- Videos and audio and misc files show description as alt/title properly now +- Clicking on non-image/video files no longer opens an empty modal +- Audio files can now be played back in the frontend with hidden attachments +- Videos are not cropped awkwardly in the uploads section anymore - Reply filtering options in Settings -> Filtering now work again using filtering on server - Don't show just blank-screen when cookies are disabled diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js @@ -8,7 +8,6 @@ const Attachment = { props: [ 'attachment', 'nsfw', - 'statusId', 'size', 'allowPlay', 'setMedia', @@ -30,9 +29,21 @@ const Attachment = { VideoAttachment }, computed: { - usePlaceHolder () { + usePlaceholder () { return this.size === 'hide' || this.type === 'unknown' }, + placeholderName () { + if (this.attachment.description === '' || !this.attachment.description) { + return this.type.toUpperCase() + } + return this.attachment.description + }, + placeholderIconClass () { + if (this.type === 'image') return 'icon-picture' + if (this.type === 'video') return 'icon-video' + if (this.type === 'audio') return 'icon-music' + return 'icon-doc' + }, referrerpolicy () { return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' }, @@ -49,7 +60,15 @@ const Attachment = { return this.size === 'small' }, fullwidth () { - return this.type === 'html' || this.type === 'audio' + if (this.size === 'hide') return false + return this.type === 'html' || this.type === 'audio' || this.type === 'unknown' + }, + useModal () { + const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio'] + : this.mergedConfig.playVideosInModal + ? ['image', 'video'] + : ['image'] + return modalTypes.includes(this.type) }, ...mapGetters(['mergedConfig']) }, @@ -60,12 +79,7 @@ const Attachment = { } }, openModal (event) { - const modalTypes = this.mergedConfig.playVideosInModal - ? ['image', 'video'] - : ['image'] - if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) || - this.usePlaceHolder - ) { + if (this.useModal) { event.stopPropagation() event.preventDefault() this.setMedia() diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue @@ -1,6 +1,7 @@ <template> <div - v-if="usePlaceHolder" + v-if="usePlaceholder" + :class="{ 'fullwidth': fullwidth }" @click="openModal" > <a @@ -8,8 +9,11 @@ class="placeholder" target="_blank" :href="attachment.url" + :alt="attachment.description" + :title="attachment.description" > - [{{ nsfw ? "NSFW/" : "" }}{{ type.toUpperCase() }}] + <span :class="placeholderIconClass" /> + <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }} </a> </div> <div @@ -22,6 +26,8 @@ v-if="hidden" class="image-attachment" :href="attachment.url" + :alt="attachment.description" + :title="attachment.description" @click.prevent="toggleHidden" > <img @@ -51,7 +57,6 @@ :class="{'hidden': hidden && preloadImage }" :href="attachment.url" target="_blank" - :title="attachment.description" @click="openModal" > <StillImage @@ -59,6 +64,7 @@ :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url" :image-load-handler="onImageLoad" + :alt="attachment.description" /> </a> @@ -83,6 +89,8 @@ <audio v-if="type === 'audio'" :src="attachment.url" + :alt="attachment.description" + :title="attachment.description" controls /> @@ -116,22 +124,19 @@ display: flex; flex-wrap: wrap; - .attachment.media-upload-container { - flex: 0 0 auto; - max-height: 200px; + .non-gallery { max-width: 100%; - display: flex; - align-items: center; - video { - max-width: 100%; - } } .placeholder { - margin-right: 8px; - margin-bottom: 4px; + display: inline-block; + padding: 0.3em 1em 0.3em 0; color: $fallback--link; color: var(--postLink, $fallback--link); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 100%; } .nsfw-placeholder { diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue @@ -50,9 +50,7 @@ align-content: stretch; } - // FIXME: specificity problem with this and .attachments.attachment - // we shouldn't have the need for .image here - .attachment.image { + .gallery-row-inner .attachment { margin: 0 0.5em 0 0; flex-grow: 1; height: 100%; diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue @@ -8,6 +8,8 @@ v-if="type === 'image'" class="modal-image" :src="currentMedia.url" + :alt="currentMedia.description" + :title="currentMedia.description" @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" @click="hide" @@ -18,6 +20,14 @@ :attachment="currentMedia" :controls="true" /> + <audio + v-if="type === 'audio'" + class="modal-image" + :src="currentMedia.url" + :alt="currentMedia.description" + :title="currentMedia.description" + controls + /> <button v-if="canNavigate" :title="$t('media_modal.previous')" diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js @@ -3,6 +3,7 @@ import MediaUpload from '../media_upload/media_upload.vue' import ScopeSelector from '../scope_selector/scope_selector.vue' import EmojiInput from '../emoji_input/emoji_input.vue' import PollForm from '../poll/poll_form.vue' +import Attachment from '../attachment/attachment.vue' import StatusContent from '../status_content/status_content.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' @@ -40,6 +41,7 @@ const PostStatusForm = { PollForm, ScopeSelector, Checkbox, + Attachment, StatusContent }, mounted () { @@ -80,6 +82,7 @@ const PostStatusForm = { nsfw: false, files: [], poll: {}, + mediaDescriptions: {}, visibility: scope, contentType }, @@ -184,7 +187,7 @@ const PostStatusForm = { } }, methods: { - postStatus (newStatus) { + async postStatus (newStatus) { if (this.posting) { return } if (this.submitDisabled) { return } if (this.emptyStatus) { @@ -199,7 +202,16 @@ const PostStatusForm = { } this.posting = true - statusPoster.postStatus({ + + try { + await this.setAllMediaDescriptions() + } catch (e) { + this.error = this.$t('post_status.media_description_error') + this.posting = false + return + } + + const data = await statusPoster.postStatus({ status: newStatus.status, spoilerText: newStatus.spoilerText || null, visibility: newStatus.visibility, @@ -209,30 +221,32 @@ const PostStatusForm = { inReplyToStatusId: this.replyTo, contentType: newStatus.contentType, poll - }).then((data) => { - if (!data.error) { - this.newStatus = { - status: '', - spoilerText: '', - files: [], - visibility: newStatus.visibility, - contentType: newStatus.contentType, - poll: {} - } - this.pollFormVisible = false - this.$refs.mediaUpload.clearFile() - this.clearPollForm() - this.$emit('posted') - let el = this.$el.querySelector('textarea') - el.style.height = 'auto' - el.style.height = undefined - this.error = null - if (this.preview) this.previewStatus() - } else { - this.error = data.error - } - this.posting = false }) + + if (!data.error) { + this.newStatus = { + status: '', + spoilerText: '', + files: [], + visibility: newStatus.visibility, + contentType: newStatus.contentType, + poll: {}, + mediaDescriptions: {} + } + this.pollFormVisible = false + this.$refs.mediaUpload.clearFile() + this.clearPollForm() + this.$emit('posted') + let el = this.$el.querySelector('textarea') + el.style.height = 'auto' + el.style.height = undefined + this.error = null + if (this.preview) this.previewStatus() + } else { + this.error = data.error + } + + this.posting = false }, previewStatus () { if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') { @@ -457,6 +471,15 @@ const PostStatusForm = { }, dismissScopeNotice () { this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true }) + }, + setMediaDescription (id) { + const description = this.newStatus.mediaDescriptions[id] + if (!description || description.trim() === '') return + return statusPoster.setMediaDescription({ store: this.$store, id, description }) + }, + setAllMediaDescriptions () { + const ids = this.newStatus.files.map(file => file.id) + return Promise.all(ids.map(id => this.setMediaDescription(id))) } } } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -282,27 +282,18 @@ class="fa button-icon icon-cancel" @click="removeMediaFile(file)" /> - <div class="media-upload-container attachment"> - <img - v-if="type(file) === 'image'" - class="thumbnail media-upload" - :src="file.url" - > - <video - v-if="type(file) === 'video'" - :src="file.url" - controls - /> - <audio - v-if="type(file) === 'audio'" - :src="file.url" - controls - /> - <a - v-if="type(file) === 'unknown'" - :href="file.url" - >{{ file.url }}</a> - </div> + <attachment + :attachment="file" + :set-media="() => $store.dispatch('setMedia', newStatus.files)" + size="small" + allow-play="false" + /> + <input + v-model="newStatus.mediaDescriptions[file.id]" + type="text" + :placeholder="$t('post_status.media_description')" + @keydown.enter.prevent="" + > </div> </div> <div @@ -458,11 +449,9 @@ } .media-upload-wrapper { - flex: 0 0 auto; - max-width: 100%; - min-width: 50px; margin-right: .2em; margin-bottom: .5em; + width: 18em; .icon-cancel { display: inline-block; @@ -476,6 +465,20 @@ border-bottom-left-radius: 0; border-bottom-right-radius: 0; } + + img, video { + object-fit: contain; + max-height: 10em; + } + + .video { + max-height: 10em; + } + + input { + flex: 1; + width: 100%; + } } .status-input-wrapper { @@ -490,23 +493,8 @@ .attachment { margin: 0; + padding: 0; position: relative; - flex: 0 0 auto; - border: 1px solid $fallback--border; - border: 1px solid var(--border, $fallback--border); - text-align: center; - - audio { - min-width: 300px; - flex: 1 0 auto; - } - - a { - display: block; - text-align: left; - line-height: 1.2; - padding: .5em; - } } i { diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js @@ -99,15 +99,8 @@ const StatusContent = { file => !fileType.fileMatchesSomeType(this.galleryTypes, file) ) }, - hasImageAttachments () { - return this.status.attachments.some( - file => fileType.fileType(file.mimetype) === 'image' - ) - }, - hasVideoAttachments () { - return this.status.attachments.some( - file => fileType.fileType(file.mimetype) === 'video' - ) + attachmentTypes () { + return this.status.attachments.map(file => fileType.fileType(file.mimetype)) }, maxThumbnails () { return this.mergedConfig.maxThumbnails diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue @@ -55,14 +55,22 @@ > {{ $t("status.show_content") }} <span - v-if="hasImageAttachments" + v-if="attachmentTypes.includes('image')" class="icon-picture" /> <span - v-if="hasVideoAttachments" + v-if="attachmentTypes.includes('video')" class="icon-video" /> <span + v-if="attachmentTypes.includes('audio')" + class="icon-music" + /> + <span + v-if="attachmentTypes.includes('unknown')" + class="icon-doc" + /> + <span v-if="status.card" class="icon-link" /> diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js @@ -4,7 +4,8 @@ const StillImage = { 'referrerpolicy', 'mimetype', 'imageLoadError', - 'imageLoadHandler' + 'imageLoadHandler', + 'alt' ], data () { return { diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue @@ -11,6 +11,8 @@ <img ref="src" :key="src" + :alt="alt" + :title="alt" :src="src" :referrerpolicy="referrerpolicy" @load="onLoad" diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue @@ -4,6 +4,8 @@ :src="attachment.url" :loop="loopVideo" :controls="controls" + :alt="attachment.description" + :title="attachment.description" playsinline @loadeddata="onVideoDataLoad" /> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -178,6 +178,7 @@ "account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.", "account_not_locked_warning_link": "locked", "attachments_sensitive": "Mark attachments as sensitive", + "media_description": "Media description", "content_type": { "text/plain": "Plain text", "text/html": "HTML", @@ -192,6 +193,7 @@ "preview": "Preview", "preview_empty": "Empty", "empty_status_error": "Can't post an empty status with no files", + "media_description_error": "Failed to update media, try again", "scope_notice": { "public": "This post will be visible to everyone", "private": "This post will be visible to your followers only", diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js @@ -22,7 +22,7 @@ const mediaViewer = { setMedia ({ commit }, attachments) { const media = attachments.filter(attachment => { const type = fileTypeService.fileType(attachment.mimetype) - return type === 'image' || type === 'video' + return type === 'image' || type === 'video' || type === 'audio' }) commit('setMedia', media) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -708,6 +708,17 @@ const uploadMedia = ({ formData, credentials }) => { .then((data) => parseAttachment(data)) } +const setMediaDescription = ({ id, description, credentials }) => { + return promisedRequest({ + url: `${MASTODON_MEDIA_UPLOAD_URL}/${id}`, + method: 'PUT', + headers: authHeaders(credentials), + payload: { + description + } + }).then((data) => parseAttachment(data)) +} + const importBlocks = ({ file, credentials }) => { const formData = new FormData() formData.append('list', file) @@ -1177,6 +1188,7 @@ const apiService = { postStatus, deleteStatus, uploadMedia, + setMediaDescription, fetchMutes, muteUser, unmuteUser, diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js @@ -47,13 +47,18 @@ const postStatus = ({ const uploadMedia = ({ store, formData }) => { const credentials = store.state.users.currentUser.credentials - return apiService.uploadMedia({ credentials, formData }) } +const setMediaDescription = ({ store, id, description }) => { + const credentials = store.state.users.currentUser.credentials + return apiService.setMediaDescription({ credentials, id, description }) +} + const statusPosterService = { postStatus, - uploadMedia + uploadMedia, + setMediaDescription } export default statusPosterService diff --git a/static/fontello.json b/static/fontello.json @@ -387,6 +387,18 @@ "css": "bookmark-empty", "code": 61591, "src": "fontawesome" + }, + { + "uid": "9ea0a737ccc45d6c510dcbae56058849", + "css": "music", + "code": 59432, + "src": "fontawesome" + }, + { + "uid": "1b5a5d7b7e3c71437f5a26befdd045ed", + "css": "doc", + "code": 59433, + "src": "fontawesome" } ] } \ No newline at end of file