Compare commits
19 commits
develop
...
feat/virtu
Author | SHA1 | Date | |
---|---|---|---|
|
900f05557e | ||
|
abf8121638 | ||
|
db9471cd3e | ||
|
0723c07571 | ||
|
f007a795ac | ||
|
b19d51c3dc | ||
|
3e971f0f25 | ||
|
10fc666a49 | ||
|
7ac1a4a9fe | ||
|
3c136c241f | ||
|
94eeca3e7e | ||
|
5262676e0e | ||
|
f73e107a76 | ||
|
bbd964753e | ||
|
ac8df82bb7 | ||
|
2a9356209b | ||
|
c49b8e2089 | ||
|
42f8fb2dca | ||
|
9eae4d07c1 |
31 changed files with 439 additions and 311 deletions
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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"
|
<div
|
||||||
:offset="5"
|
slot="content"
|
||||||
|
class="account-tools-popover"
|
||||||
>
|
>
|
||||||
<div slot="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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
143
src/components/popover/popover.js
Normal file
143
src/components/popover/popover.js
Normal 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
|
98
src/components/popover/popover.vue
Normal file
98
src/components/popover/popover.vue
Normal 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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
<li>
|
<li>
|
||||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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="trigger">
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
slot="content"
|
||||||
>
|
>
|
||||||
<template slot="popover">
|
|
||||||
<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,13 +33,11 @@
|
||||||
<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;
|
||||||
|
@ -44,29 +46,6 @@
|
||||||
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;
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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ä",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue