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 { 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 Conversation from 'src/components/conversation/conversation.vue'
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue' import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
@ -29,23 +39,9 @@ const Timeline = {
timelineRef: Object, timelineRef: Object,
footerSlipgate: Object, // reference to an element where we should put our footer footerSlipgate: Object, // reference to an element where we should put our footer
embedded: Boolean, embedded: Boolean,
inProfile: Boolean,
skipPinned: Boolean, skipPinned: Boolean,
hideEmpty: Boolean, hideEmpty: Boolean,
}, },
data() {
return {
showScrollTop: false,
paused: false,
unfocused: false,
blockingClicks: false,
}
},
provide() {
return {
profileUserId: this.inProfile && this.timelineRef.argument,
}
},
components: { components: {
ScrollTopButton, ScrollTopButton,
Conversation, Conversation,
@ -53,93 +49,68 @@ const Timeline = {
QuickFilterSettings, QuickFilterSettings,
QuickViewSettings, QuickViewSettings,
}, },
computed: { setup(props, ctx) {
timeline() { const { t } = useI18n()
return useTimelinesStore()[this.timelineRef.name] const unfocused = ref(false)
},
filteredVisibleStatuses() { // Timeline
return this.timeline.order const { timelineRef } = toRefs(props)
.filter((id) => this.timeline.visibleStatusIds.has(id)) 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)) .map((id) => useStatusesStore().allStatuses.get(id))
.filter(({ pinned }) => (this.skipPinned ? !pinned : true)) .filter(({ pinned }) => (skipPinned.value ? !pinned : true))
}, })
count() {
return this.timeline.order.length // Counter
}, const count = computed(() => timeline.value.order.length)
newStatusCount() { const newStatusCount = computed(() => timeline.value.newStatusCount)
return this.timeline.newStatusCount const showLoadButton = computed(
}, () => timeline.value.newStatusCount > 0 || timeline.value.reloadNeeded,
showLoadButton() {
return this.timeline.newStatusCount > 0 || this.timeline.reloadNeeded
},
loadButtonString() {
if (this.timeline.reloadNeeded) {
return this.$t('timeline.reload')
} else {
return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
}
},
mobileLoadButtonString() {
if (this.timeline.reloadNeeded) {
return '+'
} else {
return this.newStatusCount > 99 ? '∞' : this.newStatusCount
}
},
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
// Showing new
const paused = ref(false)
watch(newStatusCount, (count) => {
if (!useMergedConfigStore().mergedConfig.streaming) {
return
} }
window.addEventListener('keydown', this.handleShortKey) if (count <= 0) return
window.addEventListener('scroll', this.handleScroll) // only 'stream' them when you're scrolled to the top
}, const doc = document.documentElement
unmounted() { const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
this.timelineChange(null, this.timelineRef) if (
window.removeEventListener('scroll', this.handleScroll) top < 15 &&
window.removeEventListener('keydown', this.handleShortKey) !paused.value &&
if (document.hidden !== undefined) !(
document.removeEventListener( unfocused.value &&
'visibilitychange', useMergedConfigStore().mergedConfig.pauseOnUnfocused
this.handleVisibilityChange,
false,
) )
}, ) {
methods: { showNewStatuses()
timelineChange(newTimeline, oldTimeline) { } 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 sameName = newTimeline?.name === oldTimeline?.name
const sameArgument = newTimeline?.argument === oldTimeline?.argument const sameArgument = newTimeline?.argument === oldTimeline?.argument
if (sameName && sameArgument) return if (sameName && sameArgument) return
@ -150,83 +121,139 @@ const Timeline = {
if (newTimeline) { if (newTimeline) {
useTimelinesStore().activate(newTimeline.name, newTimeline.argument) useTimelinesStore().activate(newTimeline.name, newTimeline.argument)
} }
},
stopBlockingClicks: debounce(function () {
this.blockingClicks = false
}, 1000),
blockClicksTemporarily() {
if (!this.blockingClicks) {
this.blockingClicks = true
} }
this.stopBlockingClicks() watch(timelineRef, timelineChange, { immediate: true })
}, onUnmounted(() => {
handleShortKey(e) { 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 // Ignore when input fields are focused
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
if (e.key === '.') this.showNewStatuses() if (e.key === '.') 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 }) onMounted(() => {
}, window.addEventListener('keydown', handleShortKey)
fetchOlderStatuses: throttle( })
function () { onUnmounted(() => {
this.timeline.fetcher.fetchOlder() window.removeEventListener('keydown', handleShortKey)
}, })
1000,
this, // Scroll
), const scrollLoad = () => {
scrollLoad() {
// TODO simplify this logic // TODO simplify this logic
const bodyBRect = document.body.getBoundingClientRect() const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y) const height = Math.max(bodyBRect.height, -bodyBRect.y)
if ( if (
!this.timeline.fetcher.loadingOlder && !timeline.value.fetcher.loadingOlder &&
window.innerHeight + window.pageYOffset >= height - 750 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
} }
if (count > 0) { const handleScroll = throttle((e) => {
// only 'stream' them when you're scrolled to the top scrollLoad(e)
const doc = document.documentElement }, 200)
const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) onMounted(() => {
if ( window.addEventListener('scroll', handleScroll)
top < 15 && })
!this.paused && onUnmounted(() => {
!( window.removeEventListener('scroll', handleScroll)
this.unfocused && })
useMergedConfigStore().mergedConfig.pauseOnUnfocused
// Focused state
const handleVisibilityChange = () => {
unfocused.value = document.hidden
}
onMounted(() => {
if (document.hidden === undefined) return
document.addEventListener(
'visibilitychange',
handleVisibilityChange,
false,
) )
) { unfocused.value = document.hidden
this.showNewStatuses() })
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 { } else {
this.paused = true 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) this.tab = get(this.$route, 'query.tab', defaultTabKey)
useInterfaceStore().setForeignProfileBackground(this.user?.background_image) useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
}, },
provide() {
return {
profileUserId: this.userId,
}
},
updated() { updated() {
useInterfaceStore().setForeignProfileBackground(this.user?.background_image) useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
}, },