diff --git a/CHANGELOG.md b/CHANGELOG.md
index c56ac821a..3fff34db9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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/).
## [Unreleased]
+### Added
+- New option to optimize timeline rendering to make the site more responsive (enabled by default)
+
## [Unreleased patch]
### Fixed
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 63e0cebad..7fabc963e 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -80,6 +80,8 @@
class="video"
:attachment="attachment"
:controls="allowPlay"
+ @play="$emit('play')"
+ @pause="$emit('pause')"
/>
{{ $t('timeline.conversation') }}
@@ -19,6 +20,7 @@
+
@@ -55,8 +60,8 @@
.conversation-status {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
- border-left: 4px solid $fallback--cRed;
- border-left: 4px solid var(--cRed, $fallback--cRed);
+ border-left-color: $fallback--cRed;
+ border-left-color: var(--cRed, $fallback--cRed);
}
.conversation-status:last-child {
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index abcf04555..11627e9cb 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -1,5 +1,4 @@
import Popover from '../popover/popover.vue'
-import { mapGetters } from 'vuex'
const ReactButton = {
props: ['status'],
@@ -35,7 +34,9 @@ const ReactButton = {
}
return this.$store.state.instance.emoji || []
},
- ...mapGetters(['mergedConfig'])
+ mergedConfig () {
+ return this.$store.getters.mergedConfig
+ }
}
}
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
index d9a0f92e6..5a41f22d8 100644
--- a/src/components/retweet_button/retweet_button.js
+++ b/src/components/retweet_button/retweet_button.js
@@ -1,4 +1,3 @@
-import { mapGetters } from 'vuex'
const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
@@ -28,7 +27,9 @@ const RetweetButton = {
'animate-spin': this.animated
}
},
- ...mapGetters(['mergedConfig'])
+ mergedConfig () {
+ return this.$store.getters.mergedConfig
+ }
}
}
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index 7f06d0bd3..13482de70 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -58,6 +58,11 @@
{{ $t('settings.emoji_reactions_on_timeline') }}
+
+
+ {{ $t('settings.virtual_scrolling') }}
+
+
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 0b3166c19..cd6e2f729 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -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 { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash'
-import { mapGetters, mapState } from 'vuex'
const Status = {
name: 'Status',
@@ -47,14 +46,15 @@ const Status = {
'inlineExpanded',
'showPinned',
'inProfile',
- 'profileUserId',
- 'virtualHidden'
+ 'profileUserId'
],
data () {
return {
replying: false,
unmuted: false,
userExpanded: false,
+ mediaPlaying: [],
+ suspendable: true,
error: null
}
},
@@ -158,7 +158,7 @@ const Status = {
return this.mergedConfig.hideFilteredStatuses
},
hideStatus () {
- return this.deleted || (this.muted && this.hideFilteredStatuses)
+ return this.deleted || (this.muted && this.hideFilteredStatuses) || this.virtualHidden
},
isFocused () {
// retweet or root of an expanded conversation
@@ -208,11 +208,18 @@ const Status = {
hidePostStats () {
return this.mergedConfig.hidePostStats
},
- ...mapGetters(['mergedConfig']),
- ...mapState({
- betterShadow: state => state.interface.browserSupport.cssFilter,
- currentUser: state => state.users.currentUser
- })
+ currentUser () {
+ return this.$store.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: {
visibilityIcon (visibility) {
@@ -252,6 +259,12 @@ const Status = {
},
generateUserProfileLink (id, name) {
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: {
@@ -281,6 +294,9 @@ const Status = {
if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
this.$store.dispatch('fetchFavs', this.status.id)
}
+ },
+ 'isSuspendable': function (val) {
+ this.suspendable = val
}
},
filters: {
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 8d292d3f7..c92d870bb 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -25,6 +25,11 @@ $status-margin: 0.75em;
--icon: var(--selectedPostIcon, $fallback--icon);
}
+ &.-conversation {
+ border-left-width: 4px;
+ border-left-style: solid;
+ }
+
.status-container {
display: flex;
padding: $status-margin;
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 282ad37dd..aa67e433f 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -16,7 +16,7 @@
/>
-
+
+
@@ -354,6 +357,7 @@
@onSuccess="clearError"
/>
+
+
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 76fe32789..f7fb5ee23 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -107,6 +107,8 @@
:attachment="attachment"
:allow-play="true"
:set-media="setMedia()"
+ @play="$emit('mediaplay', attachment.id)"
+ @pause="$emit('mediapause', attachment.id)"
/>
_.id)
@@ -115,6 +115,7 @@ const Timeline = {
this.unfocused = document.hidden
}
window.addEventListener('keydown', this.handleShortKey)
+ setTimeout(this.determineVisibleStatuses, 250)
},
destroyed () {
window.removeEventListener('scroll', this.handleScroll)
@@ -159,13 +160,14 @@ const Timeline = {
}, 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 bodyBRect = document.body.getBoundingClientRect()
- const height = Math.max(bodyBRect.height, -(bodyBRect.y))
+ const height = Math.max(document.body.offsetHeight, window.pageYOffset)
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
// 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
+
+ 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 < -100 && approxIndex < statuses.length - 1) {
+ while (err < -20 && approxIndex < statuses.length - 1) {
+ err += statuses[approxIndex].offsetHeight
approxIndex++
- err = statuses[approxIndex].getBoundingClientRect().y
}
while (err > window.innerHeight + 100 && approxIndex > 0) {
approxIndex--
- err = statuses[approxIndex].getBoundingClientRect().y
+ err -= statuses[approxIndex].offsetHeight
}
// this status is now the center point for virtual scrolling and visible
@@ -211,7 +212,7 @@ const Timeline = {
handleScroll: throttle(function (e) {
this.determineVisibleStatuses()
this.scrollLoad(e)
- }, 100),
+ }, 200),
handleVisibilityChange () {
this.unfocused = document.hidden
}
diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js
index f0ca7e899..107b89855 100644
--- a/src/components/video_attachment/video_attachment.js
+++ b/src/components/video_attachment/video_attachment.js
@@ -3,27 +3,48 @@ const VideoAttachment = {
props: ['attachment', 'controls'],
data () {
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: {
- 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
+ // 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') {
// non-zero if video has audio track
- if (target.webkitAudioDecodedByteCount > 0) {
- 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 (target.webkitAudioDecodedByteCount > 0) return
}
+ 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
}
}
}
diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue
index 1ffed4e04..a4bf01e8e 100644
--- a/src/components/video_attachment/video_attachment.vue
+++ b/src/components/video_attachment/video_attachment.vue
@@ -7,7 +7,8 @@
:alt="attachment.description"
:title="attachment.description"
playsinline
- @loadeddata="onVideoDataLoad"
+ @playing="onPlaying"
+ @pause="onPaused"
/>
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 3fe3bbf3c..b3cbffc6c 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -41,6 +41,7 @@ const defaultState = {
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
+ virtualScrolling: true,
// Nasty stuff
customEmoji: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index e108b2a79..155cc4b91 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -568,6 +568,9 @@ export const mutations = {
updateStatusWithPoll (state, { id, poll }) {
const status = state.allStatusesObject[id]
status.poll = poll
+ },
+ setVirtualHeight (state, { statusId, height }) {
+ state.allStatusesObject[statusId].virtualHeight = height
}
}
@@ -753,6 +756,9 @@ const statuses = {
store.commit('addNewStatuses', { statuses: data.statuses })
return data
})
+ },
+ setVirtualHeight ({ commit }, { statusId, height }) {
+ commit('setVirtualHeight', { statusId, height })
}
},
mutations
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index da5190012..d1842e17c 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -539,8 +539,10 @@ const fetchTimeline = ({
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
+
let status = ''
let statusText = ''
+
let pagination = {}
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {