aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--src/App.js3
-rw-r--r--src/App.vue2
-rw-r--r--src/components/attachment/attachment.js72
-rw-r--r--src/components/attachment/attachment.vue458
-rw-r--r--src/components/chat_message/chat_message.scss6
-rw-r--r--src/components/flash/flash.js5
-rw-r--r--src/components/flash/flash.vue28
-rw-r--r--src/components/gallery/gallery.js75
-rw-r--r--src/components/gallery/gallery.vue177
-rw-r--r--src/components/media_modal/media_modal.js8
-rw-r--r--src/components/media_modal/media_modal.vue7
-rw-r--r--src/components/post_status_form/post_status_form.js8
-rw-r--r--src/components/post_status_form/post_status_form.vue57
-rw-r--r--src/components/rich_content/rich_content.jsx69
-rw-r--r--src/components/settings_modal/tabs/general_tab.js1
-rw-r--r--src/components/settings_modal/tabs/general_tab.vue5
-rw-r--r--src/components/status_body/status_body.js15
-rw-r--r--src/components/status_body/status_body.scss2
-rw-r--r--src/components/status_body/status_body.vue38
-rw-r--r--src/components/status_content/status_content.js27
-rw-r--r--src/components/status_content/status_content.vue33
-rw-r--r--src/components/user_card/user_card.js2
-rw-r--r--src/i18n/en.json7
-rw-r--r--src/modules/config.js1
-rw-r--r--src/modules/media_viewer.js9
-rw-r--r--test/unit/specs/components/rich_content.spec.js272
-rw-r--r--test/unit/specs/services/html_converter/html_line_converter.spec.js2
28 files changed, 821 insertions, 569 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 905d9f65..ec9cfde8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added option to mark posts as sensitive by default
- Added quick filters for notifications
- Implemented user option to change sidebar position to the right side
+- Implemented user option to hide floating shout panel
## [2.3.0] - 2021-03-01
diff --git a/src/App.js b/src/App.js
index fe4c30cb..362ac19d 100644
--- a/src/App.js
+++ b/src/App.js
@@ -73,6 +73,9 @@ export default {
this.$store.state.instance.instanceSpecificPanelContent
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
+ hideShoutbox () {
+ return this.$store.getters.mergedConfig.hideShoutbox
+ },
isMobileLayout () { return this.$store.state.interface.mobileLayout },
privateMode () { return this.$store.state.instance.private },
sidebarAlign () {
diff --git a/src/App.vue b/src/App.vue
index 6c582c03..c30f5e98 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -50,7 +50,7 @@
<media-modal />
</div>
<shout-panel
- v-if="currentUser && shout"
+ v-if="currentUser && shout && !hideShoutbox"
:floating="true"
class="floating-shout mobile-hidden"
/>
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 8849f501..a80c5c2e 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -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,22 @@ const Attachment = {
VideoAttachment
},
computed: {
+ classNames () {
+ return [
+ {
+ '-loading': this.loading,
+ '-nsfw-placeholder': this.hidden
+ },
+ '-' + this.type,
+ `-${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,13 +101,6 @@ 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
@@ -92,6 +110,11 @@ const Attachment = {
},
...mapGetters(['mergedConfig'])
},
+ watch: {
+ localDescription (newVal) {
+ this.onEdit(newVal)
+ }
+ },
methods: {
linkClicked ({ target }) {
if (target.tagName === 'A') {
@@ -100,12 +123,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 +170,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.vue b/src/components/attachment/attachment.vue
index f80badfd..9b1e83a7 100644
--- 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
@@ -11,318 +12,183 @@
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
+ @click.prevent=""
>
<FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
</a>
- </div>
+ </button>
<div
+ class="Attachment"
+ :class="classNames"
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"
+ <div
+ class="attachment-wrapper"
+ v-show="!isEmpty"
>
- <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
+ 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>
- <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 === '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>
- <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>
+ <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="type === 'html' && attachment.oembed"
- class="oembed"
- @click.prevent="linkClicked"
+ v-if="size !== 'hide' && !hideDescription && (edit || localDescription)"
+ 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
index de6990b6..1dbe1cad 100644
--- 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%;
}
@@ -91,7 +87,7 @@
.without-attachment {
.message-content {
// TODO figure out how to do it properly
- .rich-content-wrapper::after {
+ .RichContent::after {
margin-right: 5.4em;
content: " ";
display: inline-block;
diff --git a/src/components/flash/flash.js b/src/components/flash/flash.js
index d03384c7..87f940a7 100644
--- 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
index d20d037b..95f71950 100644
--- 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
index f856fd0a..cca67dbd 100644
--- a/src/components/gallery/gallery.js
+++ b/src/components/gallery/gallery.js
@@ -1,15 +1,23 @@
import Attachment from '../attachment/attachment.vue'
-import { chunk, last, dropRight, sumBy } from 'lodash'
+import { sumBy } from 'lodash'
const Gallery = {
props: [
'attachments',
+ 'limitRows',
+ 'descriptions',
'nsfw',
- 'setMedia'
+ 'setMedia',
+ 'size',
+ 'editable',
+ 'removeAttachment',
+ 'editAttachment',
+ 'maxPerRow'
],
data () {
return {
- sizes: {}
+ sizes: {},
+ hidingLong: true
}
},
components: { Attachment },
@@ -18,26 +26,51 @@ 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
+ if (this.size === 'hide') {
+ return this.attachments.map(item => ({ minimal: true, items: [item] }))
}
+ const rows = this.attachments.reduce((acc, attachment, i) => {
+ if (this.size === 'small' && acc.length === 2) return acc
+ if (attachment.mimetype.includes('audio')) {
+ return [...acc, { audio: true, items: [attachment] }, { items: [] }]
+ }
+ const maxPerRow = this.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) => {
+ return acc + (row.audio ? 0.25 : (1 / (row.items.length + 0.6)))
+ }, 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) {
+ return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
+ }
},
itemStyle (id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id))
@@ -46,6 +79,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
index 3e967e9c..cedf64d3 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -1,26 +1,79 @@
<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 }"
+ >
+ <div class="gallery-row-inner">
+ <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="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,16 +84,65 @@
<style lang="scss">
@import '../../_variables.scss';
-.gallery-row {
- position: relative;
- height: 0;
- width: 100%;
- flex-grow: 1;
+.Gallery {
+ .gallery-rows {
+ display: flex;
+ flex-direction: column;
+ }
- &:not(:first-child) {
+ .gallery-row {
+ position: relative;
+ height: 0;
+ width: 100%;
+ flex-grow: 1;
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 {
+
+ &.-minimal {
+ height: auto;
+
+ .gallery-row-inner {
+ position: relative;
+ }
+ }
+ }
+
.gallery-row-inner {
position: absolute;
top: 0;
@@ -53,7 +155,7 @@
align-content: stretch;
}
- .gallery-row-inner .attachment {
+ .gallery-item {
margin: 0 0.5em 0 0;
flex-grow: 1;
height: 100%;
@@ -64,32 +166,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
index e7384c93..b6919366 100644
--- a/src/components/media_modal/media_modal.js
+++ b/src/components/media_modal/media_modal.js
@@ -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) {
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 54bc5335..a578bc71 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -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')"
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 5342894f..1a12db9c 100644
--- 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,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)
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index fbda41d6..c6f84a4b 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -287,32 +287,21 @@
@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"
+ :maxPerRow="1"
+ :nsfw="false"
+ :attachments="newStatus.files"
+ :descriptions="newStatus.mediaDescriptions"
+ :set-media="() => $store.dispatch('setMedia', newStatus.files)"
+ :editable="true"
+ :editAttachment="editAttachment"
+ :removeAttachment="removeMediaFile"
+ size="small"
+ @play="$emit('mediaplay', attachment.id)"
+ @pause="$emit('mediapause', attachment.id)"
+ />
<div
v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox"
class="upload_settings"
@@ -507,15 +496,6 @@
flex-direction: column;
}
- .attachments .media-upload-wrapper {
- position: relative;
-
- .attachment {
- margin: 0;
- padding: 0;
- }
- }
-
.btn {
cursor: pointer;
}
@@ -616,11 +596,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/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index ffb36f50..ce562f13 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -5,6 +5,7 @@ import { convertHtmlToTree } from 'src/services/html_converter/html_tree_convert
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
import StillImage from 'src/components/still-image/still-image.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue'
+import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import './rich_content.scss'
@@ -51,6 +52,11 @@ export default Vue.component('RichContent', {
required: false,
type: Boolean,
default: false
+ },
+ hideMentions: {
+ required: false,
+ type: Boolean,
+ default: false
}
},
// NEVER EVER TOUCH DATA INSIDE RENDER
@@ -64,6 +70,7 @@ export default Vue.component('RichContent', {
// unique index for vue "tag" property
let mentionIndex = 0
let tagsIndex = 0
+ let firstMentionReplaced = false
const renderImage = (tag) => {
return <StillImage
@@ -78,7 +85,6 @@ export default Vue.component('RichContent', {
attrs.target = '_blank'
if (!encounteredTextReverse) {
lastTags.push(linkData)
- attrs['data-parser-last'] = true
}
return <a {...{ attrs }}>
{ children.map(processItem) }
@@ -90,7 +96,12 @@ export default Vue.component('RichContent', {
writtenMentions.push(linkData)
if (!encounteredText) {
firstMentions.push(linkData)
- return ''
+ if (!firstMentionReplaced && !this.hideMentions) {
+ firstMentionReplaced = true
+ return <MentionsLine mentions={ firstMentions } />
+ } else {
+ return ''
+ }
} else {
return <MentionLink
url={attrs.href}
@@ -116,7 +127,7 @@ export default Vue.component('RichContent', {
encounteredText = true
}
if (item.includes(':')) {
- unescapedItem = processTextForEmoji(
+ unescapedItem = ['', processTextForEmoji(
unescapedItem,
this.emoji,
({ shortcode, url }) => {
@@ -127,14 +138,14 @@ export default Vue.component('RichContent', {
alt={`:${shortcode}:`}
/>
}
- )
+ )]
}
return unescapedItem
}
// Handle tag nodes
if (Array.isArray(item)) {
- const [opener, children] = item
+ const [opener, children, closer] = item
const Tag = getTagName(opener)
const attrs = getAttrs(opener)
switch (Tag) {
@@ -143,7 +154,7 @@ export default Vue.component('RichContent', {
if (firstMentions.length > 1 && lastMentions.length > 1) {
break
} else {
- return ''
+ return !this.hideMentions ? <MentionsLine mentions={lastMentions} /> : ''
}
} else {
break
@@ -153,25 +164,19 @@ export default Vue.component('RichContent', {
case 'a': // replace mentions with MentionLink
if (!this.handleLinks) break
if (attrs['class'] && attrs['class'].includes('mention')) {
+ // Handling mentions here
return renderMention(attrs, children, encounteredText)
- } else if (attrs['class'] && attrs['class'].includes('hashtag')) {
+ } else {
+ // Everything else will be handled in reverse pass
encounteredText = true
return item // We'll handle it later
- } else {
- attrs.target = '_blank'
- return <a {...{ attrs }}>
- { children.map(processItem) }
- </a>
}
}
- // Render tag as is
if (children !== undefined) {
- return <Tag {...{ attrs: getAttrs(opener) }}>
- { children.map(processItem) }
- </Tag>
+ return [opener, children.map(processItem), closer]
} else {
- return <Tag/>
+ return item
}
}
}
@@ -188,7 +193,7 @@ export default Vue.component('RichContent', {
} else if (Array.isArray(item)) {
// Handle tag nodes
const [opener, children] = item
- const Tag = getTagName(opener)
+ const Tag = opener === '' ? '' : getTagName(opener)
switch (Tag) {
case 'a': // replace mentions with MentionLink
if (!this.handleLinks) break
@@ -196,17 +201,43 @@ export default Vue.component('RichContent', {
// should only be this
if (attrs['class'] && attrs['class'].includes('hashtag')) {
return renderHashtag(attrs, children, encounteredTextReverse)
+ } else {
+ attrs.target = '_blank'
+ html.includes('freenode') && console.log('PASS1', children)
+ const newChildren = [...children].reverse().map(processItemReverse).reverse()
+ html.includes('freenode') && console.log('PASS1b', newChildren)
+
+ return <a {...{ attrs }}>
+ { newChildren }
+ </a>
}
+ case '':
+ return [...children].reverse().map(processItemReverse).reverse()
+ }
+
+ // Render tag as is
+ if (children !== undefined) {
+ html.includes('freenode') && console.log('PASS2', children)
+ const newChildren = Array.isArray(children)
+ ? [...children].reverse().map(processItemReverse).reverse()
+ : children
+ return <Tag {...{ attrs: getAttrs(opener) }}>
+ { newChildren }
+ </Tag>
+ } else {
+ return <Tag/>
}
}
return item
}
+ const pass1 = convertHtmlToTree(html).map(processItem)
+ const pass2 = [...pass1].reverse().map(processItemReverse).reverse()
// DO NOT USE SLOTS they cause a re-render feedback loop here.
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
// at least until vue3?
const result = <span class="RichContent">
- { convertHtmlToTree(html).map(processItem).reverse().map(processItemReverse).reverse() }
+ { pass2 }
</span>
const event = {
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 07fccf57..eeda61bf 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -50,6 +50,7 @@ const GeneralTab = {
return this.$store.state.instance.background &&
!this.$store.state.users.currentUser.background_image
},
+ instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
...SharedComputedObject()
}
}
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index e62df290..71780e00 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -21,6 +21,11 @@
{{ $t('settings.hide_wallpaper') }}
</BooleanSetting>
</li>
+ <li v-if="instanceShoutboxPresent">
+ <BooleanSetting path="hideShoutbox">
+ {{ $t('settings.hide_shoutbox') }}
+ </BooleanSetting>
+ </li>
</ul>
</div>
<div class="setting-item">
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index 6dc028a6..94366c6c 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -1,9 +1,7 @@
import fileType from 'src/services/file_type/file_type.service'
import RichContent from 'src/components/rich_content/rich_content.jsx'
-import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
-import { set } from 'vue'
import {
faFile,
faMusic,
@@ -36,10 +34,7 @@ const StatusContent = {
showingTall: this.fullContent || (this.inConversation && this.focused),
showingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
- expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
- headTailLinks: null,
- firstMentions: [],
- lastMentions: []
+ expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
computed: {
@@ -82,8 +77,7 @@ const StatusContent = {
...mapGetters(['mergedConfig'])
},
components: {
- RichContent,
- MentionsLine
+ RichContent
},
mounted () {
this.status.attentions && this.status.attentions.forEach(attn => {
@@ -99,11 +93,6 @@ const StatusContent = {
this.expandingSubject = !this.expandingSubject
}
},
- setHeadTailLinks (headTailLinks) {
- set(this, 'headTailLinks', headTailLinks)
- set(this, 'firstMentions', headTailLinks.firstMentions)
- set(this, 'lastMentions', headTailLinks.lastMentions)
- },
generateTagLink (tag) {
return `/tag/${tag}`
}
diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss
index a93c92e0..516ced9d 100644
--- a/src/components/status_body/status_body.scss
+++ b/src/components/status_body/status_body.scss
@@ -68,7 +68,7 @@
overflow-y: hidden;
z-index: 1;
- .rich-content-wrapper {
+ .media-body {
min-height: 0;
mask:
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 6609d989..2be46303 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -1,8 +1,5 @@
<template>
-<div
- class="StatusBody"
- :class="{ '-compact': compact }"
->
+ <div class="StatusBody">
<div class="body">
<div
v-if="status.summary_raw_html"
@@ -11,7 +8,6 @@
>
<RichContent
class="media-body summary"
- :single-line="compact"
:html="status.summary_raw_html"
:emoji="status.emojis"
/>
@@ -42,29 +38,17 @@
>
{{ $t("general.show_more") }}
</button>
- <span
+ <RichContent
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
- class="rich-content-wrapper"
- >
- <MentionsLine
- v-if="!hideMentions && firstMentions && firstMentions.length > 0"
- :mentions="firstMentions"
- />
- <RichContent
- :class="{ '-single-line': singleLine }"
- class="text media-body"
- :html="status.raw_html"
- :emoji="status.emojis"
- :handle-links="true"
- :single-line="compact"
- :greentext="mergedConfig.greentext"
- @parseReady="setHeadTailLinks"
- />
- <MentionsLine
- v-if="!hideMentions && lastMentions.length > 1 && firstMentions.length <= 1"
- :mentions="lastMentions"
- />
- </span>
+ :class="{ '-single-line': singleLine }"
+ class="text media-body"
+ :html="status.raw_html"
+ :emoji="status.emojis"
+ :handle-links="true"
+ :hide-mentions="hideMentions"
+ :greentext="mergedConfig.greentext"
+ @parseReady="$emit('parseReady', $event)"
+ />
<button
v-if="hideSubjectStatus"
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 4d6f0a14..55f701d0 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -59,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
},
@@ -91,15 +73,6 @@ const StatusContent = {
Gallery,
LinkPreview,
StatusBody
- },
- methods: {
- setHeadTailLinks (headTailLinks) {
- this.$emit('parseReady', headTailLinks)
- },
- 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
index 06edd780..c1533b19 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -9,35 +9,22 @@
:compact="compact"
:single-line="singleLine"
:hide-mentions="hideMentions"
- @parseReady="setHeadTailLinks"
+ @parseReady="$emit('parseReady', $event)"
>
<div v-if="status.poll && status.poll.options">
<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"
+ :size="attachmentSize"
+ @setMedia="onMedia"
+ @play="$emit('mediaplay', attachment.id)"
+ @pause="$emit('mediapause', attachment.id)"
+ />
<div
v-if="status.card && !noHeading && !compact"
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index d9fb64d1..23e6358f 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -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 })
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 2b253efb..a4f24c64 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -350,6 +350,7 @@
"hide_all_muted_posts": "Hide muted posts",
"max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel",
+ "hide_shoutbox": "Hide instance shoutbox",
"right_sidebar": "Show sidebar on the right side",
"hide_wallpaper": "Hide instance wallpaper",
"preload_images": "Preload images",
@@ -716,7 +717,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",
diff --git a/src/modules/config.js b/src/modules/config.js
index ad9c8be5..db9d5ffb 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -21,6 +21,7 @@ export const defaultState = {
customThemeSource: undefined,
hideISP: false,
hideInstanceWallpaper: false,
+ hideShoutbox: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js
index 721c25e6..ebcba01d 100644
--- 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')
diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js
index 835fbea2..96c480ea 100644
--- a/test/unit/specs/components/rich_content.spec.js
+++ b/test/unit/specs/components/rich_content.spec.js
@@ -1,4 +1,4 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils'
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
import RichContent from 'src/components/rich_content/rich_content.jsx'
const localVue = createLocalVue()
@@ -16,6 +16,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -38,6 +39,34 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
+ handleLinks: true,
+ greentext: true,
+ emoji: [],
+ html
+ }
+ })
+
+ expect(wrapper.html()).to.eql(compwrap(expected))
+ })
+
+ it('replaces first mention with mentionsline if hideMentions=false', () => {
+ const html = p(
+ makeMention('John'),
+ ' how are you doing thoday?'
+ )
+ const expected = p(
+ '<span class="h-card">',
+ '<mentionsline-stub mentions="',
+ '[object Object]',
+ '"></mentionsline-stub>',
+ '</span>',
+ 'how are you doing thoday?'
+ )
+ const wrapper = shallowMount(RichContent, {
+ localVue,
+ propsData: {
+ hideMentions: false,
handleLinks: true,
greentext: true,
emoji: [],
@@ -68,6 +97,44 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
+ handleLinks: true,
+ greentext: true,
+ emoji: [],
+ html
+ }
+ })
+
+ expect(wrapper.html()).to.eql(compwrap(expected))
+ })
+
+ it('replaces mentions at the end of the hellpost if hideMentions=false (<p>)', () => {
+ const html = [
+ p('How are you doing today, fine gentlemen?'),
+ p(
+ makeMention('John'),
+ makeMention('Josh'),
+ makeMention('Jeremy')
+ )
+ ].join('')
+ const expected = [
+ p(
+ 'How are you doing today, fine gentlemen?'
+ ),
+ // TODO fix this extra line somehow?
+ p(
+ '<mentionsline-stub mentions="',
+ '[object Object],',
+ '[object Object],',
+ '[object Object]',
+ '"></mentionsline-stub>'
+ )
+ ].join('')
+
+ const wrapper = shallowMount(RichContent, {
+ localVue,
+ propsData: {
+ hideMentions: false,
handleLinks: true,
greentext: true,
emoji: [],
@@ -96,6 +163,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -124,6 +192,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -165,6 +234,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -199,6 +269,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -240,6 +311,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -267,6 +339,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: false,
greentext: true,
emoji: [],
@@ -290,6 +363,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: false,
greentext: true,
emoji: [],
@@ -309,6 +383,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: false,
greentext: false,
emoji: [],
@@ -329,6 +404,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: false,
greentext: false,
emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
@@ -345,6 +421,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: false,
greentext: false,
emoji: [],
@@ -407,6 +484,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -425,10 +503,18 @@ describe('RichContent', () => {
makeMention('bar'),
makeMention('baz')
].join('<br>')
+ const expected = [
+ 'Bruh',
+ 'Bruh',
+ stubMention('foo'),
+ stubMention('bar'),
+ stubMention('baz')
+ ].join('<br>')
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -436,7 +522,7 @@ describe('RichContent', () => {
}
})
- expect(wrapper.html()).to.eql(compwrap(html))
+ expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Don\'t remove last mentions if there are more than one first mention - remove first instead', () => {
@@ -471,6 +557,7 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
handleLinks: true,
greentext: true,
emoji: [],
@@ -506,6 +593,187 @@ describe('RichContent', () => {
const wrapper = shallowMount(RichContent, {
localVue,
propsData: {
+ hideMentions: true,
+ handleLinks: true,
+ greentext: true,
+ emoji: [],
+ html
+ }
+ })
+
+ expect(wrapper.html()).to.eql(compwrap(expected))
+ })
+
+ it('buggy example/hashtags', () => {
+ const html = [
+ '<p>',
+ '<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg">',
+ 'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
+ ' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou">',
+ '#nou</a>',
+ ' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap">',
+ '#screencap</a>',
+ ' </p>'
+ ].join('')
+ const expected = [
+ '<p>',
+ '<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">',
+ 'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
+ ' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou" target="_blank">',
+ '#nou</a>',
+ ' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap" target="_blank">',
+ '#screencap</a>',
+ ' </p>'
+ ].join('')
+
+ const wrapper = shallowMount(RichContent, {
+ localVue,
+ propsData: {
+ hideMentions: true,
+ handleLinks: true,
+ greentext: true,
+ emoji: [],
+ html
+ }
+ })
+
+ expect(wrapper.html()).to.eql(compwrap(expected))
+ })
+
+ it('rich contents of a mention are handled properly', () => {
+ const html = [
+ p(
+ 'Testing'
+ ),
+ p(
+ '<a href="lol" class="mention">',
+ '<span>',
+ 'https://</span>',
+ '<span>',
+ 'lol.tld/</span>',
+ '<span>',
+ '</span>',
+ '</a>'
+ )
+ ].join('')
+ const expected = [
+ p(
+ 'Testing'
+ ),
+ p(
+ '<mentionlink-stub url="lol" content="',
+ '<span>',
+ 'https://</span>',
+ '<span>',
+ 'lol.tld/</span>',
+ '<span>',
+ '</span>',
+ '">',
+ '</mentionlink-stub>'
+ )
+ ].join('')
+
+ const wrapper = shallowMount(RichContent, {
+ localVue,
+ propsData: {
+ hideMentions: false,
+ handleLinks: true,
+ greentext: true,
+ emoji: [],
+ html
+ }
+ })
+
+ expect(wrapper.html()).to.eql(compwrap(expected))
+ })
+
+ it('rich contents of a mention in beginning are handled properly', () => {
+ const html = [
+ p(
+ '<a href="lol" class="mention">',
+ '<span>',
+ 'https://</span>',
+ '<span>',
+ 'lol.tld/</span>',
+ '<span>',
+ '</span>',
+ '</a>'
+ ),
+ p(
+ 'Testing'
+ )
+ ].join('')
+ const expected = [
+ p(
+ '<span class="MentionsLine">',
+ '<mentionlink-stub content="',
+ '<span>',
+ 'https://</span>',
+ '<span>',
+ 'lol.tld/</span>',
+ '<span>',
+ '</span>',
+ '" url="lol" class="mention-link">',
+ '</mentionlink-stub>',
+ '<!---->', // v-if placeholder
+ '</span>'
+ ),
+ p(
+ 'Testing'
+ )
+ ].join('')
+
+ const wrapper = mount(RichContent, {
+ localVue,
+ stubs: {
+ MentionLink: true
+ },
+ propsData: {
+ hideMentions: false,
+ handleLinks: true,
+ greentext: true,
+ emoji: [],
+ html
+ }
+ })
+
+ expect(wrapper.html()).to.eql(compwrap(expected))
+ })
+
+ it('rich contents of a link are handled properly', () => {
+ const html = [
+ '<p>',
+ 'Freenode is dead.</p>',
+ '<p>',
+ '<a href="https://isfreenodedeadyet.com/">',
+ '<span>',
+ 'https://</span>',
+ '<span>',
+ 'isfreenodedeadyet.com/</span>',
+ '<span>',
+ '</span>',
+ '</a>',
+ '</p>'
+ ].join('')
+ const expected = [
+ '<p>',
+ 'Freenode is dead.</p>',
+ '<p>',
+ '<a href="https://isfreenodedeadyet.com/" target="_blank">',
+ '<span>',
+ 'https://</span>',
+ '<span>',
+ 'isfreenodedeadyet.com/</span>',
+ '<span>',
+ '</span>',
+ '</a>',
+ '</p>'
+ ].join('')
+
+ const wrapper = shallowMount(RichContent, {
+ localVue,
+ propsData: {
+ hideMentions: false,
handleLinks: true,
greentext: true,
emoji: [],
diff --git a/test/unit/specs/services/html_converter/html_line_converter.spec.js b/test/unit/specs/services/html_converter/html_line_converter.spec.js
index c8c89700..de7c7fc2 100644
--- a/test/unit/specs/services/html_converter/html_line_converter.spec.js
+++ b/test/unit/specs/services/html_converter/html_line_converter.spec.js
@@ -11,7 +11,7 @@ const mapOnlyText = (processor) => (input) => {
}
}
-describe.only('html_line_converter', () => {
+describe('html_line_converter', () => {
describe('with processor that keeps original line should not make any changes to HTML when', () => {
const processorKeep = (line) => line
it('fed with regular HTML with newlines', () => {