Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
17 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 1fcce80075..3a4239cb2f 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,334 +1,367 @@
import { debounce, throttle } from 'lodash-es'
import { storeToRefs } from 'pinia'
import {
computed,
inject,
nextTick,
onMounted,
onUnmounted,
ref,
toRefs,
useId,
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 { 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 { mergedConfig } = storeToRefs(useMergedConfigStore())
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,
scrollToPriority,
} = scroller
// Pagination
const paginationEnabled = computed(
() => mergedConfig.value.timelinePagination,
)
const page = ref(0)
+ const pageLoading = ref(false)
const PAGE_SIZE = 20
+
const pageStart = computed(() => page.value * PAGE_SIZE)
const pageEnd = computed(() => pageStart.value + PAGE_SIZE)
+
const currentSlice = computed(() => {
if (paginationEnabled.value) {
return filteredVisibleStatuses.value.slice(
pageStart.value,
pageEnd.value,
)
} else {
return filteredVisibleStatuses.value
}
})
+ const nextSlice = computed(() => {
+ if (paginationEnabled.value) {
+ return filteredVisibleStatuses.value.slice(
+ pageStart.value + PAGE_SIZE,
+ pageEnd.value + PAGE_SIZE,
+ )
+ } else {
+ return filteredVisibleStatuses.value
+ }
+ })
+ const nextSliceReady = computed(() => {
+ if (nextSlice.value.length >= PAGE_SIZE) {
+ return true
+ } else {
+ return nextSlice.value.find(
+ ({ id }) => timeline.value.minId,
+ )
+ }
+ })
+
const isFirstPage = computed(() => page.value <= 0)
const isLastPage = computed(() => {
const bottomedOut = timeline.value.fetcher.bottomedOut
const lastId = timeline.value.minId
const lastIdInPage = currentSlice.value.find(
({ id }) => timeline.value.minId,
)
return bottomedOut && lastId && lastIdInPage
})
- const nextPage = () => {
+ const nextPage = async () => {
if (isLastPage.value) return
- fetchOlderStatuses()
+ if (!nextSliceReady.value) {
+ pageLoading.value = true
+ await fetchOlderStatuses()
+ pageLoading.value = false
+ }
page.value++
scrollToPriority({ top: 0, behavior: 'smooth' })
}
const previousPage = () => {
if (isFirstPage.value) return
page.value--
scrollToPriority({ top: 0, behavior: 'smooth' })
}
+ watch(timeline, () => {
+ page.value = 0
+ })
// Virtual scrolling
const { fontSize, navbarSize } = useInterfaceSizes()
// Placeholder heights.
const mutedStatusHeight = computed(() => fontSize.value * 1.5)
const normalStatusHeight = computed(() => fontSize.value * 10)
const getPlaceholderHeight = (id) =>
currentSlice.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: currentSlice,
body,
offset,
scrollPositionInstance: scroller,
scrollCompensation: true,
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 unfocusedPause = computed(
() => unfocused.value && mergedConfig.value.pauseOnUnfocused,
)
const showNewAutomatically = computed(() => mergedConfig.value.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 &&
page.value === 0
) {
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)
}
page.value = 0
paused.value = false
await nextTick()
scroller.scrollToPriority({
top: 0,
behavior: fast ? 'instant' : 'smooth',
})
}
- const fetchOlderStatuses = throttle(() => {
- timeline.value.fetcher.fetchOlder()
+ const fetchOlderStatuses = throttle(async () => {
+ await 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
if (paginationEnabled.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,
paginationEnabled,
page,
+ pageLoading,
nextPage,
previousPage,
+ isLastPage,
+ isFirstPage,
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 73ee229dd3..bb539dedf5 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,213 +1,223 @@
<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-' + 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
v-if="paginationEnabled && page > 0"
class="pagination panel-subheading"
>
<button
class="panel-body button-unstyled"
@click.left.prevent="previousPage"
>
<div class="new-status-notification text-center">
{{ $t('timeline.previous_page') }}
</div>
</button>
</div>
<div :class="classes.body">
<div
class="timeline"
ref="timeline"
role="feed"
: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"
:scroll-compensation="element.visible"
:top="element.top"
:data-vs-height="element.height"
:data-vs-top="element.top"
:data-vs-id="element.id"
:data-vs-visible="element.visible"
@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="paginationEnabled"
class="pagination"
>
+ <div
+ v-if="pageLoading"
+ class="new-status-notification text-center"
+ >
+ <FAIcon
+ icon="circle-notch"
+ spin
+ size="lg"
+ />
+ </div>
<button
- v-if="!isLastPage"
+ v-else-if="!isLastPage"
class="button-unstyled"
@click.left.prevent="nextPage"
>
<div class="new-status-notification text-center">
{{ $t('timeline.next_page') }}
</div>
</button>
<div
v-else
class="new-status-notification text-center faint"
>
{{ $t('timeline.no_more_statuses') }}
</div>
</div>
<div
v-else
class="infinite-scroll"
>
<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"
@click.prevent="fetchOlderStatuses()"
>
<div class="new-status-notification text-center">
{{ $t('timeline.load_older') }}
</div>
</button>
</div>
</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
Fri, Oct 30, 3:06 PM (19 h, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1797362
Default Alt Text
(17 KB)

Event Timeline