commit: 921eedfd84007da72619a553ba8d074076559e7a
parent: ec7bee6bd6c41fcd2f2006c9e3ac5f91def1b768
Author: Shpuld Shpludson <shp@cock.li>
Date: Fri, 8 May 2020 08:33:55 +0000
Merge branch 'feat/status-content-separation' into 'develop'
Refactor: Separate status content from status
See merge request pleroma/pleroma-fe!1092
Diffstat:
4 files changed, 460 insertions(+), 408 deletions(-)
diff --git a/src/components/status/status.js b/src/components/status/status.js
@@ -1,23 +1,17 @@
-import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
-import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
-import Gallery from '../gallery/gallery.vue'
-import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
+import StatusContent from '../status_content/status_content.vue'
import StatusPopover from '../status_popover/status_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.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 { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, unescape, uniqBy } from 'lodash'
import { mapGetters, mapState } from 'vuex'
@@ -43,17 +37,10 @@ const Status = {
replying: false,
unmuted: false,
userExpanded: false,
- showingTall: this.inConversation && this.focused,
- showingLongSubject: false,
- error: null,
- // not as computed because it sets the initial state which will be changed later
- expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+ error: null
}
},
computed: {
- localCollapseSubjectDefault () {
- return this.mergedConfig.collapseMessageWithSubject
- },
muteWords () {
return this.mergedConfig.muteWords
},
@@ -79,10 +66,6 @@ const Status = {
const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name])
},
- hideAttachments () {
- return (this.mergedConfig.hideAttachments && !this.inConversation) ||
- (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
- },
userProfileLink () {
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
},
@@ -135,20 +118,6 @@ const Status = {
// use conversation highlight only when in conversation
return this.status.id === this.highlight
},
- // 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 > 900
- },
isReply () {
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
},
@@ -188,32 +157,6 @@ const Status = {
}
return this.status.attentions.length > 0
},
-
- // 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.tallStatus || 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 () {
- if (!this.status.nsfw) {
- return false
- }
- if (this.status.summary && this.localCollapseSubjectDefault) {
- return false
- }
- return true
- },
replySubject () {
if (!this.status.summary) return ''
const decodedSummary = unescape(this.status.summary)
@@ -227,83 +170,6 @@ const Status = {
return ''
}
},
- attachmentSize () {
- 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)
- )
- },
- hasImageAttachments () {
- return this.status.attachments.some(
- file => fileType.fileType(file.mimetype) === 'image'
- )
- },
- hasVideoAttachments () {
- return this.status.attachments.some(
- file => fileType.fileType(file.mimetype) === 'video'
- )
- },
- maxThumbnails () {
- return this.mergedConfig.maxThumbnails
- },
- postBodyHtml () {
- const html = this.status.statusnet_html
-
- if (this.mergedConfig.greentext) {
- try {
- if (html.includes('>')) {
- // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
- return processHtml(html, (string) => {
- if (string.includes('>') &&
- string
- .replace(/<[^>]+?>/gi, '') // remove all tags
- .replace(/@\w+/gi, '') // remove mentions (even failed ones)
- .trim()
- .startsWith('>')) {
- 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
- }
- },
- contentHtml () {
- if (!this.status.summary_html) {
- return this.postBodyHtml
- }
- return this.status.summary_html + '<br />' + this.postBodyHtml
- },
combinedFavsAndRepeatsUsers () {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedUsers = [].concat(
@@ -312,9 +178,6 @@ const Status = {
)
return uniqBy(combinedUsers, 'id')
},
- ownStatus () {
- return this.status.user.id === this.currentUser.id
- },
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
},
@@ -328,21 +191,18 @@ const Status = {
})
},
components: {
- Attachment,
FavoriteButton,
ReactButton,
RetweetButton,
ExtraButtons,
PostStatusForm,
- Poll,
UserCard,
UserAvatar,
- Gallery,
- LinkPreview,
AvatarList,
Timeago,
StatusPopover,
- EmojiReactions
+ EmojiReactions,
+ StatusContent
},
methods: {
visibilityIcon (visibility) {
@@ -363,32 +223,6 @@ const Status = {
clearError () {
this.error = undefined
},
- 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 link url
- const tag = extractTagFromUrl(target.href)
- if (tag) {
- const link = this.generateTagLink(tag)
- this.$router.push(link)
- return
- }
- }
- window.open(target.href, '_blank')
- }
- },
toggleReplying () {
this.replying = !this.replying
},
@@ -406,22 +240,8 @@ const Status = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
- 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 () {
- const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
- return () => this.$store.dispatch('setMedia', attachments)
}
},
watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
@@ -226,118 +226,12 @@
</div>
</div>
- <div
- v-if="longSubject"
- class="status-content-wrapper"
- :class="{ 'tall-status': !showingLongSubject }"
- >
- <a
- v-if="!showingLongSubject"
- class="tall-status-hider"
- :class="{ 'tall-status-hider_focused': isFocused }"
- href="#"
- @click.prevent="showingLongSubject=true"
- >{{ $t("general.show_more") }}</a>
- <div
- class="status-content media-body"
- @click.prevent="linkClicked"
- v-html="contentHtml"
- />
- <a
- v-if="showingLongSubject"
- href="#"
- class="status-unhider"
- @click.prevent="showingLongSubject=false"
- >{{ $t("general.show_less") }}</a>
- </div>
- <div
- v-else
- :class="{'tall-status': hideTallStatus}"
- class="status-content-wrapper"
- >
- <a
- v-if="hideTallStatus"
- class="tall-status-hider"
- :class="{ 'tall-status-hider_focused': isFocused }"
- href="#"
- @click.prevent="toggleShowMore"
- >{{ $t("general.show_more") }}</a>
- <div
- v-if="!hideSubjectStatus"
- class="status-content media-body"
- @click.prevent="linkClicked"
- v-html="contentHtml"
- />
- <div
- v-else
- class="status-content media-body"
- @click.prevent="linkClicked"
- v-html="status.summary_html"
- />
- <a
- v-if="hideSubjectStatus"
- href="#"
- class="cw-status-hider"
- @click.prevent="toggleShowMore"
- >
- {{ $t("general.show_more") }}
- <span
- v-if="hasImageAttachments"
- class="icon-picture"
- />
- <span
- v-if="hasVideoAttachments"
- class="icon-video"
- />
- <span
- v-if="status.card"
- class="icon-link"
- />
- </a>
- <a
- v-if="showingMore"
- href="#"
- class="status-unhider"
- @click.prevent="toggleShowMore"
- >{{ $t("general.show_less") }}</a>
- </div>
-
- <div v-if="status.poll && status.poll.options">
- <poll :base-poll="status.poll" />
- </div>
-
- <div
- v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)"
- 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()"
- />
- <gallery
- v-if="galleryAttachments.length > 0"
- :nsfw="nsfwClickthrough"
- :attachments="galleryAttachments"
- :set-media="setMedia()"
- />
- </div>
-
- <div
- v-if="status.card && !hideSubjectStatus && !noHeading"
- class="link-preview media-body"
- >
- <link-preview
- :card="status.card"
- :size="attachmentSize"
- :nsfw="nsfwClickthrough"
- />
- </div>
+ <StatusContent
+ :status="status"
+ :no-heading="noHeading"
+ :highlight="highlight"
+ :focused="isFocused"
+ />
<transition name="fade">
<div
@@ -630,105 +524,6 @@ $status-margin: 0.75em;
}
}
- .tall-status {
- position: relative;
- height: 220px;
- overflow-x: hidden;
- overflow-y: hidden;
- z-index: 1;
- .status-content {
- height: 100%;
- 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;
- }
- }
-
- .tall-status-hider {
- display: inline-block;
- word-break: break-all;
- position: absolute;
- height: 70px;
- margin-top: 150px;
- width: 100%;
- text-align: center;
- line-height: 110px;
- z-index: 2;
- }
-
- .status-unhider, .cw-status-hider {
- width: 100%;
- text-align: center;
- display: inline-block;
- word-break: break-all;
- }
-
- .status-content {
- font-family: var(--postFont, sans-serif);
- line-height: 1.4em;
- white-space: pre-wrap;
-
- a {
- color: $fallback--link;
- color: var(--postLink, $fallback--link);
- }
-
- img, video {
- max-width: 100%;
- max-height: 400px;
- vertical-align: middle;
- object-fit: contain;
-
- &.emoji {
- width: 32px;
- height: 32px;
- }
- }
-
- blockquote {
- margin: 0.2em 0 0.2em 2em;
- font-style: italic;
- }
-
- pre {
- overflow: auto;
- }
-
- code, samp, kbd, var, pre {
- font-family: var(--postCodeFont, monospace);
- }
-
- p {
- margin: 0 0 1em 0;
- }
-
- p:last-child {
- margin: 0 0 0 0;
- }
-
- h1 {
- font-size: 1.1em;
- line-height: 1.2em;
- margin: 1.4em 0;
- }
-
- h2 {
- font-size: 1.1em;
- margin: 1.0em 0;
- }
-
- h3 {
- font-size: 1em;
- margin: 1.2em 0;
- }
-
- h4 {
- margin: 1.1em 0;
- }
- }
-
.retweet-info {
padding: 0.4em $status-margin;
margin: 0;
@@ -790,11 +585,6 @@ $status-margin: 0.75em;
}
}
-.greentext {
- color: $fallback--cGreen;
- color: var(--cGreen, $fallback--cGreen);
-}
-
.status-conversation {
border-left-style: solid;
}
@@ -866,14 +656,6 @@ a.unmute {
flex: 1;
}
-.timeline :not(.panel-disabled) > {
- .status-el:last-child {
- border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
- border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
- border-bottom: none;
- }
-}
-
.favs-repeated-users {
margin-top: $status-margin;
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
@@ -0,0 +1,210 @@
+import Attachment from '../attachment/attachment.vue'
+import Poll from '../poll/poll.vue'
+import Gallery from '../gallery/gallery.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 { 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'
+
+const StatusContent = {
+ name: 'StatusContent',
+ props: [
+ 'status',
+ 'focused',
+ 'noHeading',
+ 'fullContent'
+ ],
+ data () {
+ return {
+ showingTall: 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
+ },
+ hideAttachments () {
+ return (this.mergedConfig.hideAttachments && !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 > 900
+ },
+ // 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.tallStatus || 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 () {
+ if (!this.status.nsfw) {
+ return false
+ }
+ if (this.status.summary && this.localCollapseSubjectDefault) {
+ return false
+ }
+ return true
+ },
+ attachmentSize () {
+ 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)
+ )
+ },
+ hasImageAttachments () {
+ return this.status.attachments.some(
+ file => fileType.fileType(file.mimetype) === 'image'
+ )
+ },
+ hasVideoAttachments () {
+ return this.status.attachments.some(
+ file => fileType.fileType(file.mimetype) === 'video'
+ )
+ },
+ maxThumbnails () {
+ return this.mergedConfig.maxThumbnails
+ },
+ postBodyHtml () {
+ const html = this.status.statusnet_html
+
+ if (this.mergedConfig.greentext) {
+ try {
+ if (html.includes('>')) {
+ // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+ return processHtml(html, (string) => {
+ if (string.includes('>') &&
+ string
+ .replace(/<[^>]+?>/gi, '') // remove all tags
+ .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+ .trim()
+ .startsWith('>')) {
+ 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
+ }
+ },
+ contentHtml () {
+ if (!this.status.summary_html) {
+ return this.postBodyHtml
+ }
+ return this.status.summary_html + '<br />' + this.postBodyHtml
+ },
+ ...mapGetters(['mergedConfig']),
+ ...mapState({
+ betterShadow: state => state.interface.browserSupport.cssFilter,
+ currentUser: state => state.users.currentUser
+ })
+ },
+ components: {
+ Attachment,
+ Poll,
+ Gallery,
+ LinkPreview
+ },
+ 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 link url
+ const 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 () {
+ const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
+ return () => this.$store.dispatch('setMedia', attachments)
+ }
+ }
+}
+
+export default StatusContent
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
@@ -0,0 +1,240 @@
+<template>
+ <!-- eslint-disable vue/no-v-html -->
+ <div class="status-body">
+ <slot name="header" />
+ <div
+ v-if="longSubject"
+ class="status-content-wrapper"
+ :class="{ 'tall-status': !showingLongSubject }"
+ >
+ <a
+ v-if="!showingLongSubject"
+ class="tall-status-hider"
+ :class="{ 'tall-status-hider_focused': focused }"
+ href="#"
+ @click.prevent="showingLongSubject=true"
+ >
+ {{ $t("general.show_more") }}
+ <span
+ v-if="hasImageAttachments"
+ class="icon-picture"
+ />
+ <span
+ v-if="hasVideoAttachments"
+ class="icon-video"
+ />
+ <span
+ v-if="status.card"
+ class="icon-link"
+ />
+ </a>
+ <div
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="contentHtml"
+ />
+ <a
+ v-if="showingLongSubject"
+ href="#"
+ class="status-unhider"
+ @click.prevent="showingLongSubject=false"
+ >{{ $t("general.show_less") }}</a>
+ </div>
+ <div
+ v-else
+ :class="{'tall-status': hideTallStatus}"
+ class="status-content-wrapper"
+ >
+ <a
+ v-if="hideTallStatus"
+ class="tall-status-hider"
+ :class="{ 'tall-status-hider_focused': focused }"
+ href="#"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_more") }}</a>
+ <div
+ v-if="!hideSubjectStatus"
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="contentHtml"
+ />
+ <div
+ v-else
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="status.summary_html"
+ />
+ <a
+ v-if="hideSubjectStatus"
+ href="#"
+ class="cw-status-hider"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_more") }}</a>
+ <a
+ v-if="showingMore"
+ href="#"
+ class="status-unhider"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_less") }}</a>
+ </div>
+
+ <div v-if="status.poll && status.poll.options">
+ <poll :base-poll="status.poll" />
+ </div>
+
+ <div
+ v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
+ 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()"
+ />
+ <gallery
+ v-if="galleryAttachments.length > 0"
+ :nsfw="nsfwClickthrough"
+ :attachments="galleryAttachments"
+ :set-media="setMedia()"
+ />
+ </div>
+
+ <div
+ v-if="status.card && !hideSubjectStatus && !noHeading"
+ class="link-preview media-body"
+ >
+ <link-preview
+ :card="status.card"
+ :size="attachmentSize"
+ :nsfw="nsfwClickthrough"
+ />
+ </div>
+ <slot name="footer" />
+ </div>
+ <!-- eslint-enable vue/no-v-html -->
+</template>
+
+<script src="./status_content.js" ></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+$status-margin: 0.75em;
+
+.status-body {
+ flex: 1;
+ min-width: 0;
+
+ .tall-status {
+ position: relative;
+ height: 220px;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ z-index: 1;
+ .status-content {
+ height: 100%;
+ 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;
+ }
+ }
+
+ .tall-status-hider {
+ display: inline-block;
+ word-break: break-all;
+ position: absolute;
+ height: 70px;
+ margin-top: 150px;
+ width: 100%;
+ text-align: center;
+ line-height: 110px;
+ z-index: 2;
+ }
+
+ .status-unhider, .cw-status-hider {
+ width: 100%;
+ text-align: center;
+ display: inline-block;
+ word-break: break-all;
+ }
+
+ .status-content {
+ font-family: var(--postFont, sans-serif);
+ line-height: 1.4em;
+ white-space: pre-wrap;
+
+ img, video {
+ max-width: 100%;
+ max-height: 400px;
+ vertical-align: middle;
+ object-fit: contain;
+
+ &.emoji {
+ width: 32px;
+ height: 32px;
+ }
+ }
+
+ blockquote {
+ margin: 0.2em 0 0.2em 2em;
+ font-style: italic;
+ }
+
+ pre {
+ overflow: auto;
+ }
+
+ code, samp, kbd, var, pre {
+ font-family: var(--postCodeFont, monospace);
+ }
+
+ p {
+ margin: 0 0 1em 0;
+ }
+
+ p:last-child {
+ margin: 0 0 0 0;
+ }
+
+ h1 {
+ font-size: 1.1em;
+ line-height: 1.2em;
+ margin: 1.4em 0;
+ }
+
+ h2 {
+ font-size: 1.1em;
+ margin: 1.0em 0;
+ }
+
+ h3 {
+ font-size: 1em;
+ margin: 1.2em 0;
+ }
+
+ h4 {
+ margin: 1.1em 0;
+ }
+ }
+}
+
+.greentext {
+ color: $fallback--cGreen;
+ color: var(--cGreen, $fallback--cGreen);
+}
+
+.timeline :not(.panel-disabled) > {
+ .status-el:last-child {
+ border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+ border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
+ border-bottom: none;
+ }
+}
+
+</style>