Merge branch 'develop' into feature/add-polls

This commit is contained in:
Maxim Filippov 2019-04-10 01:46:23 +02:00
commit 20766b8522
34 changed files with 774 additions and 73 deletions

View File

@ -24,12 +24,14 @@
"node-sass": "^3.10.1", "node-sass": "^3.10.1",
"object-path": "^0.11.3", "object-path": "^0.11.3",
"phoenix": "^1.3.0", "phoenix": "^1.3.0",
"popper.js": "^1.14.7",
"sanitize-html": "^1.13.0", "sanitize-html": "^1.13.0",
"sass-loader": "^4.0.2", "sass-loader": "^4.0.2",
"vue": "^2.5.13", "vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1", "vue-chat-scroll": "^1.2.1",
"vue-compose": "^0.7.1", "vue-compose": "^0.7.1",
"vue-i18n": "^7.3.2", "vue-i18n": "^7.3.2",
"vue-popperjs": "^2.0.3",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4", "vue-template-compiler": "^2.3.4",
"vue-timeago": "^3.1.2", "vue-timeago": "^3.1.2",

View File

@ -101,6 +101,14 @@ button {
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg) background-color: var(--bg, $fallback--bg)
} }
&.danger {
// TODO: add better color variable
color: $fallback--text;
color: var(--alertErrorPanelText, $fallback--text);
background-color: $fallback--alertError;
background-color: var(--alertError, $fallback--alertError);
}
} }
label.select { label.select {

View File

@ -9,12 +9,13 @@ const getStatusnetConfig = async ({ store }) => {
const res = await window.fetch('/api/statusnet/config.json') const res = await window.fetch('/api/statusnet/config.json')
if (res.ok) { if (res.ok) {
const data = await res.json() const data = await res.json()
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey, safeDMMentionsEnabled } = data.site
store.dispatch('setInstanceOption', { name: 'name', value: name }) store.dispatch('setInstanceOption', { name: 'name', value: name })
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
store.dispatch('setInstanceOption', { name: 'server', value: server }) store.dispatch('setInstanceOption', { name: 'server', value: server })
store.dispatch('setInstanceOption', { name: 'safeDM', value: safeDMMentionsEnabled !== '0' })
// TODO: default values for this stuff, added if to not make it break on // TODO: default values for this stuff, added if to not make it break on
// my dev config out of the box. // my dev config out of the box.
@ -210,6 +211,7 @@ const getNodeInfo = async ({ store }) => {
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
} else { } else {
throw (res) throw (res)
} }

View File

@ -13,7 +13,7 @@
:key="status.id" :key="status.id"
:inlineExpanded="collapsable" :inlineExpanded="collapsable"
:statusoid="status" :statusoid="status"
:expandable='!expanded' :expandable='!isExpanded'
:focused="focused(status.id)" :focused="focused(status.id)"
:inConversation="isExpanded" :inConversation="isExpanded"
:highlight="getHighlight()" :highlight="getHighlight()"

View File

@ -10,7 +10,11 @@ const DeleteButton = {
}, },
computed: { computed: {
currentUser () { return this.$store.state.users.currentUser }, currentUser () { return this.$store.state.users.currentUser },
canDelete () { return this.currentUser && this.currentUser.rights.delete_others_notice || this.status.user.id === this.currentUser.id } canDelete () {
if (!this.currentUser) { return }
const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
return superuser || this.status.user.id === this.currentUser.id
}
} }
} }

View File

@ -0,0 +1,14 @@
const DialogModal = {
props: {
darkOverlay: {
default: true,
type: Boolean
},
onCancel: {
default: () => {},
type: Function
}
}
}
export default DialogModal

View File

@ -0,0 +1,92 @@
<template>
<span v-bind:class="{ 'dark-overlay': darkOverlay }" @click.self.stop='onCancel()'>
<div class="dialog-modal panel panel-default" @click.stop=''>
<div class="panel-heading dialog-modal-heading">
<div class="title">
<slot name="header"></slot>
</div>
</div>
<div class="dialog-modal-content">
<slot name="default"></slot>
</div>
<div class="dialog-modal-footer user-interactions panel-footer">
<slot name="footer"></slot>
</div>
</div>
</span>
</template>
<script src="./dialog_modal.js"></script>
<style lang="scss">
@import '../../_variables.scss';
// TODO: unify with other modals.
.dark-overlay {
&::before {
bottom: 0;
content: " ";
display: block;
cursor: default;
left: 0;
position: fixed;
right: 0;
top: 0;
background: rgba(27,31,35,.5);
z-index: 99;
}
}
.dialog-modal.panel {
top: 0;
left: 50%;
max-height: 80vh;
max-width: 90vw;
margin: 15vh auto;
position: fixed;
transform: translateX(-50%);
z-index: 999;
cursor: default;
display: block;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
.dialog-modal-heading {
padding: .5em .5em;
margin-right: auto;
margin-bottom: 0;
white-space: nowrap;
color: var(--panelText);
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
.title {
margin-bottom: 0;
}
}
.dialog-modal-content {
margin: 0;
padding: 1rem 1rem;
background-color: $fallback--lightBg;
background-color: var(--lightBg, $fallback--lightBg);
white-space: normal;
}
.dialog-modal-footer {
margin: 0;
padding: .5em .5em;
background-color: $fallback--lightBg;
background-color: var(--lightBg, $fallback--lightBg);
border-top: 1px solid $fallback--bg;
border-top: 1px solid var(--bg, $fallback--bg);
justify-content: flex-end;
button {
width: auto;
margin-left: .5rem;
}
}
}
</style>

View File

@ -31,15 +31,19 @@ const LoginForm = {
username: this.user.username, username: this.user.username,
password: this.user.password password: this.user.password
} }
).then((result) => { ).then(async (result) => {
if (result.error) { if (result.error) {
this.authError = result.error this.authError = result.error
this.user.password = '' this.user.password = ''
return return
} }
this.$store.commit('setToken', result.access_token) this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token) try {
this.$router.push({name: 'friends'}) await this.$store.dispatch('loginUser', result.access_token)
this.$router.push({name: 'friends'})
} catch (e) {
console.log(e)
}
}) })
}) })
}, },

View File

@ -0,0 +1,106 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
import Popper from 'vue-popperjs/src/component/popper.js.vue'
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip'
const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription'
const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'
const SANDBOX = 'mrf_tag:sandbox'
const QUARANTINE = 'mrf_tag:quarantine'
const ModerationTools = {
props: [
'user'
],
data () {
return {
showDropDown: false,
tags: {
FORCE_NSFW,
STRIP_MEDIA,
FORCE_UNLISTED,
DISABLE_REMOTE_SUBSCRIPTION,
DISABLE_ANY_SUBSCRIPTION,
SANDBOX,
QUARANTINE
},
showDeleteUserDialog: false
}
},
components: {
DialogModal,
Popper
},
computed: {
tagsSet () {
return new Set(this.user.tags)
},
hasTagPolicy () {
return this.$store.state.instance.tagPolicyAvailable
}
},
methods: {
toggleMenu () {
this.showDropDown = !this.showDropDown
},
hasTag (tagName) {
return this.tagsSet.has(tagName)
},
toggleTag (tag) {
const store = this.$store
if (this.tagsSet.has(tag)) {
store.state.api.backendInteractor.untagUser(this.user, tag).then(response => {
if (!response.ok) { return }
store.commit('untagUser', {user: this.user, tag})
})
} else {
store.state.api.backendInteractor.tagUser(this.user, tag).then(response => {
if (!response.ok) { return }
store.commit('tagUser', {user: this.user, tag})
})
}
},
toggleRight (right) {
const store = this.$store
if (this.user.rights[right]) {
store.state.api.backendInteractor.deleteRight(this.user, right).then(response => {
if (!response.ok) { return }
store.commit('updateRight', {user: this.user, right: right, value: false})
})
} else {
store.state.api.backendInteractor.addRight(this.user, right).then(response => {
if (!response.ok) { return }
store.commit('updateRight', {user: this.user, right: right, value: true})
})
}
},
toggleActivationStatus () {
const store = this.$store
const status = !!this.user.deactivated
store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => {
if (!response.ok) { return }
store.commit('updateActivationStatus', {user: this.user, status: status})
})
},
deleteUserDialog (show) {
this.showDeleteUserDialog = show
},
deleteUser () {
const store = this.$store
const user = this.user
const {id, name} = user
store.state.api.backendInteractor.deleteUser(user)
.then(e => {
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
const isTargetUser = this.$route.params.name === name || this.$route.params.id === id
if (isProfile && isTargetUser) {
window.history.back()
}
})
}
}
}
export default ModerationTools

View File

@ -0,0 +1,158 @@
<template>
<div class='block' style='position: relative'>
<Popper
trigger="click"
@hide='showDropDown = false'
append-to-body
:options="{
placement: 'bottom-end',
modifiers: {
arrow: { enabled: true },
offset: { offset: '0, 5px' },
}
}">
<div class="popper-wrapper">
<div class="dropdown-menu">
<span v-if='user.is_local'>
<button class="dropdown-item" @click='toggleRight("admin")'>
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
</button>
<button class="dropdown-item" @click='toggleRight("moderator")'>
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
</button>
<div role="separator" class="dropdown-divider"></div>
</span>
<button class="dropdown-item" @click='toggleActivationStatus()'>
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button>
<button class="dropdown-item" @click='deleteUserDialog(true)'>
{{ $t('user_card.admin_menu.delete_account') }}
</button>
<div role="separator" class="dropdown-divider" v-if='hasTagPolicy'></div>
<span v-if='hasTagPolicy'>
<button class="dropdown-item" @click='toggleTag(tags.FORCE_NSFW)'>
{{ $t('user_card.admin_menu.force_nsfw') }}
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"></span>
</button>
<button class="dropdown-item" @click='toggleTag(tags.STRIP_MEDIA)'>
{{ $t('user_card.admin_menu.strip_media') }}
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"></span>
</button>
<button class="dropdown-item" @click='toggleTag(tags.FORCE_UNLISTED)'>
{{ $t('user_card.admin_menu.force_unlisted') }}
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"></span>
</button>
<button class="dropdown-item" @click='toggleTag(tags.SANDBOX)'>
{{ $t('user_card.admin_menu.sandbox') }}
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"></span>
</button>
<button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)'>
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"></span>
</button>
<button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)'>
{{ $t('user_card.admin_menu.disable_any_subscription') }}
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"></span>
</button>
<button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.QUARANTINE)'>
{{ $t('user_card.admin_menu.quarantine') }}
<span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"></span>
</button>
</span>
</div>
</div>
<button slot="reference" v-bind:class="{ pressed: showDropDown }" @click='toggleMenu'>
{{ $t('user_card.admin_menu.moderation') }}
</button>
</Popper>
<DialogModal v-if="showDeleteUserDialog" :onCancel='deleteUserDialog.bind(this, false)'>
<span slot="header">{{ $t('user_card.admin_menu.delete_user') }}</span>
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
<span slot="footer">
<button @click='deleteUserDialog(false)'>
{{ $t('general.cancel') }}
</button>
<button class="danger" @click='deleteUser()'>
{{ $t('user_card.admin_menu.delete_user') }}
</button>
</span>
</DialogModal>
</div>
</template>
<script src="./moderation_tools.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import '../popper/popper.scss';
.dropdown-menu {
display: block;
padding: .5rem 0;
font-size: 1rem;
text-align: left;
list-style: none;
max-width: 100vw;
z-index: 10;
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
box-shadow: var(--panelShadow);
border: none;
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
.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%;
&:hover {
// TODO: improve the look on breeze themes
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
box-shadow: none;
}
}
}
.menu-checkbox {
float: right;
min-width: 22px;
max-width: 22px;
min-height: 22px;
max-height: 22px;
line-height: 22px;
text-align: center;
border-radius: 0px;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
&.menu-checkbox-checked::after {
content: '✔';
}
}
</style>

View File

@ -21,6 +21,9 @@ const Notification = {
}, },
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
},
getUser (notification) {
return this.$store.state.users.usersObject[notification.action.user.id]
} }
}, },
computed: { computed: {

View File

@ -5,7 +5,7 @@
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/> <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
</a> </a>
<div class='notification-right'> <div class='notification-right'>
<UserCard :user="user" :rounded="true" :bordered="true" v-if="userExpanded"/> <UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded"/>
<span class="notification-details"> <span class="notification-details">
<div class="name-and-action"> <div class="name-and-action">
<span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span> <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>

View File

@ -10,13 +10,6 @@ const Notifications = {
props: [ props: [
'noHeading' 'noHeading'
], ],
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
const fetcherId = notificationsFetcher.startFetching({ store, credentials })
this.$store.commit('setNotificationFetcher', { fetcherId })
},
data () { data () {
return { return {
bottomedOut: false bottomedOut: false

View File

@ -0,0 +1,70 @@
@import '../../_variables.scss';
.popper-wrapper {
z-index: 8;
}
.popper-wrapper .popper__arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
}
.popper-wrapper[x-placement^="top"] {
margin-bottom: 5px;
}
.popper-wrapper[x-placement^="top"] .popper__arrow {
border-width: 5px 5px 0 5px;
border-color: $fallback--bg transparent transparent transparent;
border-color: var(--bg, $fallback--bg) transparent transparent transparent;
bottom: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.popper-wrapper[x-placement^="bottom"] {
margin-top: 5px;
}
.popper-wrapper[x-placement^="bottom"] .popper__arrow {
border-width: 0 5px 5px 5px;
border-color: transparent transparent $fallback--bg transparent;
border-color: transparent transparent var(--bg, $fallback--bg) transparent;
top: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.popper-wrapper[x-placement^="right"] {
margin-left: 5px;
}
.popper-wrapper[x-placement^="right"] .popper__arrow {
border-width: 5px 5px 5px 0;
border-color: transparent $fallback--bg transparent transparent;
border-color: transparent var(--bg, $fallback--bg) transparent transparent;
left: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
.popper-wrapper[x-placement^="left"] {
margin-right: 5px;
}
.popper-wrapper[x-placement^="left"] .popper__arrow {
border-width: 5px 0 5px 5px;
border-color: transparent transparent transparent $fallback--bg;
border-color: transparent transparent transparent var(--bg, $fallback--bg);
right: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}

View File

@ -185,6 +185,9 @@ const PostStatusForm = {
}, },
postFormats () { postFormats () {
return this.$store.state.instance.postFormats || [] return this.$store.state.instance.postFormats || []
},
safeDMEnabled () {
return this.$store.state.instance.safeDM
} }
}, },
methods: { methods: {

View File

@ -3,13 +3,16 @@
<form @submit.prevent="postStatus(newStatus)"> <form @submit.prevent="postStatus(newStatus)">
<div class="form-group" > <div class="form-group" >
<i18n <i18n
v-if="!this.$store.state.users.currentUser.locked && this.newStatus.visibility == 'private'" v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
path="post_status.account_not_locked_warning" path="post_status.account_not_locked_warning"
tag="p" tag="p"
class="visibility-notice"> class="visibility-notice">
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link> <router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
</i18n> </i18n>
<p v-if="this.newStatus.visibility == 'direct'" class="visibility-notice">{{ $t('post_status.direct_warning') }}</p> <p v-if="newStatus.visibility === 'direct'" class="visibility-notice">
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
</p>
<EmojiInput <EmojiInput
v-if="newStatus.spoilerText || alwaysShowSubject" v-if="newStatus.spoilerText || alwaysShowSubject"
type="text" type="text"

View File

@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
timeline () { return this.$store.state.statuses.timelines.publicAndExternal } timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
}, },
created () { created () {
this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'publicAndExternal') this.$store.dispatch('stopFetching', 'publicAndExternal')

View File

@ -7,7 +7,7 @@ const PublicTimeline = {
timeline () { return this.$store.state.statuses.timelines.public } timeline () { return this.$store.state.statuses.timelines.public }
}, },
created () { created () {
this.$store.dispatch('startFetching', { timeline: 'public' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'public') this.$store.dispatch('stopFetching', 'public')

View File

@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
const TagTimeline = { const TagTimeline = {
created () { created () {
this.$store.commit('clearTimeline', { timeline: 'tag' }) this.$store.commit('clearTimeline', { timeline: 'tag' })
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag }) this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag })
}, },
components: { components: {
Timeline Timeline
@ -15,7 +15,7 @@ const TagTimeline = {
watch: { watch: {
tag () { tag () {
this.$store.commit('clearTimeline', { timeline: 'tag' }) this.$store.commit('clearTimeline', { timeline: 'tag' })
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag }) this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag })
} }
}, },
destroyed () { destroyed () {

View File

@ -52,7 +52,7 @@ const Timeline = {
window.addEventListener('scroll', this.scrollLoad) window.addEventListener('scroll', this.scrollLoad)
if (this.timelineName === 'friends' && !credentials) { return false } if (store.state.api.fetchers[this.timelineName]) { return false }
timelineFetcher.fetchAndUpdate({ timelineFetcher.fetchAndUpdate({
store, store,

View File

@ -1,5 +1,6 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -93,15 +94,17 @@ export default {
} }
}, },
visibleRole () { visibleRole () {
const validRole = (this.user.role === 'admin' || this.user.role === 'moderator') const rights = this.user.rights
const showRole = this.isOtherUser || this.user.show_role if (!rights) { return }
const validRole = rights.admin || rights.moderator
return validRole && showRole && this.user.role const roleTitle = rights.admin ? 'admin' : 'moderator'
return validRole && roleTitle
} }
}, },
components: { components: {
UserAvatar, UserAvatar,
RemoteFollow RemoteFollow,
ModerationTools
}, },
methods: { methods: {
followUser () { followUser () {

View File

@ -99,6 +99,8 @@
</button> </button>
</span> </span>
</div> </div>
<ModerationTools :user='user' v-if='loggedIn.role === "admin"'>
</ModerationTools>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@ import get from 'lodash/get'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more' import withLoadMore from '../../hocs/with_load_more/with_load_more'
import withList from '../../hocs/with_list/with_list' import withList from '../../hocs/with_list/with_list'
@ -90,7 +91,7 @@ const UserProfile = {
methods: { methods: {
startFetchFavorites () { startFetchFavorites () {
if (this.isUs) { if (this.isUs) {
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId: this.userId })
} }
}, },
fetchUserId () { fetchUserId () {
@ -118,8 +119,8 @@ const UserProfile = {
}, },
startUp () { startUp () {
if (this.userId) { if (this.userId) {
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId: this.userId })
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId: this.userId })
this.startFetchFavorites() this.startFetchFavorites()
} }
}, },
@ -155,7 +156,8 @@ const UserProfile = {
UserCard, UserCard,
Timeline, Timeline,
FollowerList, FollowerList,
FriendList FriendList,
ModerationTools
} }
} }

View File

@ -22,7 +22,8 @@
"generic_error": "An error occured", "generic_error": "An error occured",
"optional": "optional", "optional": "optional",
"show_more": "Show more", "show_more": "Show more",
"show_less": "Show less" "show_less": "Show less",
"cancel": "Cancel"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Crop picture", "crop_picture": "Crop picture",
@ -85,7 +86,8 @@
}, },
"content_warning": "Subject (optional)", "content_warning": "Subject (optional)",
"default": "Just landed in L.A.", "default": "Just landed in L.A.",
"direct_warning": "This post will only be visible to all the mentioned users.", "direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"posting": "Posting", "posting": "Posting",
"scope": { "scope": {
"direct": "Direct - Post to mentioned users only", "direct": "Direct - Post to mentioned users only",
@ -407,7 +409,26 @@
"block_progress": "Blocking...", "block_progress": "Blocking...",
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting...", "unmute_progress": "Unmuting...",
"mute_progress": "Muting..." "mute_progress": "Muting...",
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Grant Admin",
"revoke_admin": "Revoke Admin",
"grant_moderator": "Grant Moderator",
"revoke_moderator": "Revoke Moderator",
"activate_account": "Activate account",
"deactivate_account": "Deactivate account",
"delete_account": "Delete account",
"force_nsfw": "Mark all posts as NSFW",
"strip_media": "Remove media from posts",
"force_unlisted": "Force posts to be unlisted",
"sandbox": "Force posts to be followers-only",
"disable_remote_subscription": "Disallow following user from remote instances",
"disable_any_subscription": "Disallow following user at all",
"quarantine": "Disallow user posts from federating",
"delete_user": "Delete user",
"delete_user_confirmation": "Are you absolutely sure? This action cannot be undone."
}
}, },
"user_profile": { "user_profile": {
"timeline_title": "User Timeline", "timeline_title": "User Timeline",

View File

@ -8,7 +8,8 @@
}, },
"general": { "general": {
"apply": "Применить", "apply": "Применить",
"submit": "Отправить" "submit": "Отправить",
"cancel": "Отмена"
}, },
"login": { "login": {
"login": "Войти", "login": "Войти",
@ -311,7 +312,26 @@
"muted": "Игнорирую", "muted": "Игнорирую",
"per_day": "в день", "per_day": "в день",
"remote_follow": "Читать удалённо", "remote_follow": "Читать удалённо",
"statuses": "Статусы" "statuses": "Статусы",
"admin_menu": {
"moderation": "Опции модератора",
"grant_admin": "Сделать администратором",
"revoke_admin": "Забрать права администратора",
"grant_moderator": "Сделать модератором",
"revoke_moderator": "Забрать права модератора",
"activate_account": "Активировать аккаунт",
"deactivate_account": "Деактивировать аккаунт",
"delete_account": "Удалить аккаунт",
"force_nsfw": "Отмечать посты пользователя как NSFW",
"strip_media": "Убирать вложения из постов пользователя",
"force_unlisted": "Не добавлять посты в публичные ленты",
"sandbox": "Посты доступны только для подписчиков",
"disable_remote_subscription": "Запретить подписываться с удаленных серверов",
"disable_any_subscription": "Запретить подписываться на пользователя",
"quarantine": "Не федерировать посты пользователя",
"delete_user": "Удалить пользователя",
"delete_user_confirmation": "Вы уверены? Это действие нельзя отменить."
}
}, },
"user_profile": { "user_profile": {
"timeline_title": "Лента пользователя" "timeline_title": "Лента пользователя"

View File

@ -13,11 +13,11 @@ const api = {
setBackendInteractor (state, backendInteractor) { setBackendInteractor (state, backendInteractor) {
state.backendInteractor = backendInteractor state.backendInteractor = backendInteractor
}, },
addFetcher (state, {timeline, fetcher}) { addFetcher (state, { fetcherName, fetcher }) {
state.fetchers[timeline] = fetcher state.fetchers[fetcherName] = fetcher
}, },
removeFetcher (state, {timeline}) { removeFetcher (state, { fetcherName }) {
delete state.fetchers[timeline] delete state.fetchers[fetcherName]
}, },
setWsToken (state, token) { setWsToken (state, token) {
state.wsToken = token state.wsToken = token
@ -33,17 +33,24 @@ const api = {
} }
}, },
actions: { actions: {
startFetching (store, {timeline = 'friends', tag = false, userId = false}) { startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) {
// Don't start fetching if we already are. // Don't start fetching if we already are.
if (store.state.fetchers[timeline]) return if (store.state.fetchers[timeline]) return
const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag }) const fetcher = store.state.backendInteractor.startFetchingTimeline({ timeline, store, userId, tag })
store.commit('addFetcher', { timeline, fetcher }) store.commit('addFetcher', { fetcherName: timeline, fetcher })
}, },
stopFetching (store, timeline) { startFetchingNotifications (store) {
const fetcher = store.state.fetchers[timeline] // Don't start fetching if we already are.
if (store.state.fetchers['notifications']) return
const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
},
stopFetching (store, fetcherName) {
const fetcher = store.state.fetchers[fetcherName]
window.clearInterval(fetcher) window.clearInterval(fetcher)
store.commit('removeFetcher', {timeline}) store.commit('removeFetcher', { fetcherName })
}, },
setWsToken (store, token) { setWsToken (store, token) {
store.commit('setWsToken', token) store.commit('setWsToken', token)

View File

@ -5,6 +5,7 @@ const defaultState = {
// Stuff from static/config.json and apiConfig // Stuff from static/config.json and apiConfig
name: 'Pleroma FE', name: 'Pleroma FE',
registrationOpen: true, registrationOpen: true,
safeDM: true,
textlimit: 5000, textlimit: 5000,
server: 'http://localhost:4040/', server: 'http://localhost:4040/',
theme: 'pleroma-dark', theme: 'pleroma-dark',

View File

@ -48,7 +48,6 @@ const interfaceMod = {
commit('setNotificationPermission', permission) commit('setNotificationPermission', permission)
}, },
setMobileLayout ({ commit }, value) { setMobileLayout ({ commit }, value) {
console.log('setMobileLayout called')
commit('setMobileLayout', value) commit('setMobileLayout', value)
} }
} }

View File

@ -20,20 +20,21 @@ const emptyTl = (userId = 0) => ({
flushMarker: 0 flushMarker: 0
}) })
const emptyNotifications = () => ({
desktopNotificationSilence: true,
maxId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
loading: false,
error: false
})
export const defaultState = () => ({ export const defaultState = () => ({
allStatuses: [], allStatuses: [],
allStatusesObject: {}, allStatusesObject: {},
maxId: 0, maxId: 0,
notifications: { notifications: emptyNotifications(),
desktopNotificationSilence: true,
maxId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
loading: false,
error: false,
fetcherId: null
},
favorites: new Set(), favorites: new Set(),
error: false, error: false,
timelines: { timelines: {
@ -340,9 +341,6 @@ export const mutations = {
oldTimeline.visibleStatusesObject = {} oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
}, },
setNotificationFetcher (state, { fetcherId }) {
state.notifications.fetcherId = fetcherId
},
resetStatuses (state) { resetStatuses (state) {
const emptyState = defaultState() const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => { Object.entries(emptyState).forEach(([key, value]) => {
@ -352,6 +350,9 @@ export const mutations = {
clearTimeline (state, { timeline }) { clearTimeline (state, { timeline }) {
state.timelines[timeline] = emptyTl(state.timelines[timeline].userId) state.timelines[timeline] = emptyTl(state.timelines[timeline].userId)
}, },
clearNotifications (state) {
state.notifications = emptyNotifications()
},
setFavorited (state, { status, value }) { setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = value newStatus.favorited = value
@ -378,6 +379,13 @@ export const mutations = {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
newStatus.deleted = true newStatus.deleted = true
}, },
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
if (condition(status)) {
status.deleted = true
}
})
},
setLoading (state, { timeline, value }) { setLoading (state, { timeline, value }) {
state.timelines[timeline].loading = value state.timelines[timeline].loading = value
}, },
@ -428,16 +436,13 @@ const statuses = {
setNotificationsSilence ({ rootState, commit }, { value }) { setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value }) commit('setNotificationsSilence', { value })
}, },
stopFetchingNotifications ({ rootState, commit }) {
if (rootState.statuses.notifications.fetcherId) {
window.clearInterval(rootState.statuses.notifications.fetcherId)
}
commit('setNotificationFetcher', { fetcherId: null })
},
deleteStatus ({ rootState, commit }, status) { deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status }) commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
}, },
markStatusesAsDeleted ({ commit }, condition) {
commit('setManyDeleted', condition)
},
favorite ({ rootState, commit }, status) { favorite ({ rootState, commit }, status) {
// Optimistic favoriting... // Optimistic favoriting...
commit('setFavorited', { status, value: true }) commit('setFavorited', { status, value: true })

View File

@ -37,6 +37,28 @@ export const mutations = {
const user = state.usersObject[id] const user = state.usersObject[id]
set(user, 'muted', muted) set(user, 'muted', muted)
}, },
tagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.concat([tag])
set(user, 'tags', newTags)
},
untagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.filter(t => t !== tag)
set(user, 'tags', newTags)
},
updateRight (state, { user: { id }, right, value }) {
const user = state.usersObject[id]
let newRights = user.rights
newRights[right] = value
set(user, 'rights', newRights)
},
updateActivationStatus (state, { user: { id }, status }) {
const user = state.usersObject[id]
set(user, 'deactivated', !status)
},
setCurrentUser (state, user) { setCurrentUser (state, user) {
state.lastLoginName = user.screen_name state.lastLoginName = user.screen_name
state.currentUser = merge(state.currentUser || {}, user) state.currentUser = merge(state.currentUser || {}, user)
@ -331,7 +353,8 @@ const users = {
store.commit('setToken', false) store.commit('setToken', false)
store.dispatch('stopFetching', 'friends') store.dispatch('stopFetching', 'friends')
store.commit('setBackendInteractor', backendInteractorService()) store.commit('setBackendInteractor', backendInteractorService())
store.dispatch('stopFetchingNotifications') store.dispatch('stopFetching', 'notifications')
store.commit('clearNotifications')
store.commit('resetStatuses') store.commit('resetStatuses')
}, },
loginUser (store, accessToken) { loginUser (store, accessToken) {
@ -363,7 +386,10 @@ const users = {
} }
// Start getting fresh posts. // Start getting fresh posts.
store.dispatch('startFetching', { timeline: 'friends' }) store.dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
store.dispatch('startFetchingNotifications')
// Get user mutes // Get user mutes
store.dispatch('fetchMutes') store.dispatch('fetchMutes')

View File

@ -16,6 +16,10 @@ const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests' const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests'
const APPROVE_USER_URL = '/api/pleroma/friendships/approve' const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
const DENY_USER_URL = '/api/pleroma/friendships/deny' const DENY_USER_URL = '/api/pleroma/friendships/deny'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = '/api/pleroma/admin/permission_group'
const ACTIVATION_STATUS_URL = '/api/pleroma/admin/activation_status'
const ADMIN_USER_URL = '/api/pleroma/admin/user'
const SUGGESTIONS_URL = '/api/v1/suggestions' const SUGGESTIONS_URL = '/api/v1/suggestions'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
@ -354,6 +358,86 @@ const fetchStatus = ({id, credentials}) => {
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const tagUser = ({tag, credentials, ...options}) => {
const screenName = options.screen_name
const form = {
nicknames: [screenName],
tags: [tag]
}
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(TAG_USER_URL, {
method: 'PUT',
headers: headers,
body: JSON.stringify(form)
})
}
const untagUser = ({tag, credentials, ...options}) => {
const screenName = options.screen_name
const body = {
nicknames: [screenName],
tags: [tag]
}
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(TAG_USER_URL, {
method: 'DELETE',
headers: headers,
body: JSON.stringify(body)
})
}
const addRight = ({right, credentials, ...user}) => {
const screenName = user.screen_name
return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, {
method: 'POST',
headers: authHeaders(credentials),
body: {}
})
}
const deleteRight = ({right, credentials, ...user}) => {
const screenName = user.screen_name
return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, {
method: 'DELETE',
headers: authHeaders(credentials),
body: {}
})
}
const setActivationStatus = ({status, credentials, ...user}) => {
const screenName = user.screen_name
const body = {
status: status
}
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(`${ACTIVATION_STATUS_URL}/${screenName}.json`, {
method: 'PUT',
headers: headers,
body: JSON.stringify(body)
})
}
const deleteUser = ({credentials, ...user}) => {
const screenName = user.screen_name
const headers = authHeaders(credentials)
return fetch(`${ADMIN_USER_URL}.json?nickname=${screenName}`, {
method: 'DELETE',
headers: headers
})
}
const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => { const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
const timelineUrls = { const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE, public: MASTODON_PUBLIC_TIMELINE,
@ -697,6 +781,12 @@ const apiService = {
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
tagUser,
untagUser,
deleteUser,
addRight,
deleteRight,
setActivationStatus,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View File

@ -1,5 +1,6 @@
import apiService from '../api/api.service.js' import apiService from '../api/api.service.js'
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
const backendInteractorService = (credentials) => { const backendInteractorService = (credentials) => {
const fetchStatus = ({id}) => { const fetchStatus = ({id}) => {
@ -58,6 +59,38 @@ const backendInteractorService = (credentials) => {
return apiService.denyUser({credentials, id}) return apiService.denyUser({credentials, id})
} }
const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
}
const startFetchingNotifications = ({ store }) => {
return notificationsFetcher.startFetching({ store, credentials })
}
const tagUser = ({screen_name}, tag) => {
return apiService.tagUser({screen_name, tag, credentials})
}
const untagUser = ({screen_name}, tag) => {
return apiService.untagUser({screen_name, tag, credentials})
}
const addRight = ({screen_name}, right) => {
return apiService.addRight({screen_name, right, credentials})
}
const deleteRight = ({screen_name}, right) => {
return apiService.deleteRight({screen_name, right, credentials})
}
const setActivationStatus = ({screen_name}, status) => {
return apiService.setActivationStatus({screen_name, status, credentials})
}
const deleteUser = ({screen_name}) => {
return apiService.deleteUser({screen_name, credentials})
}
const vote = (pollID, optionName) => { const vote = (pollID, optionName) => {
return apiService.vote({credentials, pollID, optionName}) return apiService.vote({credentials, pollID, optionName})
} }
@ -66,10 +99,6 @@ const backendInteractorService = (credentials) => {
return apiService.fetchPoll({credentials, pollID}) return apiService.fetchPoll({credentials, pollID})
} }
const startFetching = ({timeline, store, userId = false, tag}) => {
return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag})
}
const fetchMutes = () => apiService.fetchMutes({credentials}) const fetchMutes = () => apiService.fetchMutes({credentials})
const muteUser = (id) => apiService.muteUser({credentials, id}) const muteUser = (id) => apiService.muteUser({credentials, id})
const unmuteUser = (id) => apiService.unmuteUser({credentials, id}) const unmuteUser = (id) => apiService.unmuteUser({credentials, id})
@ -105,13 +134,20 @@ const backendInteractorService = (credentials) => {
fetchUserRelationship, fetchUserRelationship,
fetchAllFollowing, fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials, verifyCredentials: apiService.verifyCredentials,
startFetching, startFetchingTimeline,
startFetchingNotifications,
fetchMutes, fetchMutes,
muteUser, muteUser,
unmuteUser, unmuteUser,
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
tagUser,
untagUser,
addRight,
deleteRight,
deleteUser,
setActivationStatus,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View File

@ -67,6 +67,11 @@ export const parseUser = (data) => {
output.statusnet_blocking = relationship.blocking output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting output.muted = relationship.muting
} }
output.rights = {
moderator: data.pleroma.is_moderator,
admin: data.pleroma.is_admin
}
} }
// Missing, trying to recover // Missing, trying to recover
@ -103,7 +108,12 @@ export const parseUser = (data) => {
// QVITTER ONLY FOR NOW // QVITTER ONLY FOR NOW
// Really only applies to logged in user, really.. I THINK // Really only applies to logged in user, really.. I THINK
output.rights = data.rights if (data.rights) {
output.rights = {
moderator: data.rights.delete_others_notice,
admin: data.rights.admin
}
}
output.no_rich_text = data.no_rich_text output.no_rich_text = data.no_rich_text
output.default_scope = data.default_scope output.default_scope = data.default_scope
output.hide_follows = data.hide_follows output.hide_follows = data.hide_follows
@ -125,6 +135,13 @@ export const parseUser = (data) => {
output.follow_request_count = data.pleroma.follow_request_count output.follow_request_count = data.pleroma.follow_request_count
} }
if (data.pleroma) {
output.tags = data.pleroma.tags
output.deactivated = data.pleroma.deactivated
}
output.tags = output.tags || []
return output return output
} }

View File

@ -4831,6 +4831,10 @@ pluralize@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
popper.js@^1.14.7:
version "1.14.7"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e"
posix-character-classes@^0.1.0: posix-character-classes@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -6457,6 +6461,12 @@ vue-loader@^11.1.0:
vue-style-loader "^2.0.0" vue-style-loader "^2.0.0"
vue-template-es2015-compiler "^1.2.2" vue-template-es2015-compiler "^1.2.2"
vue-popperjs@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/vue-popperjs/-/vue-popperjs-2.0.3.tgz#7c446d0ba7c63170ccb33a02669d0df4efc3d8cd"
dependencies:
popper.js "^1.14.7"
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"