new component - StatusText, to separate post's text from its attachments

This commit is contained in:
Henry Jameson 2021-06-07 18:41:55 +03:00
parent 04fa1f0b2d
commit 50aa379038
4 changed files with 282 additions and 248 deletions

View file

@ -1,12 +1,9 @@
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import Poll from '../poll/poll.vue' import Poll from '../poll/poll.vue'
import Gallery from '../gallery/gallery.vue' import Gallery from '../gallery/gallery.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import StatusText from 'src/components/status_text/status_text.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -36,52 +33,11 @@ const StatusContent = {
'fullContent', 'fullContent',
'singleLine' 'singleLine'
], ],
data () {
return {
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
}
},
computed: { computed: {
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
hideAttachments () { hideAttachments () {
return (this.mergedConfig.hideAttachments && !this.inConversation) || return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
}, },
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80
// per line.
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
longSubject () {
return this.status.summary.length > 240
},
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
mightHideBecauseSubject () {
return !!this.status.summary && this.localCollapseSubjectDefault
},
mightHideBecauseTall () {
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
},
hideSubjectStatus () {
return this.mightHideBecauseSubject && !this.expandingSubject
},
hideTallStatus () {
return this.mightHideBecauseTall && !this.showingTall
},
showingMore () {
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
},
nsfwClickthrough () { nsfwClickthrough () {
if (!this.status.nsfw) { if (!this.status.nsfw) {
return false return false
@ -119,45 +75,11 @@ const StatusContent = {
file => !fileType.fileMatchesSomeType(this.galleryTypes, file) file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
) )
}, },
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
maxThumbnails () { maxThumbnails () {
return this.mergedConfig.maxThumbnails return this.mergedConfig.maxThumbnails
}, },
postBodyHtml () {
const html = this.status.raw_html
if (this.mergedConfig.greentext) {
try {
if (html.includes('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else {
return string
}
})
} else {
return html
}
} catch (e) {
console.err('Failed to process status html', e)
return html
}
} else {
return html
}
},
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser currentUser: state => state.users.currentUser
}) })
}, },
@ -166,54 +88,9 @@ const StatusContent = {
Poll, Poll,
Gallery, Gallery,
LinkPreview, LinkPreview,
RichContent StatusText
},
mounted () {
this.status.attentions.forEach(attn => {
const { id } = attn
this.$store.dispatch('fetchUserIfMissing', id)
})
}, },
methods: { methods: {
linkClicked (event) {
const target = event.target.closest('.status-content a')
if (target) {
if (target.className.match(/mention/)) {
const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
if (attn) {
event.stopPropagation()
event.preventDefault()
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
this.$router.push(link)
return
}
}
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
// Extract tag name from dataset or link url
const tag = target.dataset.tag || extractTagFromUrl(target.href)
if (tag) {
const link = this.generateTagLink(tag)
this.$router.push(link)
return
}
}
window.open(target.href, '_blank')
}
},
toggleShowMore () {
if (this.mightHideBecauseTall) {
this.showingTall = !this.showingTall
} else if (this.mightHideBecauseSubject) {
this.expandingSubject = !this.expandingSubject
}
},
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
generateTagLink (tag) {
return `/tag/${tag}`
},
setMedia () { setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments) return () => this.$store.dispatch('setMedia', attachments)

View file

@ -1,99 +1,13 @@
<template> <template>
<div class="StatusContent"> <div class="StatusContent">
<slot name="header" /> <slot name="header" />
<div <StatusText :status="status">
v-if="status.summary_html" <div v-if="status.poll && status.poll.options">
class="summary-wrapper"
:class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
>
<RichContent
class="media-body summary"
:html="status.summary_raw_html"
:emoji="status.emojis"
@click.prevent="linkClicked"
/>
<button
v-if="longSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=false"
>
{{ $t("status.hide_full_subject") }}
</button>
<button
v-else-if="longSubject"
class="button-unstyled -link tall-subject-hider"
:class="{ 'tall-subject-hider_focused': focused }"
@click.prevent="showingLongSubject=true"
>
{{ $t("status.show_full_subject") }}
</button>
</div>
<div
:class="{'tall-status': hideTallStatus}"
class="status-content-wrapper"
>
<button
v-if="hideTallStatus"
class="button-unstyled -link tall-status-hider"
:class="{ 'tall-status-hider_focused': focused }"
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
</button>
<RichContent
v-if="!hideSubjectStatus"
:class="{ 'single-line': singleLine }"
class="status-content media-body"
:html="postBodyHtml"
:emoji="status.emojis"
@click.prevent="linkClicked"
/>
<button
v-if="hideSubjectStatus"
class="button-unstyled -link cw-status-hider"
@click.prevent="toggleShowMore"
>
{{ $t("status.show_content") }}
<FAIcon
v-if="attachmentTypes.includes('image')"
icon="image"
/>
<FAIcon
v-if="attachmentTypes.includes('video')"
icon="video"
/>
<FAIcon
v-if="attachmentTypes.includes('audio')"
icon="music"
/>
<FAIcon
v-if="attachmentTypes.includes('unknown')"
icon="file"
/>
<FAIcon
v-if="status.poll && status.poll.options"
icon="poll-h"
/>
<FAIcon
v-if="status.card"
icon="link"
/>
</button>
<button
v-if="showingMore && !fullContent"
class="button-unstyled -link status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
</button>
</div>
<div v-if="status.poll && status.poll.options && !hideSubjectStatus">
<poll :base-poll="status.poll" /> <poll :base-poll="status.poll" />
</div> </div>
<div <div
v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)" v-if="status.attachments.length !== 0"
class="attachments media-body" class="attachments media-body"
> >
<attachment <attachment
@ -117,7 +31,7 @@
</div> </div>
<div <div
v-if="status.card && !hideSubjectStatus && !noHeading" v-if="status.card && !noHeading"
class="link-preview media-body" class="link-preview media-body"
> >
<link-preview <link-preview
@ -126,6 +40,7 @@
:nsfw="nsfwClickthrough" :nsfw="nsfwClickthrough"
/> />
</div> </div>
</StatusText>
<slot name="footer" /> <slot name="footer" />
</div> </div>
</template> </template>

View file

@ -0,0 +1,147 @@
import fileType from 'src/services/file_type/file_type.service'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFile,
faMusic,
faImage,
faLink,
faPollH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFile,
faMusic,
faImage,
faLink,
faPollH
)
const StatusContent = {
name: 'StatusContent',
props: [
'status',
'focused',
'noHeading',
'fullContent',
'singleLine'
],
data () {
return {
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
}
},
computed: {
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80
// per line.
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
longSubject () {
return this.status.summary.length > 240
},
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
mightHideBecauseSubject () {
return !!this.status.summary && this.localCollapseSubjectDefault
},
mightHideBecauseTall () {
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
},
hideSubjectStatus () {
return this.mightHideBecauseSubject && !this.expandingSubject
},
hideTallStatus () {
return this.mightHideBecauseTall && !this.showingTall
},
showingMore () {
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
},
postBodyHtml () {
const html = this.status.raw_html
if (this.mergedConfig.greentext) {
try {
if (html.includes('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else {
return string
}
})
} else {
return html
}
} catch (e) {
console.err('Failed to process status html', e)
return html
}
} else {
return html
}
},
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
...mapGetters(['mergedConfig']),
},
components: {
RichContent
},
mounted () {
this.status.attentions.forEach(attn => {
const { id } = attn
this.$store.dispatch('fetchUserIfMissing', id)
})
},
methods: {
linkClicked (event) {
const target = event.target.closest('.status-content a')
if (target) {
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
// Extract tag name from dataset or link url
const tag = target.dataset.tag || extractTagFromUrl(target.href)
if (tag) {
const link = this.generateTagLink(tag)
this.$router.push(link)
return
}
}
window.open(target.href, '_blank')
}
},
toggleShowMore () {
if (this.mightHideBecauseTall) {
this.showingTall = !this.showingTall
} else if (this.mightHideBecauseSubject) {
this.expandingSubject = !this.expandingSubject
}
},
generateTagLink (tag) {
return `/tag/${tag}`
}
}
}
export default StatusContent

View file

@ -0,0 +1,95 @@
<template>
<div class="StatusText">
<div
v-if="status.summary_html"
class="summary-wrapper"
:class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
>
<RichContent
class="media-body summary"
:html="status.summary_raw_html"
:emoji="status.emojis"
@click.prevent="linkClicked"
/>
<button
v-if="longSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=false"
>
{{ $t("status.hide_full_subject") }}
</button>
<button
v-else-if="longSubject"
class="button-unstyled -link tall-subject-hider"
:class="{ 'tall-subject-hider_focused': focused }"
@click.prevent="showingLongSubject=true"
>
{{ $t("status.show_full_subject") }}
</button>
</div>
<div
:class="{'tall-status': hideTallStatus}"
class="status-content-wrapper"
>
<button
v-if="hideTallStatus"
class="button-unstyled -link tall-status-hider"
:class="{ 'tall-status-hider_focused': focused }"
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
</button>
<RichContent
v-if="!hideSubjectStatus"
:class="{ 'single-line': singleLine }"
class="status-content media-body"
:html="postBodyHtml"
:emoji="status.emojis"
:handleLinks="true"
@click.prevent="linkClicked"
/>
<button
v-if="hideSubjectStatus"
class="button-unstyled -link cw-status-hider"
@click.prevent="toggleShowMore"
>
{{ $t("status.show_content") }}
<FAIcon
v-if="attachmentTypes.includes('image')"
icon="image"
/>
<FAIcon
v-if="attachmentTypes.includes('video')"
icon="video"
/>
<FAIcon
v-if="attachmentTypes.includes('audio')"
icon="music"
/>
<FAIcon
v-if="attachmentTypes.includes('unknown')"
icon="file"
/>
<FAIcon
v-if="status.poll && status.poll.options"
icon="poll-h"
/>
<FAIcon
v-if="status.card"
icon="link"
/>
</button>
<button
v-if="showingMore && !fullContent"
class="button-unstyled -link status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
</button>
</div>
<div v-if="!hideSubjectStatus">
<slot/>
</div>
</div>
</template>
<script src="./status_text.js" ></script>