convert timeline to composition api

This commit is contained in:
Henry Jameson 2026-09-10 22:20:38 +03:00
commit b6874f12ef
2 changed files with 193 additions and 161 deletions

View file

@ -1,5 +1,15 @@
import { debounce, throttle } from 'lodash-es'
import { mapState } from 'pinia'
import { storeToRefs } from 'pinia'
import {
computed,
onMounted,
onUnmounted,
provide,
ref,
toRefs,
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'
@ -29,23 +39,9 @@ const Timeline = {
timelineRef: Object,
footerSlipgate: Object, // reference to an element where we should put our footer
embedded: Boolean,
inProfile: Boolean,
skipPinned: Boolean,
hideEmpty: Boolean,
},
data() {
return {
showScrollTop: false,
paused: false,
unfocused: false,
blockingClicks: false,
}
},
provide() {
return {
profileUserId: this.inProfile && this.timelineRef.argument,
}
},
components: {
ScrollTopButton,
Conversation,
@ -53,93 +49,68 @@ const Timeline = {
QuickFilterSettings,
QuickViewSettings,
},
computed: {
timeline() {
return useTimelinesStore()[this.timelineRef.name]
},
filteredVisibleStatuses() {
return this.timeline.order
.filter((id) => this.timeline.visibleStatusIds.has(id))
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 }) => (this.skipPinned ? !pinned : true))
},
count() {
return this.timeline.order.length
},
newStatusCount() {
return this.timeline.newStatusCount
},
showLoadButton() {
return this.timeline.newStatusCount > 0 || this.timeline.reloadNeeded
},
loadButtonString() {
if (this.timeline.reloadNeeded) {
return this.$t('timeline.reload')
.filter(({ pinned }) => (skipPinned.value ? !pinned : true))
})
// 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 {
return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
paused.value = true
}
},
mobileLoadButtonString() {
if (this.timeline.reloadNeeded) {
return '+'
})
const showNewStatuses = () => {
if (timeline.value.reloadNeeded) {
useTimelinesStore().clearTimeline(timelineRef.value.name)
fetchOlderStatuses()
} else {
return this.newStatusCount > 99 ? '∞' : this.newStatusCount
blockClicksTemporarily()
useTimelinesStore().showNewStatuses(timelineRef.value.name)
paused.value = false
}
},
classes() {
let rootClasses = !this.embedded
? ['panel', 'panel-default']
: ['-embedded']
if (this.blockingClicks)
rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return {
root: rootClasses,
header: ['timeline-heading'].concat(
!this.embedded ? ['panel-heading', '-sticky'] : ['panel-body'],
),
body: ['timeline-body'].concat(
!this.embedded ? ['panel-body'] : ['panel-body'],
),
footer: ['timeline-footer'].concat(
!this.embedded ? ['panel-footer'] : ['panel-body'],
),
}
},
statusesToDisplay() {
return new Set(this.filteredVisibleStatuses.map(({ id }) => id))
},
...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile',
}),
},
created() {
this.timelineChange(this.timelineRef)
},
mounted() {
if (document.hidden !== undefined) {
document.addEventListener(
'visibilitychange',
this.handleVisibilityChange,
false,
)
this.unfocused = document.hidden
window.scrollTo({ top: 0 })
}
window.addEventListener('keydown', this.handleShortKey)
window.addEventListener('scroll', this.handleScroll)
},
unmounted() {
this.timelineChange(null, this.timelineRef)
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey)
if (document.hidden !== undefined)
document.removeEventListener(
'visibilitychange',
this.handleVisibilityChange,
false,
)
},
methods: {
timelineChange(newTimeline, oldTimeline) {
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
@ -150,83 +121,139 @@ const Timeline = {
if (newTimeline) {
useTimelinesStore().activate(newTimeline.name, newTimeline.argument)
}
},
stopBlockingClicks: debounce(function () {
this.blockingClicks = false
}, 1000),
blockClicksTemporarily() {
if (!this.blockingClicks) {
this.blockingClicks = true
}
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
}
this.stopBlockingClicks()
},
handleShortKey(e) {
stopBlockingClicks()
}
// Shortcuts
const handleShortKey = (e) => {
// Ignore when input fields are focused
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses() {
if (this.timeline.reloadNeeded) {
useTimelinesStore().clearTimeline(this.timelineRef.name)
this.fetchOlderStatuses()
} else {
this.blockClicksTemporarily()
useTimelinesStore().showNewStatuses(this.timelineRef.name)
this.paused = false
}
window.scrollTo({ top: 0 })
},
fetchOlderStatuses: throttle(
function () {
this.timeline.fetcher.fetchOlder()
},
1000,
this,
),
scrollLoad() {
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 (
!this.timeline.fetcher.loadingOlder &&
!timeline.value.fetcher.loadingOlder &&
window.innerHeight + window.pageYOffset >= height - 750
) {
this.fetchOlderStatuses()
fetchOlderStatuses()
}
},
handleScroll: throttle(function (e) {
this.scrollLoad(e)
}, 200),
handleVisibilityChange() {
this.unfocused = document.hidden
},
},
watch: {
timelineRef(newTimeline, oldTimeline) {
this.timelineChange(newTimeline, oldTimeline)
},
newStatusCount(count) {
if (!useMergedConfigStore().mergedConfig.streaming) {
return
}
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'],
),
}
if (count > 0) {
// 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 &&
!this.paused &&
!(
this.unfocused &&
useMergedConfigStore().mergedConfig.pauseOnUnfocused
)
) {
this.showNewStatuses()
} else {
this.paused = true
}
})
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,
count,
showLoadButton,
showNewStatuses,
fetchOlderStatuses,
classes,
loadButtonString,
mobileLoadButtonString,
mobileLayout,
footerSlipgate,
embedded,
hideEmpty,
}
},
}

View file

@ -34,6 +34,11 @@ const UserProfile = {
this.tab = get(this.$route, 'query.tab', defaultTabKey)
useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
},
provide() {
return {
profileUserId: this.userId,
}
},
updated() {
useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
},