Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
32 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 72fd45ba98..d6ab361d72 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,335 +1,377 @@
import { get } from 'lodash-es'
import { storeToRefs } from 'pinia'
-import { computed, provide, ref, toRefs, useTemplateRef, watch } from 'vue'
+import {
+ computed,
+ onUnmounted,
+ 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 { useInterfaceSizes } from 'src/composables/useInterfaceSizes.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) {
+ emits: ['heightChange', 'suspendableStateChange'],
+ setup(props, { emit }) {
// # 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: 'nearest' })
}
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,
mainStatus,
conversation,
replies,
getReplies,
fetchConversation,
loadError,
} = useConversation(focusedId, isExpanded)
watch(
expanded,
async (value) => {
if (value) {
await fetchConversation()
} else {
resetDisplayState()
}
if (isPage.value) return
},
{ 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,
})
const { fontSize } = useInterfaceSizes()
+ // External virtual scrolling
+ const unsuspendableIds = ref(new Set())
+ const suspendable = computed(
+ () => !isExpanded.value && unsuspendableIds.value.size === 0,
+ )
+ const rootElement = useTemplateRef('root')
+ const updateVirtualHeight = (e) => {
+ const [entry] = e
+ emit('heightChange', {
+ id: statusId.value,
+ height: entry.contentRect.height,
+ element: rootElement,
+ })
+ }
+ const resizeObserver = ref(new ResizeObserver(updateVirtualHeight))
+ watch(rootElement, () => resizeObserver.value.observe(rootElement.value))
+ watch(suspendable, (value) =>
+ emit('suspendableStateChange', { suspend: value, id: statusId.value }),
+ )
+ onUnmounted(() => resizeObserver.value.disconnect())
+
// 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)
const {
heightChart: heightChartLinear,
changeSuspendState: changeSuspendStateLinear,
updateVirtualHeight: updateVirtualHeightLinear,
} = useVirtualScrolling({
list: conversation,
body: linearElement,
scrollPositionInstance: scroller,
scrollCompensation: linearScrollCompensation,
anchorId: mainStatus.value?.id,
anchorRepeatId: currentStatus.value?.id,
getPlaceholderHeight,
})
+ const changeSuspendStateLinearLocal = (e) => {
+ changeSuspendStateLinear(e)
+ const { id, suspend } = e
+ if (suspend) {
+ unsuspendableIds.value.add(id)
+ } else {
+ unsuspendableIds.value.delete(id)
+ }
+ }
// # 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)
const {
heightChart: heightChartAncestors,
changeSuspendState: changeSuspendStateAncestors,
updateVirtualHeight: updateVirtualHeightAncestors,
} = 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({
- list: currentLevel,
- body: currentLevelElement,
- scrollPositionInstance: scroller,
- scrollCompensation: false,
- getPlaceholderHeight,
- })
+ const changeSuspendStateAncestorsLocal = (e) => {
+ changeSuspendStateAncestors(e)
+ const { id, suspend } = e
+ if (suspend) {
+ unsuspendableIds.value.add(id)
+ } else {
+ unsuspendableIds.value.delete(id)
+ }
+ }
+ const changeSuspendStateCurrentLevelLocal = (e) => {
+ changeSuspendStateLinear(e)
+ const { id, suspend } = e
+ if (suspend) {
+ unsuspendableIds.value.add(id)
+ } else {
+ unsuspendableIds.value.delete(id)
+ }
+ }
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,
+ changeSuspendStateLinearLocal,
updateVirtualHeightLinear,
// # Tree style stuff
isTreeView,
// ## Tree virtual scrolling
heightChartAncestors,
- changeSuspendStateAncestors,
+ changeSuspendStateAncestorsLocal,
updateVirtualHeightAncestors,
- heightChartCurrentLevel,
- changeSuspendStateCurrentLevel,
- updateVirtualHeightCurrentLevel,
+
+ changeSuspendStateCurrentLevelLocal,
// ## 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 8d4c4f9670..abfd57d485 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -1,189 +1,173 @@
<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 === 'item'"
class="conversation-status panel-body"
:class="getStatusClasses(element.item)"
:status-id="element.item.id"
:replies="getReplies(element.item.id)"
:focused="focused === element.item.id"
conversation-rank="ancestor"
:data-status-id="element.id"
:data-vs-height="element.height"
:data-vs-top="element.top"
@goto="setFocused"
@dive="diveIntoStatus(element.item.id)"
- @suspendable-state-change="changeSuspendStateAncestors"
+ @suspendable-state-change="changeSuspendStateAncestorsLocal"
@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 === 'item'"
- :status-id="currentStatus.id"
- :depth="0"
+ <ThreadTree
+ :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>
+ @goto="setFocused"
+ @dive="diveIntoStatus"
+ @toggle-expanded="toggleExpanded"
+ @show-thread-recursively="showThreadRecursively"
+ @suspendable-state-change="changeSuspendStateCurrentLevelLocal"
+ />
</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 === 'item'"
class="conversation-status"
:class="getStatusClasses(element.item)"
:status-id="element.item.id"
:replies="getReplies(element.item.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"
+ @suspendable-state-change="changeSuspendStateLinearLocal"
@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/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 1cfe1b3b45..38376dc81e 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,260 +1,289 @@
import { debounce, throttle } from 'lodash-es'
import { storeToRefs } from 'pinia'
import {
computed,
onMounted,
onUnmounted,
- provide,
ref,
toRefs,
+ useTemplateRef,
watch,
} from 'vue'
import { useI18n } from 'vue-i18n'
import Conversation from 'src/components/conversation/conversation.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 ScrollTopButton from 'src/components/scroll_top_button/scroll_top_button.vue'
import TimelineMenu from 'src/components/timeline_menu/timeline_menu.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
+import { useInterfaceSizes } from 'src/composables/useInterfaceSizes.js'
+import { useScrollPosition } from 'src/composables/useScrollPosition.js'
+import { useVirtualScrolling } from 'src/composables/useVirtualScrolling.js'
+
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faArrowUp,
faCheck,
faCircleNotch,
faCirclePlus,
faCog,
faMinus,
} from '@fortawesome/free-solid-svg-icons'
library.add(faCircleNotch, faCog, faMinus, faArrowUp, faCirclePlus, faCheck)
const Timeline = {
props: {
timelineRef: Object,
footerSlipgate: Object, // reference to an element where we should put our footer
embedded: Boolean,
skipPinned: Boolean,
hideEmpty: Boolean,
},
components: {
ScrollTopButton,
Conversation,
TimelineMenu,
QuickFilterSettings,
QuickViewSettings,
},
setup(props, ctx) {
const { t } = useI18n()
const unfocused = ref(false)
// Timeline
const { timelineRef } = toRefs(props)
const timeline = computed(() => useTimelinesStore()[timelineRef.value.name])
const { skipPinned } = toRefs(props)
const filteredVisibleStatuses = computed(() => {
return timeline.value.order
.filter((id) => timeline.value.visibleStatusIds.has(id))
.map((id) => useStatusesStore().allStatuses.get(id))
.filter(({ pinned }) => (skipPinned.value ? !pinned : true))
})
+ // Virtual scrolling
+ const { fontSize } = useInterfaceSizes()
+
+ // Placeholder heights.
+ const mutedStatusHeight = computed(() => fontSize.value * 1.5)
+ const normalStatusHeight = computed(() => fontSize.value * 10)
+ const getPlaceholderHeight = (id) =>
+ filteredVisibleStatuses.value.find((item) => item.id === id)?.muted
+ ? mutedStatusHeight
+ : normalStatusHeight
+
+ const body = useTemplateRef('timeline')
+ const { heightChart, changeSuspendState, updateVirtualHeight } =
+ useVirtualScrolling({
+ list: filteredVisibleStatuses,
+ body,
+ scrollPositionInstance: useScrollPosition(),
+ scrollCompensation: false,
+ getPlaceholderHeight,
+ })
+
// Counter
const count = computed(() => timeline.value.order.length)
const newStatusCount = computed(() => timeline.value.newStatusCount)
const showLoadButton = computed(
() => timeline.value.newStatusCount > 0 || timeline.value.reloadNeeded,
)
// Showing new
const paused = ref(false)
watch(newStatusCount, (count) => {
if (!useMergedConfigStore().mergedConfig.streaming) {
return
}
if (count <= 0) return
// only 'stream' them when you're scrolled to the top
const doc = document.documentElement
const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
if (
top < 15 &&
!paused.value &&
!(
unfocused.value &&
useMergedConfigStore().mergedConfig.pauseOnUnfocused
)
) {
showNewStatuses()
} else {
paused.value = true
}
})
const showNewStatuses = () => {
if (timeline.value.reloadNeeded) {
useTimelinesStore().clearTimeline(timelineRef.value.name)
fetchOlderStatuses()
} else {
blockClicksTemporarily()
useTimelinesStore().showNewStatuses(timelineRef.value.name)
paused.value = false
}
window.scrollTo({ top: 0 })
}
const fetchOlderStatuses = throttle(() => {
timeline.value.fetcher.fetchOlder()
}, 1000)
// Timeline change
const timelineChange = (newTimeline, oldTimeline) => {
const sameName = newTimeline?.name === oldTimeline?.name
const sameArgument = newTimeline?.argument === oldTimeline?.argument
if (sameName && sameArgument) return
if (oldTimeline) {
useTimelinesStore().deactivate(oldTimeline.name)
}
if (newTimeline) {
useTimelinesStore().activate(newTimeline.name, newTimeline.argument)
}
}
watch(timelineRef, timelineChange, { immediate: true })
onUnmounted(() => {
timelineChange(null, timelineRef.value) // ????
})
// Misclick prevention
const blockingClicks = ref(false)
const stopBlockingClicks = debounce(() => {
blockingClicks.value = false
}, 1000)
const blockClicksTemporarily = () => {
if (!blockingClicks.value) {
blockingClicks.value = true
}
stopBlockingClicks()
}
// Shortcuts
const handleShortKey = (e) => {
// Ignore when input fields are focused
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
if (e.key === '.') showNewStatuses()
}
onMounted(() => {
window.addEventListener('keydown', handleShortKey)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleShortKey)
})
// Scroll
const scrollLoad = () => {
// TODO simplify this logic
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y)
if (
!timeline.value.fetcher.loadingOlder &&
window.innerHeight + window.pageYOffset >= height - 750
) {
fetchOlderStatuses()
}
}
const handleScroll = throttle((e) => {
scrollLoad(e)
}, 200)
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
// Focused state
const handleVisibilityChange = () => {
unfocused.value = document.hidden
}
onMounted(() => {
if (document.hidden === undefined) return
document.addEventListener(
'visibilitychange',
handleVisibilityChange,
false,
)
unfocused.value = document.hidden
})
onUnmounted(() => {
if (document.hidden === undefined) return
document.removeEventListener(
'visibilitychange',
handleVisibilityChange,
false,
)
})
// Misc UI things
const classes = computed(() => {
let rootClasses = !embedded.value
? ['panel', 'panel-default']
: ['-embedded']
if (blockingClicks.value)
rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return {
root: rootClasses,
header: ['timeline-heading'].concat(
!embedded.value ? ['panel-heading', '-sticky'] : ['panel-body'],
),
body: ['timeline-body'].concat(
!embedded.value ? ['panel-body'] : ['panel-body'],
),
footer: ['timeline-footer'].concat(
!embedded.value ? ['panel-footer'] : ['panel-body'],
),
}
})
const loadButtonString = computed(() => {
if (timeline.value.reloadNeeded) {
return t('timeline.reload')
} else {
return `${t('timeline.show_new')} (${newStatusCount.value})`
}
})
const mobileLoadButtonString = computed(() => {
if (timeline.value.reloadNeeded) {
return '+'
} else {
return newStatusCount.value > 99 ? '∞' : newStatusCount.value
}
})
const { layoutType } = storeToRefs(useInterfaceStore())
const mobileLayout = computed(() => layoutType.value === 'mobile')
const { footerSlipgate, embedded, hideEmpty } = toRefs(props)
return {
timelineRef,
timeline,
filteredVisibleStatuses,
+ heightChart,
+ updateVirtualHeight,
+ changeSuspendState,
+
count,
showLoadButton,
showNewStatuses,
fetchOlderStatuses,
classes,
loadButtonString,
mobileLoadButtonString,
mobileLayout,
footerSlipgate,
embedded,
hideEmpty,
}
},
}
export default Timeline
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 8388f59e72..88cfdf0664 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,150 +1,161 @@
<template>
<!-- there is a brief moment during logout when old timeline gets forcibly deactivated -->
<div v-if="timeline.fetcher" :class="['Timeline', classes.root]">
<div
v-if="!embedded"
:class="classes.header"
>
<TimelineMenu
v-if="!embedded"
:timeline-name="timelineRef.name"
/>
<div
v-if="timeline.fetcher.loadingNewer && !showLoadButton"
class="loadingIndicator"
>
<FAIcon
fixed-width
icon="circle-notch"
spin
/>
</div>
<ScrollTopButton />
<template v-if="mobileLayout">
<div
v-if="showLoadButton"
class="rightside-button"
>
<button
class="button-unstyled loadmore-button"
:title="loadButtonString"
@click.prevent="showNewStatuses"
>
<FAIcon
fixed-width
icon="circle-plus"
/>
<div class="badge -counter">
{{ mobileLoadButtonString }}
</div>
</button>
</div>
<div
v-else-if="!timeline.fetcher.loadingNewer"
class="loadmore-text faint veryfaint rightside-icon"
:title="$t('timeline.up_to_date')"
:aria-disabled="true"
@click.prevent
>
<FAIcon
fixed-width
icon="check"
/>
</div>
</template>
<template v-else>
<button
v-if="showLoadButton"
class="button-default loadmore-button"
@click.prevent="showNewStatuses"
>
{{ loadButtonString }}
</button>
<div
v-else
class="loadmore-text faint"
@click.prevent
>
{{ $t('timeline.up_to_date') }}
</div>
</template>
<QuickFilterSettings
v-if="!mobileLayout"
class="rightside-button"
/>
<QuickViewSettings
class="rightside-button"
/>
</div>
<div :class="classes.body">
<div
class="timeline"
ref="timeline"
role="feed"
>
- <Conversation
- v-for="status in filteredVisibleStatuses"
- :key="status.id"
- :status-id="status.id"
- role="listitem"
- />
+ <template
+ v-for="element in heightChart"
+ :key="element.id"
+ >
+ <Conversation
+ v-if="element.type === 'item'"
+ :status-id="element.item.id"
+ role="listitem"
+ @suspendable-state-change="changeSuspendState"
+ @height-change="updateVirtualHeight"
+ />
+ <div
+ v-if="element.type === 'spacer'"
+ class="virtual-spacer"
+ :style="{ height: element.height + 'px' }"
+ />
+ </template>
</div>
<template v-if="!hideEmpty && count === 0">
<div
v-if="timeline.fetcher.loadingNewer || timeline.fetcher.loadingOlder"
class="timeline-placeholder"
>
<FAIcon
icon="circle-notch"
spin
size="4x"
/>
</div>
<div
v-else
class="timeline-placeholder faint"
>
{{ $t('timeline.no_statuses') }}
</div>
</template>
</div>
<div v-if="!embedded || footerSlipgate" :class="classes.footer">
<teleport
:to="footerSlipgate"
:disabled="!embedded || !footerSlipgate"
>
<div
v-if="timeline.fetcher.bottomedOut"
class="new-status-notification text-center faint"
>
{{ $t('timeline.no_more_statuses') }}
</div>
<div
v-else-if="timeline.fetcher.loadingOlder"
class="new-status-notification text-center"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
<button
v-else-if="timeline.minId !== ''"
class="button-unstyled -link"
@click.prevent="fetchOlderStatuses()"
>
<div class="new-status-notification text-center">
{{ $t('timeline.load_older') }}
</div>
</button>
</teleport>
<!-- spacer to avoid having empty shrug -->
<span v-if="embedded && footerSlipgate" />
</div>
</div>
</template>
<script src="./timeline.js"></script>
<style src="./timeline.scss" lang="scss"> </style>

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 31, 9:31 AM (19 h, 29 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801223
Default Alt Text
(32 KB)

Event Timeline