Compare commits
19 Commits
develop
...
feat/virtu
Author | SHA1 | Date |
---|---|---|
Shpuld Shpuldson | 900f05557e | |
Shpuld Shpuldson | abf8121638 | |
Shpuld Shpuldson | db9471cd3e | |
Shpuld Shpuldson | 0723c07571 | |
Shpuld Shpuldson | f007a795ac | |
Shpuld Shpuldson | b19d51c3dc | |
Shpuld Shpuldson | 3e971f0f25 | |
Shpuld Shpuldson | 10fc666a49 | |
Shpuld Shpuldson | 7ac1a4a9fe | |
Shpuld Shpuldson | 3c136c241f | |
Shpuld Shpuldson | 94eeca3e7e | |
Shpuld Shpuldson | 5262676e0e | |
Shpuld Shpuldson | f73e107a76 | |
Shpuld Shpuldson | bbd964753e | |
Shpuld Shpuldson | ac8df82bb7 | |
Shpuld Shpuldson | 2a9356209b | |
Shpuld Shpuldson | c49b8e2089 | |
Shpuld Shpuldson | 42f8fb2dca | |
Shpuld Shpuldson | 9eae4d07c1 |
|
@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Registration fixed
|
||||
- Deactivation of remote accounts from frontend
|
||||
- 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
|
||||
### Added
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
"portal-vue": "^2.1.4",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.5.13",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const AccountActions = {
|
||||
props: [
|
||||
|
@ -8,7 +9,8 @@ const AccountActions = {
|
|||
return { }
|
||||
},
|
||||
components: {
|
||||
ProgressButton
|
||||
ProgressButton,
|
||||
Popover
|
||||
},
|
||||
methods: {
|
||||
showRepeats () {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div class="account-actions">
|
||||
<v-popover
|
||||
<Popover
|
||||
trigger="click"
|
||||
class="account-tools-popover"
|
||||
:container="false"
|
||||
placement="bottom-end"
|
||||
:offset="5"
|
||||
placement="bottom"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="account-tools-popover"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="user.following">
|
||||
<button
|
||||
|
@ -51,10 +51,13 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-default ellipsis-button">
|
||||
<div
|
||||
slot="trigger"
|
||||
class="btn btn-default ellipsis-button"
|
||||
>
|
||||
<i class="icon-ellipsis trigger-button" />
|
||||
</div>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -62,11 +65,13 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
.account-actions {
|
||||
margin: 0 .8em;
|
||||
}
|
||||
|
||||
.account-tools-popover {
|
||||
}
|
||||
|
||||
.account-actions button.dropdown-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,9 @@ const conversation = {
|
|||
data () {
|
||||
return {
|
||||
highlight: null,
|
||||
expanded: false
|
||||
expanded: false,
|
||||
// Approximate minimum height of a status, gets overwritten with real one
|
||||
virtualHeight: '120px'
|
||||
}
|
||||
},
|
||||
props: [
|
||||
|
@ -44,7 +46,8 @@ const conversation = {
|
|||
'isPage',
|
||||
'pinnedStatusIdsObject',
|
||||
'inProfile',
|
||||
'profileUserId'
|
||||
'profileUserId',
|
||||
'virtualHidden'
|
||||
],
|
||||
created () {
|
||||
if (this.isPage) {
|
||||
|
@ -102,6 +105,9 @@ const conversation = {
|
|||
},
|
||||
isExpanded () {
|
||||
return this.expanded || this.isPage
|
||||
},
|
||||
hiddenStyle () {
|
||||
return this.virtualHidden ? { height: this.virtualHeight } : {}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -121,6 +127,9 @@ const conversation = {
|
|||
if (value) {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
virtualHidden (value) {
|
||||
this.virtualHeight = `${this.$el.clientHeight}px`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<div
|
||||
:style="hiddenStyle"
|
||||
class="timeline panel-default"
|
||||
:class="[isExpanded ? 'panel' : 'panel-disabled']"
|
||||
>
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
v-if="isExpanded && !virtualHidden"
|
||||
class="panel-heading conversation-heading"
|
||||
>
|
||||
<span class="title"> {{ $t('timeline.conversation') }} </span>
|
||||
|
@ -28,6 +29,7 @@
|
|||
:replies="getReplies(status.id)"
|
||||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
:virtual-hidden="virtualHidden"
|
||||
class="status-fadein panel-body"
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||
|
||||
const EmojiReactions = {
|
||||
name: 'EmojiReactions',
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
Popover
|
||||
},
|
||||
props: ['status'],
|
||||
data: () => ({
|
||||
showAll: false,
|
||||
popperOptions: {
|
||||
modifiers: {
|
||||
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
|
||||
}
|
||||
}
|
||||
showAll: false
|
||||
}),
|
||||
computed: {
|
||||
tooManyReactions () {
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
<template>
|
||||
<div class="emoji-reactions">
|
||||
<v-popover
|
||||
<Popover
|
||||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.name"
|
||||
:popper-options="popperOptions"
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
>
|
||||
|
||||
<div
|
||||
slot="popover"
|
||||
slot="content"
|
||||
class="reacted-users"
|
||||
>
|
||||
<div v-if="accountsForEmoji[reaction.name].length">
|
||||
|
@ -34,6 +33,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="emoji-reaction btn btn-default"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||
@click="emojiOnClick(reaction.name, $event)"
|
||||
|
@ -42,7 +42,7 @@
|
|||
<span class="reaction-emoji">{{ reaction.name }}</span>
|
||||
<span>{{ reaction.count }}</span>
|
||||
</button>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
<a
|
||||
v-if="tooManyReactions"
|
||||
@click="toggleShowAll"
|
||||
|
@ -78,6 +78,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0.5em;
|
||||
min-width: 5em;
|
||||
|
||||
img {
|
||||
width: 1em;
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const ExtraButtons = {
|
||||
props: [ 'status' ],
|
||||
components: { Popover },
|
||||
methods: {
|
||||
deleteStatus () {
|
||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<v-popover
|
||||
<Popover
|
||||
v-if="canDelete || canMute || canPin"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
class="extra-button-popover"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div slot="content">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="canMute && !status.thread_muted"
|
||||
|
@ -47,17 +47,19 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-icon">
|
||||
<div
|
||||
slot="trigger"
|
||||
class="button-icon"
|
||||
>
|
||||
<i class="icon-ellipsis" />
|
||||
</div>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./extra_buttons.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
|
||||
.icon-ellipsis {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
||||
|
@ -14,7 +15,6 @@ const ModerationTools = {
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
showDropDown: false,
|
||||
tags: {
|
||||
FORCE_NSFW,
|
||||
STRIP_MEDIA,
|
||||
|
@ -28,7 +28,8 @@ const ModerationTools = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal
|
||||
DialogModal,
|
||||
Popover
|
||||
},
|
||||
computed: {
|
||||
tagsSet () {
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-popover
|
||||
<Popover
|
||||
trigger="click"
|
||||
class="moderation-tools-popover"
|
||||
placement="bottom-end"
|
||||
@show="showDropDown = true"
|
||||
@hide="showDropDown = false"
|
||||
placement="bottom"
|
||||
:offset="{ y: 5 }"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div slot="content">
|
||||
<div class="dropdown-menu">
|
||||
<span v-if="user.is_local">
|
||||
<button
|
||||
|
@ -122,12 +121,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="btn btn-default btn-block"
|
||||
:class="{ pressed: showDropDown }"
|
||||
>
|
||||
{{ $t('user_card.admin_menu.moderation') }}
|
||||
</button>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
<portal to="modal">
|
||||
<DialogModal
|
||||
v-if="showDeleteUserDialog"
|
||||
|
@ -160,7 +159,6 @@
|
|||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
@import '../popper/popper.scss';
|
||||
|
||||
.menu-checkbox {
|
||||
float: right;
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
||||
const ReactButton = {
|
||||
props: ['status', 'loggedIn'],
|
||||
data () {
|
||||
return {
|
||||
showTooltip: false,
|
||||
filterWord: '',
|
||||
popperOptions: {
|
||||
modifiers: {
|
||||
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
|
||||
}
|
||||
}
|
||||
filterWord: ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Popover
|
||||
},
|
||||
methods: {
|
||||
openReactionSelect () {
|
||||
this.showTooltip = true
|
||||
this.filterWord = ''
|
||||
},
|
||||
closeReactionSelect () {
|
||||
this.showTooltip = false
|
||||
},
|
||||
addReaction (event, emoji) {
|
||||
addReaction (event, emoji, close) {
|
||||
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
|
||||
if (existingReaction && existingReaction.me) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
} else {
|
||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
}
|
||||
this.closeReactionSelect()
|
||||
close()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<v-popover
|
||||
:popper-options="popperOptions"
|
||||
:open="showTooltip"
|
||||
trigger="manual"
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
class="react-button-popover"
|
||||
@hide="closeReactionSelect"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div
|
||||
slot="content"
|
||||
slot-scope="{close}"
|
||||
>
|
||||
<div class="reaction-picker-filter">
|
||||
<input
|
||||
v-model="filterWord"
|
||||
|
@ -19,7 +19,7 @@
|
|||
v-for="emoji in commonEmojis"
|
||||
:key="emoji"
|
||||
class="emoji-button"
|
||||
@click="addReaction($event, emoji)"
|
||||
@click="addReaction($event, emoji, close)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</span>
|
||||
|
@ -28,7 +28,7 @@
|
|||
v-for="(emoji, key) in emojis"
|
||||
:key="key"
|
||||
class="emoji-button"
|
||||
@click="addReaction($event, emoji.replacement)"
|
||||
@click="addReaction($event, emoji.replacement, close)"
|
||||
>
|
||||
{{ emoji.replacement }}
|
||||
</span>
|
||||
|
@ -37,14 +37,14 @@
|
|||
</div>
|
||||
<div
|
||||
v-if="loggedIn"
|
||||
@click.prevent="openReactionSelect"
|
||||
slot="trigger"
|
||||
>
|
||||
<i
|
||||
class="icon-smile button-icon add-reaction-button"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
/>
|
||||
</div>
|
||||
</v-popover>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./react_button.js" ></script>
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
<li>
|
||||
<Checkbox v-model="useStreamingApi">
|
||||
{{ $t('settings.useStreamingApi') }}
|
||||
<br/>
|
||||
<br>
|
||||
<small>
|
||||
{{ $t('settings.useStreamingApiWarning') }}
|
||||
</small>
|
||||
|
@ -92,6 +92,11 @@
|
|||
{{ $t('settings.reply_link_preview') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="virtualScrolling">
|
||||
{{ $t('settings.virtual_scrolling') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="emojiReactionsOnTimeline">
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
|
|
|
@ -36,7 +36,8 @@ const Status = {
|
|||
'inlineExpanded',
|
||||
'showPinned',
|
||||
'inProfile',
|
||||
'profileUserId'
|
||||
'profileUserId',
|
||||
'virtualHidden'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -123,7 +124,7 @@ const Status = {
|
|||
return this.mergedConfig.hideFilteredStatuses
|
||||
},
|
||||
hideStatus () {
|
||||
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
|
||||
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) || this.virtualHidden
|
||||
},
|
||||
isFocused () {
|
||||
// retweet or root of an expanded conversation
|
||||
|
|
|
@ -177,6 +177,8 @@
|
|||
<StatusPopover
|
||||
v-if="!isPreview"
|
||||
:status-id="status.in_reply_to_status_id"
|
||||
class="reply-to-popover"
|
||||
style="min-width: 0"
|
||||
>
|
||||
<a
|
||||
class="reply-to"
|
||||
|
@ -564,11 +566,10 @@ $status-margin: 0.75em;
|
|||
align-items: stretch;
|
||||
|
||||
> .reply-to-and-accountname > a {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
@ -577,7 +578,6 @@ $status-margin: 0.75em;
|
|||
display: flex;
|
||||
height: 18px;
|
||||
margin-right: 0.5em;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
.icon-reply {
|
||||
transform: scaleX(-1);
|
||||
|
@ -588,6 +588,10 @@ $status-margin: 0.75em;
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.reply-to-popover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -595,6 +599,7 @@ $status-margin: 0.75em;
|
|||
.reply-to-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin: 0 0.4em 0 0.2em;
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
|
|
|
@ -5,22 +5,14 @@ const StatusPopover = {
|
|||
props: [
|
||||
'statusId'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
popperOptions: {
|
||||
modifiers: {
|
||||
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
status () {
|
||||
return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Status: () => import('../status/status.vue')
|
||||
Status: () => import('../status/status.vue'),
|
||||
Popover: () => import('../popover/popover.vue')
|
||||
},
|
||||
methods: {
|
||||
enter () {
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
<template>
|
||||
<v-popover
|
||||
<Popover
|
||||
trigger="hover"
|
||||
popover-class="status-popover"
|
||||
placement="top-start"
|
||||
:popper-options="popperOptions"
|
||||
@show="enter()"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:offset="{ x: 0, y: 5 }"
|
||||
@show="enter"
|
||||
>
|
||||
<template slot="trigger">
|
||||
<slot />
|
||||
</template>
|
||||
<div
|
||||
slot="content"
|
||||
>
|
||||
<template slot="popover">
|
||||
<Status
|
||||
v-if="status"
|
||||
:is-preview="true"
|
||||
|
@ -18,10 +24,8 @@
|
|||
>
|
||||
<i class="icon-spin4 animate-spin" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</v-popover>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script src="./status_popover.js" ></script>
|
||||
|
@ -29,13 +33,11 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.tooltip.popover.status-popover {
|
||||
.status-popover {
|
||||
font-size: 1rem;
|
||||
min-width: 15em;
|
||||
max-width: 95%;
|
||||
margin-left: 0.5em;
|
||||
|
||||
.popover-inner {
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
border-style: solid;
|
||||
|
@ -44,29 +46,6 @@
|
|||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
||||
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 {
|
||||
border: none;
|
||||
|
|
|
@ -18,14 +18,16 @@ const StillImage = {
|
|||
},
|
||||
methods: {
|
||||
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
|
||||
if (!canvas) return
|
||||
const width = this.$refs.src.naturalWidth
|
||||
const height = this.$refs.src.naturalHeight
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
|
||||
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
|
||||
},
|
||||
onError () {
|
||||
this.imageLoadError && this.imageLoadError()
|
||||
|
|
|
@ -32,7 +32,8 @@ const Timeline = {
|
|||
return {
|
||||
paused: false,
|
||||
unfocused: false,
|
||||
bottomedOut: false
|
||||
bottomedOut: false,
|
||||
virtualScrollIndex: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -68,6 +69,16 @@ const Timeline = {
|
|||
},
|
||||
pinnedStatusIdsObject () {
|
||||
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: {
|
||||
|
@ -79,7 +90,7 @@ const Timeline = {
|
|||
const credentials = store.state.users.currentUser.credentials
|
||||
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 }
|
||||
|
||||
|
@ -100,7 +111,7 @@ const Timeline = {
|
|||
window.addEventListener('keydown', this.handleShortKey)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.scrollLoad)
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('keydown', this.handleShortKey)
|
||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||
|
@ -142,6 +153,49 @@ const Timeline = {
|
|||
}
|
||||
})
|
||||
}, 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) {
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
|
||||
|
@ -152,6 +206,10 @@ const Timeline = {
|
|||
this.fetchOlderStatuses()
|
||||
}
|
||||
},
|
||||
handleScroll: throttle(function (e) {
|
||||
this.determineVisibleStatuses()
|
||||
this.scrollLoad(e)
|
||||
}, 100),
|
||||
handleVisibilityChange () {
|
||||
this.unfocused = document.hidden
|
||||
}
|
||||
|
|
|
@ -34,7 +34,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div :class="classes.body">
|
||||
<div class="timeline">
|
||||
<div
|
||||
ref="timeline"
|
||||
class="timeline"
|
||||
>
|
||||
<template v-for="statusId in pinnedStatusIds">
|
||||
<conversation
|
||||
v-if="timeline.statusesObject[statusId]"
|
||||
|
@ -56,6 +59,7 @@
|
|||
:collapsable="true"
|
||||
:in-profile="inProfile"
|
||||
:profile-user-id="userId"
|
||||
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -397,6 +397,7 @@
|
|||
"false": "no",
|
||||
"true": "yes"
|
||||
},
|
||||
"virtual_scrolling": "Optimize timeline rendering",
|
||||
"fun": "Fun",
|
||||
"greentext": "Meme arrows",
|
||||
"notifications": "Notifications",
|
||||
|
|
|
@ -231,7 +231,8 @@
|
|||
"values": {
|
||||
"false": "pois päältä",
|
||||
"true": "päällä"
|
||||
}
|
||||
},
|
||||
"virtual_scrolling": "Optimoi aikajanan suorituskykyä"
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} päivä",
|
||||
|
|
|
@ -31,7 +31,6 @@ import VueChatScroll from 'vue-chat-scroll'
|
|||
import VueClickOutside from 'v-click-outside'
|
||||
import PortalVue from 'portal-vue'
|
||||
import VBodyScrollLock from './directives/body_scroll_lock'
|
||||
import VTooltip from 'v-tooltip'
|
||||
|
||||
import afterStoreSetup from './boot/after_store.js'
|
||||
|
||||
|
@ -44,13 +43,6 @@ Vue.use(VueChatScroll)
|
|||
Vue.use(VueClickOutside)
|
||||
Vue.use(PortalVue)
|
||||
Vue.use(VBodyScrollLock)
|
||||
Vue.use(VTooltip, {
|
||||
popover: {
|
||||
defaultTrigger: 'hover click',
|
||||
defaultContainer: false,
|
||||
defaultOffset: 5
|
||||
}
|
||||
})
|
||||
|
||||
const i18n = new VueI18n({
|
||||
// By default, use the browser locale, we will update it if neccessary
|
||||
|
|
|
@ -51,7 +51,8 @@ export const defaultState = {
|
|||
useContainFit: false,
|
||||
greentext: undefined, // instance default
|
||||
hidePostStats: undefined, // instance default
|
||||
hideUserStats: undefined // instance default
|
||||
hideUserStats: undefined, // instance default
|
||||
virtualScrolling: undefined // instance default
|
||||
}
|
||||
|
||||
// caching the instance default properties
|
||||
|
|
|
@ -34,6 +34,7 @@ const defaultState = {
|
|||
showFeaturesPanel: true,
|
||||
minimalScopesMode: false,
|
||||
greentext: false,
|
||||
virtualScrolling: true,
|
||||
|
||||
// Nasty stuff
|
||||
pleromaBackend: true,
|
||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -5941,11 +5941,6 @@ pngjs@^3.3.0:
|
|||
version "3.3.3"
|
||||
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:
|
||||
version "2.1.4"
|
||||
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"
|
||||
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:
|
||||
version "3.0.4"
|
||||
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-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:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be"
|
||||
|
|
Loading…
Reference in New Issue