mirror of
https://iceshrimp.dev/iceshrimp/akkoma-fe.git
synced 2025-12-17 00:43:02 +01:00
* origin/develop: (475 commits) Apply 1 suggestion(s) to 1 file(s) Update dependency @ungap/event-target to v0.2.3 Update package.json fix broken icons after FA upgrade Update Font Awesome Update dependency webpack-dev-middleware to v3.7.3 Update dependency vuelidate to v0.7.7 Pin dependency @kazvmoe-infra/pinch-zoom-element to 1.2.0 lint Make media modal buttons larger Add English translation for hide tooltip Add hide button to media modal Lint Prevent hiding media viewer if swiped over SwipeClick Fix webkit image blurs Fix video in media modal not displaying properly Add changelog for https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1403 Remove image box-shadow in media modal Clean up debug code for image pinch zoom Bump @kazvmoe-infra/pinch-zoom-element to 1.2.0 on npm ...
240 lines
8 KiB
JavaScript
240 lines
8 KiB
JavaScript
import Status from '../status/status.vue'
|
|
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
|
import Conversation from '../conversation/conversation.vue'
|
|
import TimelineMenu from '../timeline_menu/timeline_menu.vue'
|
|
import TimelineQuickSettings from './timeline_quick_settings.vue'
|
|
import { debounce, throttle, keyBy } from 'lodash'
|
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons'
|
|
|
|
library.add(
|
|
faCircleNotch,
|
|
faCog
|
|
)
|
|
|
|
const Timeline = {
|
|
props: [
|
|
'timeline',
|
|
'timelineName',
|
|
'title',
|
|
'userId',
|
|
'tag',
|
|
'embedded',
|
|
'count',
|
|
'pinnedStatusIds',
|
|
'inProfile'
|
|
],
|
|
data () {
|
|
return {
|
|
paused: false,
|
|
unfocused: false,
|
|
bottomedOut: false,
|
|
virtualScrollIndex: 0,
|
|
blockingClicks: false
|
|
}
|
|
},
|
|
components: {
|
|
Status,
|
|
Conversation,
|
|
TimelineMenu,
|
|
TimelineQuickSettings
|
|
},
|
|
computed: {
|
|
newStatusCount () {
|
|
return this.timeline.newStatusCount
|
|
},
|
|
showLoadButton () {
|
|
return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
|
|
},
|
|
loadButtonString () {
|
|
if (this.timeline.flushMarker !== 0) {
|
|
return this.$t('timeline.reload')
|
|
} else {
|
|
return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
|
|
}
|
|
},
|
|
classes () {
|
|
let rootClasses = !this.embedded ? ['panel', 'panel-default'] : []
|
|
if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
|
|
return {
|
|
root: rootClasses,
|
|
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
|
|
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
|
|
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
|
|
}
|
|
},
|
|
// id map of statuses which need to be hidden in the main list due to pinning logic
|
|
pinnedStatusIdsObject () {
|
|
return keyBy(this.pinnedStatusIds)
|
|
},
|
|
statusesToDisplay () {
|
|
const amount = this.timeline.visibleStatuses.length
|
|
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
|
|
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
|
|
const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide)
|
|
return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id)
|
|
},
|
|
virtualScrollingEnabled () {
|
|
return this.$store.getters.mergedConfig.virtualScrolling
|
|
}
|
|
},
|
|
created () {
|
|
const store = this.$store
|
|
const credentials = store.state.users.currentUser.credentials
|
|
const showImmediately = this.timeline.visibleStatuses.length === 0
|
|
|
|
window.addEventListener('scroll', this.handleScroll)
|
|
|
|
if (store.state.api.fetchers[this.timelineName]) { return false }
|
|
|
|
timelineFetcher.fetchAndUpdate({
|
|
store,
|
|
credentials,
|
|
timeline: this.timelineName,
|
|
showImmediately,
|
|
userId: this.userId,
|
|
tag: this.tag
|
|
})
|
|
},
|
|
mounted () {
|
|
if (typeof document.hidden !== 'undefined') {
|
|
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
|
this.unfocused = document.hidden
|
|
}
|
|
window.addEventListener('keydown', this.handleShortKey)
|
|
setTimeout(this.determineVisibleStatuses, 250)
|
|
},
|
|
unmounted () {
|
|
window.removeEventListener('scroll', this.handleScroll)
|
|
window.removeEventListener('keydown', this.handleShortKey)
|
|
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
|
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
|
|
},
|
|
methods: {
|
|
stopBlockingClicks: debounce(function () {
|
|
this.blockingClicks = false
|
|
}, 1000),
|
|
blockClicksTemporarily () {
|
|
if (!this.blockingClicks) {
|
|
this.blockingClicks = true
|
|
}
|
|
this.stopBlockingClicks()
|
|
},
|
|
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.flushMarker !== 0) {
|
|
this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
|
|
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
|
|
this.fetchOlderStatuses()
|
|
} else {
|
|
this.blockClicksTemporarily()
|
|
this.$store.commit('showNewStatuses', { timeline: this.timelineName })
|
|
this.paused = false
|
|
}
|
|
},
|
|
fetchOlderStatuses: throttle(function () {
|
|
const store = this.$store
|
|
const credentials = store.state.users.currentUser.credentials
|
|
store.commit('setLoading', { timeline: this.timelineName, value: true })
|
|
timelineFetcher.fetchAndUpdate({
|
|
store,
|
|
credentials,
|
|
timeline: this.timelineName,
|
|
older: true,
|
|
showImmediately: true,
|
|
userId: this.userId,
|
|
tag: this.tag
|
|
}).then(({ statuses }) => {
|
|
if (statuses && statuses.length === 0) {
|
|
this.bottomedOut = true
|
|
}
|
|
}).finally(() =>
|
|
store.commit('setLoading', { timeline: this.timelineName, value: false })
|
|
)
|
|
}, 1000, this),
|
|
determineVisibleStatuses () {
|
|
if (!this.$refs.timeline) return
|
|
if (!this.virtualScrollingEnabled) return
|
|
|
|
const statuses = this.$refs.timeline.children
|
|
const cappedScrollIndex = Math.max(0, Math.min(this.virtualScrollIndex, statuses.length - 1))
|
|
|
|
if (statuses.length === 0) return
|
|
|
|
const height = Math.max(document.body.offsetHeight, window.pageYOffset)
|
|
|
|
const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5)
|
|
|
|
// Start from approximating the index of some visible status by using the
|
|
// the center of the screen on the timeline.
|
|
let approxIndex = Math.floor(statuses.length * (centerOfScreen / height))
|
|
let err = statuses[approxIndex].getBoundingClientRect().y
|
|
|
|
// if we have a previous scroll index that can be used, test if it's
|
|
// closer than the previous approximation, use it if so
|
|
|
|
const virtualScrollIndexY = statuses[cappedScrollIndex].getBoundingClientRect().y
|
|
if (Math.abs(err) > virtualScrollIndexY) {
|
|
approxIndex = cappedScrollIndex
|
|
err = virtualScrollIndexY
|
|
}
|
|
|
|
// if the status is too far from viewport, check the next/previous ones if
|
|
// they happen to be better
|
|
while (err < -20 && approxIndex < statuses.length - 1) {
|
|
err += statuses[approxIndex].offsetHeight
|
|
approxIndex++
|
|
}
|
|
while (err > window.innerHeight + 100 && approxIndex > 0) {
|
|
approxIndex--
|
|
err -= statuses[approxIndex].offsetHeight
|
|
}
|
|
|
|
// this status is now the center point for virtual scrolling and visible
|
|
// statuses will be nearby statuses before and after it
|
|
this.virtualScrollIndex = approxIndex
|
|
},
|
|
scrollLoad (e) {
|
|
const bodyBRect = document.body.getBoundingClientRect()
|
|
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
|
|
if (this.timeline.loading === false &&
|
|
this.$el.offsetHeight > 0 &&
|
|
(window.innerHeight + window.pageYOffset) >= (height - 750)) {
|
|
this.fetchOlderStatuses()
|
|
}
|
|
},
|
|
handleScroll: throttle(function (e) {
|
|
this.determineVisibleStatuses()
|
|
this.scrollLoad(e)
|
|
}, 200),
|
|
handleVisibilityChange () {
|
|
this.unfocused = document.hidden
|
|
}
|
|
},
|
|
watch: {
|
|
newStatusCount (count) {
|
|
if (!this.$store.getters.mergedConfig.streaming) {
|
|
return
|
|
}
|
|
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 && this.$store.getters.mergedConfig.pauseOnUnfocused)
|
|
) {
|
|
this.showNewStatuses()
|
|
} else {
|
|
this.paused = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default Timeline
|