Merge branch 'proper-attachments' into shigusegubu

* proper-attachments:
  ability to move attachments around when making a new post
  inline description display
  add media description into media modal
  add key attribute to make image refresh in media modal to give feedback when images are still loaded
  fix "+X more" sticking
  fix video attachments in notifications not having pointer cursor
  replace poll with an icon in notifications
  fix long posts double-fading in notifications
  fix links sticking to mentionsline
  fix console errors
This commit is contained in:
Henry Jameson 2021-08-15 21:06:07 +03:00
commit 42dd1d8611
15 changed files with 185 additions and 81 deletions

View file

@ -15,7 +15,8 @@ import {
faStop, faStop,
faSearchPlus, faSearchPlus,
faTrashAlt, faTrashAlt,
faPencilAlt faPencilAlt,
faAlignRight
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -28,7 +29,8 @@ library.add(
faStop, faStop,
faSearchPlus, faSearchPlus,
faTrashAlt, faTrashAlt,
faPencilAlt faPencilAlt,
faAlignRight
) )
const Attachment = { const Attachment = {
@ -40,6 +42,8 @@ const Attachment = {
'size', 'size',
'setMedia', 'setMedia',
'remove', 'remove',
'shiftUp',
'shiftDn',
'edit' 'edit'
], ],
data () { data () {
@ -52,7 +56,8 @@ const Attachment = {
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
modalOpen: false, modalOpen: false,
showHidden: false, showHidden: false,
flashLoaded: false flashLoaded: false,
showDescription: false
} }
}, },
components: { components: {
@ -118,6 +123,9 @@ const Attachment = {
} }
return modalTypes.includes(this.type) return modalTypes.includes(this.type)
}, },
videoTag () {
return this.useModal ? 'button' : 'span'
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
watch: { watch: {
@ -148,12 +156,21 @@ const Attachment = {
onRemove () { onRemove () {
this.remove && this.remove(this.attachment) this.remove && this.remove(this.attachment)
}, },
onShiftUp () {
this.shiftUp && this.shiftUp(this.attachment)
},
onShiftDn () {
this.shiftDn && this.shiftDn(this.attachment)
},
stopFlash () { stopFlash () {
this.$refs.flash.closePlayer() this.$refs.flash.closePlayer()
}, },
setFlashLoaded (event) { setFlashLoaded (event) {
this.flashLoaded = event this.flashLoaded = event
}, },
toggleDescription () {
this.showDescription = !this.showDescription
},
toggleHidden (event) { toggleHidden (event) {
if ( if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) && (this.mergedConfig.useOneClickNsfw && !this.showHidden) &&

View file

@ -42,19 +42,10 @@
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
top: 0; bottom: 0;
padding-top: 0;
background: var(--popover); background: var(--popover);
box-shadow: var(--popupShadow); box-shadow: var(--popupShadow);
opacity: 0;
transition: 0.35s all;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
}
&:hover {
.description-container.-static {
opacity: 1;
transform: translateY(-3em);
} }
} }

View file

@ -30,7 +30,7 @@
</button> </button>
</div> </div>
<div <div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription)" v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
class="description-container" class="description-container"
:class="{ '-static': !edit }" :class="{ '-static': !edit }"
> >
@ -83,13 +83,23 @@
v-if="type === 'flash' && flashLoaded" v-if="type === 'flash' && flashLoaded"
class="button-unstyled attachment-button" class="button-unstyled attachment-button"
@click.prevent="stopFlash" @click.prevent="stopFlash"
:title="$t('status.attachment_stop_flash')"
> >
<FAIcon icon="stop" /> <FAIcon icon="stop" />
</button> </button>
<button
v-if="attachment.description && size !== 'small' && !edit"
class="button-unstyled attachment-button"
@click.prevent="toggleDescription"
:title="$t('status.show_attachment_description')"
>
<FAIcon icon="align-right" />
</button>
<button <button
v-if="!useModal" v-if="!useModal"
class="button-unstyled attachment-button" class="button-unstyled attachment-button"
@click.prevent="openModalForce" @click.prevent="openModalForce"
:title="$t('status.show_attachment_in_modal')"
> >
<FAIcon icon="search-plus" /> <FAIcon icon="search-plus" />
</button> </button>
@ -97,13 +107,31 @@
v-if="nsfw && hideNsfwLocal" v-if="nsfw && hideNsfwLocal"
class="button-unstyled attachment-button" class="button-unstyled attachment-button"
@click.prevent="toggleHidden" @click.prevent="toggleHidden"
:title="$t('status.hide_attachment')"
> >
<FAIcon icon="times" /> <FAIcon icon="times" />
</button> </button>
<button
v-if="shiftUp"
class="button-unstyled attachment-button"
@click.prevent="onShiftUp"
:title="$t('status.move_up')"
>
<FAIcon icon="chevron-left" />
</button>
<button
v-if="shiftDn"
class="button-unstyled attachment-button"
@click.prevent="onShiftDn"
:title="$t('status.move_down')"
>
<FAIcon icon="chevron-right" />
</button>
<button <button
v-if="remove" v-if="remove"
class="button-unstyled attachment-button" class="button-unstyled attachment-button"
@click.prevent="onRemove" @click.prevent="onRemove"
:title="$t('status.remove_attachment')"
> >
<FAIcon icon="trash-alt" /> <FAIcon icon="trash-alt" />
</button> </button>
@ -127,9 +155,11 @@
/> />
</a> </a>
<span <component
:is="videoTag"
v-if="type === 'video' && !hidden" v-if="type === 'video' && !hidden"
class="video-container" class="video-container"
:class="{ 'button-unstyled': 'isModal' }"
:href="attachment.url" :href="attachment.url"
@click.stop.prevent="openModal" @click.stop.prevent="openModal"
> >
@ -145,7 +175,7 @@
class="play-icon" class="play-icon"
icon="play-circle" icon="play-circle"
/> />
</span> </component>
<span <span
v-if="type === 'audio' && !hidden" v-if="type === 'audio' && !hidden"
@ -199,7 +229,7 @@
</span> </span>
</div> </div>
<div <div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription)" v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
class="description-container" class="description-container"
:class="{ '-static': !edit }" :class="{ '-static': !edit }"
> >

View file

@ -12,6 +12,8 @@ const Gallery = {
'size', 'size',
'editable', 'editable',
'removeAttachment', 'removeAttachment',
'shiftUpAttachment',
'shiftDnAttachment',
'editAttachment', 'editAttachment',
'grid' 'grid'
], ],

View file

@ -6,8 +6,8 @@
> >
<div class="gallery-rows"> <div class="gallery-rows">
<div <div
v-for="(row, index) in rows" v-for="(row, rowIndex) in rows"
:key="index" :key="rowIndex"
class="gallery-row" class="gallery-row"
:style="rowStyle(row)" :style="rowStyle(row)"
:class="{ '-audio': row.audio, '-minimal': row.minimal, '-grid': grid }" :class="{ '-audio': row.audio, '-minimal': row.minimal, '-grid': grid }"
@ -16,8 +16,8 @@
class="gallery-row-inner" class="gallery-row-inner"
:class="{ '-grid': grid }" :class="{ '-grid': grid }"
> >
<attachment <Attachment
v-for="attachment in row.items" v-for="(attachment, attachmentIndex) in row.items"
:key="attachment.id" :key="attachment.id"
class="gallery-item" class="gallery-item"
:nsfw="nsfw" :nsfw="nsfw"
@ -26,6 +26,8 @@
:size="size" :size="size"
:editable="editable" :editable="editable"
:remove="removeAttachment" :remove="removeAttachment"
:shiftUp="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
:shiftDn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
:edit="editAttachment" :edit="editAttachment"
:description="descriptions && descriptions[attachment.id]" :description="descriptions && descriptions[attachment.id]"
:hide-description="size === 'small' || tooManyAttachments && hidingLong" :hide-description="size === 'small' || tooManyAttachments && hidingLong"

View file

@ -29,6 +29,9 @@ const MediaModal = {
media () { media () {
return this.$store.state.mediaViewer.media return this.$store.state.mediaViewer.media
}, },
description () {
return this.currentMedia.description
},
currentIndex () { currentIndex () {
return this.$store.state.mediaViewer.currentIndex return this.$store.state.mediaViewer.currentIndex
}, },

View file

@ -8,6 +8,7 @@
v-if="type === 'image'" v-if="type === 'image'"
class="modal-image" class="modal-image"
:src="currentMedia.url" :src="currentMedia.url"
:key="currentMedia.url"
:alt="currentMedia.description" :alt="currentMedia.description"
:title="currentMedia.description" :title="currentMedia.description"
@touchstart.stop="mediaTouchStart" @touchstart.stop="mediaTouchStart"
@ -19,11 +20,13 @@
class="modal-image" class="modal-image"
:attachment="currentMedia" :attachment="currentMedia"
:controls="true" :controls="true"
:key="currentMedia.url"
/> />
<audio <audio
v-if="type === 'audio'" v-if="type === 'audio'"
class="modal-image" class="modal-image"
:src="currentMedia.url" :src="currentMedia.url"
:key="currentMedia.url"
:alt="currentMedia.description" :alt="currentMedia.description"
:title="currentMedia.description" :title="currentMedia.description"
controls controls
@ -31,6 +34,7 @@
<Flash <Flash
v-if="type === 'flash'" v-if="type === 'flash'"
class="modal-image" class="modal-image"
:key="currentMedia.url"
:src="currentMedia.url" :src="currentMedia.url"
:alt="currentMedia.description" :alt="currentMedia.description"
:title="currentMedia.description" :title="currentMedia.description"
@ -57,6 +61,12 @@
icon="chevron-right" icon="chevron-right"
/> />
</button> </button>
<span
v-if="description"
class="description"
>
{{ description }}
</span>
</Modal> </Modal>
</template> </template>
@ -65,6 +75,7 @@
<style lang="scss"> <style lang="scss">
.modal-view.media-modal-view { .modal-view.media-modal-view {
z-index: 1001; z-index: 1001;
flex-direction: column;
.modal-view-button-arrow { .modal-view-button-arrow {
opacity: 0.75; opacity: 0.75;
@ -80,63 +91,76 @@
} }
} }
@keyframes media-fadein { .media-modal-view {
from { @keyframes media-fadein {
opacity: 0; from {
} opacity: 0;
to { }
opacity: 1; to {
} opacity: 1;
}
.modal-image {
max-width: 90%;
max-height: 90%;
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
image-orientation: from-image; // NOTE: only FF supports this
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
}
.modal-view-button-arrow {
position: absolute;
display: block;
top: 50%;
margin-top: -50px;
width: 70px;
height: 100px;
border: 0;
padding: 0;
opacity: 0;
box-shadow: none;
background: none;
appearance: none;
overflow: visible;
cursor: pointer;
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
.arrow-icon {
position: absolute;
top: 35px;
height: 30px;
width: 32px;
font-size: 14px;
line-height: 30px;
color: #FFF;
text-align: center;
background-color: rgba(0,0,0,.3);
}
&--prev {
left: 0;
.arrow-icon {
left: 6px;
} }
} }
&--next { /* Hardcoded since background is also hardcoded */
right: 0; .description {
color: white;
margin-top: 1em;
text-shadow: 0 0 10px black, 0 0 10px black;
max-width: 500px;
max-height: 9.5em;
overflow-y: auto;
padding: 0 2em;
}
.modal-image {
max-width: 90%;
max-height: 90%;
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
image-orientation: from-image; // NOTE: only FF supports this
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
}
.modal-view-button-arrow {
position: absolute;
display: block;
top: 50%;
margin-top: -50px;
width: 70px;
height: 100px;
border: 0;
padding: 0;
opacity: 0;
box-shadow: none;
background: none;
appearance: none;
overflow: visible;
cursor: pointer;
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
.arrow-icon { .arrow-icon {
right: 6px; position: absolute;
top: 35px;
height: 30px;
width: 32px;
font-size: 14px;
line-height: 30px;
color: #FFF;
text-align: center;
background-color: rgba(0,0,0,.3);
}
&--prev {
left: 0;
.arrow-icon {
left: 6px;
}
}
&--next {
right: 0;
.arrow-icon {
right: 6px;
}
} }
} }
} }

View file

@ -2,9 +2,10 @@
.showMoreLess { .showMoreLess {
white-space: normal; white-space: normal;
color: var(--link); color: var(--link);
margin-right: 0.25em;
} }
.mention-link:not(:last-child) { .mention-link {
margin-right: 0.25em; margin-right: 0.25em;
} }
} }

View file

@ -391,9 +391,20 @@ const PostStatusForm = {
this.$emit('resize') this.$emit('resize')
}, },
editAttachment (fileInfo, newText) { editAttachment (fileInfo, newText) {
console.log(fileInfo, newText)
this.newStatus.mediaDescriptions[fileInfo.id] = newText this.newStatus.mediaDescriptions[fileInfo.id] = newText
}, },
shiftUpMediaFile (fileInfo) {
const { files } = this.newStatus
const index = this.newStatus.files.indexOf(fileInfo)
files.splice(index, 1)
files.splice(index - 1, 0, fileInfo)
},
shiftDnMediaFile (fileInfo) {
const { files } = this.newStatus
const index = this.newStatus.files.indexOf(fileInfo)
files.splice(index, 1)
files.splice(index + 1, 0, fileInfo)
},
uploadFailed (errString, templateArgs) { uploadFailed (errString, templateArgs) {
templateArgs = templateArgs || {} templateArgs = templateArgs || {}
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs) this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)

View file

@ -298,6 +298,8 @@
:editable="true" :editable="true"
:edit-attachment="editAttachment" :edit-attachment="editAttachment"
:remove-attachment="removeMediaFile" :remove-attachment="removeMediaFile"
:shift-up-attachment="newStatus.files.length > 1 && shiftUpMediaFile"
:shift-dn-attachment="newStatus.files.length > 1 && shiftDnMediaFile"
@play="$emit('mediaplay', attachment.id)" @play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)" @pause="$emit('mediapause', attachment.id)"
/> />

View file

@ -121,6 +121,13 @@ export default Vue.component('RichContent', {
// in MentionsLine // in MentionsLine
return currentMentions !== null ? item.trim() : item return currentMentions !== null ? item.trim() : item
} }
// We add space with mentionsLine, otherwise non-text elements will
// stick to them.
if (currentMentions !== null) {
// single whitespace trim
item = item[0].match(/\s/) ? item.slice(1) : item
}
currentMentions = null currentMentions = null
if (item.includes(':')) { if (item.includes(':')) {
item = ['', processTextForEmoji( item = ['', processTextForEmoji(

View file

@ -50,6 +50,7 @@ const StatusContent = {
// Using max-height + overflow: auto for status components resulted in false positives // Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying. // very often with japanese characters, and it was very annoying.
tallStatus () { tallStatus () {
if (this.singleLine || this.compact) return false
const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80 const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
return lengthScore > 20 return lengthScore > 20
}, },

View file

@ -10,13 +10,20 @@
:single-line="singleLine" :single-line="singleLine"
@parseReady="$emit('parseReady', $event)" @parseReady="$emit('parseReady', $event)"
> >
<div v-if="status.poll && status.poll.options"> <div v-if="status.poll && status.poll.options && !compact">
<Poll <Poll
:base-poll="status.poll" :base-poll="status.poll"
:emoji="status.emojis" :emoji="status.emojis"
/> />
</div> </div>
<div v-else-if="status.poll && status.poll.options && compact">
<FAIcon
icon="poll-h"
size="2x"
/>
</div>
<gallery <gallery
v-if="status.attachments.length !== 0" v-if="status.attachments.length !== 0"
class="attachments media-body" class="attachments media-body"
@ -24,7 +31,6 @@
:attachments="status.attachments" :attachments="status.attachments"
:limit="compact ? 1 : 0" :limit="compact ? 1 : 0"
:size="attachmentSize" :size="attachmentSize"
@setMedia="onMedia"
@play="$emit('mediaplay', attachment.id)" @play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)" @pause="$emit('mediapause', attachment.id)"
/> />

View file

@ -727,6 +727,13 @@
"many_attachments": "Post has {number} attachment(s)", "many_attachments": "Post has {number} attachment(s)",
"collapse_attachments": "Collapse attachments", "collapse_attachments": "Collapse attachments",
"show_all_attachments": "Show all attachments", "show_all_attachments": "Show all attachments",
"show_attachment_in_modal": "Show in media modal",
"show_attachment_description": "Preview description (open attachment for full description)",
"hide_attachment": "Hide attachment",
"remove_attachment": "Remove attachment",
"attachment_stop_flash": "Stop Flash player",
"move_up": "Shift attachment left",
"move_down": "Shift attachment right",
"open_gallery": "Open gallery" "open_gallery": "Open gallery"
}, },
"user_card": { "user_card": {

View file

@ -59,7 +59,7 @@ describe('RichContent', () => {
it('replaces mention with mentionsline', () => { it('replaces mention with mentionsline', () => {
const html = p( const html = p(
makeMention('John'), makeMention('John'),
' how are you doing today?' ' how are you doing today?' // also testing single-trimming function
) )
const wrapper = shallowMount(RichContent, { const wrapper = shallowMount(RichContent, {
localVue, localVue,
@ -74,7 +74,7 @@ describe('RichContent', () => {
expect(wrapper.html()).to.eql(compwrap(p( expect(wrapper.html()).to.eql(compwrap(p(
mentionsLine(1), mentionsLine(1),
' how are you doing today?' ' how are you doing today?' // space removed to compensate for <ML> padding
))) )))
}) })