Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85712684
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
26 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 1fed4c4589..316a33bb72 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,319 +1,348 @@
import { get } from 'lodash-es'
import { storeToRefs } from 'pinia'
import { computed, provide, ref, toRefs, useTemplateRef, watch } from 'vue'
import { useRouter } from 'vue-router'
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useStreamingStore } from 'src/stores/streaming.js'
import { useConversation } from 'src/composables/useConversation.js'
import { useScrollPosition } from 'src/composables/useScrollPosition.js'
import { useTreeConversationTopology } from 'src/composables/useTreeConversationTopology.js'
import { useVirtualScrolling } from 'src/composables/useVirtualScrolling.js'
import { WSConnectionStatus } from 'src/api/websocket.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft,
faReply,
faTimes,
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft,
faReply,
faTimes,
)
export default {
props: {
statusId: {
// Main thing
type: String,
required: true,
},
isPage: {
// Whether conversation is rendered as a standalone page
// as opposed to embedded into a timeline
type: Boolean,
default: false,
},
},
components: {
ThreadTree,
QuickFilterSettings,
QuickViewSettings,
ChatMessageList,
PostStatusForm,
RichContent,
},
setup(props) {
// # Helpers
const getStatusObject = (id) => useStatusesStore().allStatuses.get(id)
const getConversationId = (statusId) => {
const status = getStatusObject(statusId)
return get(
status,
'retweeted_status.statusnet_conversation_id',
get(status, 'statusnet_conversation_id'),
)
}
const scroller = useScrollPosition()
const tryScrollTo = async (id) => {
if (!id) {
return
}
if (isPage.value) {
router.push({ name: 'conversation', params: { statusId: id } })
}
setFocused(id)
const target = document.querySelector(`.Status[data-status-id=${id}]`)
return await scroller.scrollIntoView(target, { block: 'start' })
}
const { statusId } = toRefs(props)
const router = useRouter()
// # Main Configuration / global state
const { mergedConfig } = storeToRefs(useMergedConfigStore())
const { mastoUserSocketStatus } = storeToRefs(useStreamingStore())
const displayStyle = computed(() => mergedConfig.value.conversationDisplay)
const streamingEnabled = computed(
() =>
mergedConfig.value.useStreamingApi &&
mastoUserSocketStatus === WSConnectionStatus.JOINED,
)
const { layoutType } = storeToRefs(useInterfaceStore())
const mobileLayout = computed(() => layoutType.value === 'mobile')
// # Conversation Expansion
const expanded = ref(false)
const { isPage } = toRefs(props)
const isExpanded = computed(() => !!(expanded.value || isPage.value))
const toggleExpanded = () => {
expanded.value = !expanded.value
}
provide('isExpanded', isExpanded)
provide('isPage', isPage)
provide('expandable', true)
// # Focus
const focusedId = ref(statusId.value)
const focused = computed(() => (isExpanded.value ? focusedId.value : null))
const setFocused = (id) => {
if (!id) return
focusedId.value = id
if (!streamingEnabled.value) {
useStatusesStore().fetchStatus(id)
}
useStatusesStore().fetchFavsAndRepeats(id)
useStatusesStore().fetchEmojiReactions(id)
}
provide('focused', focused)
// # Main things
const {
currentStatus,
conversation,
replies,
getReplies,
fetchConversation,
loadError,
} = useConversation(focusedId, isExpanded)
watch(
expanded,
async (value) => {
if (value) {
await fetchConversation()
} else {
resetDisplayState()
}
if (isPage.value) return
await tryScrollTo(currentStatus.value.id)
},
{ flush: 'post' },
)
const resetDisplayState = () => {
setFocused(statusId.value)
resetThreadDisplay()
}
watch(statusId, (newVal, oldVal) => {
const newConversationId = getConversationId(newVal)
const oldConversationId = getConversationId(oldVal)
if (
newConversationId &&
oldConversationId &&
newConversationId === oldConversationId
) {
setFocused(newVal)
} else {
resetDisplayState()
fetchConversation()
}
})
// Component created
if (isPage.value) {
fetchConversation()
}
// # Misc UI things
const firstStatus = computed(() => conversation.value[0])
const lastStatus = computed(
() => conversation.value[conversation.value.legnth - 1],
)
const getStatusClasses = (status, active) => ({
'-first': status.id === firstStatus.value?.id,
'-last': status.id === lastStatus.value?.id,
})
+ // Getting the actual font size in pixels since UI might have
+ // a different scale
+ const fontSizeSetting = computed(() => mergedConfig.value.textSize)
+ const fontSize = ref(0)
+ const updateFontSize = () => {
+ const string = window
+ .getComputedStyle(document.body)
+ .getPropertyValue('font-size')
+ fontSize.value = Number.parseInt(string.slice(0, -2), 10) // remove the 'px'
+ }
+ // Update font size if user changed UI scale
+ watch(fontSizeSetting, updateFontSize, { immediate: true })
+
+ // Placeholder heights.
+ const mutedStatusHeight = computed(() => fontSize.value * 1.5)
+ const normalStatusHeight = computed(() => fontSize.value * 10)
+ const getPlaceholderHeight = (id) =>
+ conversation.value.find((item) => item.id === id)?.muted
+ ? mutedStatusHeight
+ : normalStatusHeight
+
// # Linear style stuff
const isLinearView = computed(() => displayStyle.value !== 'tree')
const linearElement = useTemplateRef('linear')
const linearScrollCompensation = computed(
() => isLinearView.value && isExpanded.value,
)
const {
heightChart: heightChartLinear,
changeSuspendState: changeSuspendStateLinear,
updateVirtualHeight: updateVirtualHeightLinear,
- } = useVirtualScrolling(
- conversation,
- linearElement,
- scroller,
- linearScrollCompensation,
- currentStatus,
- )
+ } = useVirtualScrolling({
+ list: conversation,
+ body: linearElement,
+ scrollPositionInstance: scroller,
+ scrollCompensation: linearScrollCompensation,
+ anchorId: currentStatus.id,
+ getPlaceholderHeight,
+ })
// # Tree style stuff
const isTreeView = computed(() => displayStyle.value === 'tree')
const {
topLevel,
currentAncestors,
threadDisplay,
showThreadRecursively,
resetThreadDisplay,
} = useTreeConversationTopology(conversation, replies, focusedId)
provide('threadDisplay', threadDisplay)
const ancestorsElement = useTemplateRef('ancestors')
const treeScrollCompensation = computed(
() => isTreeView.value && isExpanded.value,
)
const {
heightChart: heightChartAncestors,
changeSuspendState: changeSuspendStateAncestors,
updateVirtualHeight: updateVirtualHeightAncestors,
- } = useVirtualScrolling(
- currentAncestors,
- ancestorsElement,
- scroller,
- treeScrollCompensation,
- )
+ } = useVirtualScrolling({
+ list: currentAncestors,
+ body: ancestorsElement,
+ scrollPositionInstance: scroller,
+ scrollCompensation: treeScrollCompensation,
+ getPlaceholderHeight,
+ })
const currentLevel = computed(() => [currentStatus.value].filter(Boolean))
const currentLevelElement = useTemplateRef('currentLevel')
const {
heightChart: heightChartCurrentLevel,
changeSuspendState: changeSuspendStateCurrentLevel,
updateVirtualHeight: updateVirtualHeightCurrentLevel,
- } = useVirtualScrolling(currentLevel, currentLevelElement, scroller, false)
+ } = useVirtualScrolling({
+ list: currentLevel,
+ body: currentLevelElement,
+ scrollPositionInstance: scroller,
+ scrollCompensation: false,
+ getPlaceholderHeight,
+ })
const treeViewIsSimple = computed(
() => !mergedConfig.value.conversationTreeAdvanced,
)
const shouldShowAllConversationButton = computed(
() => currentAncestors.value.length > 0 && topLevel.value.length > 1,
)
const shouldShowAncestors = computed(
() => isExpanded.value && heightChartAncestors.value.length > 0,
)
const shouldFadeAncestors = computed(
() => mergedConfig.value.conversationTreeFadeAncestors,
)
// # Scrolling
const diveIntoStatus = (id) => tryScrollTo(id)
const diveToTopLevel = () => tryScrollTo(currentAncestors.value[0].id)
return {
// # Misc
loadError,
mobileLayout,
// # Conversation Expansion
isPage,
isExpanded,
toggleExpanded,
// # Focus
focused,
setFocused,
// # Main things
conversation,
currentStatus,
getReplies,
// # Misc UI things
getStatusClasses,
// # Linear style stuff
isLinearView,
// ## Linear virtual scrolling
heightChartLinear,
changeSuspendStateLinear,
updateVirtualHeightLinear,
// # Tree style stuff
isTreeView,
// ## Tree virtual scrolling
heightChartAncestors,
changeSuspendStateAncestors,
updateVirtualHeightAncestors,
heightChartCurrentLevel,
changeSuspendStateCurrentLevel,
updateVirtualHeightCurrentLevel,
// ## Tree state
// ### Topology
topLevel,
currentAncestors,
// ### Thread Display
showThreadRecursively,
// ### Derived values and config
treeViewIsSimple,
shouldShowAllConversationButton,
shouldShowAncestors,
shouldFadeAncestors,
// # Scrolling
diveToTopLevel,
diveIntoStatus,
}
},
}
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 3a61decf55..744fea0fdf 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -1,187 +1,187 @@
<template>
<div
ref="root"
class="Conversation"
:class="{ '-expanded' : isExpanded, '-page': isPage, 'panel' : isExpanded }"
>
<div
v-if="isExpanded"
class="panel-heading conversation-heading -sticky"
>
<h1 class="title">
<RichContent
v-if="conversation[0]?.summary_raw_html"
:html="conversation[0].summary_raw_html"
:emoji="conversation[0].emojis"
/>
<template v-else>
{{ $t('timeline.conversation') }}
</template>
</h1>
<button
v-if="!isPage"
class="button-unstyled -link"
@click.prevent="toggleExpanded"
>
{{ $t('timeline.collapse') }}
</button>
<QuickFilterSettings
v-if="isPage && mobileLayout"
:conversation="true"
class="rightside-button"
/>
<QuickViewSettings
v-if="isPage"
:conversation="true"
class="rightside-button"
/>
</div>
<div
v-if="isPage && !currentStatus"
class="conversation-body"
ref="body"
:class="{ 'panel-body': isExpanded }"
>
<p v-if="!loadError">
<FAIcon
spin
icon="circle-notch"
/>
{{ $t('status.loading') }}
</p>
<p v-else>
{{ $t('status.load_error', { error: loadError }) }}
</p>
</div>
<div
v-else
class="conversation-body"
ref="body"
:class="{ 'panel-body': isExpanded }"
>
<div
v-if="isTreeView"
class="thread-body"
>
<div
v-if="shouldShowAllConversationButton"
class="conversation-dive-to-top-level-box"
>
<i18n-t
keypath="currentStatus.show_all_conversation_with_icon"
tag="button"
class="button-unstyled -link"
scope="global"
@click.prevent="diveToTopLevel"
>
<template #icon>
<FAIcon
icon="angle-double-left"
/>
</template>
<template #text>
<span>
{{ $t('status.show_all_conversation', { numStatus: topLevel.length - 1 }, topLevel.length - 1) }}
</span>
</template>
</i18n-t>
</div>
<div
v-if="shouldShowAncestors"
ref="ancestors"
class="thread-ancestors"
>
<article
v-for="element in heightChartAncestors"
class="thread-ancestor"
:class="{'thread-ancestor-has-other-replies': getReplies(element.id).size > 1, '-faded': shouldFadeAncestors}"
>
<Status
v-if="element.type === 'status'"
class="conversation-status panel-body"
- :class="getStatusClasses(element.status)"
+ :class="getStatusClasses(element.item)"
- :status-id="element.status.id"
- :replies="getReplies(element.status.id)"
+ :status-id="element.item.id"
+ :replies="getReplies(element.item.id)"
- :focused="focused === element.status.id"
+ :focused="focused === element.item.id"
conversation-rank="ancestor"
:data-status-id="element.id"
@goto="setFocused"
- @dive="diveIntoStatus(element.status.id)"
+ @dive="diveIntoStatus(element.item.id)"
@suspendable-state-change="changeSuspendStateAncestors"
@height-change="updateVirtualHeightAncestors"
/>
<div
v-if="element.type === 'spacer'"
class="virtual-spacer"
:style="{ height: element.height + 'px' }"
/>
</article>
</div>
<div
class="currentLevel"
ref="currentLevel"
>
<!-- Technically this will always have a single element but -->
<!-- it's more convenient for us to use a v-for here -->
<template v-for="element in heightChartCurrentLevel">
<ThreadTree
- v-if="element.type === 'status'"
+ v-if="element.type === 'item'"
:status-id="currentStatus.id"
:depth="0"
@goto="setFocused"
@dive="diveIntoStatus"
@toggle-expanded="toggleExpanded"
@show-thread-recursively="showThreadRecursively"
@suspendable-state-change="changeSuspendStateCurrentLevel"
@height-change="updateVirtualHeightCurrentLevel"
/>
<div
v-if="element.type === 'spacer'"
class="virtual-spacer"
:style="{ height: element.height + 'px' }"
/>
</template>
</div>
</div>
<div
v-else-if="isLinearView"
ref="linear"
class="thread-body"
>
<article
v-for="element in heightChartLinear"
class="panel-body"
:key="element.id ?? element.ids"
>
<Status
- v-if="element.type === 'status'"
+ v-if="element.type === 'item'"
class="conversation-status"
- :class="getStatusClasses(element.status)"
- :status-id="element.status.id"
- :replies="getReplies(element.status.id)"
+ :class="getStatusClasses(element.item)"
+ :status-id="element.item.id"
+ :replies="getReplies(element.item.id)"
- :focused="focused === element.id || focused === element.status.retweeted_status?.id"
+ :focused="focused === element.id || focused === element.item.retweeted_status?.id"
:data-status-id="element.id"
@goto="setFocused"
@toggle-expanded="toggleExpanded"
@suspendable-state-change="changeSuspendStateLinear"
@height-change="updateVirtualHeightLinear"
/>
<div
v-if="element.type === 'spacer'"
class="virtual-spacer"
:style="{ height: element.height + 'px' }"
/>
</article>
</div>
</div>
</div>
</template>
<script src="./conversation.js"></script>
<style src="./conversation.scss" />
diff --git a/src/composables/useVirtualScrolling.js b/src/composables/useVirtualScrolling.js
index 679f418d90..2062d97f0d 100644
--- a/src/composables/useVirtualScrolling.js
+++ b/src/composables/useVirtualScrolling.js
@@ -1,219 +1,188 @@
-import { storeToRefs } from 'pinia'
import { computed, ref, toValue, watch } from 'vue'
-import { useMergedConfigStore } from 'src/stores/merged_config.js'
-import { useStatusesStore } from 'src/stores/statuses.js'
-
import { useWindowSize } from 'src/composables/useWindowSize.js'
-export function useVirtualScrolling(
- conversation,
+export function useVirtualScrolling({
+ list,
body,
- scrollPosition,
+ scrollPositionInstance,
scrollCompensation,
- anchorStatus,
-) {
- const getStatusObject = (id) => useStatusesStore().allStatuses.get(id)
-
- const { mergedConfig } = storeToRefs(useMergedConfigStore())
- const anchor = computed(() => anchorStatus?.value.id)
-
+ anchorId,
+ getPlaceholderHeight,
+}) {
const unsuspendibleIds = ref(new Set())
const changeSuspendState = ({ id, suspend }) => {
if (!suspend) {
unsuspendibleIds.value.add(id)
} else {
unsuspendibleIds.value.delete(id)
}
}
- // Getting the actual font size in pixels since UI might have
- // a different scale
- const fontSizeSetting = computed(() => mergedConfig.value.textSize)
- const fontSize = ref(0)
- const updateFontSize = () => {
- const string = window
- .getComputedStyle(document.body)
- .getPropertyValue('font-size')
- fontSize.value = Number.parseInt(string.slice(0, -2), 10) // remove the 'px'
- }
- // Update font size if user changed UI scale
- watch(fontSizeSetting, updateFontSize, { immediate: true })
-
- // Placeholder heights.
- const mutedStatusHeight = computed(() => {
- return fontSize.value * 1.5
- })
- const normalStatusHeight = computed(() => {
- return fontSize.value * 10
- })
-
- // Add buffer zone to boundary, equal to approx 3 statuses heights
- const buffer = computed(() => normalStatusHeight.value * 3)
+ // Add buffer zone to boundary, equal to approx 3 items heights
+ const buffer = computed(() => getPlaceholderHeight().value * 3)
// Heights map.
const heights = ref(new Map())
- const totalHeight = computed(() =>
- conversation.value.reduce((acc, item) => {
- if (heights.value.has(item.id)) {
- return acc + heights.value.get(item.id)
- } else if (item.muted) {
- return acc + mutedStatusHeight.value
- } else {
- return acc + normalStatusHeight.value
- }
- }, 0),
- )
- const updateVirtualHeight = ({ id, height }) => {
- heights.value.set(id, height)
- }
-
- // Scrolling
- const { y: scrollY, inProgress: scrollInProgress, scrollBy } = scrollPosition
- const { height: windowHeight } = useWindowSize()
-
- const topScrollBoundary = ref(0)
- const bottomScrollBoundary = ref(0)
- const updateBoundaries = () => {
- if (!body.value) return // Not mounted yet
-
- const { top } = body.value.getBoundingClientRect()
-
- const distanceItemTopToWindowTop = 0 - top
- const distanceItemTopToWindowBottom = windowHeight.value - top
-
- topScrollBoundary.value = distanceItemTopToWindowTop
- bottomScrollBoundary.value = distanceItemTopToWindowBottom
- }
- const windowWatcher = watch(windowHeight, updateBoundaries)
- const scrollWatcher = watch(scrollY, updateBoundaries)
- const heightWatcher = watch(totalHeight, updateBoundaries)
- const bodyWatcher = watch(body, updateBoundaries)
- const pauseWatchers = () => {
- windowWatcher.pause()
- scrollWatcher.pause()
- heightWatcher.pause()
- bodyWatcher.pause()
- }
- const resumeWatchers = () => {
- windowWatcher.resume()
- scrollWatcher.resume()
- heightWatcher.resume()
- bodyWatcher.resume()
- }
-
const heightChart = computed(() => {
// Map every height and suspendable state
- const chart = conversation.value.map(({ id }) => {
- const status = getStatusObject(id)
+ const chart = list.value.map((item) => {
+ const { id } = item
const height =
(() => {
if (heights.value.has(id)) {
return heights.value.get(id)
- } else if (status?.muted) {
- return mutedStatusHeight.value
} else {
- return normalStatusHeight.value
+ return getPlaceholderHeight(id).value
}
})() + 1 //including border
const suspendable = !unsuspendibleIds.value.has(id)
- return { id, height, suspendable, status }
+ return { id, height, suspendable, item }
})
// Walk over the list to set top offsets
chart.reduce((sum, item) => {
item.top = sum
return sum + item.height
}, 0)
return chart
})
+ // Scroll compensation
watch(heightChart, async (newVal, oldVal) => {
if (!toValue(scrollCompensation)) return
if (scrollInProgress.value) return
pauseWatchers()
+
+ // If we're not given an achor, treat last element as one
const getAnchoredEl = (list) =>
- anchor.value
- ? list.find(({ id }) => id === anchor.value)
+ anchorId?.value
+ ? list.find(({ id }) => id === anchorId?.value)
: list[list.length - 1]
+
const oldElement = getAnchoredEl(oldVal)
const newElement = getAnchoredEl(newVal)
const oldOffset = oldElement?.top ?? 0
const newOffset = newElement?.top ?? 0
const diff = newOffset - oldOffset // Positive = down, Negative = up
if (diff !== 0) {
topScrollBoundary.value += diff
bottomScrollBoundary.value += diff
scrollBy(0, diff)
}
updateBoundaries()
resumeWatchers()
})
+ const updateVirtualHeight = ({ id, height }) => {
+ heights.value.set(id, height)
+ }
+
+ // Scrolling
+ const {
+ y: scrollY,
+ inProgress: scrollInProgress,
+ scrollBy,
+ } = scrollPositionInstance
+ const { height: windowHeight } = useWindowSize()
+
+ // Real scroll boundary relative to body's bounds
+ const topScrollBoundary = ref(0)
+ const bottomScrollBoundary = ref(0)
+
+ const updateBoundaries = () => {
+ if (!body.value) return // Not mounted yet
+
+ const { top } = body.value.getBoundingClientRect()
+
+ const distanceItemTopToWindowTop = 0 - top
+ const distanceItemTopToWindowBottom = windowHeight.value - top
+
+ topScrollBoundary.value = distanceItemTopToWindowTop
+ bottomScrollBoundary.value = distanceItemTopToWindowBottom
+ }
+
+ const windowWatcher = watch(windowHeight, updateBoundaries)
+ const scrollWatcher = watch(scrollY, updateBoundaries)
+ const heightWatcher = watch(heightChart, updateBoundaries)
+ const bodyWatcher = watch(body, updateBoundaries)
+
+ const pauseWatchers = () => {
+ windowWatcher.pause()
+ scrollWatcher.pause()
+ heightWatcher.pause()
+ bodyWatcher.pause()
+ }
+ const resumeWatchers = () => {
+ windowWatcher.resume()
+ scrollWatcher.resume()
+ heightWatcher.resume()
+ bodyWatcher.resume()
+ }
const heightChartGrouped = computed(() => {
// Determine visibility state
const chart = heightChart.value.map((heightChartItem) => {
const itemBottomBoundary = heightChartItem.top + heightChartItem.height
const itemTopBoundary = heightChartItem.top
const finalTopScrollBoundary = topScrollBoundary.value - buffer.value
const finalBottomScrollBoundary =
bottomScrollBoundary.value + buffer.value
// To be visible, item's bottom boundary shoud be below top scroll boundary)
const isBelowTopBoundary = itemBottomBoundary > finalTopScrollBoundary
// To be visible, item's top boundary shoud be above bottom scroll boundary)
const isAboveBottomBoundary = itemTopBoundary < finalBottomScrollBoundary
// This accounts for the case where item's boundaries exceed scroll boundary
return {
...heightChartItem,
visible: isBelowTopBoundary && isAboveBottomBoundary,
}
})
- // Group invisible statuses into spacers
+ // Group invisible items into spacers
return chart.reduce((acc, heightChartItem) => {
- const { suspendable, visible, height, top, bottom, id, status } =
+ const { suspendable, visible, height, top, bottom, id, item } =
heightChartItem
const present = visible || !suspendable
if (present) {
- return [...acc, { type: 'status', height, top, bottom, id, status }]
+ return [...acc, { type: 'item', height, top, bottom, id, item }]
} else {
const previousItem = acc[acc.length - 1]
const spacer =
previousItem?.type === 'spacer'
? previousItem
: {
type: 'spacer',
top: Number.POSITIVE_INFINITY,
bottom: Number.POSITIVE_INFINITY,
height: 0,
ids: new Set(),
}
spacer.ids.add(id)
spacer.id = [...spacer.ids].join()
spacer.height += height
if (top < spacer.top) spacer.top = top
if (bottom < spacer.bottom) spacer.bottom = bottom
if (previousItem?.type === 'spacer') {
return acc
} else {
return [...acc, spacer]
}
}
}, [])
})
return {
heightChart: heightChartGrouped,
changeSuspendState,
updateVirtualHeight,
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 12:11 PM (1 d, 22 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1769455
Default Alt Text
(26 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment