Merge branch 'proper-attachments' into shigusegubu

* proper-attachments:
  better attachments in uploading (grid layout)
  better handling of unknown files, better upload display
  better compact attachments
  merged in compact notifs and improved upon it
  gallery in post status form!
  gallery now supports flash, fixes for flash component. refactored media modal
  refactored attachments and gallery. All attachments now are in gallery.
  Use proper setting name
  Use cleaner instance config check for shoutbox setting
  Make locale language cleaner
  Don't shorten shoutbox to SB
  Fix lint error
  Update CHANGELOG.md
  New option: Hide shoutbox
  compact notifs
This commit is contained in:
Henry Jameson 2021-06-18 17:33:54 +03:00
commit 08ce9ba30b
20 changed files with 640 additions and 534 deletions

View file

@ -11,7 +11,11 @@ import {
faImage,
faVideo,
faPlayCircle,
faTimes
faTimes,
faStop,
faSearchPlus,
faTrashAlt,
faPencilAlt
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -20,27 +24,35 @@ library.add(
faImage,
faVideo,
faPlayCircle,
faTimes
faTimes,
faStop,
faSearchPlus,
faTrashAlt,
faPencilAlt
)
const Attachment = {
props: [
'attachment',
'description',
'hideDescription',
'nsfw',
'size',
'allowPlay',
'setMedia',
'naturalSizeLoad'
'remove',
'edit'
],
data () {
return {
localDescription: this.description || this.attachment.description,
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false,
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
modalOpen: false,
showHidden: false
showHidden: false,
flashLoaded: false
}
},
components: {
@ -49,9 +61,24 @@ const Attachment = {
VideoAttachment
},
computed: {
classNames () {
return [
{
'-loading': this.loading,
'-nsfw-placeholder': this.hidden,
'-editable': this.edit !== undefined
},
'-type-' + this.type,
this.size && '-size-' + this.size,
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
]
},
usePlaceholder () {
return this.size === 'hide' || this.type === 'unknown'
},
useContainFit () {
return this.$store.getters.mergedConfig.useContainFit
},
placeholderName () {
if (this.attachment.description === '' || !this.attachment.description) {
return this.type.toUpperCase()
@ -76,22 +103,28 @@ const Attachment = {
isEmpty () {
return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'
},
isSmall () {
return this.size === 'small'
},
fullwidth () {
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']
let modalTypes = []
switch (this.size) {
case 'hide':
case 'small':
modalTypes = ['image', 'video', 'audio', 'flash']
break
default:
modalTypes = this.mergedConfig.playVideosInModal
? ['image', 'video', 'flash']
: ['image']
break
}
return modalTypes.includes(this.type)
},
...mapGetters(['mergedConfig'])
},
watch: {
localDescription (newVal) {
this.onEdit(newVal)
}
},
methods: {
linkClicked ({ target }) {
if (target.tagName === 'A') {
@ -100,12 +133,27 @@ const Attachment = {
},
openModal (event) {
if (this.useModal) {
event.stopPropagation()
event.preventDefault()
this.setMedia()
this.$store.dispatch('setCurrent', this.attachment)
this.$emit('setMedia')
this.$store.dispatch('setCurrentMedia', this.attachment)
}
},
openModalForce (event) {
this.$emit('setMedia')
this.$store.dispatch('setCurrentMedia', this.attachment)
},
onEdit (event) {
console.log('ONEDIT', event)
this.edit && this.edit(this.attachment, event)
},
onRemove () {
this.remove && this.remove(this.attachment)
},
stopFlash () {
this.$refs.flash.closePlayer()
},
setFlashLoaded (event) {
this.flashLoaded = event
},
toggleHidden (event) {
if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
@ -132,7 +180,7 @@ const Attachment = {
onImageLoad (image) {
const width = image.naturalWidth
const height = image.naturalHeight
this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
}
}
}

View file

@ -1,7 +1,8 @@
<template>
<div
<button
v-if="usePlaceholder"
:class="{ 'fullwidth': fullwidth }"
class="Attachment -placeholder button-unstyled"
:class="classNames"
@click="openModal"
>
<a
@ -11,318 +12,212 @@
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
@click.prevent=""
>
<FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ this.edit ? '' : placeholderName }}
</a>
</div>
<div
v-else
v-show="!isEmpty"
class="attachment"
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
>
<a
v-if="hidden"
class="image-attachment"
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
@click.prevent.stop="toggleHidden"
>
<img
:key="nsfwImage"
class="nsfw"
:src="nsfwImage"
:class="{'small': isSmall}"
>
<FAIcon
v-if="type === 'video'"
class="play-icon"
icon="play-circle"
/>
</a>
<button
v-if="nsfw && hideNsfwLocal && !hidden"
class="button-unstyled hider"
@click.prevent="toggleHidden"
>
<FAIcon icon="times" />
</button>
<a
v-if="type === 'image' && (!hidden || preloadImage)"
class="image-attachment"
:class="{'hidden': hidden && preloadImage }"
:href="attachment.url"
target="_blank"
@click="openModal"
>
<StillImage
class="image"
:referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url"
:image-load-handler="onImageLoad"
:alt="attachment.description"
/>
</a>
<a
v-if="type === 'video' && !hidden"
class="video-container"
:class="{'small': isSmall}"
:href="allowPlay ? undefined : attachment.url"
@click="openModal"
>
<VideoAttachment
class="video"
:attachment="attachment"
:controls="allowPlay"
@play="$emit('play')"
@pause="$emit('pause')"
/>
<FAIcon
v-if="!allowPlay"
class="play-icon"
icon="play-circle"
/>
</a>
<audio
v-if="type === 'audio'"
:src="attachment.url"
:alt="attachment.description"
:title="attachment.description"
controls
@play="$emit('play')"
@pause="$emit('pause')"
/>
<div
v-if="type === 'html' && attachment.oembed"
class="oembed"
@click.prevent="linkClicked"
>
<div
v-if="attachment.thumb_url"
class="image"
class="attachment-buttons"
v-if="edit || remove"
>
<img :src="attachment.thumb_url">
</div>
<div class="text">
<!-- eslint-disable vue/no-v-html -->
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
<div v-html="attachment.oembed.oembedHTML" />
<!-- eslint-enable vue/no-v-html -->
</div>
<button
v-if="remove"
class="button-unstyled attachment-button"
@click.prevent="onRemove"
>
<FAIcon icon="trash-alt" />
</button>
</div>
<div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription)"
class="description-container"
:class="{ '-static': !edit }"
>
<input
v-if="edit"
v-model="localDescription"
type="text"
class="description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>
<p v-else>
{{ localDescription }}
</p>
</div>
</button>
<div
class="Attachment"
:class="classNames"
v-else
>
<div
class="attachment-wrapper"
v-show="!isEmpty"
>
<a
v-if="hidden"
class="image-container"
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
@click.prevent.stop="toggleHidden"
>
<img
:key="nsfwImage"
class="nsfw"
:src="nsfwImage"
>
<FAIcon
v-if="type === 'video'"
class="play-icon"
icon="play-circle"
/>
</a>
<div
class="attachment-buttons"
v-if="!hidden"
>
<button
v-if="type === 'flash' && flashLoaded"
class="button-unstyled attachment-button"
@click.prevent="stopFlash"
>
<FAIcon icon="stop" />
</button>
<button
v-if="!useModal"
class="button-unstyled attachment-button"
@click.prevent="openModalForce"
>
<FAIcon icon="search-plus" />
</button>
<button
v-if="nsfw && hideNsfwLocal"
class="button-unstyled attachment-button"
@click.prevent="toggleHidden"
>
<FAIcon icon="times" />
</button>
<button
v-if="remove"
class="button-unstyled attachment-button"
@click.prevent="onRemove"
>
<FAIcon icon="trash-alt" />
</button>
</div>
<Flash
v-if="type === 'flash'"
:src="attachment.large_thumb_url || attachment.url"
/>
<a
v-if="type === 'image' && (!hidden || preloadImage)"
class="image-container"
:class="{'-hidden': hidden && preloadImage }"
:href="attachment.url"
target="_blank"
@click.stop.prevent="openModal"
>
<StillImage
class="image"
:referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url"
:image-load-handler="onImageLoad"
:alt="attachment.description"
/>
</a>
<a
v-if="type === 'video' && !hidden"
class="video-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<VideoAttachment
class="video"
:attachment="attachment"
:controls="!useModal"
@play="$emit('play')"
@pause="$emit('pause')"
/>
<FAIcon
v-if="useModal"
class="play-icon"
icon="play-circle"
/>
</a>
<a
v-if="type === 'audio' && !hidden"
class="audio-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<audio
v-if="type === 'audio'"
:src="attachment.url"
:alt="attachment.description"
:title="attachment.description"
controls
@play="$emit('play')"
@pause="$emit('pause')"
/>
</a>
<div
v-if="type === 'html' && attachment.oembed"
class="oembed-container"
@click.prevent="linkClicked"
>
<div
v-if="attachment.thumb_url"
class="image"
>
<img :src="attachment.thumb_url">
</div>
<div class="text">
<!-- eslint-disable vue/no-v-html -->
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
<div v-html="attachment.oembed.oembedHTML" />
<!-- eslint-enable vue/no-v-html -->
</div>
</div>
<a
v-if="type === 'flash' && !hidden"
class="flash-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<Flash
class="flash"
ref="flash"
:src="attachment.large_thumb_url || attachment.url"
@playerOpened="setFlashLoaded(true)"
@playerClosed="setFlashLoaded(false)"
/>
</a>
</div>
<div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription)"
class="description-container"
:class="{ '-static': !edit }"
>
<input
v-if="edit"
v-model="localDescription"
type="text"
class="description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>
<p v-else>
{{ localDescription }}
</p>
</div>
</div>
</template>
<script src="./attachment.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.attachments {
display: flex;
flex-wrap: wrap;
.non-gallery {
max-width: 100%;
}
.placeholder {
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%;
svg {
color: inherit;
}
}
.nsfw-placeholder {
cursor: pointer;
&.loading {
cursor: progress;
}
}
.attachment {
position: relative;
margin-top: 0.5em;
align-self: flex-start;
line-height: 0;
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
overflow: hidden;
}
.non-gallery.attachment {
&.flash,
&.video {
flex: 1 0 40%;
}
.nsfw {
height: 260px;
}
.small {
height: 120px;
flex-grow: 0;
}
.video {
height: 260px;
display: flex;
}
video {
max-height: 100%;
object-fit: contain;
}
}
.fullwidth {
flex-basis: 100%;
}
// fixes small gap below video
&.video {
line-height: 0;
}
.video-container {
display: flex;
max-height: 100%;
}
.video {
width: 100%;
height: 100%;
}
.play-icon {
position: absolute;
font-size: 64px;
top: calc(50% - 32px);
left: calc(50% - 32px);
color: rgba(255, 255, 255, 0.75);
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
}
.play-icon::before {
margin: 0;
}
&.html {
flex-basis: 90%;
width: 100%;
display: flex;
}
.hider {
position: absolute;
right: 0;
margin: 10px;
padding: 0;
z-index: 4;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
text-align: center;
width: 2em;
height: 2em;
font-size: 1.25em;
// TODO: theming? hard to theme with unknown background image color
background: rgba(230, 230, 230, 0.7);
.svg-inline--fa {
color: rgba(0, 0, 0, 0.6);
}
&:hover .svg-inline--fa {
color: rgba(0, 0, 0, 0.9);
}
}
video {
z-index: 0;
}
audio {
width: 100%;
}
img.media-upload {
line-height: 0;
max-height: 200px;
max-width: 100%;
}
.oembed {
line-height: 1.2em;
flex: 1 0 100%;
width: 100%;
margin-right: 15px;
display: flex;
img {
width: 100%;
}
.image {
flex: 1;
img {
border: 0px;
border-radius: 5px;
height: 100%;
object-fit: cover;
}
}
.text {
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 14px;
margin: 0px;
}
}
}
.image-attachment {
&,
& .image {
width: 100%;
height: 100%;
}
&.hidden {
display: none;
}
.nsfw {
object-fit: cover;
width: 100%;
height: 100%;
}
img {
image-orientation: from-image; // NOTE: only FF supports this
}
}
}
</style>
<style src="./attachment.scss" lang="scss"></style>

View file

@ -62,10 +62,6 @@
&.with-media {
width: 100%;
.gallery-row {
overflow: hidden;
}
.status {
width: 100%;
}

View file

@ -39,12 +39,13 @@ const Flash = {
this.player = 'error'
})
this.ruffleInstance = player
this.$emit('playerOpened')
})
},
closePlayer () {
console.log(this.ruffleInstance)
this.ruffleInstance.remove()
this.ruffleInstance && this.ruffleInstance.remove()
this.player = false
this.$emit('playerClosed')
}
}
}

View file

@ -36,13 +36,6 @@
</p>
</span>
</button>
<button
v-if="player"
class="button-unstyled hider"
@click="closePlayer"
>
<FAIcon icon="stop" />
</button>
</div>
</template>
@ -51,8 +44,9 @@
<style lang="scss">
@import '../../_variables.scss';
.Flash {
display: inline-block;
width: 100%;
height: 260px;
height: 100%;
position: relative;
.player {
@ -60,6 +54,16 @@
width: 100%;
}
.placeholder {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
color: var(--link);
}
.hider {
top: 0;
}
@ -76,13 +80,5 @@
display: none;
visibility: 'hidden';
}
.placeholder {
height: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View file

@ -1,15 +1,24 @@
import Attachment from '../attachment/attachment.vue'
import { chunk, last, dropRight, sumBy } from 'lodash'
import { sumBy } from 'lodash'
const Gallery = {
props: [
'attachments',
'limitRows',
'descriptions',
'limit',
'nsfw',
'setMedia'
'setMedia',
'size',
'editable',
'removeAttachment',
'editAttachment',
'grid'
],
data () {
return {
sizes: {}
sizes: {},
hidingLong: true
}
},
components: { Attachment },
@ -18,26 +27,70 @@ const Gallery = {
if (!this.attachments) {
return []
}
const rows = chunk(this.attachments, 3)
if (last(rows).length === 1 && rows.length > 1) {
// if 1 attachment on last row -> add it to the previous row instead
const lastAttachment = last(rows)[0]
const allButLastRow = dropRight(rows)
last(allButLastRow).push(lastAttachment)
return allButLastRow
const attachments = this.limit > 0
? this.attachments.slice(0, this.limit)
: this.attachments
if (this.size === 'hide') {
return attachments.map(item => ({ minimal: true, items: [item] }))
}
const rows = this.grid
? [{ grid: true, items: attachments }]
: attachments.reduce((acc, attachment, i) => {
if (attachment.mimetype.includes('audio')) {
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
}
if (!(
attachment.mimetype.includes('image') ||
attachment.mimetype.includes('video') ||
attachment.mimetype.includes('flash')
)) {
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
}
const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1
const currentRow = acc[acc.length - 1].items
currentRow.push(attachment)
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
return [...acc, { items: [] }]
} else {
return acc
}
}, [{ items: [] }]).filter(_ => _.items.length > 0)
return rows
},
useContainFit () {
return this.$store.getters.mergedConfig.useContainFit
attachmentsDimensionalScore () {
return this.rows.reduce((acc, row) => {
let size = 0
if (row.minimal) {
size += 1 / 8
} else if (row.audio) {
size += 1 / 4
} else {
size += 1 / (row.items.length + 0.6)
}
return acc + size
}, 0)
},
tooManyAttachments () {
if (this.editable || this.size === 'small') {
return false
} else if (this.size === 'hide') {
return this.attachments.length > 8
} else {
return this.attachmentsDimensionalScore > 1
}
}
},
methods: {
onNaturalSizeLoad (id, size) {
this.$set(this.sizes, id, size)
onNaturalSizeLoad ({ id, width, height }) {
this.$set(this.sizes, id, { width, height })
},
rowStyle (itemsPerRow) {
return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
rowStyle (row) {
if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
}
},
itemStyle (id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id))
@ -46,6 +99,16 @@ const Gallery = {
getAspectRatio (id) {
const size = this.sizes[id]
return size ? size.width / size.height : 1
},
toggleHidingLong (event) {
this.hidingLong = event
},
openGallery () {
this.$store.dispatch('setMedia', this.attachments)
this.$store.dispatch('setCurrentMedia', this.attachments[0])
},
onMedia () {
this.$store.dispatch('setMedia', this.attachments)
}
}
}

View file

@ -1,26 +1,82 @@
<template>
<div
class="Gallery"
ref="galleryContainer"
style="width: 100%;"
:class="{ '-long': tooManyAttachments && hidingLong }"
>
<div class="gallery-rows">
<div
v-for="(row, index) in rows"
:key="index"
class="gallery-row"
:style="rowStyle(row)"
:class="{ '-audio': row.audio, '-minimal': row.minimal, '-grid': grid }"
>
<div
class="gallery-row-inner"
:class="{ '-grid': grid }"
>
<attachment
v-for="attachment in row.items"
class="gallery-item"
:key="attachment.id"
:nsfw="nsfw"
:attachment="attachment"
:allow-play="false"
:size="size"
:editable="editable"
:remove="removeAttachment"
:edit="editAttachment"
:description="descriptions && descriptions[attachment.id]"
:hideDescription="size === 'small' || tooManyAttachments && hidingLong"
:style="itemStyle(attachment.id, row.items)"
@setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad"
/>
</div>
</div>
</div>
<div
v-for="(row, index) in rows"
:key="index"
class="gallery-row"
:style="rowStyle(row.length)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
v-if="tooManyAttachments"
class="many-attachments"
>
<div class="gallery-row-inner">
<attachment
v-for="attachment in row"
:key="attachment.id"
:set-media="setMedia"
:nsfw="nsfw"
:attachment="attachment"
:allow-play="false"
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
:style="itemStyle(attachment.id, row)"
/>
<div class="many-attachments-text">
{{ $t("status.many_attachments", { number: attachments.length })}}
</div>
<div class="many-attachments-buttons">
<span
v-if="!hidingLong"
class="many-attachments-button"
>
<button
class="button-unstyled -link"
@click="toggleHidingLong(true)"
>
{{ $t("status.collapse_attachments") }}
</button>
</span>
<span
v-if="hidingLong"
class="many-attachments-button"
>
<button
class="button-unstyled -link"
@click="toggleHidingLong(false)"
>
{{ $t("status.show_all_attachments") }}
</button>
</span>
<span
class="many-attachments-button"
v-if="hidingLong"
>
<button
class="button-unstyled -link"
@click="openGallery"
>
{{ $t("status.open_gallery") }}
</button>
</span>
</div>
</div>
</div>
@ -31,12 +87,66 @@
<style lang="scss">
@import '../../_variables.scss';
.gallery-row {
position: relative;
height: 0;
width: 100%;
flex-grow: 1;
margin-top: 0.5em;
.Gallery {
.gallery-rows {
display: flex;
flex-direction: column;
}
.gallery-row {
position: relative;
height: 0;
width: 100%;
flex-grow: 1;
&:not(:first-child) {
margin-top: 0.5em;
}
}
&.-long {
.gallery-rows {
max-height: 25em;
overflow: hidden;
mask:
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
}
.many-attachments-text {
text-align: center;
line-height: 2;
}
.many-attachments-buttons {
display: flex;
}
.many-attachments-button {
display: flex;
flex: 1;
justify-content: center;
line-height: 2;
button {
padding: 0 2em;
}
}
.gallery-row {
&.-grid,
&.-minimal {
height: auto;
.gallery-row-inner {
position: relative;
}
}
}
.gallery-row-inner {
position: absolute;
@ -48,9 +158,24 @@
flex-direction: row;
flex-wrap: nowrap;
align-content: stretch;
&.-grid {
width: 100%;
height: auto;
position: relative;
display: grid;
grid-column-gap: 0.5em;
grid-row-gap: 0.5em;
grid-template-columns: repeat(auto-fit, minmax(15em, 1fr));
.gallery-item {
margin: 0;
height: 200px;
}
}
}
.gallery-row-inner .attachment {
.gallery-item {
margin: 0 0.5em 0 0;
flex-grow: 1;
height: 100%;
@ -61,32 +186,5 @@
margin: 0;
}
}
.image-attachment {
width: 100%;
height: 100%;
}
.video-container {
height: 100%;
}
&.contain-fit {
img,
video,
canvas {
object-fit: contain;
height: 100%;
}
}
&.cover-fit {
img,
video,
canvas {
object-fit: cover;
}
}
}
</style>

View file

@ -3,6 +3,7 @@ import VideoAttachment from '../video_attachment/video_attachment.vue'
import Modal from '../modal/modal.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import GestureService from '../../services/gesture_service/gesture_service'
import Flash from 'src/components/flash/flash.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronLeft,
@ -18,7 +19,8 @@ const MediaModal = {
components: {
StillImage,
VideoAttachment,
Modal
Modal,
Flash
},
computed: {
showing () {
@ -67,13 +69,13 @@ const MediaModal = {
goPrev () {
if (this.canNavigate) {
const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1)
this.$store.dispatch('setCurrent', this.media[prevIndex])
this.$store.dispatch('setCurrentMedia', this.media[prevIndex])
}
},
goNext () {
if (this.canNavigate) {
const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1)
this.$store.dispatch('setCurrent', this.media[nextIndex])
this.$store.dispatch('setCurrentMedia', this.media[nextIndex])
}
},
handleKeyupEvent (e) {

View file

@ -28,6 +28,13 @@
:title="currentMedia.description"
controls
/>
<Flash
v-if="type === 'flash'"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
:title="currentMedia.description"
/>
<button
v-if="canNavigate"
:title="$t('media_modal.previous')"

View file

@ -182,8 +182,9 @@
</router-link>
</div>
<template v-else>
<status-content
<StatusContent
class="faint"
:compact="true"
:status="notification.action"
/>
</template>

View file

@ -4,6 +4,7 @@ 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 Gallery from 'src/components/gallery/gallery.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'
@ -85,7 +86,8 @@ const PostStatusForm = {
Checkbox,
Select,
Attachment,
StatusContent
StatusContent,
Gallery
},
mounted () {
this.updateIdempotencyKey()
@ -388,6 +390,10 @@ const PostStatusForm = {
this.newStatus.files.splice(index, 1)
this.$emit('resize')
},
editAttachment (fileInfo, newText) {
console.log(fileInfo, newText)
this.newStatus.mediaDescriptions[fileInfo.id] = newText
},
uploadFailed (errString, templateArgs) {
templateArgs = templateArgs || {}
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)

View file

@ -287,32 +287,20 @@
@click="clearError"
/>
</div>
<div class="attachments">
<div
v-for="file in newStatus.files"
:key="file.url"
class="media-upload-wrapper"
>
<button
class="button-unstyled hider"
@click="removeMediaFile(file)"
>
<FAIcon icon="times" />
</button>
<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>
<gallery
v-if="newStatus.files && newStatus.files.length > 0"
class="attachments"
:grid="true"
:nsfw="false"
:attachments="newStatus.files"
:descriptions="newStatus.mediaDescriptions"
:set-media="() => $store.dispatch('setMedia', newStatus.files)"
:editable="true"
:editAttachment="editAttachment"
:removeAttachment="removeMediaFile"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<div
v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox"
class="upload_settings"
@ -330,26 +318,13 @@
<style lang="scss">
@import '../../_variables.scss';
.tribute-container {
ul {
padding: 0px;
li {
display: flex;
align-items: center;
}
}
img {
padding: 3px;
width: 16px;
height: 16px;
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
}
}
.post-status-form {
position: relative;
.attachments {
margin-bottom: 0.5em;
}
.form-bottom {
display: flex;
justify-content: space-between;
@ -507,15 +482,6 @@
flex-direction: column;
}
.attachments .media-upload-wrapper {
position: relative;
.attachment {
margin: 0;
padding: 0;
}
}
.btn {
cursor: pointer;
}
@ -616,11 +582,4 @@
border: 2px dashed var(--text, $fallback--text);
}
}
// todo: unify with attachment.vue (otherwise the uploaded images are not minified unless a status with an attachment was displayed before)
img.media-upload, .media-upload-container > video {
line-height: 0;
max-height: 200px;
max-width: 100%;
}
</style>

View file

@ -21,6 +21,7 @@ library.add(
const StatusContent = {
name: 'StatusContent',
props: [
'compact',
'status',
'focused',
'noHeading',

View file

@ -1,11 +1,17 @@
@import '../../_variables.scss';
.StatusBody {
display: flex;
flex-direction: column;
.emoji {
--_still_image-label-scale: 0.5;
}
.attachments {
margin-top: 0.5em;
}
& .text,
& .summary {
font-family: var(--postFont, sans-serif);
@ -115,4 +121,54 @@
.cyantext {
color: var(--postCyantext, $fallback--cBlue);
}
&.-compact {
align-items: top;
flex-direction: row;
--emoji-size: 16px;
& .body,
& .attachments {
max-height: 3.25em;
}
.body {
overflow: hidden;
white-space: normal;
min-width: 5em;
flex: 5 1 auto;
mask-size: auto 3.5em, auto auto;
mask-position: 0 0, 0 0;
mask-repeat: repeat-x, repeat;
mask-image: linear-gradient(to bottom, white 2em, transparent 3em);
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.attachments {
margin-top: 0;
flex: 1 1 0;
min-width: 5em;
height: 100%;
margin-left: 0.5em;
}
.summary-wrapper {
.summary::after {
content: ': ';
}
line-height: inherit;
margin: 0;
border: none;
display: inline-block;
}
.text-wrapper {
display: inline-block;
}
}
}

View file

@ -1,5 +1,8 @@
<template>
<div class="StatusBody">
<div
class="StatusBody"
:class="{ '-compact': compact }"
>
<div class="body">
<div
v-if="status.summary_raw_html"

View file

@ -28,6 +28,7 @@ const StatusContent = {
name: 'StatusContent',
props: [
'status',
'compact',
'focused',
'noHeading',
'fullContent',
@ -58,24 +59,6 @@ const StatusContent = {
}
return 'normal'
},
galleryTypes () {
if (this.attachmentSize === 'hide') {
return []
}
return this.mergedConfig.playVideosInModal
? ['image', 'video']
: ['image']
},
galleryAttachments () {
return this.status.attachments.filter(
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
nonGalleryAttachments () {
return this.status.attachments.filter(
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
@ -90,12 +73,6 @@ const StatusContent = {
Gallery,
LinkPreview,
StatusBody
},
methods: {
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)
}
}
}

View file

@ -1,8 +1,12 @@
<template>
<div class="StatusContent">
<div
class="StatusContent"
:class="{ '-compact': compact }"
>
<slot name="header" />
<StatusBody
:status="status"
:compact="compact"
:single-line="singleLine"
:hide-mentions="hideMentions"
@parseReady="$emit('parseReady', $event)"
@ -11,32 +15,20 @@
<poll :base-poll="status.poll" />
</div>
<div
v-if="status.attachments.length !== 0"
<gallery
class="attachments media-body"
>
<attachment
v-for="attachment in nonGalleryAttachments"
:key="attachment.id"
class="non-gallery"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
:attachment="attachment"
:allow-play="true"
:set-media="setMedia()"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<gallery
v-if="galleryAttachments.length > 0"
:nsfw="nsfwClickthrough"
:attachments="galleryAttachments"
:set-media="setMedia()"
/>
</div>
v-if="status.attachments.length !== 0"
:nsfw="nsfwClickthrough"
:attachments="status.attachments"
:limit="this.compact ? 1 : 0"
:size="attachmentSize"
@setMedia="onMedia"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<div
v-if="status.card && !noHeading"
v-if="status.card && !noHeading && !compact"
class="link-preview media-body"
>
<link-preview

View file

@ -159,7 +159,7 @@ export default {
mimetype: 'image'
}
this.$store.dispatch('setMedia', [attachment])
this.$store.dispatch('setCurrent', attachment)
this.$store.dispatch('setCurrentMedia', attachment)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })

View file

@ -723,7 +723,11 @@
"nsfw": "NSFW",
"expand": "Expand",
"you": "(You)",
"plus_more": "+{number} more"
"plus_more": "+{number} more",
"many_attachments": "Post has {number} attachment(s)",
"collapse_attachments": "Collapse attachments",
"show_all_attachments": "Show all attachments",
"open_gallery": "Open gallery"
},
"user_card": {
"approve": "Approve",

View file

@ -1,4 +1,5 @@
import fileTypeService from '../services/file_type/file_type.service.js'
const supportedTypes = new Set(['image', 'video', 'audio', 'flash'])
const mediaViewer = {
state: {
@ -10,7 +11,7 @@ const mediaViewer = {
setMedia (state, media) {
state.media = media
},
setCurrent (state, index) {
setCurrentMedia (state, index) {
state.activated = true
state.currentIndex = index
},
@ -22,13 +23,13 @@ const mediaViewer = {
setMedia ({ commit }, attachments) {
const media = attachments.filter(attachment => {
const type = fileTypeService.fileType(attachment.mimetype)
return type === 'image' || type === 'video' || type === 'audio'
return supportedTypes.has(type)
})
commit('setMedia', media)
},
setCurrent ({ commit, state }, current) {
setCurrentMedia ({ commit, state }, current) {
const index = state.media.indexOf(current)
commit('setCurrent', index || 0)
commit('setCurrentMedia', index || 0)
},
closeMediaViewer ({ commit }) {
commit('close')