forked from AkkomaGang/akkoma-fe
Add reports management (#186)
implements part of #178, other parts will come later Co-authored-by: Sol Fisher Romanoff <sol@solfisher.com> Reviewed-on: AkkomaGang/pleroma-fe#186 Co-authored-by: sfr <sol@solfisher.com> Co-committed-by: sfr <sol@solfisher.com>
This commit is contained in:
parent
23b0b01829
commit
15bac1e401
30 changed files with 1082 additions and 32 deletions
|
@ -5,6 +5,7 @@ import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||||
import MediaModal from './components/media_modal/media_modal.vue'
|
import MediaModal from './components/media_modal/media_modal.vue'
|
||||||
|
import ModModal from './components/mod_modal/mod_modal.vue'
|
||||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||||
|
@ -33,6 +34,7 @@ export default {
|
||||||
MobileNav,
|
MobileNav,
|
||||||
DesktopNav,
|
DesktopNav,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
|
ModModal,
|
||||||
UserReportingModal,
|
UserReportingModal,
|
||||||
PostStatusModal,
|
PostStatusModal,
|
||||||
EditStatusModal,
|
EditStatusModal,
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
<EditStatusModal v-if="editingAvailable" />
|
<EditStatusModal v-if="editingAvailable" />
|
||||||
<StatusHistoryModal v-if="editingAvailable" />
|
<StatusHistoryModal v-if="editingAvailable" />
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
|
<ModModal />
|
||||||
<GlobalNoticeList />
|
<GlobalNoticeList />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -397,6 +397,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
// Start fetching things that don't need to block the UI
|
// Start fetching things that don't need to block the UI
|
||||||
store.dispatch('fetchMutes')
|
store.dispatch('fetchMutes')
|
||||||
store.dispatch('startFetchingAnnouncements')
|
store.dispatch('startFetchingAnnouncements')
|
||||||
|
store.dispatch('startFetchingReports')
|
||||||
getTOS({ store })
|
getTOS({ store })
|
||||||
getStickers({ store })
|
getStickers({ store })
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@ import {
|
||||||
faUsers,
|
faUsers,
|
||||||
faCommentMedical,
|
faCommentMedical,
|
||||||
faBookmark,
|
faBookmark,
|
||||||
faInfoCircle
|
faInfoCircle,
|
||||||
|
faUserTie
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -34,7 +35,8 @@ library.add(
|
||||||
faUsers,
|
faUsers,
|
||||||
faCommentMedical,
|
faCommentMedical,
|
||||||
faBookmark,
|
faBookmark,
|
||||||
faInfoCircle
|
faInfoCircle,
|
||||||
|
faUserTie
|
||||||
)
|
)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -109,6 +111,9 @@ export default {
|
||||||
},
|
},
|
||||||
openSettingsModal () {
|
openSettingsModal () {
|
||||||
this.$store.dispatch('openSettingsModal')
|
this.$store.dispatch('openSettingsModal')
|
||||||
|
},
|
||||||
|
openModModal () {
|
||||||
|
this.$store.dispatch('openModModal')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,6 +151,18 @@
|
||||||
:title="$t('nav.preferences')"
|
:title="$t('nav.preferences')"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||||
|
class="button-unstyled nav-icon"
|
||||||
|
@click.stop="openModModal"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="user-tie"
|
||||||
|
:title="$t('nav.moderation')"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
<a
|
<a
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
href="/pleroma/admin/#/login-pleroma"
|
||||||
|
|
58
src/components/mod_modal/mod_modal.js
Normal file
58
src/components/mod_modal/mod_modal.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import Modal from 'src/components/modal/modal.vue'
|
||||||
|
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||||
|
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||||
|
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faTimes,
|
||||||
|
faChevronDown
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import {
|
||||||
|
faWindowMinimize
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faTimes,
|
||||||
|
faWindowMinimize,
|
||||||
|
faChevronDown
|
||||||
|
)
|
||||||
|
|
||||||
|
const ModModal = {
|
||||||
|
components: {
|
||||||
|
Modal,
|
||||||
|
ModModalContent: getResettableAsyncComponent(
|
||||||
|
() => import('./mod_modal_content.vue'),
|
||||||
|
{
|
||||||
|
loadingComponent: PanelLoading,
|
||||||
|
errorComponent: AsyncComponentError,
|
||||||
|
delay: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeModal () {
|
||||||
|
this.$store.dispatch('closeModModal')
|
||||||
|
},
|
||||||
|
peekModal () {
|
||||||
|
this.$store.dispatch('togglePeekModModal')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
moderator () {
|
||||||
|
return this.$store.state.users.currentUser &&
|
||||||
|
(this.$store.state.users.currentUser.role === 'admin' ||
|
||||||
|
this.$store.state.users.currentUser.role === 'moderator')
|
||||||
|
},
|
||||||
|
modalActivated () {
|
||||||
|
return this.$store.state.interface.modModalState !== 'hidden'
|
||||||
|
},
|
||||||
|
modalOpenedOnce () {
|
||||||
|
return this.$store.state.interface.modModalLoaded
|
||||||
|
},
|
||||||
|
modalPeeked () {
|
||||||
|
return this.$store.state.interface.modModalState === 'minimized'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModModal
|
44
src/components/mod_modal/mod_modal.scss
Normal file
44
src/components/mod_modal/mod_modal.scss
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
@import 'src/_variables.scss';
|
||||||
|
.mod-modal {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.peek {
|
||||||
|
.mod-modal-panel {
|
||||||
|
/* Explanation:
|
||||||
|
* Modal is positioned vertically centered.
|
||||||
|
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
|
||||||
|
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
|
||||||
|
* + 100% - we move modal completely off-screen, it's top boundary touches
|
||||||
|
* bottom of the screen
|
||||||
|
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
|
||||||
|
*/
|
||||||
|
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||||
|
|
||||||
|
@media all and (max-width: 800px) {
|
||||||
|
/* For mobile, the modal takes 100% of the available screen.
|
||||||
|
This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
|
||||||
|
*/
|
||||||
|
transform: translateY(calc(100% - 50px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-modal-panel {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
transition-duration: 300ms;
|
||||||
|
width: 1000px;
|
||||||
|
max-width: 90vw;
|
||||||
|
height: 90vh;
|
||||||
|
|
||||||
|
@media all and (max-width: 800px) {
|
||||||
|
max-width: 100vw;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
src/components/mod_modal/mod_modal.vue
Normal file
43
src/components/mod_modal/mod_modal.vue
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
v-if="moderator"
|
||||||
|
:is-open="modalActivated"
|
||||||
|
class="mod-modal"
|
||||||
|
:class="{ peek: modalPeeked }"
|
||||||
|
:no-background="modalPeeked"
|
||||||
|
>
|
||||||
|
<div class="mod-modal-panel panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="title">
|
||||||
|
{{ $t('moderation.moderation') }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:title="$t('general.peek')"
|
||||||
|
@click="peekModal"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
:icon="['far', 'window-minimize']"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:title="$t('general.close')"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
icon="times"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<ModModalContent v-if="modalOpenedOnce" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./mod_modal.js"></script>
|
||||||
|
<style src="./mod_modal.scss" lang="scss"></style>
|
63
src/components/mod_modal/mod_modal_content.js
Normal file
63
src/components/mod_modal/mod_modal_content.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
|
|
||||||
|
import ReportsTab from './tabs/reports_tab/reports_tab.vue'
|
||||||
|
// import StatusesTab from './tabs/statuses_tab.vue'
|
||||||
|
// import UsersTab from './tabs/users_tab.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faFlag,
|
||||||
|
faMessage,
|
||||||
|
faUsers
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faFlag,
|
||||||
|
faMessage,
|
||||||
|
faUsers
|
||||||
|
)
|
||||||
|
|
||||||
|
const ModModalContent = {
|
||||||
|
components: {
|
||||||
|
TabSwitcher,
|
||||||
|
|
||||||
|
ReportsTab
|
||||||
|
// StatusesTab,
|
||||||
|
// UsersTab
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
open () {
|
||||||
|
return this.$store.state.interface.modModalState !== 'hidden'
|
||||||
|
},
|
||||||
|
bodyLock () {
|
||||||
|
return this.$store.state.interface.modModalState === 'visible'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onOpen () {
|
||||||
|
const targetTab = this.$store.state.interface.modModalTargetTab
|
||||||
|
// We're being told to open in specific tab
|
||||||
|
if (targetTab) {
|
||||||
|
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
|
||||||
|
return elm.props && elm.props['data-tab-name'] === targetTab
|
||||||
|
})
|
||||||
|
if (tabIndex >= 0) {
|
||||||
|
this.$refs.tabSwitcher.setTab(tabIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear the state of target tab, so that next time moderation is opened
|
||||||
|
// it doesn't force it.
|
||||||
|
this.$store.dispatch('clearModModalTargetTab')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.onOpen()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
open: function (value) {
|
||||||
|
if (value) this.onOpen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModModalContent
|
21
src/components/mod_modal/mod_modal_content.scss
Normal file
21
src/components/mod_modal/mod_modal_content.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
@import 'src/_variables.scss';
|
||||||
|
.mod_tab-switcher {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 1em 1em 1.4em;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: .5em;
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
src/components/mod_modal/mod_modal_content.vue
Normal file
20
src/components/mod_modal/mod_modal_content.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<tab-switcher
|
||||||
|
ref="tabSwitcher"
|
||||||
|
class="mod_tab-switcher"
|
||||||
|
:side-tab-bar="true"
|
||||||
|
:scrollable-tabs="true"
|
||||||
|
:body-scroll-lock="bodyLock"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:label="$t('moderation.reports.reports')"
|
||||||
|
icon="flag"
|
||||||
|
data-tab-name="reports"
|
||||||
|
>
|
||||||
|
<ReportsTab />
|
||||||
|
</div>
|
||||||
|
</tab-switcher>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./mod_modal_content.js"></script>
|
||||||
|
<style src="./mod_modal_content.scss" lang="scss"></style>
|
124
src/components/mod_modal/tabs/reports_tab/report_card.js
Normal file
124
src/components/mod_modal/tabs/reports_tab/report_card.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
import Status from 'src/components/status/status.vue'
|
||||||
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
import ReportNote from './report_note.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp
|
||||||
|
)
|
||||||
|
|
||||||
|
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||||
|
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
||||||
|
const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
|
||||||
|
const SANDBOX = 'mrf_tag:sandbox'
|
||||||
|
|
||||||
|
const ReportCard = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
hidden: true,
|
||||||
|
statusesHidden: true,
|
||||||
|
notesHidden: true,
|
||||||
|
note: null,
|
||||||
|
tags: {
|
||||||
|
FORCE_NSFW,
|
||||||
|
STRIP_MEDIA,
|
||||||
|
FORCE_UNLISTED,
|
||||||
|
SANDBOX
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'account',
|
||||||
|
'actor',
|
||||||
|
'content',
|
||||||
|
'id',
|
||||||
|
'notes',
|
||||||
|
'state',
|
||||||
|
'statuses'
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
ReportNote,
|
||||||
|
Popover,
|
||||||
|
Status,
|
||||||
|
UserAvatar
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$store.dispatch('fetchUser', this.account.id)
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isOpen () {
|
||||||
|
return this.state === 'open'
|
||||||
|
},
|
||||||
|
tagPolicyEnabled () {
|
||||||
|
return this.$store.state.instance.federationPolicy.mrf_policies.includes('TagPolicy')
|
||||||
|
},
|
||||||
|
user () {
|
||||||
|
return this.$store.getters.findUser(this.account.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleHidden () {
|
||||||
|
this.hidden = !this.hidden
|
||||||
|
},
|
||||||
|
decode (content) {
|
||||||
|
content = content.replaceAll('<br/>', '\n')
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.innerHTML = content
|
||||||
|
return textarea.value
|
||||||
|
},
|
||||||
|
updateReportState (state) {
|
||||||
|
this.$store.dispatch('updateReportStates', { reports: [{ id: this.id, state }] })
|
||||||
|
},
|
||||||
|
toggleNotes () {
|
||||||
|
this.notesHidden = !this.notesHidden
|
||||||
|
},
|
||||||
|
addNoteToReport () {
|
||||||
|
if (this.note.length > 0) {
|
||||||
|
this.$store.dispatch('addNoteToReport', { id: this.id, note: this.note })
|
||||||
|
this.note = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleStatuses () {
|
||||||
|
this.statusesHidden = !this.statusesHidden
|
||||||
|
},
|
||||||
|
hasTag (tag) {
|
||||||
|
return this.user.tags.includes(tag)
|
||||||
|
},
|
||||||
|
toggleTag (tag) {
|
||||||
|
if (this.hasTag(tag)) {
|
||||||
|
this.$store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
|
||||||
|
if (!response.ok) { return }
|
||||||
|
this.$store.commit('untagUser', { user: this.user, tag })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
|
||||||
|
if (!response.ok) { return }
|
||||||
|
this.$store.commit('tagUser', { user: this.user, tag })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleActivationStatus () {
|
||||||
|
this.$store.dispatch('toggleActivationStatus', { user: this.user })
|
||||||
|
},
|
||||||
|
deleteUser () {
|
||||||
|
this.$store.state.backendInteractor.deleteUser({ user: this.user })
|
||||||
|
.then(e => {
|
||||||
|
this.$store.dispatch('markStatusesAsDeleted', status => this.user.id === status.user.id)
|
||||||
|
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
|
||||||
|
const isTargetUser = this.$route.params.name === this.user.name || this.$route.params.id === this.user.id
|
||||||
|
if (isProfile && isTargetUser) {
|
||||||
|
window.history.back()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportCard
|
202
src/components/mod_modal/tabs/reports_tab/report_card.vue
Normal file
202
src/components/mod_modal/tabs/reports_tab/report_card.vue
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
<template>
|
||||||
|
<div class="report-card panel">
|
||||||
|
<div
|
||||||
|
class="panel-heading"
|
||||||
|
@click="toggleHidden"
|
||||||
|
>
|
||||||
|
<h4>{{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}</h4>
|
||||||
|
<button
|
||||||
|
v-if="isOpen"
|
||||||
|
class="button-default"
|
||||||
|
@click.stop="updateReportState('closed')"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.reports.close') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isOpen"
|
||||||
|
class="button-default"
|
||||||
|
@click.stop="updateReportState('resolved')"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.reports.resolve') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="button-default"
|
||||||
|
@click.stop="updateReportState('open')"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.reports.reopen') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!hidden"
|
||||||
|
class="panel-body report-body"
|
||||||
|
>
|
||||||
|
<div class="report-content">
|
||||||
|
<div v-if="content">
|
||||||
|
{{ decode(content) }}
|
||||||
|
</div>
|
||||||
|
<i v-else class="faint">
|
||||||
|
{{ $t('moderation.reports.no_content') }}
|
||||||
|
</i>
|
||||||
|
<div class="report-author">
|
||||||
|
<UserAvatar
|
||||||
|
class="small-avatar"
|
||||||
|
:user="actor"
|
||||||
|
/>
|
||||||
|
{{ this.actor.screen_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown"
|
||||||
|
v-if="!hidden && this.statuses.length > 0"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button button-unstyled dropdown-header"
|
||||||
|
@click="toggleStatuses"
|
||||||
|
>
|
||||||
|
{{ this.statuses.length + ' ' + $t('moderation.reports.statuses') }}
|
||||||
|
<FAIcon
|
||||||
|
class="timelines-chevron"
|
||||||
|
fixed-width
|
||||||
|
:icon="statusesHidden ? 'chevron-down' : 'chevron-up'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="!statusesHidden">
|
||||||
|
<Status
|
||||||
|
v-for="status in statuses"
|
||||||
|
:key="status.id"
|
||||||
|
:collapsable="false"
|
||||||
|
:expandable="false"
|
||||||
|
:compact="false"
|
||||||
|
:statusoid="status"
|
||||||
|
:no-heading="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown"
|
||||||
|
v-if="!hidden && this.notes.length > 0"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button button-unstyled dropdown-header"
|
||||||
|
@click="toggleNotes"
|
||||||
|
>
|
||||||
|
{{ this.notes.length + ' ' + $t('moderation.reports.notes') }}
|
||||||
|
<FAIcon
|
||||||
|
class="timelines-chevron"
|
||||||
|
fixed-width
|
||||||
|
:icon="notesHidden ? 'chevron-down' : 'chevron-up'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div v-if="!notesHidden">
|
||||||
|
<ReportNote
|
||||||
|
v-for="note in notes"
|
||||||
|
:key="note.id"
|
||||||
|
:report_id="id"
|
||||||
|
v-bind="note"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="report-add-note">
|
||||||
|
<textarea
|
||||||
|
rows="1"
|
||||||
|
cols="1"
|
||||||
|
v-model.trim="note"
|
||||||
|
:placeholder="$t('moderation.reports.note_placeholder')"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.stop="addNoteToReport"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.reports.add_note') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!hidden"
|
||||||
|
class="panel-footer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.stop="toggleActivationStatus"
|
||||||
|
>
|
||||||
|
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.stop="deleteUser"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.admin_menu.delete_account') }}
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
placement="top"
|
||||||
|
:offset="{ y: 5 }"
|
||||||
|
remove-padding
|
||||||
|
>
|
||||||
|
<template v-slot:trigger>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:disabled="!tagPolicyEnabled"
|
||||||
|
:title="tagPolicyEnabled ? '' : $t('moderation.reports.account.tag_policy_notice')"
|
||||||
|
>
|
||||||
|
<span>{{ $t("moderation.reports.tags") }}</span>
|
||||||
|
{{ ' ' }}
|
||||||
|
<FAIcon
|
||||||
|
icon="chevron-down"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-slot:content="{close}">
|
||||||
|
<div
|
||||||
|
class="dropdown-menu"
|
||||||
|
:disabled="!tagPolicyEnabled"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="toggleTag(tags.FORCE_NSFW)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox"
|
||||||
|
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
|
||||||
|
/>
|
||||||
|
{{ $t('user_card.admin_menu.force_nsfw') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="toggleTag(tags.STRIP_MEDIA)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox"
|
||||||
|
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
|
||||||
|
/>
|
||||||
|
{{ $t('user_card.admin_menu.strip_media') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="toggleTag(tags.FORCE_UNLISTED)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox"
|
||||||
|
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
|
||||||
|
/>
|
||||||
|
{{ $t('user_card.admin_menu.force_unlisted') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="toggleTag(tags.SANDBOX)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox"
|
||||||
|
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
|
||||||
|
/>
|
||||||
|
{{ $t('user_card.admin_menu.sandbox') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./report_card.js"></script>
|
37
src/components/mod_modal/tabs/reports_tab/report_note.js
Normal file
37
src/components/mod_modal/tabs/reports_tab/report_note.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||||
|
import Timeago from 'src/components/timeago/timeago.vue'
|
||||||
|
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||||
|
|
||||||
|
const ReportNote = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showingDeleteDialog: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'content',
|
||||||
|
'created_at',
|
||||||
|
'user',
|
||||||
|
'report_id',
|
||||||
|
'id'
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
ConfirmModal,
|
||||||
|
Timeago,
|
||||||
|
UserAvatar
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteNoteFromReport () {
|
||||||
|
this.$store.dispatch('deleteNoteFromReport', { id: this.report_id, note: this.id })
|
||||||
|
this.showingDeleteDialog = false
|
||||||
|
},
|
||||||
|
showDeleteDialog () {
|
||||||
|
this.showingDeleteDialog = true
|
||||||
|
},
|
||||||
|
hideDeleteDialog () {
|
||||||
|
this.showingDeleteDialog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportNote
|
43
src/components/mod_modal/tabs/reports_tab/report_note.vue
Normal file
43
src/components/mod_modal/tabs/reports_tab/report_note.vue
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div class="report-note">
|
||||||
|
<div class="note-header">
|
||||||
|
<div class="note-author">
|
||||||
|
<UserAvatar
|
||||||
|
class="small-avatar"
|
||||||
|
:user="user"
|
||||||
|
/>
|
||||||
|
{{ this.user.screen_name }}
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<Timeago
|
||||||
|
class="faint"
|
||||||
|
:time="created_at"
|
||||||
|
:auto-update="60"
|
||||||
|
:long-format="true"
|
||||||
|
:with-direction="true"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click.stop="showDeleteDialog"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.reports.delete_note') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="note-content">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
<confirm-modal
|
||||||
|
v-if="showingDeleteDialog"
|
||||||
|
:title="$t('moderation.reports.delete_note_title')"
|
||||||
|
:confirm-text="$t('moderation.reports.delete_note_accept')"
|
||||||
|
:cancel-text="$t('moderation.reports.delete_note_cancel')"
|
||||||
|
@accepted="deleteNoteFromReport"
|
||||||
|
@cancelled="hideDeleteDialog"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.reports.delete_note_confirm') }}
|
||||||
|
</confirm-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./report_note.js"></script>
|
26
src/components/mod_modal/tabs/reports_tab/reports_tab.js
Normal file
26
src/components/mod_modal/tabs/reports_tab/reports_tab.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { filter } from 'lodash'
|
||||||
|
|
||||||
|
import ReportCard from './report_card.vue'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const ReportsTab = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showClosed: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
ReportCard
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
reports () {
|
||||||
|
return this.$store.state.reports.reports
|
||||||
|
},
|
||||||
|
openReports () {
|
||||||
|
return filter(this.reports, { state: 'open' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportsTab
|
83
src/components/mod_modal/tabs/reports_tab/reports_tab.scss
Normal file
83
src/components/mod_modal/tabs/reports_tab/reports_tab.scss
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
@import '../../../../_variables.scss';
|
||||||
|
.report-card {
|
||||||
|
.report-body {
|
||||||
|
& > * {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :not(:last-child) {
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
border-bottom-color: var(--border, #222);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-author {
|
||||||
|
padding-top: 0.5em;
|
||||||
|
}
|
||||||
|
.small-avatar {
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
padding-right: 0.4em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
padding: 1em;
|
||||||
|
color: var(--link, $fallback--link);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||||
|
color: var(--selectedMenuText, $fallback--link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-note {
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
.note-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-add-note {
|
||||||
|
textarea {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-height: 2em;
|
||||||
|
min-width: 10em;
|
||||||
|
padding: 0 2em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
display: flex;
|
||||||
|
& > * {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reports-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
20
src/components/mod_modal/tabs/reports_tab/reports_tab.vue
Normal file
20
src/components/mod_modal/tabs/reports_tab/reports_tab.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div :label="$t('moderation.reports.reports')">
|
||||||
|
<div class="content">
|
||||||
|
<div class="reports-header">
|
||||||
|
<h2>{{ $t('moderation.reports.reports') }}</h2>
|
||||||
|
<Checkbox v-model="showClosed">
|
||||||
|
{{ $t('moderation.reports.show_closed') }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<ReportCard
|
||||||
|
v-for="report in (showClosed ? reports : openReports)"
|
||||||
|
:key="report.id"
|
||||||
|
v-bind="report"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./reports_tab.js"></script>
|
||||||
|
<style src="./reports_tab.scss" lang="scss"></style>
|
|
@ -15,7 +15,8 @@ import {
|
||||||
faTachometerAlt,
|
faTachometerAlt,
|
||||||
faCog,
|
faCog,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faList
|
faList,
|
||||||
|
faUserTie
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -30,7 +31,8 @@ library.add(
|
||||||
faTachometerAlt,
|
faTachometerAlt,
|
||||||
faCog,
|
faCog,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faList
|
faList,
|
||||||
|
faUserTie
|
||||||
)
|
)
|
||||||
|
|
||||||
const SideDrawer = {
|
const SideDrawer = {
|
||||||
|
@ -102,6 +104,9 @@ const SideDrawer = {
|
||||||
},
|
},
|
||||||
openSettingsModal () {
|
openSettingsModal () {
|
||||||
this.$store.dispatch('openSettingsModal')
|
this.$store.dispatch('openSettingsModal')
|
||||||
|
},
|
||||||
|
openModModal () {
|
||||||
|
this.$store.dispatch('openModModal')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,6 +143,21 @@
|
||||||
/> {{ $t("nav.about") }}
|
/> {{ $t("nav.about") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||||
|
@click="toggleDrawer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button-unstyled -link -fullwidth"
|
||||||
|
@click="openModModal"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="user-tie"
|
||||||
|
/> {{ $t("nav.moderation") }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
|
|
|
@ -64,8 +64,12 @@ export default {
|
||||||
settingsModalVisible () {
|
settingsModalVisible () {
|
||||||
return this.settingsModalState === 'visible'
|
return this.settingsModalState === 'visible'
|
||||||
},
|
},
|
||||||
|
modModalVisible () {
|
||||||
|
return this.modModalState === 'visible'
|
||||||
|
},
|
||||||
...mapState({
|
...mapState({
|
||||||
settingsModalState: state => state.interface.settingsModalState
|
settingsModalState: state => state.interface.settingsModalState,
|
||||||
|
modModalState: state => state.interface.modModalState
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
|
|
|
@ -266,6 +266,31 @@
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"previous": "Previous"
|
"previous": "Previous"
|
||||||
},
|
},
|
||||||
|
"moderation": {
|
||||||
|
"moderation": "Moderation",
|
||||||
|
"reports": {
|
||||||
|
"add_note": "Add note",
|
||||||
|
"close": "Close",
|
||||||
|
"delete_note": "Delete",
|
||||||
|
"delete_note_accept": "Yes, delete it",
|
||||||
|
"delete_note_cancel": "No, keep it",
|
||||||
|
"delete_note_confirm": "Are you sure you want to delete this note?",
|
||||||
|
"delete_note_title": "Confirm deletion",
|
||||||
|
"no_content": "No description given",
|
||||||
|
"note_placeholder": "Leave a note...",
|
||||||
|
"notes": "notes",
|
||||||
|
"reopen": "Reopen",
|
||||||
|
"report": "Report on",
|
||||||
|
"reports": "Reports",
|
||||||
|
"resolve": "Resolve",
|
||||||
|
"show_closed": "Show closed",
|
||||||
|
"statuses": "statuses",
|
||||||
|
"tag_policy_notice": "Enable the TagPolicy MRF to set post restrictions",
|
||||||
|
"tags": "Set post restrictions"
|
||||||
|
},
|
||||||
|
"statuses": "Statuses",
|
||||||
|
"users": "Users"
|
||||||
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
|
@ -282,6 +307,7 @@
|
||||||
"interactions": "Interactions",
|
"interactions": "Interactions",
|
||||||
"lists": "Lists",
|
"lists": "Lists",
|
||||||
"mentions": "Mentions",
|
"mentions": "Mentions",
|
||||||
|
"moderation": "Moderation",
|
||||||
"preferences": "Preferences",
|
"preferences": "Preferences",
|
||||||
"public_timeline_description": "Public posts from this instance",
|
"public_timeline_description": "Public posts from this instance",
|
||||||
"public_tl": "Public timeline",
|
"public_tl": "Public timeline",
|
||||||
|
|
|
@ -163,6 +163,7 @@ const api = {
|
||||||
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
dispatch('startFetchingTimeline', { timeline: 'friends' })
|
||||||
dispatch('startFetchingNotifications')
|
dispatch('startFetchingNotifications')
|
||||||
dispatch('startFetchingAnnouncements')
|
dispatch('startFetchingAnnouncements')
|
||||||
|
dispatch('startFetchingReports')
|
||||||
dispatch('pushGlobalNotice', {
|
dispatch('pushGlobalNotice', {
|
||||||
level: 'error',
|
level: 'error',
|
||||||
messageKey: 'timeline.socket_broke',
|
messageKey: 'timeline.socket_broke',
|
||||||
|
@ -280,6 +281,19 @@ const api = {
|
||||||
if (!fetcher) return
|
if (!fetcher) return
|
||||||
store.commit('removeFetcher', { fetcherName: 'announcements', fetcher })
|
store.commit('removeFetcher', { fetcherName: 'announcements', fetcher })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Reports
|
||||||
|
startFetchingReports (store) {
|
||||||
|
if (store.state.fetchers['reports']) return
|
||||||
|
const fetcher = store.state.backendInteractor.startFetchingReports({ store })
|
||||||
|
store.commit('addFetcher', { fetcherName: 'reports', fetcher })
|
||||||
|
},
|
||||||
|
stopFetchingReports (store) {
|
||||||
|
const fetcher = store.state.fetchers.reports
|
||||||
|
if (!fetcher) return
|
||||||
|
store.commit('removeFetcher', { fetcherName: 'reports', fetcher })
|
||||||
|
},
|
||||||
|
|
||||||
getSupportedTranslationlanguages (store) {
|
getSupportedTranslationlanguages (store) {
|
||||||
store.state.backendInteractor.getSupportedTranslationlanguages({ store })
|
store.state.backendInteractor.getSupportedTranslationlanguages({ store })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|
|
@ -2,6 +2,9 @@ const defaultState = {
|
||||||
settingsModalState: 'hidden',
|
settingsModalState: 'hidden',
|
||||||
settingsModalLoaded: false,
|
settingsModalLoaded: false,
|
||||||
settingsModalTargetTab: null,
|
settingsModalTargetTab: null,
|
||||||
|
modModalState: 'hidden',
|
||||||
|
modModalLoaded: false,
|
||||||
|
modModalTargetTab: null,
|
||||||
settings: {
|
settings: {
|
||||||
currentSaveStateNotice: null,
|
currentSaveStateNotice: null,
|
||||||
noticeClearTimeout: null,
|
noticeClearTimeout: null,
|
||||||
|
@ -63,6 +66,30 @@ const interfaceMod = {
|
||||||
setSettingsModalTargetTab (state, value) {
|
setSettingsModalTargetTab (state, value) {
|
||||||
state.settingsModalTargetTab = value
|
state.settingsModalTargetTab = value
|
||||||
},
|
},
|
||||||
|
closeModModal (state) {
|
||||||
|
state.modModalState = 'hidden'
|
||||||
|
},
|
||||||
|
togglePeekModModal (state) {
|
||||||
|
switch (state.modModalState) {
|
||||||
|
case 'minimized':
|
||||||
|
state.modModalState = 'visible'
|
||||||
|
return
|
||||||
|
case 'visible':
|
||||||
|
state.modModalState = 'minimized'
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
throw new Error('Illegal minimization state of mod modal')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openModModal (state) {
|
||||||
|
state.modModalState = 'visible'
|
||||||
|
if (!state.modModalLoaded) {
|
||||||
|
state.modModalLoaded = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setModModalTargetTab (state, value) {
|
||||||
|
state.modModalTargetTab = value
|
||||||
|
},
|
||||||
pushGlobalNotice (state, notice) {
|
pushGlobalNotice (state, notice) {
|
||||||
state.globalNotices.push(notice)
|
state.globalNotices.push(notice)
|
||||||
},
|
},
|
||||||
|
@ -105,6 +132,18 @@ const interfaceMod = {
|
||||||
commit('setSettingsModalTargetTab', value)
|
commit('setSettingsModalTargetTab', value)
|
||||||
commit('openSettingsModal')
|
commit('openSettingsModal')
|
||||||
},
|
},
|
||||||
|
closeModModal ({ commit }) {
|
||||||
|
commit('closeModModal')
|
||||||
|
},
|
||||||
|
openModModal ({ commit }) {
|
||||||
|
commit('openModModal')
|
||||||
|
},
|
||||||
|
togglePeekModModal ({ commit }) {
|
||||||
|
commit('togglePeekModModal')
|
||||||
|
},
|
||||||
|
clearModModalTargetTab ({ commit }) {
|
||||||
|
commit('setModModalTargetTab', null)
|
||||||
|
},
|
||||||
pushGlobalNotice (
|
pushGlobalNotice (
|
||||||
{ commit, dispatch, state },
|
{ commit, dispatch, state },
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import filter from 'lodash/filter'
|
import { filter, find, forEach, remove } from 'lodash'
|
||||||
|
|
||||||
|
const getReport = (state, id) => find(state.reports, { id })
|
||||||
|
const updateReport = (state, { report, param, value }) => {
|
||||||
|
getReport(state, report.id)[param] = value
|
||||||
|
}
|
||||||
|
|
||||||
const reports = {
|
const reports = {
|
||||||
state: {
|
state: {
|
||||||
userId: null,
|
userId: null,
|
||||||
statuses: [],
|
statuses: [],
|
||||||
preTickedIds: [],
|
preTickedIds: [],
|
||||||
modalActivated: false
|
modalActivated: false,
|
||||||
|
reports: []
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
openUserReportingModal (state, { userId, statuses, preTickedIds }) {
|
openUserReportingModal (state, { userId, statuses, preTickedIds }) {
|
||||||
|
@ -16,6 +22,38 @@ const reports = {
|
||||||
},
|
},
|
||||||
closeUserReportingModal (state) {
|
closeUserReportingModal (state) {
|
||||||
state.modalActivated = false
|
state.modalActivated = false
|
||||||
|
},
|
||||||
|
setReport (state, { report }) {
|
||||||
|
let existing = getReport(state, report.id)
|
||||||
|
if (existing) {
|
||||||
|
existing = report
|
||||||
|
} else {
|
||||||
|
state.reports.push(report)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateReportStates (state, { reports }) {
|
||||||
|
forEach(reports, (report) => {
|
||||||
|
updateReport(state, { report, param: 'state', value: report.state })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addNoteToReport (state, { id, note, user }) {
|
||||||
|
// akkoma doesn't return the note from this API endpoint, and there's no
|
||||||
|
// good way to get it. the note data is spoofed in the frontend until
|
||||||
|
// reload.
|
||||||
|
// definitely worth adding this to the backend at some point
|
||||||
|
const report = getReport(state, id)
|
||||||
|
const date = new Date()
|
||||||
|
|
||||||
|
report.notes.push({
|
||||||
|
content: note,
|
||||||
|
user,
|
||||||
|
created_at: date.toISOString(),
|
||||||
|
id: date.getTime()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteNoteFromReport (state, { id, note }) {
|
||||||
|
const report = getReport(state, id)
|
||||||
|
remove(report.notes, { id: note })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -31,6 +69,22 @@ const reports = {
|
||||||
},
|
},
|
||||||
closeUserReportingModal ({ commit }) {
|
closeUserReportingModal ({ commit }) {
|
||||||
commit('closeUserReportingModal')
|
commit('closeUserReportingModal')
|
||||||
|
},
|
||||||
|
updateReportStates ({ rootState, commit }, { reports }) {
|
||||||
|
commit('updateReportStates', { reports })
|
||||||
|
return rootState.api.backendInteractor.updateReportStates({ reports })
|
||||||
|
},
|
||||||
|
getReport ({ rootState, commit }, { id }) {
|
||||||
|
return rootState.api.backendInteractor.getReport({ id })
|
||||||
|
.then(report => commit('setReport', { report }))
|
||||||
|
},
|
||||||
|
addNoteToReport ({ rootState, commit }, { id, note }) {
|
||||||
|
commit('addNoteToReport', { id, note, user: rootState.users.currentUser })
|
||||||
|
return rootState.api.backendInteractor.addNoteToReport({ id, note })
|
||||||
|
},
|
||||||
|
deleteNoteFromReport ({ rootState, commit }, { id, note }) {
|
||||||
|
commit('deleteNoteFromReport', { id, note })
|
||||||
|
return rootState.api.backendInteractor.deleteNoteFromReport({ id, note })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { each, map, concat, last, get } from 'lodash'
|
import { each, map, concat, last, get } from 'lodash'
|
||||||
import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
@ -19,6 +19,9 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users'
|
||||||
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||||
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
||||||
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
|
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
|
||||||
|
const ADMIN_REPORTS_URL = '/api/v1/pleroma/admin/reports'
|
||||||
|
const ADMIN_REPORT_NOTES_URL = id => `/api/v1/pleroma/admin/reports/${id}/notes`
|
||||||
|
const ADMIN_REPORT_NOTE_URL = (report, note) => `/api/v1/pleroma/admin/reports/${report}/notes/${note}`
|
||||||
|
|
||||||
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
|
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
|
||||||
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
|
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
|
||||||
|
@ -342,7 +345,7 @@ const fetchUserRelationship = ({ id, credentials }) => {
|
||||||
return new Promise((resolve, reject) => response.json()
|
return new Promise((resolve, reject) => response.json()
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return reject(new StatusCodeError(response.status, json, { url }, response))
|
return reject(new StatusCodeError(400, json, { url }, response))
|
||||||
}
|
}
|
||||||
return resolve(json)
|
return resolve(json)
|
||||||
}))
|
}))
|
||||||
|
@ -635,6 +638,57 @@ const deleteUser = ({ credentials, user }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getReports = ({ state, limit, page, pageSize, credentials }) => {
|
||||||
|
let url = ADMIN_REPORTS_URL
|
||||||
|
const args = [
|
||||||
|
state && `state=${state}`,
|
||||||
|
limit && `limit=${limit}`,
|
||||||
|
page && `page=${page}`,
|
||||||
|
pageSize && `page_size=${pageSize}`
|
||||||
|
].filter(_ => _).join('&')
|
||||||
|
|
||||||
|
url = url + (args ? '?' + args : '')
|
||||||
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
|
.then((data) => data.json())
|
||||||
|
.then((data) => data.reports.map(parseReport))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateReportStates = ({ credentials, reports }) => {
|
||||||
|
// reports syntax: [{ id: int, state: string }...]
|
||||||
|
const updates = {
|
||||||
|
reports: reports.map(report => {
|
||||||
|
return {
|
||||||
|
id: report.id.toString(),
|
||||||
|
state: report.state
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url: ADMIN_REPORTS_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: updates,
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNoteToReport = ({ id, note, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: ADMIN_REPORT_NOTES_URL(id),
|
||||||
|
method: 'POST',
|
||||||
|
payload: { content: note },
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteNoteFromReport = ({ report, note, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: ADMIN_REPORT_NOTE_URL(report, note),
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const fetchTimeline = ({
|
const fetchTimeline = ({
|
||||||
timeline,
|
timeline,
|
||||||
credentials,
|
credentials,
|
||||||
|
@ -1726,7 +1780,11 @@ const apiService = {
|
||||||
getSettingsProfile,
|
getSettingsProfile,
|
||||||
saveSettingsProfile,
|
saveSettingsProfile,
|
||||||
listSettingsProfiles,
|
listSettingsProfiles,
|
||||||
deleteSettingsProfile
|
deleteSettingsProfile,
|
||||||
|
getReports,
|
||||||
|
updateReportStates,
|
||||||
|
addNoteToReport,
|
||||||
|
deleteNoteFromReport
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
@ -5,6 +5,7 @@ import followRequestFetcher from '../../services/follow_request_fetcher/follow_r
|
||||||
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
|
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
|
||||||
import announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js'
|
import announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js'
|
||||||
import configFetcher from '../config_fetcher/config_fetcher.service.js'
|
import configFetcher from '../config_fetcher/config_fetcher.service.js'
|
||||||
|
import reportsFetcher from '../reports_fetcher/reports_fetcher.service.js'
|
||||||
|
|
||||||
const backendInteractorService = credentials => ({
|
const backendInteractorService = credentials => ({
|
||||||
startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) {
|
startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) {
|
||||||
|
@ -39,6 +40,10 @@ const backendInteractorService = credentials => ({
|
||||||
return announcementsFetcher.startFetching({ store, credentials })
|
return announcementsFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
startFetchingReports ({ store, state, limit, page, pageSize }) {
|
||||||
|
return reportsFetcher.startFetching({ store, credentials, state, limit, page, pageSize })
|
||||||
|
},
|
||||||
|
|
||||||
startUserSocket ({ store }) {
|
startUserSocket ({ store }) {
|
||||||
const serv = store.rootState.instance.server.replace('http', 'ws')
|
const serv = store.rootState.instance.server.replace('http', 'ws')
|
||||||
const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
|
const url = serv + getMastodonSocketURI({ credentials, stream: 'user' })
|
||||||
|
|
|
@ -429,6 +429,24 @@ export const parseNotification = (data) => {
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const parseReport = (data) => {
|
||||||
|
const report = {}
|
||||||
|
|
||||||
|
report.account = parseUser(data.account)
|
||||||
|
report.actor = parseUser(data.actor)
|
||||||
|
report.statuses = data.statuses.map(parseStatus)
|
||||||
|
report.notes = data.notes.map(note => {
|
||||||
|
note.user = parseUser(note.user)
|
||||||
|
return note
|
||||||
|
})
|
||||||
|
report.state = data.state
|
||||||
|
report.content = data.content
|
||||||
|
report.created_at = data.created_at
|
||||||
|
report.id = data.id
|
||||||
|
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
const isNsfw = (status) => {
|
const isNsfw = (status) => {
|
||||||
const nsfwRegex = /#nsfw/i
|
const nsfwRegex = /#nsfw/i
|
||||||
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
|
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
|
||||||
|
|
20
src/services/reports_fetcher/reports_fetcher.service.js
Normal file
20
src/services/reports_fetcher/reports_fetcher.service.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import apiService from '../api/api.service.js'
|
||||||
|
import { promiseInterval } from '../promise_interval/promise_interval.js'
|
||||||
|
import { forEach } from 'lodash'
|
||||||
|
|
||||||
|
const fetchAndUpdate = ({ store, credentials, state, limit, page, pageSize }) => {
|
||||||
|
return apiService.getReports({ credentials, state, limit, page, pageSize })
|
||||||
|
.then(reports => forEach(reports, report => store.commit('setReport', { report })))
|
||||||
|
}
|
||||||
|
|
||||||
|
const startFetching = ({ store, credentials, state, limit, page, pageSize }) => {
|
||||||
|
const boundFetchAndUpdate = () => fetchAndUpdate({ store, credentials, state, limit, page, pageSize })
|
||||||
|
boundFetchAndUpdate()
|
||||||
|
return promiseInterval(boundFetchAndUpdate, 60000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportsFetcher = {
|
||||||
|
startFetching
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reportsFetcher
|
31
yarn.lock
31
yarn.lock
|
@ -1092,12 +1092,12 @@
|
||||||
|
|
||||||
"@fortawesome/fontawesome-common-types@^0.3.0":
|
"@fortawesome/fontawesome-common-types@^0.3.0":
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz#949995a05c0d8801be7e0a594f775f1dbaa0d893"
|
||||||
integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w==
|
integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w==
|
||||||
|
|
||||||
"@fortawesome/fontawesome-svg-core@1.3.0":
|
"@fortawesome/fontawesome-svg-core@1.3.0":
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz#343fac91fa87daa630d26420bfedfba560f85885"
|
||||||
integrity sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg==
|
integrity sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fortawesome/fontawesome-common-types" "^0.3.0"
|
"@fortawesome/fontawesome-common-types" "^0.3.0"
|
||||||
|
@ -1141,9 +1141,9 @@
|
||||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||||
|
|
||||||
"@intlify/bundle-utils@next":
|
"@intlify/bundle-utils@next":
|
||||||
version "3.1.2"
|
version "3.2.1"
|
||||||
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.1.2.tgz"
|
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.2.1.tgz"
|
||||||
integrity sha512-amgSo0NN5OKWYdcgFmfJqo2tcUcZ6C66Bxm5ALQnB0m3MUQtS9aJzKoIo+EU9XQiOVmlBFxRtNoZm+psHa5FNA==
|
integrity sha512-rf4cLBOnbqmpXVcCdcYHilZpMt1m82syh3WLBJlZvGxN2KkH9HeHVH4+bnibF/SDXCHNh6lM6wTpS/qw+PkcMg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@intlify/message-compiler" next
|
"@intlify/message-compiler" next
|
||||||
"@intlify/shared" next
|
"@intlify/shared" next
|
||||||
|
@ -1168,7 +1168,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@intlify/shared" "9.2.2"
|
"@intlify/shared" "9.2.2"
|
||||||
|
|
||||||
"@intlify/message-compiler@9.2.2":
|
"@intlify/message-compiler@9.2.2", "@intlify/message-compiler@next":
|
||||||
version "9.2.2"
|
version "9.2.2"
|
||||||
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz"
|
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz"
|
||||||
integrity sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==
|
integrity sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==
|
||||||
|
@ -1176,24 +1176,11 @@
|
||||||
"@intlify/shared" "9.2.2"
|
"@intlify/shared" "9.2.2"
|
||||||
source-map "0.6.1"
|
source-map "0.6.1"
|
||||||
|
|
||||||
"@intlify/message-compiler@next":
|
|
||||||
version "9.3.0-beta.3"
|
|
||||||
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.3.0-beta.3.tgz"
|
|
||||||
integrity sha512-j8OwToBQgs01RBMX4GCDNQfcnmw3AiDG3moKIONTrfXcf+1yt/rWznLTYH/DXbKcFMAFijFpCzMYjUmH1jVFYA==
|
|
||||||
dependencies:
|
|
||||||
"@intlify/shared" "9.3.0-beta.3"
|
|
||||||
source-map "0.6.1"
|
|
||||||
|
|
||||||
"@intlify/shared@9.2.2", "@intlify/shared@next":
|
"@intlify/shared@9.2.2", "@intlify/shared@next":
|
||||||
version "9.2.2"
|
version "9.2.2"
|
||||||
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz"
|
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz"
|
||||||
integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==
|
integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==
|
||||||
|
|
||||||
"@intlify/shared@9.3.0-beta.3":
|
|
||||||
version "9.3.0-beta.3"
|
|
||||||
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.3.0-beta.3.tgz"
|
|
||||||
integrity sha512-Z/0TU4GhFKRxKh+0RbwJExik9zz57gXYgxSYaPn7YQdkQ/pabSioCY/SXnYxQHL6HzULF5tmqarFm6glbGqKhw==
|
|
||||||
|
|
||||||
"@intlify/vue-devtools@9.2.2":
|
"@intlify/vue-devtools@9.2.2":
|
||||||
version "9.2.2"
|
version "9.2.2"
|
||||||
resolved "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz"
|
resolved "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz"
|
||||||
|
@ -8095,9 +8082,9 @@ mute-stream@0.0.7:
|
||||||
integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==
|
integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==
|
||||||
|
|
||||||
nan@^2.12.1:
|
nan@^2.12.1:
|
||||||
version "2.16.0"
|
version "2.17.0"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||||
integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
|
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||||
|
|
||||||
nanoid@^3.3.4:
|
nanoid@^3.3.4:
|
||||||
version "3.3.4"
|
version "3.3.4"
|
||||||
|
|
Loading…
Reference in a new issue