Compare commits

...

19 commits

Author SHA1 Message Date
Shpuld Shpuldson
900f05557e Merge branch 'fix/popover-performance' into feat/virtual-with-popover 2020-02-17 16:25:26 +02:00
Shpuld Shpuldson
abf8121638 remove extra reflow causing calls 2020-02-17 16:25:06 +02:00
Shpuld Shpuldson
db9471cd3e update with develop 2020-02-17 15:53:53 +02:00
Shpuld Shpuldson
0723c07571 update changelog 2020-02-17 12:03:29 +02:00
Shpuld Shpuldson
f007a795ac remove unused popover target from portal experiment 2020-02-17 11:48:14 +02:00
Shpuld Shpuldson
b19d51c3dc fix mistakes 2020-02-17 11:34:56 +02:00
Shpuld Shpuldson
3e971f0f25 remove popper references 2020-02-17 11:14:22 +02:00
Shpuld Shpuldson
10fc666a49 separate bounds x/y 2020-02-17 10:14:06 +02:00
Shpuld Shpuldson
7ac1a4a9fe remove v-tooltip completely 2020-02-16 09:58:05 +02:00
Shpuld Shpuldson
3c136c241f make more components use new popover, fix some things 2020-02-14 09:05:33 +02:00
Shpuld Shpuldson
94eeca3e7e Merge branch 'develop' into fix/popover-performance 2020-02-12 19:23:07 +02:00
Shpuld Shpuldson
5262676e0e rewrite popover because v-tooltip is slow as heck 2020-02-12 19:22:40 +02:00
Shpuld Shpuldson
f73e107a76 whoops 2020-01-15 19:21:11 +02:00
Shpuld Shpuldson
bbd964753e fix data property being called the wrong name in conversation 2020-01-15 18:47:14 +02:00
Shpuld Shpuldson
ac8df82bb7 fix warnings and console errors 2020-01-15 18:41:38 +02:00
Shpuld Shpuldson
2a9356209b fix minor bugs 2020-01-15 18:08:37 +02:00
Shpuld Shpuldson
c49b8e2089 make virtual scrolling optional in case people want to be able to ctrl-f all page 2020-01-15 17:01:18 +02:00
Shpuld Shpuldson
42f8fb2dca rename hidden stuff to virtualHidden, remove log 2020-01-15 16:37:08 +02:00
Shpuld Shpuldson
9eae4d07c1 add custom solution for virtual scrolling to ease ram and cpu use when scrolling for a long time 2020-01-15 15:17:05 +02:00
31 changed files with 439 additions and 311 deletions

View file

@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Registration fixed - Registration fixed
- Deactivation of remote accounts from frontend - Deactivation of remote accounts from frontend
- Fixed NSFW unhiding not working with videos when using one-click unhiding/displaying - Fixed NSFW unhiding not working with videos when using one-click unhiding/displaying
- Improved performance of anything that uses popovers (most notably statuses)
## [1.1.7 and earlier] - 2019-12-14 ## [1.1.7 and earlier] - 2019-12-14
### Added ### Added

View file

@ -29,7 +29,6 @@
"portal-vue": "^2.1.4", "portal-vue": "^2.1.4",
"sanitize-html": "^1.13.0", "sanitize-html": "^1.13.0",
"v-click-outside": "^2.1.1", "v-click-outside": "^2.1.1",
"v-tooltip": "^2.0.2",
"vue": "^2.5.13", "vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1", "vue-chat-scroll": "^1.2.1",
"vue-i18n": "^7.3.2", "vue-i18n": "^7.3.2",

View file

@ -1,4 +1,5 @@
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue'
const AccountActions = { const AccountActions = {
props: [ props: [
@ -8,7 +9,8 @@ const AccountActions = {
return { } return { }
}, },
components: { components: {
ProgressButton ProgressButton,
Popover
}, },
methods: { methods: {
showRepeats () { showRepeats () {

View file

@ -1,13 +1,13 @@
<template> <template>
<div class="account-actions"> <div class="account-actions">
<v-popover <Popover
trigger="click" trigger="click"
class="account-tools-popover" placement="bottom"
:container="false"
placement="bottom-end"
:offset="5"
> >
<div slot="popover"> <div
slot="content"
class="account-tools-popover"
>
<div class="dropdown-menu"> <div class="dropdown-menu">
<template v-if="user.following"> <template v-if="user.following">
<button <button
@ -51,10 +51,13 @@
</button> </button>
</div> </div>
</div> </div>
<div class="btn btn-default ellipsis-button"> <div
slot="trigger"
class="btn btn-default ellipsis-button"
>
<i class="icon-ellipsis trigger-button" /> <i class="icon-ellipsis trigger-button" />
</div> </div>
</v-popover> </Popover>
</div> </div>
</template> </template>
@ -62,11 +65,13 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@import '../popper/popper.scss';
.account-actions { .account-actions {
margin: 0 .8em; margin: 0 .8em;
} }
.account-tools-popover {
}
.account-actions button.dropdown-item { .account-actions button.dropdown-item {
margin-left: 0; margin-left: 0;
} }

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

@ -1,20 +1,17 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import Popover from '../popover/popover.vue'
const EMOJI_REACTION_COUNT_CUTOFF = 12 const EMOJI_REACTION_COUNT_CUTOFF = 12
const EmojiReactions = { const EmojiReactions = {
name: 'EmojiReactions', name: 'EmojiReactions',
components: { components: {
UserAvatar UserAvatar,
Popover
}, },
props: ['status'], props: ['status'],
data: () => ({ data: () => ({
showAll: false, showAll: false
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
}), }),
computed: { computed: {
tooManyReactions () { tooManyReactions () {

View file

@ -1,15 +1,14 @@
<template> <template>
<div class="emoji-reactions"> <div class="emoji-reactions">
<v-popover <Popover
v-for="(reaction) in emojiReactions" v-for="(reaction) in emojiReactions"
:key="reaction.name" :key="reaction.name"
:popper-options="popperOptions"
trigger="hover" trigger="hover"
placement="top" placement="top"
:offset="{ y: 5 }"
> >
<div <div
slot="popover" slot="content"
class="reacted-users" class="reacted-users"
> >
<div v-if="accountsForEmoji[reaction.name].length"> <div v-if="accountsForEmoji[reaction.name].length">
@ -34,6 +33,7 @@
</div> </div>
</div> </div>
<button <button
slot="trigger"
class="emoji-reaction btn btn-default" class="emoji-reaction btn btn-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@ -42,7 +42,7 @@
<span class="reaction-emoji">{{ reaction.name }}</span> <span class="reaction-emoji">{{ reaction.name }}</span>
<span>{{ reaction.count }}</span> <span>{{ reaction.count }}</span>
</button> </button>
</v-popover> </Popover>
<a <a
v-if="tooManyReactions" v-if="tooManyReactions"
@click="toggleShowAll" @click="toggleShowAll"
@ -78,6 +78,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 0.5em; margin-left: 0.5em;
min-width: 5em;
img { img {
width: 1em; width: 1em;

View file

@ -1,5 +1,8 @@
import Popover from '../popover/popover.vue'
const ExtraButtons = { const ExtraButtons = {
props: [ 'status' ], props: [ 'status' ],
components: { Popover },
methods: { methods: {
deleteStatus () { deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm')) const confirmed = window.confirm(this.$t('status.delete_confirm'))

View file

@ -1,11 +1,11 @@
<template> <template>
<v-popover <Popover
v-if="canDelete || canMute || canPin" v-if="canDelete || canMute || canPin"
trigger="click" trigger="click"
placement="top" placement="top"
class="extra-button-popover" class="extra-button-popover"
> >
<div slot="popover"> <div slot="content">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
v-if="canMute && !status.thread_muted" v-if="canMute && !status.thread_muted"
@ -47,17 +47,19 @@
</button> </button>
</div> </div>
</div> </div>
<div class="button-icon"> <div
slot="trigger"
class="button-icon"
>
<i class="icon-ellipsis" /> <i class="icon-ellipsis" />
</div> </div>
</v-popover> </Popover>
</template> </template>
<script src="./extra_buttons.js" ></script> <script src="./extra_buttons.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@import '../popper/popper.scss';
.icon-ellipsis { .icon-ellipsis {
cursor: pointer; cursor: pointer;

View file

@ -1,4 +1,5 @@
import DialogModal from '../dialog_modal/dialog_modal.vue' import DialogModal from '../dialog_modal/dialog_modal.vue'
import Popover from '../popover/popover.vue'
const FORCE_NSFW = 'mrf_tag:media-force-nsfw' const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip' const STRIP_MEDIA = 'mrf_tag:media-strip'
@ -14,7 +15,6 @@ const ModerationTools = {
], ],
data () { data () {
return { return {
showDropDown: false,
tags: { tags: {
FORCE_NSFW, FORCE_NSFW,
STRIP_MEDIA, STRIP_MEDIA,
@ -28,7 +28,8 @@ const ModerationTools = {
} }
}, },
components: { components: {
DialogModal DialogModal,
Popover
}, },
computed: { computed: {
tagsSet () { tagsSet () {

View file

@ -1,13 +1,12 @@
<template> <template>
<div> <div>
<v-popover <Popover
trigger="click" trigger="click"
class="moderation-tools-popover" class="moderation-tools-popover"
placement="bottom-end" placement="bottom"
@show="showDropDown = true" :offset="{ y: 5 }"
@hide="showDropDown = false"
> >
<div slot="popover"> <div slot="content">
<div class="dropdown-menu"> <div class="dropdown-menu">
<span v-if="user.is_local"> <span v-if="user.is_local">
<button <button
@ -122,12 +121,12 @@
</div> </div>
</div> </div>
<button <button
slot="trigger"
class="btn btn-default btn-block" class="btn btn-default btn-block"
:class="{ pressed: showDropDown }"
> >
{{ $t('user_card.admin_menu.moderation') }} {{ $t('user_card.admin_menu.moderation') }}
</button> </button>
</v-popover> </Popover>
<portal to="modal"> <portal to="modal">
<DialogModal <DialogModal
v-if="showDeleteUserDialog" v-if="showDeleteUserDialog"
@ -160,7 +159,6 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
@import '../popper/popper.scss';
.menu-checkbox { .menu-checkbox {
float: right; float: right;

View file

@ -0,0 +1,143 @@
const Popover = {
name: 'Popover',
props: [
'trigger',
'placement',
'boundTo',
'padding',
'offset',
'popoverClass'
],
data () {
return {
hidden: true,
styles: { opacity: 0 },
oldSize: { width: 0, height: 0 }
}
},
computed: {
display () {
return !this.hidden
}
},
methods: {
updateStyles () {
if (this.hidden) return { opacity: 0 }
// Popover will be anchored around this element
const anchorEl = this.$refs.trigger || this.$el
const screenBox = anchorEl.getBoundingClientRect()
// Screen position of the origin point for popover
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
const content = this.$refs.content
// Minor optimization, don't call a slow reflow call if we don't have to
const parentBounds = this.boundTo &&
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
this.$el.offsetParent.getBoundingClientRect()
const padding = this.padding || {}
// What are the screen bounds for the popover? Viewport vs container
// when using viewport, using default padding values to dodge the navbar
const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
min: parentBounds.left + (padding.left || 0),
max: parentBounds.right - (padding.right || 0)
} : {
min: 0 + (padding.left || 10),
max: window.innerWidth - (padding.right || 10)
}
const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
min: parentBounds.top + (padding.top || 0),
max: parentBounds.bottom - (padding.bottom || 0)
} : {
min: 0 + (padding.top || 50),
max: window.innerHeight - (padding.bottom || 5)
}
let horizOffset = 0
// If overflowing from left, move it so that it doesn't
if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
horizOffset = -(origin.x - content.offsetWidth * 0.5) + xBounds.min
}
// If overflowing from right, move it so that it doesn't
if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
}
// Default to whatever user wished with placement prop
let usingTop = this.placement !== 'bottom'
// Handle special cases, first force to displaying on top if there's not space on bottom,
// regardless of what placement value was. Then check if there's not space on top, and
// force to bottom, again regardless of what placement value was.
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
const yOffset = (this.offset && this.offset.y) || 0
const translateY = usingTop
? -anchorEl.offsetHeight - yOffset - content.offsetHeight
: yOffset + yOffset
const xOffset = (this.offset && this.offset.x) || 0
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
this.styles = {
opacity: 1,
transform: `translate(${Math.floor(translateX)}px, ${Math.floor(translateY)}px)`
}
},
showPopover () {
if (this.hidden) this.$emit('show')
this.hidden = false
this.$nextTick(this.updateStyles)
},
hidePopover () {
if (!this.hidden) this.$emit('close')
this.hidden = true
this.styles = { opacity: 0 }
},
onMouseenter (e) {
if (this.trigger === 'hover') this.showPopover()
},
onMouseleave (e) {
if (this.trigger === 'hover') this.hidePopover()
},
onClick (e) {
if (this.trigger === 'click') {
if (this.hidden) {
this.showPopover()
} else {
this.hidePopover()
}
}
},
onClickOutside (e) {
if (this.hidden) return
if (this.$el.contains(e.target)) return
this.hidePopover()
}
},
updated () {
// Monitor changes to content size, update styles only when content sizes have changed,
// that should be the only time we need to move the popover box if we don't care about scroll
// or resize
const content = this.$refs.content
if (!content) return
if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
this.updateStyles()
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
}
},
created () {
document.addEventListener('click', this.onClickOutside)
},
destroyed () {
document.removeEventListener('click', this.onClickOutside)
this.hidePopover()
}
}
export default Popover

View file

@ -0,0 +1,98 @@
<template>
<div
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
>
<div
ref="trigger"
@click="onClick"
>
<slot name="trigger" />
</div>
<div
v-if="display"
ref="content"
:style="styles"
class="popover"
:class="popoverClass"
>
<slot
name="content"
class="popover-inner"
:close="hidePopover"
/>
</div>
</div>
</template>
<script src="./popover.js" />
<style lang=scss>
@import '../../_variables.scss';
.popover {
z-index: 8;
position: absolute;
min-width: 0;
transition: opacity 0.3s;
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
box-shadow: var(--panelShadow);
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
.dropdown-menu {
display: block;
padding: .5rem 0;
font-size: 1rem;
text-align: left;
list-style: none;
max-width: 100vw;
z-index: 10;
white-space: nowrap;
.dropdown-divider {
height: 0;
margin: .5rem 0;
overflow: hidden;
border-top: 1px solid $fallback--border;
border-top: 1px solid var(--border, $fallback--border);
}
.dropdown-item {
line-height: 21px;
margin-right: 5px;
overflow: auto;
display: block;
padding: .25rem 1.0rem .25rem 1.5rem;
clear: both;
font-weight: 400;
text-align: inherit;
white-space: nowrap;
border: none;
border-radius: 0px;
background-color: transparent;
box-shadow: none;
width: 100%;
height: 100%;
&-icon {
padding-left: 0.5rem;
i {
margin-right: 0.25rem;
}
}
&:hover {
// TODO: improve the look on breeze themes
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
box-shadow: none;
}
}
}
</style>

View file

@ -1,147 +0,0 @@
@import '../../_variables.scss';
.tooltip.popover {
z-index: 8;
.popover-inner {
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
box-shadow: var(--panelShadow);
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
.popover-arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
border-color: $fallback--bg;
border-color: var(--bg, $fallback--bg);
}
&[x-placement^="top"] {
margin-bottom: 5px;
.popover-arrow {
border-width: 5px 5px 0 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
bottom: -4px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
}
&[x-placement^="bottom"] {
margin-top: 5px;
.popover-arrow {
border-width: 0 5px 5px 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
top: -4px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
}
&[x-placement^="right"] {
margin-left: 5px;
.popover-arrow {
border-width: 5px 5px 5px 0;
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
left: -4px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
}
&[x-placement^="left"] {
margin-right: 5px;
.popover-arrow {
border-width: 5px 0 5px 5px;
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
right: -4px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
}
&[aria-hidden='true'] {
visibility: hidden;
opacity: 0;
transition: opacity .15s, visibility .15s;
}
&[aria-hidden='false'] {
visibility: visible;
opacity: 1;
transition: opacity .15s;
}
}
.dropdown-menu {
display: block;
padding: .5rem 0;
font-size: 1rem;
text-align: left;
list-style: none;
max-width: 100vw;
z-index: 10;
.dropdown-divider {
height: 0;
margin: .5rem 0;
overflow: hidden;
border-top: 1px solid $fallback--border;
border-top: 1px solid var(--border, $fallback--border);
}
.dropdown-item {
line-height: 21px;
margin-right: 5px;
overflow: auto;
display: block;
padding: .25rem 1.0rem .25rem 1.5rem;
clear: both;
font-weight: 400;
text-align: inherit;
white-space: normal;
border: none;
border-radius: 0px;
background-color: transparent;
box-shadow: none;
width: 100%;
height: 100%;
&-icon {
padding-left: 0.5rem;
i {
margin-right: 0.25rem;
}
}
&:hover {
// TODO: improve the look on breeze themes
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
box-shadow: none;
}
}
}

View file

@ -1,34 +1,25 @@
import Popover from '../popover/popover.vue'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
const ReactButton = { const ReactButton = {
props: ['status', 'loggedIn'], props: ['status', 'loggedIn'],
data () { data () {
return { return {
showTooltip: false, filterWord: ''
filterWord: '',
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
} }
}, },
components: {
Popover
},
methods: { methods: {
openReactionSelect () { addReaction (event, emoji, close) {
this.showTooltip = true
this.filterWord = ''
},
closeReactionSelect () {
this.showTooltip = false
},
addReaction (event, emoji) {
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji) const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
if (existingReaction && existingReaction.me) { if (existingReaction && existingReaction.me) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
} else { } else {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
} }
this.closeReactionSelect() close()
} }
}, },
computed: { computed: {

View file

@ -1,13 +1,13 @@
<template> <template>
<v-popover <Popover
:popper-options="popperOptions" trigger="click"
:open="showTooltip"
trigger="manual"
placement="top" placement="top"
class="react-button-popover" class="react-button-popover"
@hide="closeReactionSelect"
> >
<div slot="popover"> <div
slot="content"
slot-scope="{close}"
>
<div class="reaction-picker-filter"> <div class="reaction-picker-filter">
<input <input
v-model="filterWord" v-model="filterWord"
@ -19,7 +19,7 @@
v-for="emoji in commonEmojis" v-for="emoji in commonEmojis"
:key="emoji" :key="emoji"
class="emoji-button" class="emoji-button"
@click="addReaction($event, emoji)" @click="addReaction($event, emoji, close)"
> >
{{ emoji }} {{ emoji }}
</span> </span>
@ -28,7 +28,7 @@
v-for="(emoji, key) in emojis" v-for="(emoji, key) in emojis"
:key="key" :key="key"
class="emoji-button" class="emoji-button"
@click="addReaction($event, emoji.replacement)" @click="addReaction($event, emoji.replacement, close)"
> >
{{ emoji.replacement }} {{ emoji.replacement }}
</span> </span>
@ -37,14 +37,14 @@
</div> </div>
<div <div
v-if="loggedIn" v-if="loggedIn"
@click.prevent="openReactionSelect" slot="trigger"
> >
<i <i
class="icon-smile button-icon add-reaction-button" class="icon-smile button-icon add-reaction-button"
:title="$t('tool_tip.add_reaction')" :title="$t('tool_tip.add_reaction')"
/> />
</div> </div>
</v-popover> </Popover>
</template> </template>
<script src="./react_button.js" ></script> <script src="./react_button.js" ></script>

View file

@ -76,9 +76,9 @@
<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>
</Checkbox> </Checkbox>
</li> </li>
@ -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>
<li> <li>
<Checkbox v-model="emojiReactionsOnTimeline"> <Checkbox v-model="emojiReactionsOnTimeline">
{{ $t('settings.emoji_reactions_on_timeline') }} {{ $t('settings.emoji_reactions_on_timeline') }}

View file

@ -36,7 +36,8 @@ const Status = {
'inlineExpanded', 'inlineExpanded',
'showPinned', 'showPinned',
'inProfile', 'inProfile',
'profileUserId' 'profileUserId',
'virtualHidden'
], ],
data () { data () {
return { return {
@ -123,7 +124,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

@ -177,6 +177,8 @@
<StatusPopover <StatusPopover
v-if="!isPreview" v-if="!isPreview"
:status-id="status.in_reply_to_status_id" :status-id="status.in_reply_to_status_id"
class="reply-to-popover"
style="min-width: 0"
> >
<a <a
class="reply-to" class="reply-to"
@ -564,11 +566,10 @@ $status-margin: 0.75em;
align-items: stretch; align-items: stretch;
> .reply-to-and-accountname > a { > .reply-to-and-accountname > a {
overflow: hidden;
max-width: 100%; max-width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap; white-space: nowrap;
display: inline-block;
word-break: break-all; word-break: break-all;
} }
} }
@ -577,7 +578,6 @@ $status-margin: 0.75em;
display: flex; display: flex;
height: 18px; height: 18px;
margin-right: 0.5em; margin-right: 0.5em;
overflow: hidden;
max-width: 100%; max-width: 100%;
.icon-reply { .icon-reply {
transform: scaleX(-1); transform: scaleX(-1);
@ -588,6 +588,10 @@ $status-margin: 0.75em;
display: flex; display: flex;
} }
.reply-to-popover {
min-width: 0;
}
.reply-to { .reply-to {
display: flex; display: flex;
} }
@ -595,6 +599,7 @@ $status-margin: 0.75em;
.reply-to-text { .reply-to-text {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
margin: 0 0.4em 0 0.2em; margin: 0 0.4em 0 0.2em;
color: $fallback--faint; color: $fallback--faint;
color: var(--faint, $fallback--faint); color: var(--faint, $fallback--faint);

View file

@ -5,22 +5,14 @@ const StatusPopover = {
props: [ props: [
'statusId' 'statusId'
], ],
data () {
return {
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
}
},
computed: { computed: {
status () { status () {
return find(this.$store.state.statuses.allStatuses, { id: this.statusId }) return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
} }
}, },
components: { components: {
Status: () => import('../status/status.vue') Status: () => import('../status/status.vue'),
Popover: () => import('../popover/popover.vue')
}, },
methods: { methods: {
enter () { enter () {

View file

@ -1,11 +1,17 @@
<template> <template>
<v-popover <Popover
trigger="hover"
popover-class="status-popover" popover-class="status-popover"
placement="top-start" :bound-to="{ x: 'container' }"
:popper-options="popperOptions" :offset="{ x: 0, y: 5 }"
@show="enter()" @show="enter"
> >
<template slot="popover"> <template slot="trigger">
<slot />
</template>
<div
slot="content"
>
<Status <Status
v-if="status" v-if="status"
:is-preview="true" :is-preview="true"
@ -18,10 +24,8 @@
> >
<i class="icon-spin4 animate-spin" /> <i class="icon-spin4 animate-spin" />
</div> </div>
</template> </div>
</Popover>
<slot />
</v-popover>
</template> </template>
<script src="./status_popover.js" ></script> <script src="./status_popover.js" ></script>
@ -29,44 +33,19 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.tooltip.popover.status-popover { .status-popover {
font-size: 1rem; font-size: 1rem;
min-width: 15em; min-width: 15em;
max-width: 95%; max-width: 95%;
margin-left: 0.5em;
.popover-inner { border-color: $fallback--border;
border-color: $fallback--border; border-color: var(--border, $fallback--border);
border-color: var(--border, $fallback--border); border-style: solid;
border-style: solid; border-width: 1px;
border-width: 1px; border-radius: $fallback--tooltipRadius;
border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
border-radius: var(--tooltipRadius, $fallback--tooltipRadius); box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); box-shadow: var(--popupShadow);
box-shadow: var(--popupShadow);
}
.popover-arrow::before {
position: absolute;
content: '';
left: -7px;
border: solid 7px transparent;
z-index: -1;
}
&[x-placement^="bottom-start"] .popover-arrow::before {
top: -2px;
border-top-width: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
}
&[x-placement^="top-start"] .popover-arrow::before {
bottom: -2px;
border-bottom-width: 0;
border-top-color: $fallback--border;
border-top-color: var(--border, $fallback--border);
}
.status-el.status-el { .status-el.status-el {
border: none; border: none;

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(15, 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
} }
}, },
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,49 @@ const Timeline = {
} }
}) })
}, 1000, this), }, 1000, this),
determineVisibleStatuses () {
if (!this.$refs.timeline) return
const statuses = this.$refs.timeline.children
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[this.virtualScrollIndex].getBoundingClientRect().y
if (
this.virtualScrollIndex < statuses.length &&
Math.abs(err) > virtualScrollIndexY
) {
approxIndex = this.virtualScrollIndex
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) {
approxIndex++
err += statuses[approxIndex].offsetHeight
}
while (err > window.innerHeight + 100 && approxIndex > 0) {
err -= statuses[approxIndex].offsetHeight
approxIndex--
}
// 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 +206,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

@ -397,6 +397,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

@ -231,7 +231,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

@ -31,7 +31,6 @@ import VueChatScroll from 'vue-chat-scroll'
import VueClickOutside from 'v-click-outside' import VueClickOutside from 'v-click-outside'
import PortalVue from 'portal-vue' import PortalVue from 'portal-vue'
import VBodyScrollLock from './directives/body_scroll_lock' import VBodyScrollLock from './directives/body_scroll_lock'
import VTooltip from 'v-tooltip'
import afterStoreSetup from './boot/after_store.js' import afterStoreSetup from './boot/after_store.js'
@ -44,13 +43,6 @@ Vue.use(VueChatScroll)
Vue.use(VueClickOutside) Vue.use(VueClickOutside)
Vue.use(PortalVue) Vue.use(PortalVue)
Vue.use(VBodyScrollLock) Vue.use(VBodyScrollLock)
Vue.use(VTooltip, {
popover: {
defaultTrigger: 'hover click',
defaultContainer: false,
defaultOffset: 5
}
})
const i18n = new VueI18n({ const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary // By default, use the browser locale, we will update it if neccessary

View file

@ -51,7 +51,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,

View file

@ -5941,11 +5941,6 @@ pngjs@^3.3.0:
version "3.3.3" version "3.3.3"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b"
popper.js@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
portal-vue@^2.1.4: portal-vue@^2.1.4:
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.4.tgz#1fc679d77e294dc8d026f1eb84aa467de11b392e" resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.4.tgz#1fc679d77e294dc8d026f1eb84aa467de11b392e"
@ -7823,15 +7818,6 @@ v-click-outside@^2.1.1:
version "2.1.3" version "2.1.3"
resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7" resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7"
v-tooltip@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.2.tgz#8610d9eece2cc44fd66c12ef2f12eec6435cab9b"
integrity sha512-xQ+qzOFfywkLdjHknRPgMMupQNS8yJtf9Utd5Dxiu/0n4HtrxqsgDtN2MLZ0LKbburtSAQgyypuE/snM8bBZhw==
dependencies:
lodash "^4.17.11"
popper.js "^1.15.0"
vue-resize "^0.4.5"
validate-npm-package-license@^3.0.1: validate-npm-package-license@^3.0.1:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@ -7906,11 +7892,6 @@ vue-loader@^14.0.0:
vue-style-loader "^4.0.1" vue-style-loader "^4.0.1"
vue-template-es2015-compiler "^1.6.0" vue-template-es2015-compiler "^1.6.0"
vue-resize@^0.4.5:
version "0.4.5"
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea"
integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==
vue-router@^3.0.1: vue-router@^3.0.1:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be"