Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85826592
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
13 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 2f9b01aef7..01a36ca7b6 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,276 +1,279 @@
import { debounce, throttle } from 'lodash-es'
import { storeToRefs } from 'pinia'
import {
computed,
inject,
nextTick,
onMounted,
onUnmounted,
ref,
toRefs,
useTemplateRef,
watch,
+ useId,
} 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 { useDocumentFocus } from 'src/composables/useDocumentFocus.js'
import { useInterfaceSizes } from 'src/composables/useInterfaceSizes.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 { focused } = useDocumentFocus()
const unfocused = computed(() => !focused.value)
// 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))
})
// Scroll position
const scroller = inject('bodyScrollPosition')
const { hasReachedTop, shouldLoadBottom, top: scrollY } = scroller
// Virtual scrolling
const { fontSize, navbarSize } = 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 offset = computed(() => {
if (embedded.value) {
// The fontsize after navbar is the little gap between navbar and content
return navbarSize.value + fontSize.value
} else {
return 0
}
})
const { heightChart, changeSuspendState, updateVirtualHeight } =
useVirtualScrolling({
name: 'Timeline',
enabled: ref(true),
list: filteredVisibleStatuses,
body,
offset,
scrollPositionInstance: scroller,
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)
const { mergedConfig } = storeToRefs(useMergedConfigStore())
const unfocusedPause = computed(
() => unfocused.value && mergedConfig.value.pauseOnUnfocused,
)
const showNewAutomatically = computed(
() => useMergedConfigStore().mergedConfig.streaming,
)
watch(newStatusCount, (count) => {
if (!showNewAutomatically.value) return
if (count <= 0) return
// only 'stream' them when you're scrolled to the top
if (hasReachedTop.value && !paused.value && !unfocusedPause.value) {
showNewStatuses(true)
} else {
paused.value = true
}
})
const showNewStatuses = async (fast = false) => {
if (timeline.value.reloadNeeded) {
useTimelinesStore().clearTimeline(timelineRef.value.name)
fetchOlderStatuses()
} else {
blockClicksTemporarily()
useTimelinesStore().showNewStatuses(timelineRef.value.name)
}
paused.value = false
await nextTick()
scroller.scrollToPriority({
top: 0,
behavior: fast ? 'instant' : 'smooth',
})
}
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)
})
const infiniteLoadWatcher = debounce((value) => {
if (!value) return
fetchOlderStatuses()
}, 100)
// Scroll
watch([shouldLoadBottom, scrollY], infiniteLoadWatcher)
onUnmounted(() => {
infiniteLoadWatcher.cancel()
})
// 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)
+ const uniqueId = useId()
return {
timelineRef,
timeline,
filteredVisibleStatuses,
heightChart,
updateVirtualHeight,
changeSuspendState,
count,
showLoadButton,
showNewStatuses,
fetchOlderStatuses,
classes,
loadButtonString,
mobileLoadButtonString,
mobileLayout,
footerSlipgate,
embedded,
hideEmpty,
+ uniqueId,
}
},
}
export default Timeline
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index f1ce7645e0..f8774fbc7f 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,172 +1,172 @@
<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"
- id="timeline-header"
+ :id="'timeline-header-' + uniqueId"
/>
<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(false)"
>
<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(false)"
>
{{ 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"
- aria-labelledby="timeline-header"
+ :aria-labelledby="embedded ? undefined : ('timeline-header-'+ uniqueId)"
>
<template
v-for="element in heightChart"
:key="element.id"
>
<Conversation
v-if="element.type === 'item'"
class="timeline-conversation"
:status-id="element.id"
:data-vs-height="element.height"
:data-vs-top="element.top"
:data-vs-id="element.id"
@suspendable-state-change="changeSuspendState"
@height-change="updateVirtualHeight"
/>
<div
v-if="element.type === 'spacer'"
class="virtual-spacer"
aria-hidden="true"
: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
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 30, 8:22 AM (20 h, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799875
Default Alt Text
(13 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline