Merge branch 'more-fixes' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-06-30 06:21:02 +03:00
commit c97ccd4de5
28 changed files with 245 additions and 392 deletions

View file

@ -52,6 +52,7 @@ const Attachment = {
'shiftDn', 'shiftDn',
'edit', 'edit',
], ],
emits: ['play', 'pause', 'naturalSizeLoad'],
data() { data() {
return { return {
localDescription: this.description || this.attachment.description, localDescription: this.description || this.attachment.description,

View file

@ -55,16 +55,6 @@ const sortAndFilterConversation = (conversation, statusoid) => {
} }
const conversation = { const conversation = {
data() {
return {
highlight: null,
expanded: false,
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
statusContentPropertiesObject: {},
inlineDivePosition: null,
loadStatusError: null,
}
},
props: [ props: [
'statusId', 'statusId',
'collapsable', 'collapsable',
@ -74,6 +64,16 @@ const conversation = {
'profileUserId', 'profileUserId',
'virtualHidden', 'virtualHidden',
], ],
data() {
return {
highlight: null,
expanded: false,
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
inlineDivePosition: null,
loadStatusError: null,
unsuspendibleIds: new Set(),
}
},
created() { created() {
if (this.isPage) { if (this.isPage) {
this.fetchConversation() this.fetchConversation()
@ -118,16 +118,7 @@ const conversation = {
return this.otherRepliesButtonPosition === 'inside' return this.otherRepliesButtonPosition === 'inside'
}, },
suspendable() { suspendable() {
if (this.isTreeView) { return this.unsuspendibleIds.size > 0
return Object.entries(this.statusContentProperties).every(
([, prop]) => !prop.replying && prop.mediaPlaying.length === 0,
)
}
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.$refs.statusComponent.every((s) => s.suspendable)
} else {
return true
}
}, },
hideStatus() { hideStatus() {
return this.virtualHidden && this.suspendable return this.virtualHidden && this.suspendable
@ -364,31 +355,6 @@ const conversation = {
return a return a
}, {}) }, {})
}, },
statusContentProperties() {
return this.conversation.reduce((a, k) => {
const id = k.id
const props = (() => {
const def = {
showingTall: false,
expandingSubject: false,
showingLongSubject: false,
isReplying: false,
mediaPlaying: [],
}
if (this.statusContentPropertiesObject[id]) {
return {
...def,
...this.statusContentPropertiesObject[id],
}
}
return def
})()
a[id] = props
return a
}, {})
},
canDive() { canDive() {
return this.isTreeView && this.isExpanded return this.isTreeView && this.isExpanded
}, },
@ -514,22 +480,6 @@ const conversation = {
showThreadRecursively(id) { showThreadRecursively(id) {
this.setThreadDisplayRecursively(id, 'showing') this.setThreadDisplayRecursively(id, 'showing')
}, },
setStatusContentProperty(id, name, value) {
this.statusContentPropertiesObject = {
...this.statusContentPropertiesObject,
[id]: {
...this.statusContentPropertiesObject[id],
[name]: value,
},
}
},
toggleStatusContentProperty(id, name) {
this.setStatusContentProperty(
id,
name,
!this.statusContentProperties[id][name],
)
},
leastVisibleAncestor(id) { leastVisibleAncestor(id) {
let cur = id let cur = id
let parent = this.parentOf(cur) let parent = this.parentOf(cur)
@ -629,6 +579,13 @@ const conversation = {
this.undive() this.undive()
this.threadDisplayStatusObject = {} this.threadDisplayStatusObject = {}
}, },
onStatusSuspendStateChange({ id, suspend }) {
if (!suspend) {
this.unsuspendibleIds.add(id)
} else {
this.unsuspendibleIds.delete(id)
}
},
}, },
} }

View file

@ -97,7 +97,7 @@
:expandable="!isExpanded" :expandable="!isExpanded"
:focused="isFocused(status.id)" :focused="isFocused(status.id)"
:highlight="getHighlight()" :highlight="maybeHighlight"
:inline-expanded="collapsable && isExpanded" :inline-expanded="collapsable && isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:in-profile="inProfile" :in-profile="inProfile"
@ -105,21 +105,11 @@
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
:simple-tree="treeViewIsSimple" :simple-tree="treeViewIsSimple"
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus" :show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
:dive="() => diveIntoStatus(status.id)" can-dive
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
:controlled-replying="statusContentProperties[status.id].replying"
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
@goto="setHighlight" @goto="setHighlight"
@toggle-expanded="toggleExpanded" @dive="() => diveIntoStatus(status.id)"
@suspendable-state-change="onStatusSuspendStateChange"
/> />
<div <div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1" v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
@ -150,7 +140,7 @@
</div> </div>
</article> </article>
</div> </div>
<thread-tree <ThreadTree
v-for="status in showingTopLevel" v-for="status in showingTopLevel"
:key="status.id" :key="status.id"
ref="statusComponent" ref="statusComponent"
@ -166,20 +156,19 @@
:is-focused-function="isFocused" :is-focused-function="isFocused"
:get-replies="getReplies" :get-replies="getReplies"
:highlight="maybeHighlight" :highlight="maybeHighlight === status.id"
:set-highlight="setHighlight"
:toggle-expanded="toggleExpanded" :toggle-expanded="toggleExpanded"
:simple="treeViewIsSimple" :simple="treeViewIsSimple"
:toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus" :thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively" :show-thread-recursively="showThreadRecursively"
:total-reply-count="totalReplyCount" :total-reply-count="totalReplyCount"
:total-reply-depth="totalReplyDepth" :total-reply-depth="totalReplyDepth"
:status-content-properties="statusContentProperties" :can-dive="canDive"
:set-status-content-property="setStatusContentProperty"
:toggle-status-content-property="toggleStatusContentProperty" @goto="setHighlight"
:dive="canDive ? diveIntoStatus : undefined" @dive="diveIntoStatus"
@suspendable-state-change="onStatusSuspendStateChange"
/> />
</div> </div>
<div <div
@ -187,7 +176,7 @@
class="thread-body" class="thread-body"
> >
<article> <article>
<status <Status
v-for="status in conversation" v-for="status in conversation"
:key="status.id" :key="status.id"
ref="statusComponent" ref="statusComponent"
@ -197,7 +186,7 @@
:expandable="!isExpanded" :expandable="!isExpanded"
:focused="isFocused(status.id)" :focused="isFocused(status.id)"
:highlight="getHighlight()" :highlight="maybeHighlight === status.id"
:inline-expanded="collapsable && isExpanded" :inline-expanded="collapsable && isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:in-profile="inProfile" :in-profile="inProfile"
@ -206,6 +195,7 @@
@goto="setHighlight" @goto="setHighlight"
@toggle-expanded="toggleExpanded" @toggle-expanded="toggleExpanded"
@suspendable-state-change="onStatusSuspendStateChange"
/> />
</article> </article>
</div> </div>

View file

@ -30,7 +30,7 @@ const ErrorModal = {
type: Error, type: Error,
}, },
}, },
emits: ['clear', 'recover'] emits: ['clear', 'recover'],
} }
export default ErrorModal export default ErrorModal

View file

@ -27,8 +27,10 @@ const Gallery = {
return { return {
sizes: {}, sizes: {},
hidingLong: true, hidingLong: true,
playingMedia: new Set(),
} }
}, },
emits: ['play', 'pause'],
components: { Attachment }, components: { Attachment },
computed: { computed: {
rows() { rows() {
@ -115,11 +117,21 @@ const Gallery = {
return this.attachmentsDimensionalScore > 1 return this.attachmentsDimensionalScore > 1
} }
}, },
hasPlayingMedia() {
return this.playingMedia.size > 0
},
}, },
methods: { methods: {
onNaturalSizeLoad({ id, width, height }) { onNaturalSizeLoad({ id, width, height }) {
set(this.sizes, id, { width, height }) set(this.sizes, id, { width, height })
}, },
onMediaStateChange(playing, id) {
if (playing) {
this.playingMedia.add(id)
} else {
this.playingMedia.delete(id)
}
},
rowStyle(row) { rowStyle(row) {
if (row.audio) { if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio return { 'padding-bottom': '25%' } // fixed reduced height for audio
@ -146,6 +158,15 @@ const Gallery = {
useMediaViewerStore().setMedia(this.attachments) useMediaViewerStore().setMedia(this.attachments)
}, },
}, },
watch: {
hasPlayingMedia(newValue) {
if (newValue) {
this.$emit('play')
} else {
this.$emit('pause')
}
},
},
} }
export default Gallery export default Gallery

View file

@ -34,6 +34,8 @@
:style="itemStyle(attachment.id, row.items)" :style="itemStyle(attachment.id, row.items)"
@set-media="onMedia" @set-media="onMedia"
@natural-size-load="onNaturalSizeLoad" @natural-size-load="onNaturalSizeLoad"
@play="() => onMediaStateChange(true, attachment.id)"
@pause="() => onMediaStateChange(false, attachment.id)"
/> />
</div> </div>
</div> </div>

View file

@ -1,9 +1,9 @@
import { mapActions, mapState } from 'pinia'
import ErrorModal from 'src/components/error_modal/error_modal.vue' import ErrorModal from 'src/components/error_modal/error_modal.vue'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { mapState, mapActions } from 'pinia'
const GlobalError = { const GlobalError = {
components: { components: {
ErrorModal, ErrorModal,
@ -24,7 +24,11 @@ const GlobalError = {
details() { details() {
if (this.globalError == null) return null if (this.globalError == null) return null
if (this.globalError.error != null) { if (this.globalError.error != null) {
return this.globalError.error.toString() + '\n\n' + this.globalError.error.stack return (
this.globalError.error.toString() +
'\n\n' +
this.globalError.error.stack
)
} else { } else {
return this.globalError.details return this.globalError.details
} }

View file

@ -50,7 +50,7 @@
</div> </div>
<tab-switcher <tab-switcher
class="list-member-management" class="list-member-management"
:scrollable-tabs scrollable-tabs
> >
<div <div
v-if="id || addedUserIds.size > 0" v-if="id || addedUserIds.size > 0"

View file

@ -133,7 +133,7 @@ const PostStatusForm = {
'resize', 'resize',
'mediaplay', 'mediaplay',
'mediapause', 'mediapause',
'can-close', 'close-accepted',
'update', 'update',
], ],
components: { components: {
@ -963,19 +963,19 @@ const PostStatusForm = {
}, },
requestClose() { requestClose() {
if (!this.saveable) { if (!this.saveable) {
this.$emit('can-close') this.$emit('close-accepted')
} else { } else {
this.$refs.draftCloser.requestClose() this.$refs.draftCloser.requestClose()
} }
}, },
saveAndCloseDraft() { saveAndCloseDraft() {
this.saveDraft().then(() => { this.saveDraft().then(() => {
this.$emit('can-close') this.$emit('close-accepted')
}) })
}, },
discardAndCloseDraft() { discardAndCloseDraft() {
this.abandonDraft().then(() => { this.abandonDraft().then(() => {
this.$emit('can-close') this.$emit('close-accepted')
}) })
}, },
addBeforeUnloadListener() { addBeforeUnloadListener() {

View file

@ -63,7 +63,6 @@
:compact="false" :compact="false"
class="search-result" class="search-result"
:statusoid="status" :statusoid="status"
:no-heading="false"
/> />
<button <button
v-if="!loading && loaded && lastStatusFetchCount > 0" v-if="!loading && loaded && lastStatusFetchCount > 0"

View file

@ -22,7 +22,7 @@
</transition> </transition>
<button <button
class="btn button-default" class="btn button-default"
:title="$t('general.minimize')" :title="$t('general.peek')"
@click="toggleMinimizeModal" @click="toggleMinimizeModal"
> >
<FAIcon <FAIcon

View file

@ -4,7 +4,7 @@
ref="tabSwitcher" ref="tabSwitcher"
class="settings-admin-content settings_tab-switcher" class="settings-admin-content settings_tab-switcher"
:side-tab-bar="true" :side-tab-bar="true"
:scrollable-tabs scrollable-tabs
:render-only-focused="true" :render-only-focused="true"
:body-scroll-lock="bodyLock" :body-scroll-lock="bodyLock"
> >

View file

@ -2,7 +2,7 @@
<vertical-tab-switcher <vertical-tab-switcher
ref="tabSwitcher" ref="tabSwitcher"
class="settings_tab-switcher" class="settings_tab-switcher"
:scrollable-tabs scrollable-tabs
:body-scroll-lock="bodyLock" :body-scroll-lock="bodyLock"
:hide-header="navHideHeader" :hide-header="navHideHeader"
> >

View file

@ -1,7 +1,7 @@
<template> <template>
<tab-switcher <tab-switcher
class="mutes-and-blocks-tab" class="mutes-and-blocks-tab"
:scrollable-tabs scrollable-tabs
> >
<div <div
class="blocks" class="blocks"

View file

@ -75,44 +75,6 @@ library.add(
const camelCase = (name) => name.charAt(0).toUpperCase() + name.slice(1) const camelCase = (name) => name.charAt(0).toUpperCase() + name.slice(1)
const controlledOrUncontrolledGetters = (list) =>
list.reduce((res, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const controlledName = `controlled${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
res[name] = function () {
return (this.$data[toggle] !== undefined ||
this.$props[toggle] !== undefined) &&
this[toggle]
? this[controlledName]
: this[uncontrolledName]
}
return res
}, {})
const controlledOrUncontrolledToggle = (obj, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
if (obj[toggle]) {
obj[toggle]()
} else {
obj[uncontrolledName] = !obj[uncontrolledName]
}
}
const controlledOrUncontrolledSet = (obj, name, val) => {
const camelized = camelCase(name)
const set = `controlledSet${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
if (obj[set]) {
obj[set](val)
} else {
obj[uncontrolledName] = val
}
}
const Status = { const Status = {
name: 'Status', name: 'Status',
components: { components: {
@ -146,40 +108,33 @@ const Status = {
inProfile: Boolean, inProfile: Boolean,
inConversation: Boolean, inConversation: Boolean,
inQuote: Boolean, inQuote: Boolean,
canDive: Boolean,
profileUserId: String, profileUserId: String,
simpleTree: Boolean, simpleTree: Boolean,
showOtherRepliesAsButton: Boolean, showOtherRepliesAsButton: Boolean,
dive: Function, canDive: Boolean,
ignoreMute: Boolean, ignoreMute: Boolean,
controlledThreadDisplayStatus: String, threadDisplayStatus: String,
controlledToggleThreadDisplay: Function,
controlledShowingTall: Boolean,
controlledToggleShowingTall: Function,
controlledExpandingSubject: Boolean,
controlledToggleExpandingSubject: Function,
controlledShowingLongSubject: Boolean,
controlledToggleShowingLongSubject: Function,
controlledReplying: Boolean,
controlledToggleReplying: Function,
controlledMediaPlaying: Boolean,
controlledSetMediaPlaying: Function,
}, },
emits: ['goto', 'toggleExpanded'], emits: [
'goto',
'dive',
'toggleExpanded',
'suspendableStateChange'
],
data() { data() {
return { return {
uncontrolledReplying: false, replying: false,
unmuted: false, unmuted: false,
userExpanded: false, userExpanded: false,
uncontrolledMediaPlaying: [], mediaPlaying: new Set(),
suspendable: true,
error: null, error: null,
headTailLinks: null, headTailLinks: null,
} }
}, },
computed: { computed: {
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
showReasonMutedThread() { showReasonMutedThread() {
return ( return (
(this.status.thread_muted || (this.status.thread_muted ||
@ -489,13 +444,13 @@ const Status = {
return useMergedConfigStore().mergedConfig return useMergedConfigStore().mergedConfig
}, },
isSuspendable() { isSuspendable() {
return !this.replying && this.mediaPlaying.length === 0 return !this.replying && this.mediaPlaying.size === 0
}, },
inThreadForest() { inThreadForest() {
return !!this.controlledThreadDisplayStatus return !!this.threadDisplayStatus
}, },
threadShowing() { threadShowing() {
return this.controlledThreadDisplayStatus === 'showing' return this.threadDisplayStatus === 'showing'
}, },
visibilityLocalized() { visibilityLocalized() {
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility) return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
@ -566,15 +521,17 @@ const Status = {
clearError() { clearError() {
this.error = undefined this.error = undefined
}, },
toggleReplying() { toggleReplyForm() {
if (this.replying) { if (this.replying) {
// This emits 'close-accepted' if successful
// which in turn callse closeReply()
this.$refs.postStatusForm.requestClose() this.$refs.postStatusForm.requestClose()
} else { } else {
this.doToggleReplying() this.replying = true
} }
}, },
doToggleReplying() { closeReplyForm() {
controlledOrUncontrolledToggle(this, 'replying') this.replying = false
}, },
gotoOriginal(id) { gotoOriginal(id) {
if (this.inConversation) { if (this.inConversation) {
@ -598,18 +555,10 @@ const Status = {
) )
}, },
addMediaPlaying(id) { addMediaPlaying(id) {
controlledOrUncontrolledSet( this.mediaPlaying.add(id)
this,
'mediaPlaying',
this.mediaPlaying.concat(id),
)
}, },
removeMediaPlaying(id) { removeMediaPlaying(id) {
controlledOrUncontrolledSet( this.mediaPlaying.delete(id)
this,
'mediaPlaying',
this.mediaPlaying.filter((mediaId) => mediaId !== id),
)
}, },
setHeadTailLinks(headTailLinks) { setHeadTailLinks(headTailLinks) {
this.headTailLinks = headTailLinks this.headTailLinks = headTailLinks
@ -659,8 +608,8 @@ const Status = {
this.$store.dispatch('fetchFavs', this.status.id) this.$store.dispatch('fetchFavs', this.status.id)
} }
}, },
isSuspendable: function (val) { isSuspendable: function (suspend) {
this.suspendable = val this.$emit('suspendableStateChange', { id: this.statusoid.id, suspend })
}, },
}, },
} }

View file

@ -236,10 +236,10 @@
/> />
</button> </button>
<button <button
v-if="dive && !simpleTree" v-if="canDive && !simpleTree"
class="button-unstyled" class="button-unstyled"
:title="$t('status.show_only_conversation_under_this')" :title="$t('status.show_only_conversation_under_this')"
@click.prevent="dive" @click.prevent="$emit('dive')"
> >
<FAIcon <FAIcon
fixed-width fixed-width
@ -409,15 +409,9 @@
<StatusContent <StatusContent
ref="content" ref="content"
:status="status" :status="status"
:no-heading="noHeading"
:highlight="highlight" :highlight="highlight"
:focused="isFocused" :focused="isFocused"
:controlled-showing-tall="controlledShowingTall" :in-conversation="inConversation"
:controlled-expanding-subject="controlledExpandingSubject"
:controlled-showing-long-subject="controlledShowingLongSubject"
:controlled-toggle-showing-tall="controlledToggleShowingTall"
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
@mediaplay="addMediaPlaying($event)" @mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)" @mediapause="removeMediaPlaying($event)"
@parse-ready="setHeadTailLinks" @parse-ready="setHeadTailLinks"
@ -438,7 +432,7 @@
v-if="showOtherRepliesAsButton && replies.length > 1" v-if="showOtherRepliesAsButton && replies.length > 1"
class="button-unstyled -link" class="button-unstyled -link"
:title="$t('status.ancestor_follow', { numReplies: replies.length - 1 }, replies.length - 1)" :title="$t('status.ancestor_follow', { numReplies: replies.length - 1 }, replies.length - 1)"
@click.prevent="dive" @click.prevent="$emit('dive')"
> >
{{ $t('status.replies_list_with_others', { numReplies: replies.length - 1 }, replies.length - 1) }} {{ $t('status.replies_list_with_others', { numReplies: replies.length - 1 }, replies.length - 1) }}
</button> </button>
@ -521,7 +515,7 @@
v-if="!noHeading && !isPreview" v-if="!noHeading && !isPreview"
:status="status" :status="status"
:replying="replying" :replying="replying"
@toggle-replying="toggleReplying" @toggle-replying="toggleReplyForm"
/> />
</div> </div>
</div> </div>
@ -555,9 +549,9 @@
:replied-user="status.user" :replied-user="status.user"
:copy-message-scope="status.visibility" :copy-message-scope="status.visibility"
:subject="replySubject" :subject="replySubject"
@posted="doToggleReplying" @posted="closeReplyForm"
@draft-done="doToggleReplying" @draft-done="closeReplyForm"
@can-close="doToggleReplying" @close-accepted="closeReplyForm"
/> />
</div> </div>
</template> </template>

View file

@ -15,32 +15,47 @@ library.add(faFile, faMusic, faImage, faLink, faPollH)
const StatusBody = { const StatusBody = {
name: 'StatusBody', name: 'StatusBody',
props: [ props: {
'compact', status: {
'collapse', // replaces newlines with spaces // Main thing
'status', type: Object,
'focused', required: true,
'noHeading', },
'fullContent', compact: {
'singleLine', // Resizes emoji and minimizes vertical space used
'showingTall', // Primarily used for showing status in react notifications
'expandingSubject', type: Boolean,
'showingLongSubject', default: false,
'toggleShowingTall', },
'toggleExpandingSubject', collapse: {
'toggleShowingLongSubject', // replaces newlines with spaces
], type: Boolean,
default: false,
},
singleLine: {
// Show entire thing (subject and content) in a single line
// Primarily used in chats
type: Boolean,
default: false,
},
inConversation: {
// Is status rendered within open conversation?
// Used to automatically expand subjects (if collapsed)
type: Boolean,
default: false,
}
},
data() { data() {
return { return {
postLength: this.status.text.length, postLength: this.status.text.length,
parseReadyDone: false, parseReadyDone: false,
showingTall: false,
showingLongSubject: false,
expandingSubject: null,
} }
}, },
emits: ['parseReady'], emits: ['parseReady'],
computed: { computed: {
localCollapseSubjectDefault() {
return this.mergedConfig.collapseMessageWithSubject
},
allowNonSquareEmoji() { allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji return this.mergedConfig.nonSquareEmoji
}, },
@ -51,32 +66,31 @@ const StatusBody = {
// //
// Using max-height + overflow: auto for status components resulted in false positives // Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying. // very often with japanese characters, and it was very annoying.
tallStatus() { hasLongSubject() {
return this.status.summary.length > 240
},
hasSubject() {
return !!this.status.summary
},
// 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.inConversation && this.hasSubject && this.mergedConfig.collapseMessageWithSubject
},
mightHideBecauseTall() {
if (this.singleLine || this.compact) return false if (this.singleLine || this.compact) return false
const lengthScore = const lengthScore =
this.status.raw_html.split(/<p|<br/).length + this.postLength / 80 this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
return lengthScore > 20 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() { hideSubjectStatus() {
return this.mightHideBecauseSubject && !this.expandingSubject return this.mightHideBecauseSubject && !this.expandingSubject
}, },
hideTallStatus() { hideTallStatus() {
return this.mightHideBecauseTall && !this.showingTall return this.mightHideBecauseTall && !this.showingTall
}, },
shouldShowToggle() { shouldShowExpandToggle() {
return this.mightHideBecauseSubject || this.mightHideBecauseTall return this.mightHideBecauseSubject || this.mightHideBecauseTall
}, },
toggleButtonClasses() { toggleButtonClasses() {
@ -97,6 +111,11 @@ const StatusBody = {
: this.$t('general.show_more') : this.$t('general.show_more')
} }
}, },
shouldHide() {
return (
!this.showingMore && this.mightHideBecauseSubject && this.hasSubject
)
},
showingMore() { showingMore() {
return ( return (
(this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseTall && this.showingTall) ||
@ -147,9 +166,9 @@ const StatusBody = {
}, },
toggleShowMore() { toggleShowMore() {
if (this.mightHideBecauseTall) { if (this.mightHideBecauseTall) {
this.toggleShowingTall() this.showingTall = !this.showingTall
} else if (this.mightHideBecauseSubject) { } else if (this.mightHideBecauseSubject) {
this.toggleExpandingSubject() this.expandingSubject = !this.expandingSubject
} }
}, },
generateTagLink(tag) { generateTagLink(tag) {

View file

@ -53,6 +53,7 @@
} }
.text-wrapper { .text-wrapper {
position: relative;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow-wrap: break-word; overflow-wrap: break-word;
overflow: hidden; overflow: hidden;
@ -60,10 +61,12 @@
flex-flow: column nowrap; flex-flow: column nowrap;
&.-tall-status { &.-tall-status {
position: relative;
height: 16em;
z-index: 1; z-index: 1;
&:not(.-hidden) {
height: 16em;
}
.media-body { .media-body {
min-height: 0; min-height: 0;
mask: mask:
@ -98,6 +101,7 @@
text-wrap: pretty; text-wrap: pretty;
width: 100%; width: 100%;
text-align: center; text-align: center;
margin: 0.1em 0;
} }
.status-unhider { .status-unhider {
@ -107,17 +111,17 @@
padding-bottom: 1em; padding-bottom: 1em;
} }
.tall-status-hider {
position: absolute;
height: 5em;
margin-top: 10em;
line-height: 8em;
z-index: 2;
}
.tall-subject-hider { .tall-subject-hider {
// position: absolute; // position: absolute;
padding-bottom: 0.5em; padding-bottom: 0.5em;
&:not(.cw-status-hider) {
position: absolute;
margin-top: 10em;
height: 5em;
line-height: 8em;
z-index: 2;
}
} }
& .status-unhider, & .status-unhider,

View file

@ -5,9 +5,9 @@
> >
<div class="body"> <div class="body">
<div <div
v-if="status.summary_raw_html" v-if="hasSubject"
class="summary-wrapper" class="summary-wrapper"
:class="{ '-tall': (longSubject && !showingLongSubject) }" :class="{ '-tall': (hasLongSubject && !showingLongSubject) }"
> >
<RichContent <RichContent
class="media-body summary" class="media-body summary"
@ -18,14 +18,14 @@
:allow-non-square-emoji="allowNonSquareEmoji" :allow-non-square-emoji="allowNonSquareEmoji"
/> />
<button <button
v-show="longSubject && showingLongSubject" v-show="hasLongSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider" class="button-unstyled -link tall-subject-hider"
@click.prevent="toggleShowingLongSubject" @click.prevent="toggleShowingLongSubject"
> >
{{ $t("status.hide_full_subject") }} {{ $t("status.hide_full_subject") }}
</button> </button>
<button <button
v-show="longSubject && !showingLongSubject" v-show="hasLongSubject && !showingLongSubject"
class="button-unstyled -link tall-subject-hider" class="button-unstyled -link tall-subject-hider"
@click.prevent="toggleShowingLongSubject" @click.prevent="toggleShowingLongSubject"
> >
@ -34,10 +34,10 @@
</div> </div>
<div <div
class="text-wrapper" class="text-wrapper"
:class="{'-tall-status': hideTallStatus, '-expanded': showingMore}" :class="{'-tall-status': hideTallStatus, '-hidden': shouldHide, '-expanded': showingMore}"
> >
<RichContent <RichContent
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)" v-if="!(singleLine && hasSubject) && !shouldHide"
:class="{ '-single-line': singleLine }" :class="{ '-single-line': singleLine }"
class="text media-body" class="text media-body"
:html="status.raw_html" :html="status.raw_html"
@ -52,12 +52,11 @@
@parse-ready="onParseReady" @parse-ready="onParseReady"
/> />
<div <div
v-show="shouldShowToggle" v-show="shouldShowExpandToggle"
:class="toggleButtonClasses" :class="toggleButtonClasses"
> >
<button <button
class="btn button-default toggle-button" class="btn button-default toggle-button"
:class="{ '-focused': focused }"
:aria-expanded="showingMore" :aria-expanded="showingMore"
@click.prevent="toggleShowMore" @click.prevent="toggleShowMore"
> >

View file

@ -22,69 +22,40 @@ import {
library.add(faCircleNotch, faFile, faMusic, faImage, faLink, faPollH) library.add(faCircleNotch, faFile, faMusic, faImage, faLink, faPollH)
const camelCase = (name) => name.charAt(0).toUpperCase() + name.slice(1)
const controlledOrUncontrolledGetters = (list) =>
list.reduce((res, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const controlledName = `controlled${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
res[name] = function () {
return (this.$data[toggle] !== undefined ||
this.$props[toggle] !== undefined) &&
this[toggle]
? this[controlledName]
: this[uncontrolledName]
}
return res
}, {})
const controlledOrUncontrolledToggle = (obj, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
if (obj[toggle]) {
obj[toggle]()
} else {
obj[uncontrolledName] = !obj[uncontrolledName]
}
}
const StatusContent = { const StatusContent = {
name: 'StatusContent', name: 'StatusContent',
props: [ props: {
'status', status: {
'compact', // Main thing
'collapse', type: Object,
'focused', required: true,
'noHeading', },
'fullContent', compact: {
'singleLine', // Resizes emoji and minimizes vertical space used
'controlledShowingTall', // Primarily used for showing status in react notifications
'controlledExpandingSubject', type: Boolean,
'controlledToggleShowingTall', default: false,
'controlledToggleExpandingSubject', },
'controlledShowingLongSubject', collapse: {
'controlledToggleShowingLongSubject', // replaces newlines with spaces
], type: Boolean,
emits: ['parseReady', 'mediaplay', 'mediapause'], default: false,
data() { },
return { singleLine: {
uncontrolledShowingTall: // Show entire thing (subject and content) in a single line
this.fullContent || (this.inConversation && this.focused), // Primarily used in chats
uncontrolledShowingLongSubject: false, type: Boolean,
// not as computed because it sets the initial state which will be changed later default: false,
uncontrolledExpandingSubject: },
!useMergedConfigStore().mergedConfig.collapseMessageWithSubject, inConversation: {
} // Whether status content is being shown in an (open) conversation
// Used to control whether to display attachments or not
type: Boolean,
default: false,
},
}, },
emits: ['parseReady', 'mediaplay', 'mediapause'],
computed: { computed: {
...controlledOrUncontrolledGetters([
'showingTall',
'expandingSubject',
'showingLongSubject',
]),
statusCard() { statusCard() {
if (!this.status.card) return null if (!this.status.card) return null
return this.status.card.url === this.status.quote_url return this.status.card.url === this.status.quote_url
@ -93,22 +64,20 @@ const StatusContent = {
}, },
hideAttachments() { hideAttachments() {
return ( return (
(this.mergedConfig.hideAttachments && !this.inConversation) || !this.fullContent &&
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) ((this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation))
) )
}, },
nsfwClickthrough() { nsfwClickthrough() {
if (!this.status.nsfw) { if (!this.status.nsfw) {
return false return false
} }
if (this.status.summary && this.localCollapseSubjectDefault) { if (this.status.summary && this.mergedConfig.collapseMessageWithSubject) {
return false return false
} }
return true return true
}, },
localCollapseSubjectDefault() {
return this.mergedConfig.collapseMessageWithSubject
},
attachmentSize() { attachmentSize() {
if (this.compact) { if (this.compact) {
return 'small' return 'small'
@ -137,15 +106,6 @@ const StatusContent = {
StatusBody, StatusBody,
}, },
methods: { methods: {
toggleShowingTall() {
controlledOrUncontrolledToggle(this, 'showingTall')
},
toggleExpandingSubject() {
controlledOrUncontrolledToggle(this, 'expandingSubject')
},
toggleShowingLongSubject() {
controlledOrUncontrolledToggle(this, 'showingLongSubject')
},
setMedia() { setMedia() {
const attachments = const attachments =
this.attachmentSize === 'hide' this.attachmentSize === 'hide'

View file

@ -8,13 +8,8 @@
:status="status" :status="status"
:compact="compact" :compact="compact"
:single-line="singleLine" :single-line="singleLine"
:showing-tall="showingTall"
:expanding-subject="expandingSubject"
:showing-long-subject="showingLongSubject"
:toggle-showing-tall="toggleShowingTall"
:toggle-expanding-subject="toggleExpandingSubject"
:toggle-showing-long-subject="toggleShowingLongSubject"
:collapse="collapse" :collapse="collapse"
:in-conversation="inConversation"
@parse-ready="$emit('parseReady', $event)" @parse-ready="$emit('parseReady', $event)"
> >
<div v-if="status.poll && status.poll.options && !compact"> <div v-if="status.poll && status.poll.options && !compact">
@ -42,12 +37,12 @@
:attachments="status.attachments" :attachments="status.attachments"
:limit="compact ? 1 : 0" :limit="compact ? 1 : 0"
:size="attachmentSize" :size="attachmentSize"
@play="$emit('mediaplay', attachment.id)" @play="$emit('mediaplay')"
@pause="$emit('mediapause', attachment.id)" @pause="$emit('mediapause')"
/> />
<div <div
v-if="statusCard && !noHeading && !compact" v-if="statusCard && !compact"
class="link-preview media-body" class="link-preview media-body"
> >
<link-preview <link-preview

View file

@ -26,30 +26,14 @@ const ThreadTree = {
toggleExpanded: Function, toggleExpanded: Function,
simple: Boolean, simple: Boolean,
// to control display of the whole thread forest canDive: Boolean,
toggleThreadDisplay: Function,
threadDisplayStatus: Object, threadDisplayStatus: Object,
showThreadRecursively: Function, showThreadRecursively: Function,
totalReplyCount: Object, totalReplyCount: Object,
totalReplyDepth: Object, totalReplyDepth: Object,
statusContentProperties: Object,
setStatusContentProperty: Function,
toggleStatusContentProperty: Function,
dive: Function,
}, },
emits: ['suspendableStateChange', 'goto', 'dive'],
computed: { computed: {
suspendable() {
const selfSuspendable = this.$refs.statusComponent
? this.$refs.statusComponent.suspendable
: true
if (this.$refs.childComponent) {
return (
selfSuspendable &&
this.$refs.childComponent.every((s) => s.suspendable)
)
}
return selfSuspendable
},
reverseLookupTable() { reverseLookupTable() {
return this.conversation.reduce( return this.conversation.reduce(
(table, status, index) => { (table, status, index) => {
@ -69,29 +53,11 @@ const ThreadTree = {
threadShowing() { threadShowing() {
return this.threadDisplayStatus[this.status.id] === 'showing' return this.threadDisplayStatus[this.status.id] === 'showing'
}, },
currentProp() {
return this.statusContentProperties[this.status.id]
},
}, },
methods: { methods: {
statusById(id) { statusById(id) {
return this.conversation[this.reverseLookupTable[id]] return this.conversation[this.reverseLookupTable[id]]
}, },
collapseThread() {
/* no-op */
},
showThread() {
/* no-op */
},
showAllSubthreads() {
/* no-op */
},
toggleCurrentProp(name) {
this.toggleStatusContentProperty(this.status.id, name)
},
setCurrentProp(name) {
this.setStatusContentProperty(this.status.id, name)
},
}, },
} }

View file

@ -1,44 +1,34 @@
<template> <template>
<article class="thread-tree"> <article class="thread-tree">
<status <Status
:key="status.id" :key="status.id"
ref="statusComponent" ref="statusComponent"
:inline-expanded="collapsable && isExpanded"
:statusoid="status" :statusoid="status"
:replies="getReplies(status.id)"
:inline-expanded="collapsable && isExpanded"
:expandable="!isExpanded" :expandable="!isExpanded"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" :show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
:focused="isFocusedFunction(status.id)" :focused="isFocusedFunction(status.id)"
:in-conversation="isExpanded" :in-conversation="isExpanded"
:highlight="highlight" :highlight="highlight === status.id"
:replies="getReplies(status.id)"
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
class="conversation-status conversation-status-treeview status-fadein panel-body" class="conversation-status conversation-status-treeview status-fadein panel-body"
:simple-tree="simple" :simple-tree="simple"
:controlled-thread-display-status="threadDisplayStatus[status.id]" :thread-display-status="threadDisplayStatus[status.id]"
:controlled-toggle-thread-display="() => toggleThreadDisplay(status.id)" :can-dive="canDive"
:controlled-showing-tall="currentProp.showingTall" @dive="$emit('dive', status.id)"
:controlled-expanding-subject="currentProp.expandingSubject" @goto="$emit('goto', status.id)"
:controlled-showing-long-subject="currentProp.showingLongSubject"
:controlled-replying="currentProp.replying"
:controlled-media-playing="currentProp.mediaPlaying"
:controlled-toggle-showing-tall="() => toggleCurrentProp('showingTall')"
:controlled-toggle-expanding-subject="() => toggleCurrentProp('expandingSubject')"
:controlled-toggle-showing-long-subject="() => toggleCurrentProp('showingLongSubject')"
:controlled-toggle-replying="() => toggleCurrentProp('replying')"
:controlled-set-media-playing="(newVal) => setCurrentProp('mediaPlaying', newVal)"
:dive="dive ? () => dive(status.id) : undefined"
@goto="setHighlight"
@toggle-expanded="toggleExpanded" @toggle-expanded="toggleExpanded"
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
/> />
<div <div
v-if="currentReplies.length && threadShowing" v-if="currentReplies.length > 0 && threadShowing"
class="thread-tree-replies" class="thread-tree-replies"
> >
<thread-tree <ThreadTree
v-for="replyStatus in currentReplies" v-for="replyStatus in currentReplies"
:key="replyStatus.id" :key="replyStatus.id"
ref="childComponent" ref="childComponent"
@ -59,15 +49,15 @@
:toggle-expanded="toggleExpanded" :toggle-expanded="toggleExpanded"
:simple="simple" :simple="simple"
:toggle-thread-display="toggleThreadDisplay"
:thread-display-status="threadDisplayStatus" :thread-display-status="threadDisplayStatus"
:show-thread-recursively="showThreadRecursively" :show-thread-recursively="showThreadRecursively"
:total-reply-count="totalReplyCount" :total-reply-count="totalReplyCount"
:total-reply-depth="totalReplyDepth" :total-reply-depth="totalReplyDepth"
:status-content-properties="statusContentProperties"
:set-status-content-property="setStatusContentProperty" :can-dive="canDive"
:toggle-status-content-property="toggleStatusContentProperty" @goto="(e) => $emit('goto', e)"
:dive="dive" @dive="(e) => $emit('dive', e)"
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
/> />
</div> </div>
<div <div
@ -80,7 +70,7 @@
tag="button" tag="button"
keypath="status.thread_follow_with_icon" keypath="status.thread_follow_with_icon"
class="button-unstyled -link thread-tree-show-replies-button" class="button-unstyled -link thread-tree-show-replies-button"
@click.prevent="dive(status.id)" @click.prevent="$emit('dive', status.id)"
> >
<template #icon> <template #icon>
<FAIcon <FAIcon

View file

@ -11,11 +11,11 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
import ColorInput from 'src/components/color_input/color_input.vue' import ColorInput from 'src/components/color_input/color_input.vue'
import EmojiInput from 'src/components/emoji_input/emoji_input.vue' import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
import suggestor from 'src/components/emoji_input/suggestor.js' import suggestor from 'src/components/emoji_input/suggestor.js'
import FollowButton from 'src/components/follow_button/follow_button.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import UserLink from 'src/components/user_link/user_link.vue' import UserLink from 'src/components/user_link/user_link.vue'
import FollowButton from 'src/components/follow_button/follow_button.vue'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'

View file

@ -1,9 +1,8 @@
import { defineAsyncComponent } from 'vue'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import AuthForm from 'src/components/auth_form/auth_form.js'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import UserCard from 'src/components/user_card/user_card.vue' import UserCard from 'src/components/user_card/user_card.vue'
import AuthForm from 'src/components/auth_form/auth_form.js'
const UserPanel = { const UserPanel = {
computed: { computed: {
@ -15,6 +14,7 @@ const UserPanel = {
components: { components: {
PostStatusForm, PostStatusForm,
UserCard, UserCard,
AuthForm,
}, },
} }

View file

@ -11,7 +11,7 @@
/> />
<PostStatusForm /> <PostStatusForm />
</div> </div>
<auth-form <AuthForm
v-else v-else
key="user-panel" key="user-panel"
/> />

View file

@ -199,7 +199,7 @@ export const useInterfaceStore = defineStore('interface', {
console.log(this.globalError) console.log(this.globalError)
}, },
clearGlobalError() { clearGlobalError() {
this.globalError = null; this.globalError = null
}, },
pushGlobalNotice({ pushGlobalNotice({
messageKey, messageKey,

View file

@ -1,5 +1,8 @@
/* eslint-env serviceworker */ /* eslint-env serviceworker */
// biome-ignore: side effect import of assets list
import 'virtual:pleroma-fe/service_worker_env'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import { storage } from 'src/lib/storage.js' import { storage } from 'src/lib/storage.js'