Merge remote-tracking branch 'upstream/feat/custom-virtual-scrolling' into shigusegubu

* upstream/feat/custom-virtual-scrolling:
  whoops
  fix data property being called the wrong name in conversation
  fix warnings and console errors
  fix minor bugs
  make virtual scrolling optional in case people want to be able to ctrl-f all page
  rename hidden stuff to virtualHidden, remove log
  add custom solution for virtual scrolling to ease ram and cpu use when scrolling for a long time
This commit is contained in:
Henry Jameson 2020-01-27 23:39:41 +02:00
commit 0f743af271
11 changed files with 101 additions and 17 deletions

View file

@ -35,7 +35,9 @@ const conversation = {
data () { data () {
return { return {
highlight: null, highlight: null,
expanded: false expanded: false,
// Approximate minimum height of a status, gets overwritten with real one
virtualHeight: '120px'
} }
}, },
props: [ props: [
@ -44,7 +46,8 @@ const conversation = {
'isPage', 'isPage',
'pinnedStatusIdsObject', 'pinnedStatusIdsObject',
'inProfile', 'inProfile',
'profileUserId' 'profileUserId',
'virtualHidden'
], ],
created () { created () {
if (this.isPage) { if (this.isPage) {
@ -102,6 +105,9 @@ const conversation = {
}, },
isExpanded () { isExpanded () {
return this.expanded || this.isPage return this.expanded || this.isPage
},
hiddenStyle () {
return this.virtualHidden ? { height: this.virtualHeight } : {}
} }
}, },
components: { components: {
@ -121,6 +127,9 @@ const conversation = {
if (value) { if (value) {
this.fetchConversation() this.fetchConversation()
} }
},
virtualHidden (value) {
this.virtualHeight = `${this.$el.clientHeight}px`
} }
}, },
methods: { methods: {

View file

@ -1,10 +1,11 @@
<template> <template>
<div <div
:style="hiddenStyle"
class="timeline panel-default" class="timeline panel-default"
:class="[isExpanded ? 'panel' : 'panel-disabled']" :class="[isExpanded ? 'panel' : 'panel-disabled']"
> >
<div <div
v-if="isExpanded" v-if="isExpanded && !virtualHidden"
class="panel-heading conversation-heading" class="panel-heading conversation-heading"
> >
<span class="title"> {{ $t('timeline.conversation') }} </span> <span class="title"> {{ $t('timeline.conversation') }} </span>
@ -28,6 +29,7 @@
:replies="getReplies(status.id)" :replies="getReplies(status.id)"
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="profileUserId" :profile-user-id="profileUserId"
:virtual-hidden="virtualHidden"
class="status-fadein panel-body" class="status-fadein panel-body"
@goto="setHighlight" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"

View file

@ -76,7 +76,7 @@
<li> <li>
<Checkbox v-model="useStreamingApi"> <Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }} {{ $t('settings.useStreamingApi') }}
<br/> <br>
<small> <small>
{{ $t('settings.useStreamingApiWarning') }} {{ $t('settings.useStreamingApiWarning') }}
</small> </small>
@ -92,6 +92,11 @@
{{ $t('settings.reply_link_preview') }} {{ $t('settings.reply_link_preview') }}
</Checkbox> </Checkbox>
</li> </li>
<li>
<Checkbox v-model="virtualScrolling">
{{ $t('settings.virtual_scrolling') }}
</Checkbox>
</li>
</ul> </ul>
</div> </div>

View file

@ -34,7 +34,8 @@ const Status = {
'inlineExpanded', 'inlineExpanded',
'showPinned', 'showPinned',
'inProfile', 'inProfile',
'profileUserId' 'profileUserId',
'virtualHidden'
], ],
data () { data () {
return { return {
@ -121,7 +122,7 @@ const Status = {
return this.mergedConfig.hideFilteredStatuses return this.mergedConfig.hideFilteredStatuses
}, },
hideStatus () { hideStatus () {
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) || this.virtualHidden
}, },
isFocused () { isFocused () {
// retweet or root of an expanded conversation // retweet or root of an expanded conversation

View file

@ -18,14 +18,16 @@ const StillImage = {
}, },
methods: { methods: {
onLoad () { onLoad () {
this.imageLoadHandler && this.imageLoadHandler(this.$refs.src) const image = this.$refs.src
if (!image) return
this.imageLoadHandler && this.imageLoadHandler(image)
const canvas = this.$refs.canvas const canvas = this.$refs.canvas
if (!canvas) return if (!canvas) return
const width = this.$refs.src.naturalWidth const width = image.naturalWidth
const height = this.$refs.src.naturalHeight const height = image.naturalHeight
canvas.width = width canvas.width = width
canvas.height = height canvas.height = height
canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height) canvas.getContext('2d').drawImage(image, 0, 0, width, height)
}, },
onError () { onError () {
this.imageLoadError && this.imageLoadError() this.imageLoadError && this.imageLoadError()

View file

@ -32,7 +32,8 @@ const Timeline = {
return { return {
paused: false, paused: false,
unfocused: false, unfocused: false,
bottomedOut: false bottomedOut: false,
virtualScrollIndex: 0
} }
}, },
computed: { computed: {
@ -68,6 +69,16 @@ const Timeline = {
}, },
pinnedStatusIdsObject () { pinnedStatusIdsObject () {
return keyBy(this.pinnedStatusIds) return keyBy(this.pinnedStatusIds)
},
statusesToDisplay () {
const amount = this.timeline.visibleStatuses.length
const statusesPerSide = Math.ceil(Math.max(10, window.innerHeight / 100))
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
} }
}, },
components: { components: {
@ -79,7 +90,7 @@ const Timeline = {
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0 const showImmediately = this.timeline.visibleStatuses.length === 0
window.addEventListener('scroll', this.scrollLoad) window.addEventListener('scroll', this.handleScroll)
if (store.state.api.fetchers[this.timelineName]) { return false } if (store.state.api.fetchers[this.timelineName]) { return false }
@ -100,7 +111,7 @@ const Timeline = {
window.addEventListener('keydown', this.handleShortKey) window.addEventListener('keydown', this.handleShortKey)
}, },
destroyed () { destroyed () {
window.removeEventListener('scroll', this.scrollLoad) window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('keydown', this.handleShortKey) window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
@ -142,6 +153,48 @@ const Timeline = {
} }
}) })
}, 1000, this), }, 1000, this),
determineVisibleStatuses () {
if (!this.$refs.timeline) return
const statuses = this.$refs.timeline.children
if (statuses.length === 0) return
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
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
if (
this.virtualScrollIndex < statuses.length &&
Math.abs(err) > statuses[this.virtualScrollIndex].getBoundingClientRect().y
) {
approxIndex = this.virtualScrollIndex
err = statuses[approxIndex].getBoundingClientRect().y
}
// if the status is too far from viewport, check the next/previous ones if
// they happen to be better
while (err < -100 && approxIndex < statuses.length - 1) {
approxIndex++
err = statuses[approxIndex].getBoundingClientRect().y
}
while (err > window.innerHeight + 100 && approxIndex > 0) {
approxIndex--
err = statuses[approxIndex].getBoundingClientRect().y
}
// 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) { scrollLoad (e) {
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))
@ -152,6 +205,10 @@ const Timeline = {
this.fetchOlderStatuses() this.fetchOlderStatuses()
} }
}, },
handleScroll: throttle(function (e) {
this.determineVisibleStatuses()
this.scrollLoad(e)
}, 100),
handleVisibilityChange () { handleVisibilityChange () {
this.unfocused = document.hidden this.unfocused = document.hidden
} }

View file

@ -34,7 +34,10 @@
</div> </div>
</div> </div>
<div :class="classes.body"> <div :class="classes.body">
<div class="timeline"> <div
ref="timeline"
class="timeline"
>
<template v-for="statusId in pinnedStatusIds"> <template v-for="statusId in pinnedStatusIds">
<conversation <conversation
v-if="timeline.statusesObject[statusId]" v-if="timeline.statusesObject[statusId]"
@ -56,6 +59,7 @@
:collapsable="true" :collapsable="true"
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="userId" :profile-user-id="userId"
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)"
/> />
</template> </template>
</div> </div>

View file

@ -384,6 +384,7 @@
"false": "no", "false": "no",
"true": "yes" "true": "yes"
}, },
"virtual_scrolling": "Optimize timeline rendering",
"fun": "Fun", "fun": "Fun",
"greentext": "Meme arrows", "greentext": "Meme arrows",
"notifications": "Notifications", "notifications": "Notifications",

View file

@ -228,7 +228,8 @@
"values": { "values": {
"false": "pois päältä", "false": "pois päältä",
"true": "päällä" "true": "päällä"
} },
"virtual_scrolling": "Optimoi aikajanan suorituskykyä"
}, },
"time": { "time": {
"day": "{0} päivä", "day": "{0} päivä",

View file

@ -49,7 +49,8 @@ export const defaultState = {
useContainFit: false, useContainFit: false,
greentext: undefined, // instance default greentext: undefined, // instance default
hidePostStats: undefined, // instance default hidePostStats: undefined, // instance default
hideUserStats: undefined // instance default hideUserStats: undefined, // instance default
virtualScrolling: undefined // instance default
} }
// caching the instance default properties // caching the instance default properties

View file

@ -34,6 +34,7 @@ const defaultState = {
showFeaturesPanel: true, showFeaturesPanel: true,
minimalScopesMode: false, minimalScopesMode: false,
greentext: false, greentext: false,
virtualScrolling: true,
// Nasty stuff // Nasty stuff
pleromaBackend: true, pleromaBackend: true,