commit: ddee8bb686fb18baf5212089653a8b4e40feedd6
parent 56616787ec1b8373e5c160c6fe863694a66ca2db
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Sun, 20 Feb 2022 15:11:33 +0000
Merge branch 'proper-attachments' into 'develop'
Attachment improvements
See merge request pleroma/pleroma-fe!1399
Diffstat:
23 files changed, 1149 insertions(+), 582 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -6,12 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Fixed
- Completely hidden posts still had 1px border
+- Attachments are ALWAYS in same order as user uploaded, no more "videos first"
+- Attachment description is prefilled with backend-provided default when uploading
+- Proper visual feedback that next image is loading when browsing
### Changed
- Settings window has been throughly rearranged to make make more sense and make navication settings easier.
+- Uploaded attachments are uniform with displayed attachments
+- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
+- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
### Added
- Option to completely hide muted threads
+- Ability to open videos in modal even if you disabled that feature, via an icon button
+- New button on attachment that indicates that attachment has a description and shows a bar filled with description
+- Attachments are truncated just like post contents
+- Media modal now also displays description and counter position in gallery (i.e. 1/5)
+- Ability to rearrange order of attachments when uploading
## [2.4.2] - 2022-01-09
### Added
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
@@ -11,7 +11,12 @@ import {
faImage,
faVideo,
faPlayCircle,
- faTimes
+ faTimes,
+ faStop,
+ faSearchPlus,
+ faTrashAlt,
+ faPencilAlt,
+ faAlignRight
} from '@fortawesome/free-solid-svg-icons'
library.add(
@@ -20,27 +25,39 @@ library.add(
faImage,
faVideo,
faPlayCircle,
- faTimes
+ faTimes,
+ faStop,
+ faSearchPlus,
+ faTrashAlt,
+ faPencilAlt,
+ faAlignRight
)
const Attachment = {
props: [
'attachment',
+ 'description',
+ 'hideDescription',
'nsfw',
'size',
- 'allowPlay',
'setMedia',
- 'naturalSizeLoad'
+ 'remove',
+ 'shiftUp',
+ 'shiftDn',
+ '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,
+ showDescription: false
}
},
components: {
@@ -49,8 +66,23 @@ 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'
+ return this.size === 'hide'
+ },
+ useContainFit () {
+ return this.$store.getters.mergedConfig.useContainFit
},
placeholderName () {
if (this.attachment.description === '' || !this.attachment.description) {
@@ -74,24 +106,33 @@ const Attachment = {
return this.nsfw && this.hideNsfwLocal && !this.showHidden
},
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'
+ return (this.type === 'html' && !this.attachment.oembed)
},
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)
},
+ videoTag () {
+ return this.useModal ? 'button' : 'span'
+ },
...mapGetters(['mergedConfig'])
},
+ watch: {
+ localDescription (newVal) {
+ this.onEdit(newVal)
+ }
+ },
methods: {
linkClicked ({ target }) {
if (target.tagName === 'A') {
@@ -100,12 +141,37 @@ 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)
+ } else if (this.type === 'unknown') {
+ window.open(this.attachment.url)
}
},
+ openModalForce (event) {
+ this.$emit('setMedia')
+ this.$store.dispatch('setCurrentMedia', this.attachment)
+ },
+ onEdit (event) {
+ this.edit && this.edit(this.attachment, event)
+ },
+ onRemove () {
+ this.remove && this.remove(this.attachment)
+ },
+ onShiftUp () {
+ this.shiftUp && this.shiftUp(this.attachment)
+ },
+ onShiftDn () {
+ this.shiftDn && this.shiftDn(this.attachment)
+ },
+ stopFlash () {
+ this.$refs.flash.closePlayer()
+ },
+ setFlashLoaded (event) {
+ this.flashLoaded = event
+ },
+ toggleDescription () {
+ this.showDescription = !this.showDescription
+ },
toggleHidden (event) {
if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
@@ -132,7 +198,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 })
}
}
}
diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss
@@ -0,0 +1,268 @@
+@import '../../_variables.scss';
+
+.Attachment {
+ display: inline-flex;
+ flex-direction: column;
+ position: relative;
+ align-self: flex-start;
+ line-height: 0;
+ height: 100%;
+ 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);
+
+ .attachment-wrapper {
+ flex: 1 1 auto;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .description-container {
+ flex: 0 1 0;
+ display: flex;
+ padding-top: 0.5em;
+ z-index: 1;
+
+ p {
+ flex: 1;
+ text-align: center;
+ line-height: 1.5;
+ padding: 0.5em;
+ margin: 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ &.-static {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding-top: 0;
+ background: var(--popover);
+ box-shadow: var(--popupShadow);
+ }
+ }
+
+ .description-field {
+ flex: 1;
+ min-width: 0;
+ }
+
+ & .placeholder-container,
+ & .image-container,
+ & .audio-container,
+ & .video-container,
+ & .flash-container,
+ & .oembed-container {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ }
+
+ .image-container {
+ .image {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ & .flash-container,
+ & .video-container {
+ & .flash,
+ & video {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ align-self: center;
+ }
+ }
+
+ .audio-container {
+ display: flex;
+ align-items: flex-end;
+
+ audio {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .placeholder-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 0.5em;
+ }
+
+
+ .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);
+
+ &::before {
+ margin: 0;
+ }
+ }
+
+ .attachment-buttons {
+ display: flex;
+ position: absolute;
+ right: 0;
+ top: 0;
+ margin-top: 0.5em;
+ margin-right: 0.5em;
+ z-index: 1;
+
+ .attachment-button {
+ padding: 0;
+ border-radius: $fallback--tooltipRadius;
+ border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+ text-align: center;
+ width: 2em;
+ height: 2em;
+ margin-left: 0.5em;
+ 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);
+ }
+ }
+ }
+
+ .oembed-container {
+ 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;
+ }
+ }
+ }
+
+ &.-size-small {
+ .play-icon {
+ zoom: 0.5;
+ opacity: 0.7;
+ }
+
+ .attachment-buttons {
+ zoom: 0.7;
+ opacity: 0.5;
+ }
+ }
+
+ &.-editable {
+ padding: 0.5em;
+
+ & .description-container,
+ & .attachment-buttons {
+ margin: 0;
+ }
+ }
+
+ &.-placeholder {
+ display: inline-block;
+ color: $fallback--link;
+ color: var(--postLink, $fallback--link);
+ overflow: hidden;
+ white-space: nowrap;
+ height: auto;
+ line-height: 1.5;
+
+ &:not(.-editable) {
+ border: none;
+ }
+
+ &.-editable {
+ display: flex;
+ flex-direction: row;
+ align-items: baseline;
+
+ & .description-container,
+ & .attachment-buttons {
+ margin: 0;
+ padding: 0;
+ position: relative;
+ }
+
+ .description-container {
+ flex: 1;
+ padding-left: 0.5em;
+ }
+
+ .attachment-buttons {
+ order: 99;
+ align-self: center;
+ }
+ }
+
+ a {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ svg {
+ color: inherit;
+ }
+ }
+
+ &.-loading {
+ cursor: progress;
+ }
+
+ &.-contain-fit {
+ img,
+ canvas {
+ object-fit: contain;
+ }
+ }
+
+ &.-cover-fit {
+ img,
+ canvas {
+ object-fit: cover;
+ }
+ }
+}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
@@ -1,7 +1,8 @@
<template>
- <div
+ <button
v-if="usePlaceholder"
- :class="{ 'fullwidth': fullwidth }"
+ class="Attachment -placeholder button-unstyled"
+ :class="classNames"
@click="openModal"
>
<a
@@ -13,316 +14,251 @@
:title="attachment.description"
>
<FAIcon :icon="placeholderIconClass" />
- <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
+ <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ 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"
+ <div
+ v-if="edit || remove"
+ class="attachment-buttons"
>
- <img
- :key="nsfwImage"
- class="nsfw"
- :src="nsfwImage"
- :class="{'small': isSmall}"
+ <button
+ v-if="remove"
+ class="button-unstyled attachment-button"
+ @click.prevent="onRemove"
>
- <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="trash-alt" />
+ </button>
+ </div>
+ <div
+ v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
+ class="description-container"
+ :class="{ '-static': !edit }"
>
- <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"
+ <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
+ v-else
+ class="Attachment"
+ :class="classNames"
+ >
+ <div
+ v-show="!isEmpty"
+ class="attachment-wrapper"
>
- <StillImage
- class="image"
- :referrerpolicy="referrerpolicy"
- :mimetype="attachment.mimetype"
- :src="attachment.large_thumb_url || attachment.url"
- :image-load-handler="onImageLoad"
+ <a
+ v-if="hidden"
+ class="image-container"
+ :href="attachment.url"
:alt="attachment.description"
- />
- </a>
+ :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
+ v-if="!hidden"
+ class="attachment-buttons"
+ >
+ <button
+ v-if="type === 'flash' && flashLoaded"
+ class="button-unstyled attachment-button"
+ @click.prevent="stopFlash"
+ :title="$t('status.attachment_stop_flash')"
+ >
+ <FAIcon icon="stop" />
+ </button>
+ <button
+ v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
+ class="button-unstyled attachment-button"
+ @click.prevent="toggleDescription"
+ :title="$t('status.show_attachment_description')"
+ >
+ <FAIcon icon="align-right" />
+ </button>
+ <button
+ v-if="!useModal && type !== 'unknown'"
+ class="button-unstyled attachment-button"
+ @click.prevent="openModalForce"
+ :title="$t('status.show_attachment_in_modal')"
+ >
+ <FAIcon icon="search-plus" />
+ </button>
+ <button
+ v-if="nsfw && hideNsfwLocal"
+ class="button-unstyled attachment-button"
+ @click.prevent="toggleHidden"
+ :title="$t('status.hide_attachment')"
+ >
+ <FAIcon icon="times" />
+ </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
+ v-if="remove"
+ class="button-unstyled attachment-button"
+ @click.prevent="onRemove"
+ :title="$t('status.remove_attachment')"
+ >
+ <FAIcon icon="trash-alt" />
+ </button>
+ </div>
- <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>
+ <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 === 'unknown' && !hidden"
+ class="placeholder-container"
+ :href="attachment.url"
+ target="_blank"
+ >
+ <FAIcon size="5x" :icon="placeholderIconClass" />
+ <p>
+ {{ localDescription }}
+ </p>
+ </a>
+
+ <component
+ :is="videoTag"
+ v-if="type === 'video' && !hidden"
+ class="video-container"
+ :class="{ 'button-unstyled': 'isModal' }"
+ :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"
+ />
+ </component>
+
+ <span
+ 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')"
+ />
+ </span>
- <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-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>
+ <span
+ v-if="type === 'flash' && !hidden"
+ class="flash-container"
+ :href="attachment.url"
+ @click.stop.prevent="openModal"
+ >
+ <Flash
+ ref="flash"
+ class="flash"
+ :src="attachment.large_thumb_url || attachment.url"
+ @playerOpened="setFlashLoaded(true)"
+ @playerClosed="setFlashLoaded(false)"
+ />
+ </span>
+ </div>
<div
- v-if="type === 'html' && attachment.oembed"
- class="oembed"
- @click.prevent="linkClicked"
+ v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
+ class="description-container"
+ :class="{ '-static': !edit }"
>
- <div
- v-if="attachment.thumb_url"
- class="image"
+ <input
+ v-if="edit"
+ v-model="localDescription"
+ type="text"
+ class="description-field"
+ :placeholder="$t('post_status.media_description')"
+ @keydown.enter.prevent=""
>
- <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>
+ <p v-else>
+ {{ localDescription }}
+ </p>
</div>
-
- <Flash
- v-if="type === 'flash'"
- :src="attachment.large_thumb_url || attachment.url"
- />
</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>
diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss
@@ -62,10 +62,6 @@
&.with-media {
width: 100%;
- .gallery-row {
- overflow: hidden;
- }
-
.status {
width: 100%;
}
diff --git a/src/components/flash/flash.js b/src/components/flash/flash.js
@@ -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')
}
}
}
diff --git a/src/components/flash/flash.vue b/src/components/flash/flash.vue
@@ -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>
diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js
@@ -1,15 +1,26 @@
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',
+ 'shiftUpAttachment',
+ 'shiftDnAttachment',
+ 'editAttachment',
+ 'grid'
],
data () {
return {
- sizes: {}
+ sizes: {},
+ hidingLong: true
}
},
components: { Attachment },
@@ -18,26 +29,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 +101,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)
}
}
}
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
@@ -1,26 +1,84 @@
<template>
<div
ref="galleryContainer"
- style="width: 100%;"
+ class="Gallery"
+ :class="{ '-long': tooManyAttachments && hidingLong }"
>
+ <div class="gallery-rows">
+ <div
+ v-for="(row, rowIndex) in rows"
+ :key="rowIndex"
+ 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, attachmentIndex) in row.items"
+ :key="attachment.id"
+ class="gallery-item"
+ :nsfw="nsfw"
+ :attachment="attachment"
+ :allow-play="false"
+ :size="size"
+ :editable="editable"
+ :remove="removeAttachment"
+ :shiftUp="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
+ :shiftDn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
+ :edit="editAttachment"
+ :description="descriptions && descriptions[attachment.id]"
+ :hide-description="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
+ v-if="hidingLong"
+ class="many-attachments-button"
+ >
+ <button
+ class="button-unstyled -link"
+ @click="openGallery"
+ >
+ {{ $t("status.open_gallery") }}
+ </button>
+ </span>
</div>
</div>
</div>
@@ -31,12 +89,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 +160,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-fill, 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 +188,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>
diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js
@@ -3,22 +3,31 @@ 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,
- faChevronRight
+ faChevronRight,
+ faCircleNotch
} from '@fortawesome/free-solid-svg-icons'
library.add(
faChevronLeft,
- faChevronRight
+ faChevronRight,
+ faCircleNotch
)
const MediaModal = {
components: {
StillImage,
VideoAttachment,
- Modal
+ Modal,
+ Flash
+ },
+ data () {
+ return {
+ loading: false
+ }
},
computed: {
showing () {
@@ -27,6 +36,9 @@ const MediaModal = {
media () {
return this.$store.state.mediaViewer.media
},
+ description () {
+ return this.currentMedia.description
+ },
currentIndex () {
return this.$store.state.mediaViewer.currentIndex
},
@@ -37,7 +49,7 @@ const MediaModal = {
return this.media.length > 1
},
type () {
- return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
+ return this.currentMedia ? this.getType(this.currentMedia) : null
}
},
created () {
@@ -53,6 +65,9 @@ const MediaModal = {
)
},
methods: {
+ getType (media) {
+ return fileTypeService.fileType(media.mimetype)
+ },
mediaTouchStart (e) {
GestureService.beginSwipe(e, this.mediaSwipeGestureRight)
GestureService.beginSwipe(e, this.mediaSwipeGestureLeft)
@@ -67,15 +82,26 @@ 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])
+ const newMedia = this.media[prevIndex]
+ if (this.getType(newMedia) === 'image') {
+ this.loading = true
+ }
+ this.$store.dispatch('setCurrentMedia', newMedia)
}
},
goNext () {
if (this.canNavigate) {
const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1)
- this.$store.dispatch('setCurrent', this.media[nextIndex])
+ const newMedia = this.media[nextIndex]
+ if (this.getType(newMedia) === 'image') {
+ this.loading = true
+ }
+ this.$store.dispatch('setCurrentMedia', newMedia)
}
},
+ onImageLoaded () {
+ this.loading = false
+ },
handleKeyupEvent (e) {
if (this.showing && e.keyCode === 27) { // escape
this.hide()
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
@@ -6,6 +6,7 @@
>
<img
v-if="type === 'image'"
+ :class="{ loading }"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
@@ -13,6 +14,7 @@
@touchstart.stop="mediaTouchStart"
@touchmove.stop="mediaTouchMove"
@click="hide"
+ @load="onImageLoaded"
>
<VideoAttachment
v-if="type === 'video'"
@@ -28,6 +30,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')"
@@ -50,6 +59,27 @@
icon="chevron-right"
/>
</button>
+ <span
+ v-if="description"
+ class="description"
+ >
+ {{ description }}
+ </span>
+ <span
+ class="counter"
+ >
+ {{ currentIndex + 1 }} / {{ media.length }}
+ </span>
+ <span
+ v-if="loading"
+ class="loading-spinner"
+ >
+ <FAIcon
+ spin
+ icon="circle-notch"
+ size="5x"
+ />
+ </span>
</Modal>
</template>
@@ -58,6 +88,7 @@
<style lang="scss">
.modal-view.media-modal-view {
z-index: 1001;
+ flex-direction: column;
.modal-view-button-arrow {
opacity: 0.75;
@@ -67,69 +98,108 @@
outline: none;
box-shadow: none;
}
+
&:hover {
opacity: 1;
}
}
}
-@keyframes media-fadein {
- from {
- opacity: 0;
+.media-modal-view {
+ @keyframes media-fadein {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
}
- to {
- opacity: 1;
+
+ .description,
+ .counter {
+ /* Hardcoded since background is also hardcoded */
+ color: white;
+ margin-top: 1em;
+ text-shadow: 0 0 10px black, 0 0 10px black;
+ padding: 0.2em 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;
-}
+ .description {
+ flex: 0 0 auto;
+ overflow-y: auto;
+ min-height: 1em;
+ max-width: 500px;
+ max-height: 9.5em;
+ word-break: break-all;
+ }
-.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);
+ .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;
+
+ &.loading {
+ opacity: 0.5;
+ }
}
- &--prev {
- left: 0;
- .arrow-icon {
- left: 6px;
+ .loading-spinner {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ svg {
+ color: white;
}
}
- &--next {
- right: 0;
+ .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 {
- 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;
+ }
}
}
}
diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss
@@ -9,6 +9,12 @@
word-break: break-word;
--emoji-size: 14px;
+ &:hover {
+ --_still-image-img-visibility: visible;
+ --_still-image-canvas-visibility: hidden;
+ --_still-image-label-visibility: hidden;
+ }
+
&.-muted {
padding: 0.25em 0.6em;
height: 1.2em;
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
@@ -184,8 +184,9 @@
</router-link>
</div>
<template v-else>
- <status-content
+ <StatusContent
class="faint"
+ :compact="true"
:status="notification.action"
/>
</template>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
@@ -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,21 @@ const PostStatusForm = {
this.newStatus.files.splice(index, 1)
this.$emit('resize')
},
+ editAttachment (fileInfo, 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) {
templateArgs = templateArgs || {}
this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs)
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
@@ -287,32 +287,22 @@
@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"
+ :edit-attachment="editAttachment"
+ :remove-attachment="removeMediaFile"
+ :shift-up-attachment="newStatus.files.length > 1 && shiftUpMediaFile"
+ :shift-dn-attachment="newStatus.files.length > 1 && shiftDnMediaFile"
+ @play="$emit('mediaplay', attachment.id)"
+ @pause="$emit('mediapause', attachment.id)"
+ />
<div
v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox"
class="upload_settings"
@@ -330,26 +320,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 +484,6 @@
flex-direction: column;
}
- .attachments .media-upload-wrapper {
- position: relative;
-
- .attachment {
- margin: 0;
- padding: 0;
- }
- }
-
.btn {
cursor: pointer;
}
@@ -616,11 +584,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>
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
@@ -21,6 +21,7 @@ library.add(
const StatusContent = {
name: 'StatusContent',
props: [
+ 'compact',
'status',
'focused',
'noHeading',
@@ -49,6 +50,7 @@ const StatusContent = {
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
+ if (this.singleLine || this.compact) return false
const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
return lengthScore > 20
},
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
@@ -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;
+ }
+ }
}
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
@@ -1,5 +1,8 @@
<template>
- <div class="StatusBody">
+ <div
+ class="StatusBody"
+ :class="{ '-compact': compact }"
+ >
<div class="body">
<div
v-if="status.summary_raw_html"
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
@@ -3,7 +3,6 @@ import Poll from '../poll/poll.vue'
import Gallery from '../gallery/gallery.vue'
import StatusBody from 'src/components/status_body/status_body.vue'
import LinkPreview from '../link-preview/link-preview.vue'
-import fileType from 'src/services/file_type/file_type.service'
import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -28,6 +27,7 @@ const StatusContent = {
name: 'StatusContent',
props: [
'status',
+ 'compact',
'focused',
'noHeading',
'fullContent',
@@ -48,33 +48,15 @@ const StatusContent = {
return true
},
attachmentSize () {
- if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
+ if (this.compact) {
+ return 'small'
+ } else if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
(this.status.attachments.length > this.maxThumbnails)) {
return 'hide'
- } else if (this.compact) {
- return 'small'
}
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
},
@@ -89,12 +71,6 @@ const StatusContent = {
Gallery,
LinkPreview,
StatusBody
- },
- methods: {
- setMedia () {
- const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
- return () => this.$store.dispatch('setMedia', attachments)
- }
}
}
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
@@ -1,44 +1,42 @@
<template>
- <div class="StatusContent">
+ <div
+ class="StatusContent"
+ :class="{ '-compact': compact }"
+ >
<slot name="header" />
<StatusBody
:status="status"
+ :compact="compact"
:single-line="singleLine"
@parseReady="$emit('parseReady', $event)"
>
- <div v-if="status.poll && status.poll.options">
+ <div v-if="status.poll && status.poll.options && !compact">
<Poll
:base-poll="status.poll"
:emoji="status.emojis"
/>
</div>
- <div
- v-if="status.attachments.length !== 0"
- 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-else-if="status.poll && status.poll.options && compact">
+ <FAIcon
+ icon="poll-h"
+ size="2x"
/>
</div>
+ <gallery
+ v-if="status.attachments.length !== 0"
+ class="attachments media-body"
+ :nsfw="nsfwClickthrough"
+ :attachments="status.attachments"
+ :limit="compact ? 1 : 0"
+ :size="attachmentSize"
+ @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
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
@@ -166,7 +166,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 })
diff --git a/src/i18n/en.json b/src/i18n/en.json
@@ -733,7 +733,18 @@
"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",
+ "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"
},
"user_card": {
"approve": "Approve",
diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js
@@ -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')