Merge branch 'feat/custom-virtual-scrolling' into shigusegubu

* feat/custom-virtual-scrolling:
  invert hasAudio logic
  re-add the option, comment confusing part of setAudio
  fix red line in conversations
  Apply 1 suggestion(s) to 1 file(s)
  dont fail when opening a conversation link
  remove comments, update changelog
  experiment with storing heights in vuex
  fix build errors
  fix expanded threads disappearing
  cap virtual scroll index before use
  fix lint
  make hiding more efficient, make hiding not do its thing for reply forms or playing videos
  make playing videos stop the suspending
  remove mapgetters from status related components
  Perf test tools
  add missing css line
  remove extra reflow causing calls
This commit is contained in:
Henry Jameson 2020-09-22 21:24:54 +03:00
commit ce7f3e7a27
17 changed files with 139 additions and 51 deletions

View file

@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added
- New option to optimize timeline rendering to make the site more responsive (enabled by default)
## [Unreleased patch] ## [Unreleased patch]
### Fixed ### Fixed

View file

@ -80,6 +80,8 @@
class="video" class="video"
:attachment="attachment" :attachment="attachment"
:controls="allowPlay" :controls="allowPlay"
@play="$emit('play')"
@pause="$emit('pause')"
/> />
<i <i
v-if="!allowPlay" v-if="!allowPlay"
@ -93,6 +95,8 @@
:alt="attachment.description" :alt="attachment.description"
:title="attachment.description" :title="attachment.description"
controls controls
@play="$emit('play')"
@pause="$emit('pause')"
/> />
<div <div

View file

@ -35,9 +35,7 @@ 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: [
@ -55,6 +53,13 @@ const conversation = {
} }
}, },
computed: { computed: {
hideStatus () {
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.virtualHidden && this.$refs.statusComponent[0].suspendable
} else {
return this.virtualHidden
}
},
status () { status () {
return this.$store.state.statuses.allStatusesObject[this.statusId] return this.$store.state.statuses.allStatusesObject[this.statusId]
}, },
@ -107,7 +112,8 @@ const conversation = {
return this.expanded || this.isPage return this.expanded || this.isPage
}, },
hiddenStyle () { hiddenStyle () {
return this.virtualHidden ? { height: this.virtualHeight } : {} const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {}
} }
}, },
components: { components: {
@ -129,7 +135,10 @@ const conversation = {
} }
}, },
virtualHidden (value) { virtualHidden (value) {
this.virtualHeight = `${this.$el.clientHeight}px` this.$store.dispatch(
'setVirtualHeight',
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
)
} }
}, },
methods: { methods: {

View file

@ -1,11 +1,12 @@
<template> <template>
<div <div
v-if="!hideStatus"
:style="hiddenStyle" :style="hiddenStyle"
class="Conversation" class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" :class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
> >
<div <div
v-if="isExpanded && !virtualHidden" v-if="isExpanded"
class="panel-heading conversation-heading" class="panel-heading conversation-heading"
> >
<span class="title"> {{ $t('timeline.conversation') }} </span> <span class="title"> {{ $t('timeline.conversation') }} </span>
@ -19,6 +20,7 @@
<status <status
v-for="status in conversation" v-for="status in conversation"
:key="status.id" :key="status.id"
ref="statusComponent"
:inline-expanded="collapsable && isExpanded" :inline-expanded="collapsable && isExpanded"
:statusoid="status" :statusoid="status"
:expandable="!isExpanded" :expandable="!isExpanded"
@ -29,12 +31,15 @@
: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="conversation-status status-fadein panel-body" class="conversation-status status-fadein panel-body"
@goto="setHighlight" @goto="setHighlight"
@toggleExpanded="toggleExpanded" @toggleExpanded="toggleExpanded"
/> />
</div> </div>
<div
v-else
:style="hiddenStyle"
/>
</template> </template>
<script src="./conversation.js"></script> <script src="./conversation.js"></script>
@ -55,8 +60,8 @@
.conversation-status { .conversation-status {
border-color: $fallback--border; border-color: $fallback--border;
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
border-left: 4px solid $fallback--cRed; border-left-color: $fallback--cRed;
border-left: 4px solid var(--cRed, $fallback--cRed); border-left-color: var(--cRed, $fallback--cRed);
} }
.conversation-status:last-child { .conversation-status:last-child {

View file

@ -1,5 +1,4 @@
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import { mapGetters } from 'vuex'
const ReactButton = { const ReactButton = {
props: ['status'], props: ['status'],
@ -35,7 +34,9 @@ const ReactButton = {
} }
return this.$store.state.instance.emoji || [] return this.$store.state.instance.emoji || []
}, },
...mapGetters(['mergedConfig']) mergedConfig () {
return this.$store.getters.mergedConfig
}
} }
} }

View file

@ -1,4 +1,3 @@
import { mapGetters } from 'vuex'
const RetweetButton = { const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'], props: ['status', 'loggedIn', 'visibility'],
@ -28,7 +27,9 @@ const RetweetButton = {
'animate-spin': this.animated 'animate-spin': this.animated
} }
}, },
...mapGetters(['mergedConfig']) mergedConfig () {
return this.$store.getters.mergedConfig
}
} }
} }

View file

@ -58,6 +58,11 @@
{{ $t('settings.emoji_reactions_on_timeline') }} {{ $t('settings.emoji_reactions_on_timeline') }}
</Checkbox> </Checkbox>
</li> </li>
<li>
<Checkbox v-model="virtualScrolling">
{{ $t('settings.virtual_scrolling') }}
</Checkbox>
</li>
</ul> </ul>
</div> </div>

View file

@ -15,7 +15,6 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js' import { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash' import { unescape, uniqBy } from 'lodash'
import { mapGetters, mapState } from 'vuex'
const Status = { const Status = {
name: 'Status', name: 'Status',
@ -47,14 +46,15 @@ const Status = {
'inlineExpanded', 'inlineExpanded',
'showPinned', 'showPinned',
'inProfile', 'inProfile',
'profileUserId', 'profileUserId'
'virtualHidden'
], ],
data () { data () {
return { return {
replying: false, replying: false,
unmuted: false, unmuted: false,
userExpanded: false, userExpanded: false,
mediaPlaying: [],
suspendable: true,
error: null error: null
} }
}, },
@ -158,7 +158,7 @@ const Status = {
return this.mergedConfig.hideFilteredStatuses return this.mergedConfig.hideFilteredStatuses
}, },
hideStatus () { hideStatus () {
return this.deleted || (this.muted && this.hideFilteredStatuses) return this.deleted || (this.muted && this.hideFilteredStatuses) || this.virtualHidden
}, },
isFocused () { isFocused () {
// retweet or root of an expanded conversation // retweet or root of an expanded conversation
@ -208,11 +208,18 @@ const Status = {
hidePostStats () { hidePostStats () {
return this.mergedConfig.hidePostStats return this.mergedConfig.hidePostStats
}, },
...mapGetters(['mergedConfig']), currentUser () {
...mapState({ return this.$store.state.users.currentUser
betterShadow: state => state.interface.browserSupport.cssFilter, },
currentUser: state => state.users.currentUser betterShadow () {
}) return this.$store.state.interface.browserSupport.cssFilter
},
mergedConfig () {
return this.$store.getters.mergedConfig
},
isSuspendable () {
return !this.replying && this.mediaPlaying.length === 0
}
}, },
methods: { methods: {
visibilityIcon (visibility) { visibilityIcon (visibility) {
@ -252,6 +259,12 @@ const Status = {
}, },
generateUserProfileLink (id, name) { generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
addMediaPlaying (id) {
this.mediaPlaying.push(id)
},
removeMediaPlaying (id) {
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
} }
}, },
watch: { watch: {
@ -281,6 +294,9 @@ const Status = {
if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) { if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
this.$store.dispatch('fetchFavs', this.status.id) this.$store.dispatch('fetchFavs', this.status.id)
} }
},
'isSuspendable': function (val) {
this.suspendable = val
} }
}, },
filters: { filters: {

View file

@ -25,6 +25,11 @@ $status-margin: 0.75em;
--icon: var(--selectedPostIcon, $fallback--icon); --icon: var(--selectedPostIcon, $fallback--icon);
} }
&.-conversation {
border-left-width: 4px;
border-left-style: solid;
}
.status-container { .status-container {
display: flex; display: flex;
padding: $status-margin; padding: $status-margin;

View file

@ -16,7 +16,7 @@
/> />
</div> </div>
<template v-if="muted && !isPreview"> <template v-if="muted && !isPreview">
<div class="status-csontainer muted"> <div class="status-container muted">
<small class="status-username"> <small class="status-username">
<i <i
v-if="muted && retweet" v-if="muted && retweet"
@ -227,6 +227,7 @@
</span> </span>
</a> </a>
</StatusPopover> </StatusPopover>
<span <span
v-else v-else
class="reply-to-no-popover" class="reply-to-no-popover"
@ -272,6 +273,8 @@
:no-heading="noHeading" :no-heading="noHeading"
:highlight="highlight" :highlight="highlight"
:focused="isFocused" :focused="isFocused"
@mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)"
/> />
<transition name="fade"> <transition name="fade">
@ -354,6 +357,7 @@
@onSuccess="clearError" @onSuccess="clearError"
/> />
</div> </div>
</div> </div>
</div> </div>
<div <div
@ -376,4 +380,5 @@
</template> </template>
<script src="./status.js" ></script> <script src="./status.js" ></script>
<style src="./status.scss" lang="scss"></style> <style src="./status.scss" lang="scss"></style>

View file

@ -107,6 +107,8 @@
:attachment="attachment" :attachment="attachment"
:allow-play="true" :allow-play="true"
:set-media="setMedia()" :set-media="setMedia()"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/> />
<gallery <gallery
v-if="galleryAttachments.length > 0" v-if="galleryAttachments.length > 0"

View file

@ -82,7 +82,7 @@ const Timeline = {
}, },
statusesToDisplay () { statusesToDisplay () {
const amount = this.timeline.visibleStatuses.length const amount = this.timeline.visibleStatuses.length
const statusesPerSide = Math.ceil(Math.max(10, window.innerHeight / 100)) const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide) const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide) const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide)
return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id) return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id)
@ -115,6 +115,7 @@ const Timeline = {
this.unfocused = document.hidden this.unfocused = document.hidden
} }
window.addEventListener('keydown', this.handleShortKey) window.addEventListener('keydown', this.handleShortKey)
setTimeout(this.determineVisibleStatuses, 250)
}, },
destroyed () { destroyed () {
window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('scroll', this.handleScroll)
@ -159,13 +160,14 @@ const Timeline = {
}, 1000, this), }, 1000, this),
determineVisibleStatuses () { determineVisibleStatuses () {
if (!this.$refs.timeline) return if (!this.$refs.timeline) return
if (!this.virtualScrollingEnabled) return
const statuses = this.$refs.timeline.children const statuses = this.$refs.timeline.children
const cappedScrollIndex = Math.max(0, Math.min(this.virtualScrollIndex, statuses.length - 1))
if (statuses.length === 0) return if (statuses.length === 0) return
const bodyBRect = document.body.getBoundingClientRect() const height = Math.max(document.body.offsetHeight, window.pageYOffset)
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5) const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5)
@ -176,23 +178,22 @@ const Timeline = {
// if we have a previous scroll index that can be used, test if it's // if we have a previous scroll index that can be used, test if it's
// closer than the previous approximation, use it if so // closer than the previous approximation, use it if so
if (
this.virtualScrollIndex < statuses.length && const virtualScrollIndexY = statuses[cappedScrollIndex].getBoundingClientRect().y
Math.abs(err) > statuses[this.virtualScrollIndex].getBoundingClientRect().y if (Math.abs(err) > virtualScrollIndexY) {
) { approxIndex = cappedScrollIndex
approxIndex = this.virtualScrollIndex err = virtualScrollIndexY
err = statuses[approxIndex].getBoundingClientRect().y
} }
// if the status is too far from viewport, check the next/previous ones if // if the status is too far from viewport, check the next/previous ones if
// they happen to be better // they happen to be better
while (err < -100 && approxIndex < statuses.length - 1) { while (err < -20 && approxIndex < statuses.length - 1) {
err += statuses[approxIndex].offsetHeight
approxIndex++ approxIndex++
err = statuses[approxIndex].getBoundingClientRect().y
} }
while (err > window.innerHeight + 100 && approxIndex > 0) { while (err > window.innerHeight + 100 && approxIndex > 0) {
approxIndex-- approxIndex--
err = statuses[approxIndex].getBoundingClientRect().y err -= statuses[approxIndex].offsetHeight
} }
// this status is now the center point for virtual scrolling and visible // this status is now the center point for virtual scrolling and visible
@ -211,7 +212,7 @@ const Timeline = {
handleScroll: throttle(function (e) { handleScroll: throttle(function (e) {
this.determineVisibleStatuses() this.determineVisibleStatuses()
this.scrollLoad(e) this.scrollLoad(e)
}, 100), }, 200),
handleVisibilityChange () { handleVisibilityChange () {
this.unfocused = document.hidden this.unfocused = document.hidden
} }

View file

@ -3,27 +3,48 @@ const VideoAttachment = {
props: ['attachment', 'controls'], props: ['attachment', 'controls'],
data () { data () {
return { return {
loopVideo: this.$store.getters.mergedConfig.loopVideo blocksSuspend: false,
// Start from true because removing "loop" property seems buggy in Vue
hasAudio: true
}
},
computed: {
loopVideo () {
if (this.$store.getters.mergedConfig.loopVideoSilentOnly) {
return !this.hasAudio
}
return this.$store.getters.mergedConfig.loopVideo
} }
}, },
methods: { methods: {
onVideoDataLoad (e) { onPlaying (e) {
this.setHasAudio(e)
if (this.loopVideo) {
this.$emit('play', { looping: true })
return
}
this.$emit('play')
},
onPaused (e) {
this.$emit('pause')
},
setHasAudio (e) {
const target = e.srcElement || e.target const target = e.srcElement || e.target
// If hasAudio is false, we've already marked this video to not have audio,
// a video can't gain audio out of nowhere so don't bother checking again.
if (!this.hasAudio) return
if (typeof target.webkitAudioDecodedByteCount !== 'undefined') { if (typeof target.webkitAudioDecodedByteCount !== 'undefined') {
// non-zero if video has audio track // non-zero if video has audio track
if (target.webkitAudioDecodedByteCount > 0) { if (target.webkitAudioDecodedByteCount > 0) return
this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
} else if (typeof target.mozHasAudio !== 'undefined') {
// true if video has audio track
if (target.mozHasAudio) {
this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
} else if (typeof target.audioTracks !== 'undefined') {
if (target.audioTracks.length > 0) {
this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
} }
if (typeof target.mozHasAudio !== 'undefined') {
// true if video has audio track
if (target.mozHasAudio) return
}
if (typeof target.audioTracks !== 'undefined') {
if (target.audioTracks.length > 0) return
}
this.hasAudio = false
} }
} }
} }

View file

@ -7,7 +7,8 @@
:alt="attachment.description" :alt="attachment.description"
:title="attachment.description" :title="attachment.description"
playsinline playsinline
@loadeddata="onVideoDataLoad" @playing="onPlaying"
@pause="onPaused"
/> />
</template> </template>

View file

@ -41,6 +41,7 @@ const defaultState = {
sidebarRight: false, sidebarRight: false,
subjectLineBehavior: 'email', subjectLineBehavior: 'email',
theme: 'pleroma-dark', theme: 'pleroma-dark',
virtualScrolling: true,
// Nasty stuff // Nasty stuff
customEmoji: [], customEmoji: [],

View file

@ -568,6 +568,9 @@ export const mutations = {
updateStatusWithPoll (state, { id, poll }) { updateStatusWithPoll (state, { id, poll }) {
const status = state.allStatusesObject[id] const status = state.allStatusesObject[id]
status.poll = poll status.poll = poll
},
setVirtualHeight (state, { statusId, height }) {
state.allStatusesObject[statusId].virtualHeight = height
} }
} }
@ -753,6 +756,9 @@ const statuses = {
store.commit('addNewStatuses', { statuses: data.statuses }) store.commit('addNewStatuses', { statuses: data.statuses })
return data return data
}) })
},
setVirtualHeight ({ commit }, { statusId, height }) {
commit('setVirtualHeight', { statusId, height })
} }
}, },
mutations mutations

View file

@ -539,8 +539,10 @@ const fetchTimeline = ({
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}` url += `?${queryString}`
let status = '' let status = ''
let statusText = '' let statusText = ''
let pagination = {} let pagination = {}
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
.then((data) => { .then((data) => {