Merge pull request 'Admin Dashboard User Management' (#3503) from admin-users into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3503
This commit is contained in:
commit
00afd8fa7e
87 changed files with 3620 additions and 3828 deletions
1
changelog.d/user-management.add
Normal file
1
changelog.d/user-management.add
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
User administration + post scope/sensitivity admin change support
|
||||||
63
src/App.scss
63
src/App.scss
|
|
@ -411,6 +411,14 @@ nav {
|
||||||
button:not(.button-default) {
|
button:not(.button-default) {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
|
text-align: initial;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
display: inline;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
|
@ -418,45 +426,6 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
|
||||||
border-color: var(--border);
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0;
|
|
||||||
border-top-width: 1px;
|
|
||||||
|
|
||||||
&.-active,
|
|
||||||
&:hover {
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.-active + &,
|
|
||||||
&:hover + & {
|
|
||||||
border-top-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover + .menu-item-collapsible:not(.-expanded) + &,
|
|
||||||
&.-active + .menu-item-collapsible:not(.-expanded) + & {
|
|
||||||
border-top-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-expanded="true"] {
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top-right-radius: var(--roundness);
|
|
||||||
border-top-left-radius: var(--roundness);
|
|
||||||
border-top-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom-right-radius: var(--roundness);
|
|
||||||
border-bottom-left-radius: var(--roundness);
|
|
||||||
border-bottom-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item,
|
.menu-item,
|
||||||
.list-item {
|
.list-item {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -475,22 +444,6 @@ nav {
|
||||||
--__line-height: 1.5em;
|
--__line-height: 1.5em;
|
||||||
--__horizontal-gap: 0.75em;
|
--__horizontal-gap: 0.75em;
|
||||||
--__vertical-gap: 0.5em;
|
--__vertical-gap: 0.5em;
|
||||||
|
|
||||||
&.-non-interactive {
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
button:not(.button-default) {
|
|
||||||
text-align: initial;
|
|
||||||
padding: 0;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
display: inline;
|
|
||||||
font-family: inherit;
|
|
||||||
line-height: unset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-unstyled {
|
.button-unstyled {
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,13 @@ export default (store) => {
|
||||||
() => import('src/components/user_profile/user_profile.vue'),
|
() => import('src/components/user_profile/user_profile.vue'),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'user-profile-admin-view',
|
||||||
|
path: '/users/$:id/admin_view',
|
||||||
|
component: defineAsyncComponent(
|
||||||
|
() => import('src/components/user_profile/user_profile_admin_view.vue'),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'interactions',
|
name: 'interactions',
|
||||||
path: '/users/:username/interactions',
|
path: '/users/:username/interactions',
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export default {
|
||||||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||||
variants: {
|
variants: {
|
||||||
normal: '.neutral',
|
normal: '.neutral',
|
||||||
|
info: '.info',
|
||||||
error: '.error',
|
error: '.error',
|
||||||
warning: '.warning',
|
warning: '.warning',
|
||||||
success: '.success',
|
success: '.success',
|
||||||
|
|
@ -47,5 +48,11 @@ export default {
|
||||||
background: '--cGreen',
|
background: '--cGreen',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
variant: 'info',
|
||||||
|
directives: {
|
||||||
|
background: '--cBlue',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,7 @@ const Announcement = {
|
||||||
canEditAnnouncement() {
|
canEditAnnouncement() {
|
||||||
return (
|
return (
|
||||||
this.currentUser &&
|
this.currentUser &&
|
||||||
this.currentUser.privileges.includes(
|
this.currentUser.privileges.has('announcements_manage_announcements')
|
||||||
'announcements_manage_announcements',
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content() {
|
content() {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@ import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
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'
|
||||||
|
|
||||||
const BasicUserCard = {
|
const BasicUserCard = {
|
||||||
props: ['user'],
|
props: {
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
showLineLabels: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
UserPopover,
|
UserPopover,
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@
|
||||||
:title="user.name"
|
:title="user.name"
|
||||||
class="basic-user-card-user-name"
|
class="basic-user-card-user-name"
|
||||||
>
|
>
|
||||||
|
<strong v-if="showLineLabels">
|
||||||
|
{{ $t('admin_dash.users.labels.name_colon') }}
|
||||||
|
{{ ' ' }}
|
||||||
|
</strong>
|
||||||
<RichContent
|
<RichContent
|
||||||
class="basic-user-card-user-name-value"
|
class="basic-user-card-user-name-value"
|
||||||
:html="user.name"
|
:html="user.name"
|
||||||
|
|
@ -31,6 +35,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<strong v-if="showLineLabels">
|
||||||
|
{{ $t('admin_dash.users.labels.handle_colon') }}
|
||||||
|
{{ ' ' }}
|
||||||
|
</strong>
|
||||||
<user-link
|
<user-link
|
||||||
class="basic-user-card-screen-name"
|
class="basic-user-card-screen-name"
|
||||||
:user="user"
|
:user="user"
|
||||||
|
|
@ -48,6 +56,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
line-height: 1.25;
|
||||||
|
|
||||||
--emoji-size: 1em;
|
--emoji-size: 1em;
|
||||||
|
|
||||||
|
|
@ -69,7 +78,7 @@
|
||||||
|
|
||||||
&-user-name-value,
|
&-user-name-value,
|
||||||
&-screen-name {
|
&-screen-name {
|
||||||
display: inline-block;
|
display: inline;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<basic-user-card :user="user">
|
<BasicUserCard :user="user">
|
||||||
<div class="block-card-content-container">
|
<div class="block-card-content-container">
|
||||||
<span
|
<span
|
||||||
v-if="blocked && blockExpiryAvailable"
|
v-if="blocked && blockExpiryAvailable"
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
:is-mute="false"
|
:is-mute="false"
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
</basic-user-card>
|
</BasicUserCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./block_card.js"></script>
|
<script src="./block_card.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
v-if="sortedChatList.length > 0"
|
v-if="sortedChatList.length > 0"
|
||||||
class="timeline"
|
class="timeline"
|
||||||
>
|
>
|
||||||
<List :items="sortedChatList">
|
<List :external-items="sortedChatList">
|
||||||
<template #item="{item}">
|
<template #item="{item}">
|
||||||
<ChatListItem
|
<ChatListItem
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(faCircleQuestion)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component emits the following events:
|
* This component emits the following events:
|
||||||
* cancelled, emitted when the action should not be performed;
|
* cancelled, emitted when the action should not be performed;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,20 @@
|
||||||
<span v-text="title" />
|
<span v-text="title" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<slot />
|
<div class="content">
|
||||||
|
<FAIcon
|
||||||
|
class="confirm-icon"
|
||||||
|
icon="circle-question"
|
||||||
|
size="3x"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
<div class="text">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="below">
|
||||||
|
<slot name="below" />
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<slot name="footerLeft" />
|
<slot name="footerLeft" />
|
||||||
<button
|
<button
|
||||||
|
|
@ -29,3 +41,43 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./confirm_modal.js"></script>
|
<script src="./confirm_modal.js"></script>
|
||||||
|
<style lang="scss">
|
||||||
|
.confirm-modal {
|
||||||
|
.confirm-icon {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.below:not(:empty) {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
max-width: 50ch;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: 3.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<List
|
<List
|
||||||
:items="drafts"
|
:external-items="drafts"
|
||||||
:non-interactive="true"
|
:non-interactive="true"
|
||||||
>
|
>
|
||||||
<template #item="{ item: draft }">
|
<template #item="{ item: draft }">
|
||||||
|
|
@ -58,6 +58,7 @@
|
||||||
.Drafts {
|
.Drafts {
|
||||||
.draft {
|
.draft {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-all {
|
.remove-all {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const Interactions = {
|
||||||
allowFollowingMove:
|
allowFollowingMove:
|
||||||
this.$store.state.users.currentUser.allow_following_move,
|
this.$store.state.users.currentUser.allow_following_move,
|
||||||
filterMode: tabModeDict.mentions,
|
filterMode: tabModeDict.mentions,
|
||||||
canSeeReports: this.$store.state.users.currentUser.privileges.includes(
|
canSeeReports: this.$store.state.users.currentUser.privileges.has(
|
||||||
'reports_manage_reports',
|
'reports_manage_reports',
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
78
src/components/list/list.css
Normal file
78
src/components/list/list.css
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
.List {
|
||||||
|
--__line-height: 1.5em;
|
||||||
|
--__horizontal-gap: 0.75em;
|
||||||
|
--__vertical-gap: 0.5em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&[aria-expanded="true"] {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-right-radius: var(--roundness);
|
||||||
|
border-top-left-radius: var(--roundness);
|
||||||
|
border-top-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-right-radius: var(--roundness);
|
||||||
|
border-bottom-left-radius: var(--roundness);
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px dotted var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
border-bottom-color: var(--border);
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 0.9em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper {
|
||||||
|
padding-right: var(--__horizontal-gap);
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-scrollable {
|
||||||
|
overflow-y: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
158
src/components/list/list.js
Normal file
158
src/components/list/list.js
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const List = {
|
||||||
|
props: {
|
||||||
|
boxOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
fetchFunction: {
|
||||||
|
type: Function,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
getKey: {
|
||||||
|
type: Function,
|
||||||
|
default: (item) => item.id,
|
||||||
|
},
|
||||||
|
getClass: {
|
||||||
|
type: Function,
|
||||||
|
default: () => '',
|
||||||
|
},
|
||||||
|
preSelect: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
nonInteractive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
scrollable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
selectable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
externalItems: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['fetchRequested', 'select'],
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
selected: new Set(this.preSelect),
|
||||||
|
loading: false,
|
||||||
|
bottomedOut: true,
|
||||||
|
error: null,
|
||||||
|
page: 1,
|
||||||
|
total: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
allKeys() {
|
||||||
|
return new Set(this.finalItems.map(this.getKey))
|
||||||
|
},
|
||||||
|
selectedItems() {
|
||||||
|
return this.items.filter((item) => this.selected.has(this.getKey(item)))
|
||||||
|
},
|
||||||
|
allSelected() {
|
||||||
|
return (
|
||||||
|
this.selected.size !== 0 &&
|
||||||
|
this.selected.size === this.finalItems.length
|
||||||
|
)
|
||||||
|
},
|
||||||
|
noneSelected() {
|
||||||
|
return this.selected.size === 0
|
||||||
|
},
|
||||||
|
someSelected() {
|
||||||
|
return !this.allSelected && !this.noneSelected
|
||||||
|
},
|
||||||
|
finalItems() {
|
||||||
|
return this.externalItems || this.items
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
window.addEventListener('scroll', this.scrollLoad)
|
||||||
|
|
||||||
|
if (this.fetchFunction && this.items.length === 0) {
|
||||||
|
this.fetchEntries()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
window.removeEventListener('scroll', this.scrollLoad)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchEntries() {
|
||||||
|
if (this.loading) return
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
this.fetchFunction(this.page)
|
||||||
|
.then((result) => {
|
||||||
|
this.loading = false
|
||||||
|
this.bottomedOut = isEmpty(result.items)
|
||||||
|
if (this.externalItems) return
|
||||||
|
this.page += 1
|
||||||
|
this.total = result.count
|
||||||
|
this.items.push(...result.items)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.loading = false
|
||||||
|
this.error = error
|
||||||
|
console.error('Error loading list data:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.items = []
|
||||||
|
this.page = 1
|
||||||
|
this.total = null
|
||||||
|
this.error = null
|
||||||
|
this.loading = false
|
||||||
|
this.fetchEntries()
|
||||||
|
},
|
||||||
|
scrollLoad(e) {
|
||||||
|
if (this.fetchFunction) {
|
||||||
|
const bodyBRect = document.body.getBoundingClientRect()
|
||||||
|
const height = Math.max(bodyBRect.height, -bodyBRect.y)
|
||||||
|
if (
|
||||||
|
this.$el.offsetHeight > 0 &&
|
||||||
|
window.innerHeight + window.pageYOffset >= height - 750
|
||||||
|
) {
|
||||||
|
this.fetchEntries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isSelected(item) {
|
||||||
|
return this.selected.has(this.getKey(item))
|
||||||
|
},
|
||||||
|
toggle(checked, item) {
|
||||||
|
const key = this.getKey(item)
|
||||||
|
if (checked) {
|
||||||
|
this.selected.add(key)
|
||||||
|
} else {
|
||||||
|
this.selected.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('select', this.selected)
|
||||||
|
},
|
||||||
|
toggleAll(value) {
|
||||||
|
if (value) {
|
||||||
|
this.selected = new Set([...this.allKeys])
|
||||||
|
} else {
|
||||||
|
this.selected = new Set([])
|
||||||
|
}
|
||||||
|
this.$emit('select', this.selected)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default List
|
||||||
|
|
@ -1,48 +1,90 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="list"
|
class="List"
|
||||||
role="list"
|
role="list"
|
||||||
|
:class="{ '-scrollable': scrollable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in items"
|
v-if="selectable"
|
||||||
:key="getKey(item)"
|
class="header"
|
||||||
class="list-item"
|
|
||||||
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
|
||||||
role="listitem"
|
|
||||||
>
|
>
|
||||||
<slot
|
<div class="checkbox-wrapper">
|
||||||
name="item"
|
<Checkbox
|
||||||
:item="item"
|
:model-value="allSelected"
|
||||||
/>
|
:indeterminate="someSelected"
|
||||||
|
@update:model-value="toggleAll"
|
||||||
|
>
|
||||||
|
{{ $t('selectable_list.select_all') }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<slot
|
||||||
|
name="header"
|
||||||
|
:selected="selectedItems"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="items.length === 0 && !!$slots.empty"
|
class="list"
|
||||||
class="list-empty-content faint"
|
role="list"
|
||||||
>
|
>
|
||||||
<slot name="empty" />
|
<div
|
||||||
|
v-for="item in finalItems"
|
||||||
|
:key="getKey(item)"
|
||||||
|
class="list-item"
|
||||||
|
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
||||||
|
role="listitem"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="selectable"
|
||||||
|
class="checkbox-wrapper"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
:model-value="isSelected(item)"
|
||||||
|
@update:model-value="checked => toggle(checked, item)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<slot
|
||||||
|
name="item"
|
||||||
|
:item="item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="finalItems.length === 0 && !!$slots.empty"
|
||||||
|
class="list-item list-empty-content faint"
|
||||||
|
>
|
||||||
|
<slot name="empty" />
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<button
|
||||||
|
v-if="error"
|
||||||
|
class="button-unstyled -link -fullwidth alert error"
|
||||||
|
@click="fetchEntries"
|
||||||
|
>
|
||||||
|
{{ $t('general.generic_error') }}
|
||||||
|
</button>
|
||||||
|
<FAIcon
|
||||||
|
v-else-if="loading"
|
||||||
|
spin
|
||||||
|
icon="circle-notch"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-else-if="!bottomedOut"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="fetchEntries"
|
||||||
|
>
|
||||||
|
{{ $t('general.more') }}
|
||||||
|
</a>
|
||||||
|
<span v-else-if="finalItems.length !== 0">
|
||||||
|
{{ $t('general.no_more') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script src="./list.js"></script>
|
||||||
export default {
|
|
||||||
props: {
|
<style src="./list.css"></style>
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
getKey: {
|
|
||||||
type: Function,
|
|
||||||
default: (item) => item.id,
|
|
||||||
},
|
|
||||||
getClass: {
|
|
||||||
type: Function,
|
|
||||||
default: () => '',
|
|
||||||
},
|
|
||||||
nonInteractive: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@
|
||||||
</div>
|
</div>
|
||||||
<tab-switcher
|
<tab-switcher
|
||||||
class="list-member-management"
|
class="list-member-management"
|
||||||
:scrollable-tabs="true"
|
:scrollable-tabs
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="id || addedUserIds.size > 0"
|
v-if="id || addedUserIds.size > 0"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
import { last } from 'lodash'
|
||||||
|
|
||||||
|
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||||
import Popover from 'src/components/popover/popover.vue'
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||||
|
|
||||||
|
|
@ -16,41 +19,382 @@ const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription'
|
||||||
const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'
|
const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'
|
||||||
const SANDBOX = 'mrf_tag:sandbox'
|
const SANDBOX = 'mrf_tag:sandbox'
|
||||||
const QUARANTINE = 'mrf_tag:quarantine'
|
const QUARANTINE = 'mrf_tag:quarantine'
|
||||||
|
const TAGS = new Set([
|
||||||
|
FORCE_NSFW,
|
||||||
|
STRIP_MEDIA,
|
||||||
|
FORCE_UNLISTED,
|
||||||
|
DISABLE_REMOTE_SUBSCRIPTION,
|
||||||
|
DISABLE_ANY_SUBSCRIPTION,
|
||||||
|
SANDBOX,
|
||||||
|
QUARANTINE,
|
||||||
|
])
|
||||||
|
|
||||||
|
const ENTRIES = [
|
||||||
|
{
|
||||||
|
check: '!state:activated',
|
||||||
|
label: 'user_card.admin_menu.activate_account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'state:activated',
|
||||||
|
label: 'user_card.admin_menu.deactivate_account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
separator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: '!state:confirmed',
|
||||||
|
label: 'user_card.admin_menu.confirm_account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'action:resend_confirmation',
|
||||||
|
conditions: ['!state:confirmed'],
|
||||||
|
label: 'user_card.admin_menu.resend_confirmation',
|
||||||
|
},
|
||||||
|
// No API for revocation
|
||||||
|
// {
|
||||||
|
// check: 'state:confirmed',
|
||||||
|
// label: 'user_card.admin_menu.unconfirm_account',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
check: '!state:approved',
|
||||||
|
conditions: ['property:local'],
|
||||||
|
label: 'user_card.admin_menu.approve_account',
|
||||||
|
},
|
||||||
|
// No API for revocation
|
||||||
|
// {
|
||||||
|
// check: 'state:approved',
|
||||||
|
// label: 'user_card.admin_menu.unapprove_account',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
check: '!state:suggested',
|
||||||
|
// conditions: ['property:local'], // TODO Should we allow non-local users in suggested?
|
||||||
|
label: 'user_card.admin_menu.suggest_account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'state:suggested',
|
||||||
|
label: 'user_card.admin_menu.remove_suggested_account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
separator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'action:statuses',
|
||||||
|
label: 'user_card.admin_menu.show_statuses',
|
||||||
|
conditions: ['count:1'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
separator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'action:disable_mfa',
|
||||||
|
label: 'user_card.admin_menu.disable_mfa',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'action:require_password_change',
|
||||||
|
label: 'user_card.admin_menu.require_password_change',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
separator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: '!rights:moderator',
|
||||||
|
label: 'user_card.admin_menu.grant_moderator',
|
||||||
|
conditions: ['property:local', 'state:activated'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'rights:moderator',
|
||||||
|
label: 'user_card.admin_menu.revoke_moderator',
|
||||||
|
conditions: ['property:local', 'state:activated'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: '!rights:admin',
|
||||||
|
label: 'user_card.admin_menu.grant_admin',
|
||||||
|
conditions: ['property:local', 'state:activated'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'rights:admin',
|
||||||
|
label: 'user_card.admin_menu.revoke_admin',
|
||||||
|
conditions: ['property:local', 'state:activated'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
separator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: FORCE_NSFW,
|
||||||
|
label: 'user_card.admin_menu.force_nsfw',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: STRIP_MEDIA,
|
||||||
|
label: 'user_card.admin_menu.strip_media',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: FORCE_UNLISTED,
|
||||||
|
label: 'user_card.admin_menu.force_unlisted',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: SANDBOX,
|
||||||
|
label: 'user_card.admin_menu.sandbox',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: DISABLE_ANY_SUBSCRIPTION,
|
||||||
|
conditions: ['property:local'],
|
||||||
|
label: 'user_card.admin_menu.disable_any_subscription',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: DISABLE_REMOTE_SUBSCRIPTION,
|
||||||
|
conditions: ['property:local'],
|
||||||
|
label: 'user_card.admin_menu.disable_remote_subscription',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: QUARANTINE,
|
||||||
|
conditions: ['property:local'],
|
||||||
|
label: 'user_card.admin_menu.quarantine',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
separator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: 'action:delete',
|
||||||
|
label: 'user_card.admin_menu.delete_account',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const ModerationTools = {
|
const ModerationTools = {
|
||||||
props: ['user'],
|
props: {
|
||||||
|
users: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.users.length !== 1) return
|
||||||
|
useAdminSettingsStore().getUserData({ user: this.users[0] })
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tags: {
|
open: false,
|
||||||
FORCE_NSFW,
|
confirmDialogShow: false,
|
||||||
STRIP_MEDIA,
|
confirmDialogTitle: null,
|
||||||
FORCE_UNLISTED,
|
confirmDialogContent: null,
|
||||||
DISABLE_REMOTE_SUBSCRIPTION,
|
confirmDialogConfirm: null,
|
||||||
DISABLE_ANY_SUBSCRIPTION,
|
confirmDialogAction: null,
|
||||||
SANDBOX,
|
confirmDialogGroup: null,
|
||||||
QUARANTINE,
|
confirmDialogName: null,
|
||||||
},
|
|
||||||
showDeleteUserDialog: false,
|
|
||||||
toggled: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
DialogModal,
|
ConfirmModal,
|
||||||
Popover,
|
Popover,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
ready() {
|
||||||
|
return this.users.every((u) => u.adminData)
|
||||||
|
},
|
||||||
|
entries() {
|
||||||
|
return ENTRIES.map(({ check, label, separator, conditions }) => {
|
||||||
|
if (separator) return 'separator'
|
||||||
|
const [, negateToken, group, name] =
|
||||||
|
/^([!~]?)([a-z-_]+):([a-z-_]+)$/.exec(check)
|
||||||
|
|
||||||
|
const hasTag = this.tagsSet.has(`${group}:${name}`)
|
||||||
|
const noTag = this.tagsSet.has(`!${group}:${name}`)
|
||||||
|
const maybeTag = this.tagsSet.has(`~${group}:${name}`)
|
||||||
|
|
||||||
|
// We are checking for condition to show element, i.e. only show "activate" if user is "deactivated"
|
||||||
|
const checkNegated = negateToken === '!' || negateToken === '~'
|
||||||
|
|
||||||
|
// Naturally, new value should also be the same
|
||||||
|
const value = checkNegated
|
||||||
|
|
||||||
|
const action = (() => {
|
||||||
|
switch (group) {
|
||||||
|
case 'rights':
|
||||||
|
return () => this.setRight(name, value)
|
||||||
|
case 'state':
|
||||||
|
return () => this.setStatus(name, value)
|
||||||
|
case 'mrf_tag':
|
||||||
|
return () => this.setTag(`${group}:${name}`, noTag)
|
||||||
|
case 'action': {
|
||||||
|
switch (name) {
|
||||||
|
case 'delete':
|
||||||
|
return () => this.deleteUsers()
|
||||||
|
case 'resend_confirmation':
|
||||||
|
return () => this.resendConfirmationEmail()
|
||||||
|
case 'disable_mfa':
|
||||||
|
return () => this.disableMFA()
|
||||||
|
case 'statuses':
|
||||||
|
return () =>
|
||||||
|
this.$router.push(`/users/\$${this.users[0].id}/admin_view`)
|
||||||
|
case 'require_password_change':
|
||||||
|
return () => this.requirePasswordChange()
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown action group: ${name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown moderation group: ${group}`)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
let checkboxClass = ''
|
||||||
|
if (maybeTag) {
|
||||||
|
checkboxClass = 'menu-checkbox-indeterminate'
|
||||||
|
} else if (hasTag) {
|
||||||
|
checkboxClass = 'menu-checkbox-checked'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
check,
|
||||||
|
negateToken,
|
||||||
|
checkbox: group === 'mrf_tag',
|
||||||
|
checkboxClass,
|
||||||
|
conditions,
|
||||||
|
group,
|
||||||
|
name,
|
||||||
|
action,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((entry) => {
|
||||||
|
if (entry === 'separator') return true
|
||||||
|
const { group, name, value, conditions } = entry
|
||||||
|
|
||||||
|
if (conditions) {
|
||||||
|
// Checking that all items match positive criteria
|
||||||
|
const positive = conditions.every((condition) =>
|
||||||
|
this.totalSet.has(condition),
|
||||||
|
)
|
||||||
|
// Checking that there are no items that don't match criteria
|
||||||
|
const negative = conditions.some((condition) =>
|
||||||
|
this.totalSet.has('!' + condition),
|
||||||
|
)
|
||||||
|
if (!(positive && !negative)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (group) {
|
||||||
|
case 'action':
|
||||||
|
if (name === 'statuses') return this.privileged('users_read')
|
||||||
|
else return true
|
||||||
|
case 'rights':
|
||||||
|
return this.canGrantRole(name, value)
|
||||||
|
case 'state':
|
||||||
|
return this.canChangeState(name, value)
|
||||||
|
case 'mrf_tag':
|
||||||
|
return this.canUseTagPolicy
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unknown moderation group: ${group}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reduce((acc, entry, index) => {
|
||||||
|
// Removing any double separators as well
|
||||||
|
// as separators at very end and bery beginning
|
||||||
|
if (entry === 'separator') {
|
||||||
|
if (
|
||||||
|
acc.length === 0 ||
|
||||||
|
last(acc) === 'separator' ||
|
||||||
|
index === ENTRIES.length - 1
|
||||||
|
) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...acc, entry]
|
||||||
|
}, [])
|
||||||
|
},
|
||||||
|
rightsSet() {
|
||||||
|
return this.users.reduce((acc, user) => {
|
||||||
|
if (user.rights.admin) {
|
||||||
|
acc.add('rights:admin')
|
||||||
|
} else {
|
||||||
|
acc.add('!rights:admin')
|
||||||
|
}
|
||||||
|
if (user.rights.moderator) {
|
||||||
|
acc.add('rights:moderator')
|
||||||
|
} else {
|
||||||
|
acc.add('!rights:moderator')
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, new Set())
|
||||||
|
},
|
||||||
|
stateSet() {
|
||||||
|
return this.users.reduce((acc, user) => {
|
||||||
|
if (!user.deactivated) {
|
||||||
|
acc.add('state:activated')
|
||||||
|
} else {
|
||||||
|
acc.add('!state:activated')
|
||||||
|
}
|
||||||
|
if (user.adminData?.is_confirmed) {
|
||||||
|
acc.add('state:confirmed')
|
||||||
|
} else {
|
||||||
|
acc.add('!state:confirmed')
|
||||||
|
}
|
||||||
|
if (user.adminData?.is_approved) {
|
||||||
|
acc.add('state:approved')
|
||||||
|
} else {
|
||||||
|
acc.add('!state:approved')
|
||||||
|
}
|
||||||
|
if (user.adminData?.is_suggested) {
|
||||||
|
acc.add('state:suggested')
|
||||||
|
} else {
|
||||||
|
acc.add('!state:suggested')
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, new Set())
|
||||||
|
},
|
||||||
tagsSet() {
|
tagsSet() {
|
||||||
return new Set(this.user.tags)
|
const present = new Set()
|
||||||
|
const missing = new Set()
|
||||||
|
|
||||||
|
this.users.forEach((user) => {
|
||||||
|
TAGS.forEach((tag) => {
|
||||||
|
if (user.tags.has(tag)) {
|
||||||
|
present.add(tag)
|
||||||
|
} else {
|
||||||
|
missing.add(tag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = new Set()
|
||||||
|
|
||||||
|
// Each tag can have three states for given group of users
|
||||||
|
TAGS.forEach((tag) => {
|
||||||
|
if (present.has(tag) && missing.has(tag)) {
|
||||||
|
// Some users have tag, some don't: "~tag"
|
||||||
|
result.add(`~${tag}`)
|
||||||
|
} else if (missing.has(tag)) {
|
||||||
|
// No users have tag: "!tag"
|
||||||
|
result.add(`!${tag}`)
|
||||||
|
} else {
|
||||||
|
// All users have tag: "tag"
|
||||||
|
result.add(tag)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
canGrantRole() {
|
propertySet() {
|
||||||
return (
|
return this.users.reduce((acc, user) => {
|
||||||
this.user.is_local &&
|
if (user.is_local) {
|
||||||
!this.user.deactivated &&
|
acc.add('property:local')
|
||||||
this.$store.state.users.currentUser.role === 'admin'
|
} else {
|
||||||
)
|
acc.add('!property:local')
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, new Set())
|
||||||
},
|
},
|
||||||
canChangeActivationState() {
|
disabled() {
|
||||||
return this.privileged('users_manage_activation_state')
|
return !this.ready || this.users.length === 0
|
||||||
|
},
|
||||||
|
totalSet() {
|
||||||
|
return new Set([
|
||||||
|
...this.rightsSet,
|
||||||
|
...this.stateSet,
|
||||||
|
...this.tagsSet,
|
||||||
|
...this.propertySet,
|
||||||
|
`count:${this.users.length}`,
|
||||||
|
])
|
||||||
},
|
},
|
||||||
canDeleteAccount() {
|
canDeleteAccount() {
|
||||||
return this.privileged('users_delete')
|
return this.privileged('users_delete')
|
||||||
|
|
@ -61,89 +405,266 @@ const ModerationTools = {
|
||||||
this.privileged('users_manage_tags')
|
this.privileged('users_manage_tags')
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
isAdmin() {
|
||||||
|
this.$store.state.users.currentUser.role === 'admin'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hasTag(tagName) {
|
canGrantRole(name, value) {
|
||||||
return this.tagsSet.has(tagName)
|
const setEntry = `${value ? '!' : ''}rights:${name}`
|
||||||
|
|
||||||
|
return this.isAdmin && this.totalSet.has(setEntry)
|
||||||
|
},
|
||||||
|
canChangeState(name, value) {
|
||||||
|
const setEntry = `${value ? '!' : ''}state:${name}`
|
||||||
|
const privilege = (() => {
|
||||||
|
switch (name) {
|
||||||
|
case 'activated':
|
||||||
|
return 'users_manage_activation_state'
|
||||||
|
case 'approved':
|
||||||
|
return 'users_manage_invites'
|
||||||
|
case 'confirmed':
|
||||||
|
return 'users_manage_credentials'
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return this.privileged(privilege) && this.totalSet.has(setEntry)
|
||||||
|
},
|
||||||
|
doConfirmDialogAction() {
|
||||||
|
if (typeof this.confirmDialogAction !== 'function') {
|
||||||
|
console.error('Confirm Dialog action is not a function!!')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.confirmDialogAction()
|
||||||
|
this.clearConfirmDialog()
|
||||||
|
},
|
||||||
|
clearConfirmDialog() {
|
||||||
|
this.confirmDialogShow = false
|
||||||
|
this.confirmDialogTitle = null
|
||||||
|
this.confirmDialogContent = null
|
||||||
|
this.confirmDialogContent2 = null
|
||||||
|
this.confirmDialogDanger = false
|
||||||
|
this.confirmDialogConfirm = null
|
||||||
|
this.confirmDialogAction = null
|
||||||
|
this.confirmDialogGroup = null
|
||||||
|
this.confirmDialogName = null
|
||||||
},
|
},
|
||||||
privileged(privilege) {
|
privileged(privilege) {
|
||||||
return this.$store.state.users.currentUser.privileges.includes(privilege)
|
if (this.isAdmin) return true
|
||||||
|
return this.$store.state.users.currentUser.privileges.has(privilege)
|
||||||
},
|
},
|
||||||
toggleTag(tag) {
|
setTag(tag, value) {
|
||||||
const store = this.$store
|
useAdminSettingsStore().setUsersTags({
|
||||||
if (this.tagsSet.has(tag)) {
|
users: this.users,
|
||||||
store.state.api.backendInteractor
|
value,
|
||||||
.untagUser({ user: this.user, tag })
|
tags: [tag],
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
store.commit('untagUser', { user: this.user, tag })
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
store.state.api.backendInteractor
|
|
||||||
.tagUser({ user: 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({ user: this.user, right })
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
store.commit('updateRight', {
|
|
||||||
user: this.user,
|
|
||||||
right,
|
|
||||||
value: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
store.state.api.backendInteractor
|
|
||||||
.addRight({ user: this.user, right })
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
store.commit('updateRight', { user: this.user, right, value: true })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleActivationStatus() {
|
|
||||||
this.$store.dispatch('toggleActivationStatus', { user: this.user })
|
|
||||||
},
|
|
||||||
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(() => {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setToggled(value) {
|
setRight(right, value) {
|
||||||
this.toggled = value
|
useAdminSettingsStore().setUsersRight({ users: this.users, value, right })
|
||||||
|
},
|
||||||
|
setStatus(name, value) {
|
||||||
|
const noun = (() => {
|
||||||
|
switch (name) {
|
||||||
|
case 'activated':
|
||||||
|
return 'Activation'
|
||||||
|
case 'confirmed':
|
||||||
|
return 'Confirmation'
|
||||||
|
case 'approved':
|
||||||
|
return 'Approval'
|
||||||
|
case 'suggested':
|
||||||
|
return 'Suggestion'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
useAdminSettingsStore()[`setUsers${noun}Status`]({
|
||||||
|
users: this.users,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resendConfirmationEmail() {
|
||||||
|
useAdminSettingsStore().resendConfirmationEmail({ users: this.users })
|
||||||
|
},
|
||||||
|
requirePasswordChange() {
|
||||||
|
useAdminSettingsStore().requirePasswordChange({ users: this.users })
|
||||||
|
},
|
||||||
|
disableMFA() {
|
||||||
|
this.users.forEach((user) => {
|
||||||
|
useAdminSettingsStore().disableMFA({ user })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteUsers() {
|
||||||
|
const { id, name } = this.users[0]
|
||||||
|
|
||||||
|
useAdminSettingsStore()
|
||||||
|
.deleteUsers({ users: this.users })
|
||||||
|
.then((userIds) => {
|
||||||
|
if (userIds.length > 1) return
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setOpen(value) {
|
||||||
|
this.open = value
|
||||||
|
},
|
||||||
|
maybeShowConfirm(close, { group, name, action, value }) {
|
||||||
|
close()
|
||||||
|
this.confirmDialogName = name
|
||||||
|
this.confirmDialogGroup = group
|
||||||
|
this.confirmDialogAction = () => action()
|
||||||
|
|
||||||
|
switch (group) {
|
||||||
|
case 'action': {
|
||||||
|
switch (name) {
|
||||||
|
case 'delete': {
|
||||||
|
this.confirmDialogShow = true
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.delete_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogDanger = true
|
||||||
|
this.confirmDialogContent =
|
||||||
|
'user_card.admin_menu.confirm_modal.delete_content'
|
||||||
|
this.confirmDialogContent2 =
|
||||||
|
'user_card.admin_menu.confirm_modal.delete_content_2'
|
||||||
|
this.confirmDialogConfirm = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.delete',
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'resend_confirmation': {
|
||||||
|
this.confirmDialogShow = true
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.resend_confirmation_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent =
|
||||||
|
'user_card.admin_menu.confirm_modal.resend_confirmation_content'
|
||||||
|
this.confirmDialogConfirm = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.send',
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'disable_mfa': {
|
||||||
|
this.confirmDialogShow = true
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.disable_mfa_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent =
|
||||||
|
'user_card.admin_menu.confirm_modal.disable_mfa_content'
|
||||||
|
this.confirmDialogConfirm = this.$t('settings.confirm')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'require_password_change': {
|
||||||
|
this.confirmDialogShow = true
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.require_password_change_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent =
|
||||||
|
'user_card.admin_menu.confirm_modal.require_password_change_content'
|
||||||
|
this.confirmDialogConfirm = this.$t('settings.confirm')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'state': {
|
||||||
|
switch (name) {
|
||||||
|
case 'activated': {
|
||||||
|
this.confirmDialogShow = true
|
||||||
|
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.activate_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent = value
|
||||||
|
? 'user_card.admin_menu.confirm_modal.activate_content'
|
||||||
|
: 'user_card.admin_menu.confirm_modal.deactivate_content'
|
||||||
|
this.confirmDialogConfirm = value
|
||||||
|
? this.$t('user_card.admin_menu.confirm_modal.activate')
|
||||||
|
: this.$t('user_card.admin_menu.confirm_modal.deactivate')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Confirmation and Approval statuses cannot be revokedn(no API)
|
||||||
|
case 'confirmed': {
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.confirm_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent = //value
|
||||||
|
/*?*/ 'user_card.admin_menu.confirm_modal.confirm_content'
|
||||||
|
//: 'user_card.admin_menu.confirm_modal.confirm_revoke_content'
|
||||||
|
this.confirmDialogConfirm = value
|
||||||
|
/*?*/ this.$t('user_card.admin_menu.confirm_modal.confirm')
|
||||||
|
//: this.$t('user_card.admin_menu.confirm_modal.revoke')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'approved': {
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.approval_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent = //value
|
||||||
|
/*?*/ 'user_card.admin_menu.confirm_modal.approval_content'
|
||||||
|
//: 'user_card.admin_menu.confirm_modal.approval_revoke_content'
|
||||||
|
this.confirmDialogConfirm = value
|
||||||
|
/*?*/ this.$t('user_card.admin_menu.confirm_modal.approve')
|
||||||
|
//: this.$t('user_card.admin_menu.confirm_modal.revoke')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'suggested': {
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.suggest_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent = value
|
||||||
|
? 'user_card.admin_menu.confirm_modal.add_suggest_content'
|
||||||
|
: 'user_card.admin_menu.confirm_modal.remove_suggest_content'
|
||||||
|
this.confirmDialogConfirm = value
|
||||||
|
? this.$t('user_card.admin_menu.confirm_modal.add')
|
||||||
|
: this.$t('user_card.admin_menu.confirm_modal.remove')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'rights': {
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.confirm_modal.user_rights_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent = value
|
||||||
|
? 'user_card.admin_menu.confirm_modal.grant_role_content'
|
||||||
|
: 'user_card.admin_menu.confirm_modal.revoke_role_content'
|
||||||
|
this.confirmDialogConfirm = value
|
||||||
|
? this.$t('user_card.admin_menu.confirm_modal.grant')
|
||||||
|
: this.$t('user_card.admin_menu.confirm_modal.revoke')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'mrf_tag': {
|
||||||
|
this.confirmDialogTitle = this.$t(
|
||||||
|
'user_card.admin_menu.user_tag_title',
|
||||||
|
)
|
||||||
|
this.confirmDialogContent = value
|
||||||
|
? 'user_card.admin_menu.confirm_modal.assign_tag_content'
|
||||||
|
: 'user_card.admin_menu.confirm_modal.unassign_tag_content'
|
||||||
|
this.confirmDialogConfirm = value
|
||||||
|
? this.$t('user_card.admin_menu.confirm_modal.assign')
|
||||||
|
: this.$t('user_card.admin_menu.confirm_modal.unassign')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.users.length > 1) {
|
||||||
|
this.confirmDialogShow = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.confirmDialogShow) {
|
||||||
|
this.doConfirmDialogAction()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,154 +3,37 @@
|
||||||
<Popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
class="moderation-tools-popover"
|
class="moderation-tools-popover"
|
||||||
|
:disabled="disabled"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:offset="{ y: 5 }"
|
:offset="{ y: 5 }"
|
||||||
@show="setToggled(true)"
|
@show="setOpen(true)"
|
||||||
@close="setToggled(false)"
|
@close="setOpen(false)"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content="{close}">
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<template v-if="canGrantRole">
|
<template v-for="(entry, index) in entries">
|
||||||
<div class="menu-item dropdown-item -icon-space">
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleRight("admin")"
|
|
||||||
>
|
|
||||||
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item dropdown-item -icon-space">
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleRight("moderator")"
|
|
||||||
>
|
|
||||||
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="canChangeActivationState || canDeleteAccount"
|
v-if="entry === 'separator'"
|
||||||
|
:key="index"
|
||||||
role="separator"
|
role="separator"
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-if="canChangeActivationState"
|
|
||||||
class="menu-item dropdown-item -icon-space"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleActivationStatus()"
|
|
||||||
>
|
|
||||||
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="canDeleteAccount"
|
|
||||||
class="menu-item dropdown-item -icon-space"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="deleteUserDialog(true)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.admin_menu.delete_account') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<template v-if="canUseTagPolicy">
|
|
||||||
<div
|
<div
|
||||||
role="separator"
|
v-else
|
||||||
class="dropdown-divider"
|
:key="entry.label"
|
||||||
/>
|
class="menu-item dropdown-item"
|
||||||
<div class="menu-item dropdown-item -icon">
|
:class="entry.checkbox ? '-icon' : '-icon-space'"
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleTag(tags.FORCE_NSFW)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="input menu-checkbox"
|
|
||||||
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
|
|
||||||
/>
|
|
||||||
{{ $t('user_card.admin_menu.force_nsfw') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item dropdown-item -icon">
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleTag(tags.STRIP_MEDIA)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="input menu-checkbox"
|
|
||||||
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
|
|
||||||
/>
|
|
||||||
{{ $t('user_card.admin_menu.strip_media') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item dropdown-item -icon">
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleTag(tags.FORCE_UNLISTED)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="input menu-checkbox"
|
|
||||||
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
|
|
||||||
/>
|
|
||||||
{{ $t('user_card.admin_menu.force_unlisted') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="menu-item dropdown-item -icon">
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleTag(tags.SANDBOX)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="input menu-checkbox"
|
|
||||||
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
|
|
||||||
/>
|
|
||||||
{{ $t('user_card.admin_menu.sandbox') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="user.is_local"
|
|
||||||
class="menu-item dropdown-item -icon"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="main-button"
|
class="main-button"
|
||||||
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
|
@click="() => maybeShowConfirm(close, entry)"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
v-if="entry.checkbox"
|
||||||
class="input menu-checkbox"
|
class="input menu-checkbox"
|
||||||
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
|
:class="entry.checkboxClass"
|
||||||
/>
|
/>
|
||||||
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
|
{{ $t(entry.label) }}
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="user.is_local"
|
|
||||||
class="menu-item dropdown-item -icon"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="input menu-checkbox"
|
|
||||||
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
|
|
||||||
/>
|
|
||||||
{{ $t('user_card.admin_menu.disable_any_subscription') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="user.is_local"
|
|
||||||
class="menu-item dropdown-item -icon"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="main-button"
|
|
||||||
@click="toggleTag(tags.QUARANTINE)"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="input menu-checkbox"
|
|
||||||
:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
|
|
||||||
/>
|
|
||||||
{{ $t('user_card.admin_menu.quarantine') }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -159,37 +42,69 @@
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
class="btn button-default btn-block moderation-tools-button"
|
class="btn button-default btn-block moderation-tools-button"
|
||||||
:class="{ toggled }"
|
:class="{ toggled: open, disabled }"
|
||||||
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.admin_menu.moderation') }}
|
{{ $t('user_card.admin_menu.moderation') }}
|
||||||
<FAIcon icon="chevron-down" />
|
<FAIcon
|
||||||
|
v-if="ready"
|
||||||
|
icon="chevron-down"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="loading-spinner"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-old-padding"
|
||||||
|
spin
|
||||||
|
icon="circle-notch"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
<teleport to="#modal">
|
<teleport to="#modal">
|
||||||
<DialogModal
|
<ConfirmModal
|
||||||
v-if="showDeleteUserDialog"
|
v-if="confirmDialogShow"
|
||||||
:on-cancel="deleteUserDialog.bind(this, false)"
|
:title="$t(confirmDialogTitle)"
|
||||||
|
:confirm-text="confirmDialogConfirm"
|
||||||
|
:confirm-danger="confirmDialogDanger"
|
||||||
|
:cancel-text="$t('general.cancel')"
|
||||||
|
@accepted="doConfirmDialogAction"
|
||||||
|
@cancelled="clearConfirmDialog"
|
||||||
>
|
>
|
||||||
<template #header>
|
<i18n-t
|
||||||
{{ $t('user_card.admin_menu.delete_user') }}
|
:plural="users.length"
|
||||||
</template>
|
scope="global"
|
||||||
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
:keypath="confirmDialogContent"
|
||||||
<template #footer>
|
tag="p"
|
||||||
<button
|
>
|
||||||
class="btn button-default"
|
<template #user>
|
||||||
@click="deleteUserDialog(false)"
|
<span
|
||||||
|
v-text="users[0].screen_name_ui"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #count>
|
||||||
|
{{ users.length }}
|
||||||
|
</template>
|
||||||
|
<template #name>
|
||||||
|
<code>
|
||||||
|
{{ confirmDialogName }}
|
||||||
|
</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
<p v-if="confirmDialogContent2">
|
||||||
|
{{ $t(confirmDialogContent2) }}
|
||||||
|
</p>
|
||||||
|
<ul v-if="users.length > 1">
|
||||||
|
<li
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.screen_name"
|
||||||
>
|
>
|
||||||
{{ $t('general.cancel') }}
|
{{ user.screen_name }}
|
||||||
</button>
|
</li>
|
||||||
<button
|
</ul>
|
||||||
class="btn button-default danger"
|
</ConfirmModal>
|
||||||
@click="deleteUser()"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.admin_menu.delete_user') }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</DialogModal>
|
|
||||||
</teleport>
|
</teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<basic-user-card :user="user">
|
<BasicUserCard :user="user">
|
||||||
<div class="mute-card-content-container">
|
<div class="mute-card-content-container">
|
||||||
<span
|
<span
|
||||||
v-if="muted && muteExpiryAvailable"
|
v-if="muted && muteExpiryAvailable"
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
:is-mute="true"
|
:is-mute="true"
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
</basic-user-card>
|
</BasicUserCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./mute_card.js"></script>
|
<script src="./mute_card.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,11 @@
|
||||||
content: "✓";
|
content: "✓";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.menu-checkbox-indeterminate::after {
|
||||||
|
font-size: 1.25em;
|
||||||
|
content: "–";
|
||||||
|
}
|
||||||
|
|
||||||
&.-radio {
|
&.-radio {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
|
|
||||||
|
|
@ -103,6 +108,11 @@
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
content: "•";
|
content: "•";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.menu-checkbox-indeterminate::after {
|
||||||
|
font-size: 2em;
|
||||||
|
content: "–";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
|
||||||
import List from 'src/components/list/list.vue'
|
|
||||||
|
|
||||||
const SelectableList = {
|
|
||||||
components: {
|
|
||||||
List,
|
|
||||||
Checkbox,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
getKey: {
|
|
||||||
type: Function,
|
|
||||||
default: (item) => item.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selected: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
allKeys() {
|
|
||||||
return this.items.map(this.getKey)
|
|
||||||
},
|
|
||||||
filteredSelected() {
|
|
||||||
return this.allKeys.filter((key) => this.selected.indexOf(key) !== -1)
|
|
||||||
},
|
|
||||||
allSelected() {
|
|
||||||
return this.filteredSelected.length === this.items.length
|
|
||||||
},
|
|
||||||
noneSelected() {
|
|
||||||
return this.filteredSelected.length === 0
|
|
||||||
},
|
|
||||||
someSelected() {
|
|
||||||
return !this.allSelected && !this.noneSelected
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isSelected(item) {
|
|
||||||
return this.filteredSelected.indexOf(this.getKey(item)) !== -1
|
|
||||||
},
|
|
||||||
toggle(checked, item) {
|
|
||||||
const key = this.getKey(item)
|
|
||||||
const oldChecked = this.isSelected(key)
|
|
||||||
if (checked !== oldChecked) {
|
|
||||||
if (checked) {
|
|
||||||
this.selected.push(key)
|
|
||||||
} else {
|
|
||||||
this.selected.splice(this.selected.indexOf(key), 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleAll(value) {
|
|
||||||
if (value) {
|
|
||||||
this.selected = this.allKeys.slice(0)
|
|
||||||
} else {
|
|
||||||
this.selected = []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SelectableList
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="selectable-list">
|
|
||||||
<div
|
|
||||||
v-if="items.length > 0"
|
|
||||||
class="selectable-list-header"
|
|
||||||
>
|
|
||||||
<div class="selectable-list-checkbox-wrapper">
|
|
||||||
<Checkbox
|
|
||||||
:model-value="allSelected"
|
|
||||||
:indeterminate="someSelected"
|
|
||||||
@update:model-value="toggleAll"
|
|
||||||
>
|
|
||||||
{{ $t('selectable_list.select_all') }}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div class="selectable-list-header-actions">
|
|
||||||
<slot
|
|
||||||
name="header"
|
|
||||||
:selected="filteredSelected"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<List
|
|
||||||
:items="items"
|
|
||||||
:get-key="getKey"
|
|
||||||
:get-class="item => isSelected(item) ? '-active' : ''"
|
|
||||||
>
|
|
||||||
<template #item="{item}">
|
|
||||||
<div
|
|
||||||
class="selectable-list-item-inner"
|
|
||||||
:class="{ 'selectable-list-item-selected-inner': isSelected(item) }"
|
|
||||||
@click.stop="toggle(!isSelected(item), item)"
|
|
||||||
>
|
|
||||||
<div class="selectable-list-checkbox-wrapper">
|
|
||||||
<Checkbox
|
|
||||||
:model-value="isSelected(item)"
|
|
||||||
@update:model-value="checked => toggle(checked, item)"
|
|
||||||
@click.stop
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<slot
|
|
||||||
name="item"
|
|
||||||
:item="item"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #empty>
|
|
||||||
<slot name="empty" />
|
|
||||||
</template>
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./selectable_list.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.selectable-list {
|
|
||||||
--__line-height: 1.5em;
|
|
||||||
--__horizontal-gap: 0.75em;
|
|
||||||
--__vertical-gap: 0.5em;
|
|
||||||
|
|
||||||
&-item-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
border-bottom-color: var(--border);
|
|
||||||
|
|
||||||
&-actions {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-checkbox-wrapper {
|
|
||||||
padding-right: var(--__horizontal-gap);
|
|
||||||
flex: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
32
src/components/settings_modal/admin_tabs/admin_user_card.js
Normal file
32
src/components/settings_modal/admin_tabs/admin_user_card.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
|
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||||
|
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
|
||||||
|
|
||||||
|
const AdminUserCard = {
|
||||||
|
props: {
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BasicUserCard,
|
||||||
|
ModerationTools,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user() {
|
||||||
|
return this.$store.getters.findUser(this.userId)
|
||||||
|
},
|
||||||
|
isAdmin() {
|
||||||
|
return this.user.rights.admin
|
||||||
|
},
|
||||||
|
isModerator() {
|
||||||
|
return this.user.rights.moderator
|
||||||
|
},
|
||||||
|
isActivated() {
|
||||||
|
return !this.user.deactivated
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminUserCard
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
.AdminUserCard {
|
||||||
|
.right-side {
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: end;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/components/settings_modal/admin_tabs/admin_user_card.vue
Normal file
107
src/components/settings_modal/admin_tabs/admin_user_card.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<template v-if="!user">
|
||||||
|
<FAIcon
|
||||||
|
icon="circle-notch"
|
||||||
|
spin
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<BasicUserCard
|
||||||
|
class="AdminUserCard"
|
||||||
|
:user="user"
|
||||||
|
show-line-labels
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
{{ $t('admin_dash.users.labels.email_colon') }}
|
||||||
|
</strong>
|
||||||
|
{{ ' ' }}
|
||||||
|
<span
|
||||||
|
v-if="user.adminData.email == null"
|
||||||
|
class="faint"
|
||||||
|
>
|
||||||
|
{{ $t('general.not_available') }}
|
||||||
|
</span>
|
||||||
|
<a :href="'mailto:' + user.adminData.email">
|
||||||
|
{{ user.adminData.email }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<details
|
||||||
|
v-if="user.adminData.registration_reason != null"
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<summary>
|
||||||
|
{{ $t('user_card.admin_data.registration_reason') }}
|
||||||
|
</summary>
|
||||||
|
<span>
|
||||||
|
{{ user.adminData.registration_reason }}
|
||||||
|
</span>
|
||||||
|
</details>
|
||||||
|
<div class="right-side">
|
||||||
|
<label
|
||||||
|
v-if="user.is_local && isAdmin"
|
||||||
|
class="alert neutral user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.admin') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="user.is_local && isModerator"
|
||||||
|
class="alert neutral user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.moderator') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="isActivated"
|
||||||
|
class="alert success user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.active') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="!isActivated"
|
||||||
|
class="alert error user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.deactivated') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="user.is_local && user.adminData.is_confirmed"
|
||||||
|
class="alert success user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.confirmed') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="user.is_local && !user.adminData.is_confirmed"
|
||||||
|
class="alert warning user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.unconfirmed') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="user.is_local && user.adminData.is_approved"
|
||||||
|
class="alert success user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.approved') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="user.is_local && !user.adminData.is_approved"
|
||||||
|
class="alert warning user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.unapproved') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="user.adminData.is_suggested"
|
||||||
|
class="alert info user-role"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.indicator.suggested') }}
|
||||||
|
</label>
|
||||||
|
<ModerationTools
|
||||||
|
class="moderation-menu"
|
||||||
|
:users="[user]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BasicUserCard>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./admin_user_card.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss" src="./admin_user_card.scss"></style>
|
||||||
|
|
@ -9,6 +9,8 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import StringSetting from '../helpers/string_setting.vue'
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
import TupleSetting from '../helpers/tuple_setting.vue'
|
import TupleSetting from '../helpers/tuple_setting.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
const AuthTab = {
|
const AuthTab = {
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -30,9 +32,7 @@ const AuthTab = {
|
||||||
computed: {
|
computed: {
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
LDAPEnabled() {
|
LDAPEnabled() {
|
||||||
return this.$store.state.adminSettings.draft[':pleroma'][':ldap'][
|
return useAdminSettingsStore().draft[':pleroma'][':ldap'][':enabled']
|
||||||
':enabled'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@
|
||||||
margin: 0.5em 2em;
|
margin: 0.5em 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting-section {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import StringSetting from '../helpers/string_setting.vue'
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
@ -37,13 +38,13 @@ const FrontendsTab = {
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.user.rights.admin) {
|
if (this.user.rights.admin) {
|
||||||
this.$store.dispatch('loadFrontendsStuff')
|
useAdminSettingsStore().loadFrontendsStuff()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
frontends() {
|
frontends() {
|
||||||
return this.$store.state.adminSettings.frontends
|
return useAdminSettingsStore().frontends
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -76,7 +77,7 @@ const FrontendsTab = {
|
||||||
this.working = false
|
this.working = false
|
||||||
})
|
})
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
this.$store.dispatch('loadFrontendsStuff')
|
useAdminSettingsStore().loadFrontendsStuff()
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
const reason = await response.error.json()
|
const reason = await response.error.json()
|
||||||
useInterfaceStore().pushGlobalNotice({
|
useInterfaceStore().pushGlobalNotice({
|
||||||
|
|
@ -104,7 +105,7 @@ const FrontendsTab = {
|
||||||
const ref = suggestRef || this.getSuggestedRef(frontend)
|
const ref = suggestRef || this.getSuggestedRef(frontend)
|
||||||
const { name } = frontend
|
const { name } = frontend
|
||||||
|
|
||||||
this.$store.commit('updateAdminDraft', {
|
useAdminSettingsStore().updateAdminDraft({
|
||||||
path: [':pleroma', ':frontends', ':primary'],
|
path: [':pleroma', ':frontends', ':primary'],
|
||||||
value: { name, ref },
|
value: { name, ref },
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
.FrontendsTab {
|
.FrontendsTab {
|
||||||
|
padding: 0 1em;
|
||||||
|
|
||||||
.cards-list {
|
.cards-list {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +36,8 @@
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.15em
|
font-size: 1.15em;
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import StringSetting from '../helpers/string_setting.vue'
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
import TupleSetting from '../helpers/tuple_setting.vue'
|
import TupleSetting from '../helpers/tuple_setting.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
const HTTPTab = {
|
const HTTPTab = {
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -35,7 +37,7 @@ const HTTPTab = {
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
sslOptions() {
|
sslOptions() {
|
||||||
const desc = get(
|
const desc = get(
|
||||||
this.$store.state.adminSettings.descriptions,
|
useAdminSettingsStore().descriptions,
|
||||||
':pleroma.:http.:adapter.:ssl_options.:versions',
|
':pleroma.:http.:adapter.:ssl_options.:versions',
|
||||||
)
|
)
|
||||||
return new Set(
|
return new Set(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import PWAManifestIconsSetting from '../helpers/pwa_manifest_icons_setting.vue'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import StringSetting from '../helpers/string_setting.vue'
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
const InstanceTab = {
|
const InstanceTab = {
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -34,7 +36,7 @@ const InstanceTab = {
|
||||||
computed: {
|
computed: {
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
providersOptions() {
|
providersOptions() {
|
||||||
const desc = get(this.$store.state.adminSettings.descriptions, [
|
const desc = get(useAdminSettingsStore().descriptions, [
|
||||||
':pleroma',
|
':pleroma',
|
||||||
'Pleroma.Web.Metadata',
|
'Pleroma.Web.Metadata',
|
||||||
':providers',
|
':providers',
|
||||||
|
|
@ -47,7 +49,7 @@ const InstanceTab = {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
limitLocalContentOptions() {
|
limitLocalContentOptions() {
|
||||||
const desc = get(this.$store.state.adminSettings.descriptions, [
|
const desc = get(useAdminSettingsStore().descriptions, [
|
||||||
':pleroma',
|
':pleroma',
|
||||||
':instance',
|
':instance',
|
||||||
':limit_to_local_content',
|
':limit_to_local_content',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import ListSetting from '../helpers/list_setting.vue'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import StringSetting from '../helpers/string_setting.vue'
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
const LinksTab = {
|
const LinksTab = {
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -30,27 +32,27 @@ const LinksTab = {
|
||||||
computed: {
|
computed: {
|
||||||
classIsPresent() {
|
classIsPresent() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
|
useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
|
||||||
':class'
|
':class'
|
||||||
] !== false
|
] !== false
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
relIsPresent() {
|
relIsPresent() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
|
useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
|
||||||
':rel'
|
':rel'
|
||||||
] !== false
|
] !== false
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
truncateIsPresent() {
|
truncateIsPresent() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.adminSettings.draft[':pleroma']['Pleroma.Formatter'][
|
useAdminSettingsStore().draft[':pleroma']['Pleroma.Formatter'][
|
||||||
':truncate'
|
':truncate'
|
||||||
] !== false
|
] !== false
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
truncateDescription() {
|
truncateDescription() {
|
||||||
return get(this.$store.state.adminSettings.descriptions, [
|
return get(useAdminSettingsStore().descriptions, [
|
||||||
':pleroma',
|
':pleroma',
|
||||||
'Pleroma.Formatter',
|
'Pleroma.Formatter',
|
||||||
':truncate',
|
':truncate',
|
||||||
|
|
@ -58,7 +60,7 @@ const LinksTab = {
|
||||||
},
|
},
|
||||||
ttlSettersOptions() {
|
ttlSettersOptions() {
|
||||||
const desc = get(
|
const desc = get(
|
||||||
this.$store.state.adminSettings.descriptions,
|
useAdminSettingsStore().descriptions,
|
||||||
':pleroma.:rich_media.:ttl_setters',
|
':pleroma.:rich_media.:ttl_setters',
|
||||||
)
|
)
|
||||||
return new Set(
|
return new Set(
|
||||||
|
|
@ -70,7 +72,7 @@ const LinksTab = {
|
||||||
},
|
},
|
||||||
parsersOptions() {
|
parsersOptions() {
|
||||||
const desc = get(
|
const desc = get(
|
||||||
this.$store.state.adminSettings.descriptions,
|
useAdminSettingsStore().descriptions,
|
||||||
':pleroma.:rich_media.:parsers',
|
':pleroma.:rich_media.:parsers',
|
||||||
)
|
)
|
||||||
return new Set(
|
return new Set(
|
||||||
|
|
@ -97,12 +99,12 @@ const LinksTab = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
mediaProxyEnabled() {
|
mediaProxyEnabled() {
|
||||||
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
|
return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
|
||||||
':enabled'
|
':enabled'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
mediaInvalidationProvider() {
|
mediaInvalidationProvider() {
|
||||||
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
|
return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
|
||||||
':invalidation'
|
':invalidation'
|
||||||
][':provider']
|
][':provider']
|
||||||
},
|
},
|
||||||
|
|
@ -110,19 +112,19 @@ const LinksTab = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
checkRel(e) {
|
checkRel(e) {
|
||||||
this.$store.commit('updateAdminDraft', {
|
useAdminSettingsStore().updateAdminDraft({
|
||||||
path: [':pleroma', 'Pleroma.Formatter', ':rel'],
|
path: [':pleroma', 'Pleroma.Formatter', ':rel'],
|
||||||
value: e ? '' : false,
|
value: e ? '' : false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
checkClass(e) {
|
checkClass(e) {
|
||||||
this.$store.commit('updateAdminDraft', {
|
useAdminSettingsStore().updateAdminDraft({
|
||||||
path: [':pleroma', 'Pleroma.Formatter', ':class'],
|
path: [':pleroma', 'Pleroma.Formatter', ':class'],
|
||||||
value: e ? '' : false,
|
value: e ? '' : false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
checkTruncate(e) {
|
checkTruncate(e) {
|
||||||
this.$store.commit('updateAdminDraft', {
|
useAdminSettingsStore().updateAdminDraft({
|
||||||
path: [':pleroma', 'Pleroma.Formatter', ':truncate'],
|
path: [':pleroma', 'Pleroma.Formatter', ':truncate'],
|
||||||
value: e ? 20 : false,
|
value: e ? 20 : false,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import StringSetting from '../helpers/string_setting.vue'
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
const MailerTab = {
|
const MailerTab = {
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -26,7 +28,7 @@ const MailerTab = {
|
||||||
computed: {
|
computed: {
|
||||||
adaptersLabels() {
|
adaptersLabels() {
|
||||||
const prefix = 'Swoosh.Adapters.'
|
const prefix = 'Swoosh.Adapters.'
|
||||||
const descriptions = this.$store.state.adminSettings.descriptions
|
const descriptions = useAdminSettingsStore().descriptions
|
||||||
const options =
|
const options =
|
||||||
descriptions[':pleroma']['Pleroma.Emails.Mailer'][':adapter']
|
descriptions[':pleroma']['Pleroma.Emails.Mailer'][':adapter']
|
||||||
.suggestions
|
.suggestions
|
||||||
|
|
@ -46,20 +48,20 @@ const MailerTab = {
|
||||||
// ]))
|
// ]))
|
||||||
},
|
},
|
||||||
adapter() {
|
adapter() {
|
||||||
return this.$store.state.adminSettings.draft[':pleroma'][
|
return useAdminSettingsStore().draft[':pleroma']['Pleroma.Emails.Mailer'][
|
||||||
'Pleroma.Emails.Mailer'
|
':adapter'
|
||||||
][':adapter']
|
]
|
||||||
},
|
},
|
||||||
mailerEnabled() {
|
mailerEnabled() {
|
||||||
return this.$store.state.adminSettings.draft[':pleroma'][
|
return useAdminSettingsStore().draft[':pleroma']['Pleroma.Emails.Mailer'][
|
||||||
'Pleroma.Emails.Mailer'
|
':enabled'
|
||||||
][':enabled']
|
]
|
||||||
},
|
},
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
adapterHasKey(key) {
|
adapterHasKey(key) {
|
||||||
const descriptions = this.$store.state.adminSettings.descriptions
|
const descriptions = useAdminSettingsStore().descriptions
|
||||||
const mailerStuff = descriptions[':pleroma']['Pleroma.Emails.Mailer']
|
const mailerStuff = descriptions[':pleroma']['Pleroma.Emails.Mailer']
|
||||||
const adapterStuff = mailerStuff[':subgroup,' + this.adapter]
|
const adapterStuff = mailerStuff[':subgroup,' + this.adapter]
|
||||||
return Object.hasOwn(adapterStuff, key)
|
return Object.hasOwn(adapterStuff, key)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import ListSetting from '../helpers/list_setting.vue'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import StringSetting from '../helpers/string_setting.vue'
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
const MediaProxyTab = {
|
const MediaProxyTab = {
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -25,12 +27,12 @@ const MediaProxyTab = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
mediaProxyEnabled() {
|
mediaProxyEnabled() {
|
||||||
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
|
return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
|
||||||
':enabled'
|
':enabled'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
mediaInvalidationProvider() {
|
mediaInvalidationProvider() {
|
||||||
return this.$store.state.adminSettings.draft[':pleroma'][':media_proxy'][
|
return useAdminSettingsStore().draft[':pleroma'][':media_proxy'][
|
||||||
':invalidation'
|
':invalidation'
|
||||||
][':provider']
|
][':provider']
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import StringSetting from '../helpers/string_setting.vue'
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
const UploadsTab = {
|
const UploadsTab = {
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -40,9 +42,9 @@ const UploadsTab = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
uploader() {
|
uploader() {
|
||||||
return this.$store.state.adminSettings.draft[':pleroma'][
|
return useAdminSettingsStore().draft[':pleroma']['Pleroma.Upload'][
|
||||||
'Pleroma.Upload'
|
':uploader'
|
||||||
][':uploader']
|
]
|
||||||
},
|
},
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
119
src/components/settings_modal/admin_tabs/users_tab.js
Normal file
119
src/components/settings_modal/admin_tabs/users_tab.js
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
|
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
import List from 'src/components/list/list.vue'
|
||||||
|
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
|
||||||
|
import Select from 'src/components/select/select.vue'
|
||||||
|
import AdminUserCard from 'src/components/settings_modal/admin_tabs/admin_user_card.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
|
const UsersTab = {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
Select,
|
||||||
|
BasicUserCard,
|
||||||
|
List,
|
||||||
|
AdminUserCard,
|
||||||
|
ModerationTools,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
filtersOrigin: 'local',
|
||||||
|
filtersActivity: 'all',
|
||||||
|
filtersPrivileges: 'all',
|
||||||
|
filtersNeedApproval: false,
|
||||||
|
filtersUnconfirmed: false,
|
||||||
|
filtersQuery: '',
|
||||||
|
filtersName: '',
|
||||||
|
filtersEmail: '',
|
||||||
|
expandedUser: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* do we filter for admins?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
filtersIsAdmin() {
|
||||||
|
return (
|
||||||
|
this.filtersPrivileges === 'admin' ||
|
||||||
|
this.filtersPrivileges === 'modsnadmins'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* do we filter for moderators?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
filtersIsModerator() {
|
||||||
|
return (
|
||||||
|
this.filtersPrivileges === 'moderator' ||
|
||||||
|
this.filtersPrivileges === 'modsnadmins'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* do we filter for active users?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
filtersActive() {
|
||||||
|
return this.filtersActivity === 'active'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* do we filter for deactivated users?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
filtersDeactivated() {
|
||||||
|
return this.filtersActivity === 'deactivated'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* do we filter for local users?
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
filtersLocal() {
|
||||||
|
return this.filtersOrigin === 'local'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* do we filter for external users?
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
filtersExternal() {
|
||||||
|
return this.filtersOrigin === 'external'
|
||||||
|
},
|
||||||
|
fetchOptions() {
|
||||||
|
const filters = {
|
||||||
|
isAdmin: this.filtersIsAdmin,
|
||||||
|
isModerator: this.filtersIsModerator,
|
||||||
|
active: this.filtersActive,
|
||||||
|
deactivated: this.filtersDeactivated,
|
||||||
|
local: this.filtersLocal,
|
||||||
|
external: this.filtersExternal,
|
||||||
|
needApproval: this.filtersNeedApproval,
|
||||||
|
unconfirmed: this.filtersUnconfirmed,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: this.filtersQuery,
|
||||||
|
name: this.filtersName,
|
||||||
|
email: this.filtersEmail,
|
||||||
|
pageSize: 50,
|
||||||
|
filters,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchUsers(page) {
|
||||||
|
return useAdminSettingsStore().fetchUsers({
|
||||||
|
...this.fetchOptions,
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
fetchOptions() {
|
||||||
|
this.$refs.usersList?.reset()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersTab
|
||||||
49
src/components/settings_modal/admin_tabs/users_tab.scss
Normal file
49
src/components/settings_modal/admin_tabs/users_tab.scss
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
.UsersTab {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitter {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
overflow: auto visible;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
display: block;
|
||||||
|
min-width: 14em;
|
||||||
|
|
||||||
|
.query-label {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-list {
|
||||||
|
flex: 1 0 30em;
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/components/settings_modal/admin_tabs/users_tab.vue
Normal file
143
src/components/settings_modal/admin_tabs/users_tab.vue
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="UsersTab"
|
||||||
|
:label="$t('admin_dash.users.management')"
|
||||||
|
>
|
||||||
|
<div class="splitter">
|
||||||
|
<div class="filters-section">
|
||||||
|
<label class="filter">
|
||||||
|
<div class="query-label">
|
||||||
|
{{ $t('admin_dash.users.labels.nickname') }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="filtersQuery"
|
||||||
|
class="input string-input filter-input"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<div class="query-label">
|
||||||
|
{{ $t('admin_dash.users.labels.name') }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="filtersName"
|
||||||
|
class="input string-input filter-input"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<div class="query-label">
|
||||||
|
{{ $t('admin_dash.users.labels.email') }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="filtersEmail"
|
||||||
|
class="input string-input filter-input"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<div class="filter">
|
||||||
|
<div class="query-label">
|
||||||
|
{{ $t('admin_dash.users.labels.origin') }}
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
v-model="filtersOrigin"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="all"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.all') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="local"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.only_local') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="external"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.only_external') }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<div class="query-label">
|
||||||
|
{{ $t('admin_dash.users.labels.activity') }}
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
v-model="filtersActivity"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="all"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.all') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="active"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.only_active') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="deactivated"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.only_deactivated') }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<div class="query-label">
|
||||||
|
{{ $t('admin_dash.users.labels.privileges') }}
|
||||||
|
</div>
|
||||||
|
<Select v-model="filtersPrivileges">
|
||||||
|
<option
|
||||||
|
value="all"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.all') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="admin"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.only_admins') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="modsnadmins"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.only_privileged') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
value="moderator"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.options.only_moderators') }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<Checkbox v-model="filtersNeedApproval">
|
||||||
|
{{ $t('admin_dash.users.options.only_unapproved') }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="filter">
|
||||||
|
<Checkbox v-model="filtersUnconfirmed">
|
||||||
|
{{ $t('admin_dash.users.options.only_unconfirmed') }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
ref="usersList"
|
||||||
|
class="users-list"
|
||||||
|
:fetch-function="fetchUsers"
|
||||||
|
selectable
|
||||||
|
scrollable
|
||||||
|
@select="onSelect"
|
||||||
|
>
|
||||||
|
<template #header="{selected}">
|
||||||
|
<ModerationTools :users="selected" />
|
||||||
|
</template>
|
||||||
|
<template #item="{item}">
|
||||||
|
<AdminUserCard :user-id="item.id" />
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<span>{{ $t('admin_dash.users.no_users_found') }}</span>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script src="./users_tab.js"></script>
|
||||||
|
<style lang="scss" src="./users_tab.scss"></style>
|
||||||
|
|
@ -4,6 +4,7 @@ import DraftButtons from './draft_buttons.vue'
|
||||||
import LocalSettingIndicator from './local_setting_indicator.vue'
|
import LocalSettingIndicator from './local_setting_indicator.vue'
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
import ModifiedIndicator from './modified_indicator.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||||
|
|
@ -125,14 +126,14 @@ export default {
|
||||||
draft: {
|
draft: {
|
||||||
get() {
|
get() {
|
||||||
if (this.realSource === 'admin' || this.path == null) {
|
if (this.realSource === 'admin' || this.path == null) {
|
||||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
return get(useAdminSettingsStore().draft, this.canonPath)
|
||||||
} else {
|
} else {
|
||||||
return this.localDraft
|
return this.localDraft
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
if (this.realSource === 'admin' || this.path == null) {
|
if (this.realSource === 'admin' || this.path == null) {
|
||||||
this.$store.commit('updateAdminDraft', {
|
useAdminSettingsStore().updateAdminDraft({
|
||||||
path: this.canonPath,
|
path: this.canonPath,
|
||||||
value,
|
value,
|
||||||
})
|
})
|
||||||
|
|
@ -164,10 +165,7 @@ export default {
|
||||||
: this.draftMode
|
: this.draftMode
|
||||||
},
|
},
|
||||||
backendDescription() {
|
backendDescription() {
|
||||||
return get(
|
return get(useAdminSettingsStore().descriptions, this.descriptionPath)
|
||||||
this.$store.state.adminSettings.descriptions,
|
|
||||||
this.descriptionPath,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
backendDescriptionLabel() {
|
backendDescriptionLabel() {
|
||||||
if (this.realSource !== 'admin') return ''
|
if (this.realSource !== 'admin') return ''
|
||||||
|
|
@ -221,10 +219,7 @@ export default {
|
||||||
let parentValue = null
|
let parentValue = null
|
||||||
if (this.parentPath !== undefined && this.realSource === 'admin') {
|
if (this.parentPath !== undefined && this.realSource === 'admin') {
|
||||||
if (this.realDraftMode) {
|
if (this.realDraftMode) {
|
||||||
parentValue = get(
|
parentValue = get(useAdminSettingsStore().draft, this.parentPath)
|
||||||
this.$store.state.adminSettings.draft,
|
|
||||||
this.parentPath,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
parentValue = get(this.configSource, this.parentPath)
|
parentValue = get(this.configSource, this.parentPath)
|
||||||
}
|
}
|
||||||
|
|
@ -243,7 +238,7 @@ export default {
|
||||||
case 'profile':
|
case 'profile':
|
||||||
return this.$store.state.profileConfig
|
return this.$store.state.profileConfig
|
||||||
case 'admin':
|
case 'admin':
|
||||||
return this.$store.state.adminSettings.config
|
return useAdminSettingsStore().config
|
||||||
default:
|
default:
|
||||||
return useMergedConfigStore().mergedConfig
|
return useMergedConfigStore().mergedConfig
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +254,7 @@ export default {
|
||||||
this.$store.dispatch('setProfileOption', { name: k, value: v })
|
this.$store.dispatch('setProfileOption', { name: k, value: v })
|
||||||
case 'admin':
|
case 'admin':
|
||||||
return (k, v) =>
|
return (k, v) =>
|
||||||
this.$store.dispatch('pushAdminSetting', { path: k, value: v })
|
useAdminSettingsStore().pushAdminSetting({ path: k, value: v })
|
||||||
default:
|
default:
|
||||||
return (readPath, value) => {
|
return (readPath, value) => {
|
||||||
const writePath = `${readPath}`
|
const writePath = `${readPath}`
|
||||||
|
|
@ -372,9 +367,7 @@ export default {
|
||||||
canHardReset() {
|
canHardReset() {
|
||||||
return (
|
return (
|
||||||
this.realSource === 'admin' &&
|
this.realSource === 'admin' &&
|
||||||
this.$store.state.adminSettings.modifiedPaths?.has(
|
useAdminSettingsStore().modifiedPaths?.has(this.canonPath.join(' -> '))
|
||||||
this.canonPath.join(' -> '),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
matchesExpertLevel() {
|
matchesExpertLevel() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { mapState as mapPiniaState } from 'pinia'
|
import { mapState as mapPiniaState } from 'pinia'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
||||||
const SharedComputedObject = () => ({
|
const SharedComputedObject = () => ({
|
||||||
|
|
@ -8,9 +9,11 @@ const SharedComputedObject = () => ({
|
||||||
...mapPiniaState(useMergedConfigStore, {
|
...mapPiniaState(useMergedConfigStore, {
|
||||||
expertLevel: (store) => store.mergedConfig.expertLevel,
|
expertLevel: (store) => store.mergedConfig.expertLevel,
|
||||||
}),
|
}),
|
||||||
|
...mapPiniaState(useAdminSettingsStore, {
|
||||||
|
adminConfig: (store) => store.config,
|
||||||
|
adminDraft: (store) => store.draft,
|
||||||
|
}),
|
||||||
...mapState({
|
...mapState({
|
||||||
adminConfig: (state) => state.adminSettings.config,
|
|
||||||
adminDraft: (state) => state.adminSettings.draft,
|
|
||||||
user: (state) => state.users.currentUser,
|
user: (state) => state.users.currentUser,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -160,19 +160,25 @@ export default {
|
||||||
'tab-content-wrapper',
|
'tab-content-wrapper',
|
||||||
active ? '-active' : '-hidden',
|
active ? '-active' : '-hidden',
|
||||||
]
|
]
|
||||||
|
const slotWrapperClasses = [
|
||||||
|
'tab-slot-wrapper',
|
||||||
|
active ? '-active' : '-hidden',
|
||||||
|
]
|
||||||
const contentClasses = ['tab-content']
|
const contentClasses = ['tab-content']
|
||||||
if (props['full-width'] || props['full-width'] === '') {
|
if (props['full-width'] || props['full-width'] != null) {
|
||||||
contentClasses.push('-full-width')
|
contentClasses.push('-full-width')
|
||||||
wrapperClasses.push('-full-width')
|
wrapperClasses.push('-full-width')
|
||||||
|
slotWrapperClasses.push('-full-width')
|
||||||
}
|
}
|
||||||
if (props['full-height'] || props['full-width'] === '') {
|
if (props['full-height'] || props['full-width'] != null) {
|
||||||
contentClasses.push('-full-height')
|
contentClasses.push('-full-height')
|
||||||
wrapperClasses.push('-full-height')
|
wrapperClasses.push('-full-height')
|
||||||
|
slotWrapperClasses.push('-full-height')
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div class={wrapperClasses}>
|
<div class={wrapperClasses}>
|
||||||
<div class="tab-mobile-header">{header}</div>
|
<div class="tab-mobile-header">{header}</div>
|
||||||
<div class="tab-slot-wrapper">
|
<div class={slotWrapperClasses}>
|
||||||
<div class={contentClasses}>{renderSlot}</div>
|
<div class={contentClasses}>{renderSlot}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
|
|
||||||
.tab-slot-wrapper {
|
.tab-slot-wrapper {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -48,6 +49,16 @@
|
||||||
grid-template-areas: ". content .";
|
grid-template-areas: ". content .";
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.-full-width {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-full-height {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
|
|
||||||
|
|
@ -56,10 +67,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-full-height {
|
&.-full-height {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.-full-width.-full-height {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import Modal from 'src/components/modal/modal.vue'
|
||||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||||
import Popover from 'src/components/popover/popover.vue'
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||||
|
|
@ -92,8 +93,11 @@ const SettingsModal = {
|
||||||
closeModal() {
|
closeModal() {
|
||||||
useInterfaceStore().closeSettingsModal()
|
useInterfaceStore().closeSettingsModal()
|
||||||
},
|
},
|
||||||
peekModal() {
|
toggleMinimizeModal(state) {
|
||||||
useInterfaceStore().togglePeekSettingsModal()
|
useInterfaceStore().toggleMinimizeSettingsModal()
|
||||||
|
},
|
||||||
|
minimizeModal() {
|
||||||
|
useInterfaceStore().setSettingsModalState('minimized')
|
||||||
},
|
},
|
||||||
importValidator(data) {
|
importValidator(data) {
|
||||||
if (!Array.isArray(data._pleroma_settings_version)) {
|
if (!Array.isArray(data._pleroma_settings_version)) {
|
||||||
|
|
@ -232,10 +236,10 @@ const SettingsModal = {
|
||||||
return clone
|
return clone
|
||||||
},
|
},
|
||||||
resetAdminDraft() {
|
resetAdminDraft() {
|
||||||
this.$store.commit('resetAdminDraft')
|
useAdminSettingsStore().resetAdminDraft()
|
||||||
},
|
},
|
||||||
pushAdminDraft() {
|
pushAdminDraft() {
|
||||||
this.$store.dispatch('pushAdminDraft')
|
useAdminSettingsStore().pushAdminDraft()
|
||||||
},
|
},
|
||||||
...mapActions(useInterfaceStore, [
|
...mapActions(useInterfaceStore, [
|
||||||
'temporaryChangesRevert',
|
'temporaryChangesRevert',
|
||||||
|
|
@ -250,7 +254,7 @@ const SettingsModal = {
|
||||||
modalMode: (store) => store.settingsModalMode,
|
modalMode: (store) => store.settingsModalMode,
|
||||||
modalOpenedOnceUser: (store) => store.settingsModalLoadedUser,
|
modalOpenedOnceUser: (store) => store.settingsModalLoadedUser,
|
||||||
modalOpenedOnceAdmin: (store) => store.settingsModalLoadedAdmin,
|
modalOpenedOnceAdmin: (store) => store.settingsModalLoadedAdmin,
|
||||||
modalPeeked: (store) => store.settingsModalState === 'minimized',
|
modalMinimized: (store) => store.settingsModalState === 'minimized',
|
||||||
}),
|
}),
|
||||||
expertLevel: {
|
expertLevel: {
|
||||||
get() {
|
get() {
|
||||||
|
|
@ -265,11 +269,16 @@ const SettingsModal = {
|
||||||
},
|
},
|
||||||
adminDraftAny() {
|
adminDraftAny() {
|
||||||
return !isEqual(
|
return !isEqual(
|
||||||
this.$store.state.adminSettings.config,
|
useAdminSettingsStore().config,
|
||||||
this.$store.state.adminSettings.draft,
|
useAdminSettingsStore().draft,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
$route(r) {
|
||||||
|
this.minimizeModal()
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsModal
|
export default SettingsModal
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,38 @@
|
||||||
margin-top: 0.75em;
|
margin-top: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
.header-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
&:not(.btn-group) {
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-text {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-default {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin-left: 2em;
|
margin-left: 2em;
|
||||||
|
|
@ -268,7 +300,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.peek {
|
&.minimize {
|
||||||
.settings-modal-panel {
|
.settings-modal-panel {
|
||||||
/* Explanation:
|
/* Explanation:
|
||||||
* Modal is positioned vertically centered.
|
* Modal is positioned vertically centered.
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
<Modal
|
<Modal
|
||||||
:is-open="modalActivated"
|
:is-open="modalActivated"
|
||||||
class="settings-modal"
|
class="settings-modal"
|
||||||
:class="{ peek: modalPeeked }"
|
:class="{ minimize: modalMinimized }"
|
||||||
:no-background="modalPeeked"
|
:no-background="modalMinimized"
|
||||||
>
|
>
|
||||||
<div class="settings-modal-panel panel">
|
<div class="settings-modal-panel panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
|
@ -22,8 +22,8 @@
|
||||||
</transition>
|
</transition>
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
:title="$t('general.peek')"
|
:title="$t('general.minimize')"
|
||||||
@click="peekModal"
|
@click="toggleMinimizeModal"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
:icon="['far', 'window-minimize']"
|
:icon="['far', 'window-minimize']"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
import AuthTab from './admin_tabs/auth_tab.vue'
|
import AuthTab from './admin_tabs/auth_tab.vue'
|
||||||
import EmojiTab from './admin_tabs/emoji_tab.vue'
|
import EmojiTab from './admin_tabs/emoji_tab.vue'
|
||||||
import FederationTab from './admin_tabs/federation_tab.vue'
|
import FederationTab from './admin_tabs/federation_tab.vue'
|
||||||
|
|
@ -15,27 +16,35 @@ import PostsTab from './admin_tabs/posts_tab.vue'
|
||||||
import RatesTab from './admin_tabs/rates_tab.vue'
|
import RatesTab from './admin_tabs/rates_tab.vue'
|
||||||
import RegistrationsTab from './admin_tabs/registrations_tab.vue'
|
import RegistrationsTab from './admin_tabs/registrations_tab.vue'
|
||||||
import UploadsTab from './admin_tabs/uploads_tab.vue'
|
import UploadsTab from './admin_tabs/uploads_tab.vue'
|
||||||
|
import UsersTab from './admin_tabs/users_tab.vue'
|
||||||
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
|
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
faBell,
|
||||||
faChain,
|
faChain,
|
||||||
faChartLine,
|
faChartLine,
|
||||||
faCircleNodes,
|
faCircleNodes,
|
||||||
faDoorOpen,
|
faDoorOpen,
|
||||||
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
|
faEyeSlash,
|
||||||
faGauge,
|
faGauge,
|
||||||
faGears,
|
faGears,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
faHand,
|
faHand,
|
||||||
|
faInfo,
|
||||||
faKey,
|
faKey,
|
||||||
faLaptopCode,
|
faLaptopCode,
|
||||||
faMessage,
|
faMessage,
|
||||||
|
faPaintBrush,
|
||||||
faTowerBroadcast,
|
faTowerBroadcast,
|
||||||
faUpload,
|
faUpload,
|
||||||
|
faUser,
|
||||||
faWrench,
|
faWrench,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
|
@ -45,6 +54,12 @@ library.add(
|
||||||
faChain,
|
faChain,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
faLaptopCode,
|
faLaptopCode,
|
||||||
|
faPaintBrush,
|
||||||
|
faBell,
|
||||||
|
faDownload,
|
||||||
|
faEyeSlash,
|
||||||
|
faInfo,
|
||||||
|
faUser,
|
||||||
faTowerBroadcast,
|
faTowerBroadcast,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
faChartLine,
|
faChartLine,
|
||||||
|
|
@ -63,11 +78,12 @@ const SettingsModalAdminContent = {
|
||||||
VerticalTabSwitcher,
|
VerticalTabSwitcher,
|
||||||
|
|
||||||
InstanceTab,
|
InstanceTab,
|
||||||
|
UsersTab,
|
||||||
|
LimitsTab,
|
||||||
RegistrationsTab,
|
RegistrationsTab,
|
||||||
EmojiTab,
|
EmojiTab,
|
||||||
FrontendsTab,
|
FrontendsTab,
|
||||||
FederationTab,
|
FederationTab,
|
||||||
LimitsTab,
|
|
||||||
MailerTab,
|
MailerTab,
|
||||||
UploadsTab,
|
UploadsTab,
|
||||||
MediaProxyTab,
|
MediaProxyTab,
|
||||||
|
|
@ -94,18 +110,18 @@ const SettingsModalAdminContent = {
|
||||||
return useInterfaceStore().settingsModalState === 'visible'
|
return useInterfaceStore().settingsModalState === 'visible'
|
||||||
},
|
},
|
||||||
adminDbLoaded() {
|
adminDbLoaded() {
|
||||||
return this.$store.state.adminSettings.loaded
|
return useAdminSettingsStore().loaded
|
||||||
},
|
},
|
||||||
adminDescriptionsLoaded() {
|
adminDescriptionsLoaded() {
|
||||||
return this.$store.state.adminSettings.descriptions !== null
|
return useAdminSettingsStore().descriptions !== null
|
||||||
},
|
},
|
||||||
noDb() {
|
noDb() {
|
||||||
return this.$store.state.adminSettings.dbConfigEnabled === false
|
return useAdminSettingsStore().dbConfigEnabled === false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.user.rights.admin) {
|
if (this.user.rights.admin) {
|
||||||
this.$store.dispatch('loadAdminStuff')
|
useAdminSettingsStore().loadAdminStuff()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
class="settings-admin-content settings_tab-switcher"
|
class="settings-admin-content settings_tab-switcher"
|
||||||
:side-tab-bar="true"
|
:side-tab-bar="true"
|
||||||
:scrollable-tabs="true"
|
:scrollable-tabs
|
||||||
:render-only-focused="true"
|
:render-only-focused="true"
|
||||||
:body-scroll-lock="bodyLock"
|
:body-scroll-lock="bodyLock"
|
||||||
>
|
>
|
||||||
|
|
@ -49,6 +49,16 @@
|
||||||
<InstanceTab />
|
<InstanceTab />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:label="$t('admin_dash.tabs.users')"
|
||||||
|
icon="user"
|
||||||
|
data-tab-name="users"
|
||||||
|
full-width
|
||||||
|
full-height
|
||||||
|
>
|
||||||
|
<UsersTab />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
:label="$t('admin_dash.tabs.registrations')"
|
:label="$t('admin_dash.tabs.registrations')"
|
||||||
icon="door-open"
|
icon="door-open"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<vertical-tab-switcher
|
<vertical-tab-switcher
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
class="settings_tab-switcher"
|
class="settings_tab-switcher"
|
||||||
:scrollable-tabs="true"
|
:scrollable-tabs
|
||||||
:body-scroll-lock="bodyLock"
|
:body-scroll-lock="bodyLock"
|
||||||
:hide-header="navHideHeader"
|
:hide-header="navHideHeader"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,17 @@
|
||||||
import { get, map, reject } from 'lodash'
|
import { get, isEmpty, map, reject } from 'lodash'
|
||||||
|
|
||||||
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
|
|
||||||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
|
||||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||||
import BlockCard from 'src/components/block_card/block_card.vue'
|
import BlockCard from 'src/components/block_card/block_card.vue'
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
|
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
|
||||||
|
import List from 'src/components/list/list.vue'
|
||||||
import MuteCard from 'src/components/mute_card/mute_card.vue'
|
import MuteCard from 'src/components/mute_card/mute_card.vue'
|
||||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
|
||||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js'
|
import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js'
|
||||||
|
|
||||||
const BlockList = withLoadMore({
|
|
||||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
|
||||||
select: (props, $store) =>
|
|
||||||
get($store.state.users.currentUser, 'blockIds', []),
|
|
||||||
destroy: () => {
|
|
||||||
/* no-op */
|
|
||||||
},
|
|
||||||
childPropName: 'items',
|
|
||||||
})(SelectableList)
|
|
||||||
|
|
||||||
const MuteList = withLoadMore({
|
|
||||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
|
||||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
|
||||||
destroy: () => {
|
|
||||||
/* no-op */
|
|
||||||
},
|
|
||||||
childPropName: 'items',
|
|
||||||
})(SelectableList)
|
|
||||||
|
|
||||||
const DomainMuteList = withSubscription({
|
|
||||||
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
|
||||||
select: (props, $store) =>
|
|
||||||
get($store.state.users.currentUser, 'domainMutes', []),
|
|
||||||
childPropName: 'items',
|
|
||||||
})(SelectableList)
|
|
||||||
|
|
||||||
const MutesAndBlocks = {
|
const MutesAndBlocks = {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -52,12 +24,10 @@ const MutesAndBlocks = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
BlockList,
|
|
||||||
MuteList,
|
|
||||||
DomainMuteList,
|
|
||||||
BlockCard,
|
|
||||||
MuteCard,
|
|
||||||
DomainMuteCard,
|
DomainMuteCard,
|
||||||
|
BlockCard,
|
||||||
|
List,
|
||||||
|
MuteCard,
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Autosuggest,
|
Autosuggest,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|
@ -69,8 +39,20 @@ const MutesAndBlocks = {
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
|
blocks() {
|
||||||
|
return get(this.$store.state.users.currentUser, 'blockIds', [])
|
||||||
|
},
|
||||||
|
mutes() {
|
||||||
|
return get(this.$store.state.users.currentUser, 'muteIds', [])
|
||||||
|
},
|
||||||
|
domains() {
|
||||||
|
return get(this.$store.state.users.currentUser, 'domainMutes', [])
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
fetchItems(group) {
|
||||||
|
return () => this.$store.dispatch('fetch' + group, this.userId)
|
||||||
|
},
|
||||||
importFollows(file) {
|
importFollows(file) {
|
||||||
return this.$store.state.api.backendInteractor
|
return this.$store.state.api.backendInteractor
|
||||||
.importFollows({ file })
|
.importFollows({ file })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
.mutes-and-blocks-tab {
|
.mutes-and-blocks-tab {
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
|
|
||||||
.usersearch-wrapper {
|
.usersearch-wrapper {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
@ -26,4 +26,13 @@
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
width: 10em;
|
width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blocks,
|
||||||
|
.mutes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<tab-switcher
|
<tab-switcher
|
||||||
:scrollable-tabs="true"
|
|
||||||
class="mutes-and-blocks-tab"
|
class="mutes-and-blocks-tab"
|
||||||
|
:scrollable-tabs
|
||||||
>
|
>
|
||||||
<div :label="$t('settings.blocks_tab')">
|
<div
|
||||||
|
class="blocks"
|
||||||
|
:label="$t('settings.user_blocks')"
|
||||||
|
>
|
||||||
<div class="usersearch-wrapper">
|
<div class="usersearch-wrapper">
|
||||||
<Autosuggest
|
<Autosuggest
|
||||||
:filter="filterUnblockedUsers"
|
:filter="filterUnblockedUsers"
|
||||||
|
|
@ -17,9 +20,12 @@
|
||||||
</template>
|
</template>
|
||||||
</Autosuggest>
|
</Autosuggest>
|
||||||
</div>
|
</div>
|
||||||
<BlockList
|
<List
|
||||||
:refresh="true"
|
|
||||||
:get-key="i => i"
|
:get-key="i => i"
|
||||||
|
:external-items="blocks"
|
||||||
|
:fetch-function="fetchItems('Blocks')"
|
||||||
|
scrollable
|
||||||
|
selectable
|
||||||
>
|
>
|
||||||
<template #header="{selected}">
|
<template #header="{selected}">
|
||||||
<div class="bulk-actions">
|
<div class="bulk-actions">
|
||||||
|
|
@ -51,103 +57,109 @@
|
||||||
<template #empty>
|
<template #empty>
|
||||||
{{ $t('settings.no_blocks') }}
|
{{ $t('settings.no_blocks') }}
|
||||||
</template>
|
</template>
|
||||||
</BlockList>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.mutes_tab')">
|
<div
|
||||||
<tab-switcher>
|
class="mutes"
|
||||||
<div :label="$t('settings.user_mutes')">
|
:label="$t('settings.user_mutes2')"
|
||||||
<div class="usersearch-wrapper">
|
>
|
||||||
<Autosuggest
|
<div class="usersearch-wrapper">
|
||||||
:filter="filterUnMutedUsers"
|
<Autosuggest
|
||||||
:query="queryUserIds"
|
:filter="filterUnMutedUsers"
|
||||||
:placeholder="$t('settings.search_user_to_mute')"
|
:query="queryUserIds"
|
||||||
|
:placeholder="$t('settings.search_user_to_mute')"
|
||||||
|
>
|
||||||
|
<template #default="row">
|
||||||
|
<MuteCard
|
||||||
|
:user-id="row.item"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Autosuggest>
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
:get-key="i => i"
|
||||||
|
:external-items="mutes"
|
||||||
|
:fetch-function="fetchItems('Mutes')"
|
||||||
|
scrollable
|
||||||
|
selectable
|
||||||
|
>
|
||||||
|
<template #header="{selected}">
|
||||||
|
<div class="bulk-actions">
|
||||||
|
<ProgressButton
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
class="btn button-default"
|
||||||
|
:click="() => muteUsers(selected)"
|
||||||
>
|
>
|
||||||
<template #default="row">
|
{{ $t('user_card.mute') }}
|
||||||
<MuteCard
|
<template #progress>
|
||||||
:user-id="row.item"
|
{{ $t('user_card.mute_progress') }}
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Autosuggest>
|
</ProgressButton>
|
||||||
|
<ProgressButton
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
class="btn button-default"
|
||||||
|
:click="() => unmuteUsers(selected)"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.unmute') }}
|
||||||
|
<template #progress>
|
||||||
|
{{ $t('user_card.unmute_progress') }}
|
||||||
|
</template>
|
||||||
|
</ProgressButton>
|
||||||
</div>
|
</div>
|
||||||
<MuteList
|
</template>
|
||||||
:refresh="true"
|
<template #item="{item}">
|
||||||
:get-key="i => i"
|
<MuteCard :user-id="item" />
|
||||||
>
|
</template>
|
||||||
<template #header="{selected}">
|
<template #empty>
|
||||||
<div class="bulk-actions">
|
{{ $t('settings.no_mutes') }}
|
||||||
<ProgressButton
|
</template>
|
||||||
v-if="selected.length > 0"
|
</List>
|
||||||
class="btn button-default"
|
</div>
|
||||||
:click="() => muteUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.mute') }}
|
|
||||||
<template #progress>
|
|
||||||
{{ $t('user_card.mute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
<ProgressButton
|
|
||||||
v-if="selected.length > 0"
|
|
||||||
class="btn button-default"
|
|
||||||
:click="() => unmuteUsers(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('user_card.unmute') }}
|
|
||||||
<template #progress>
|
|
||||||
{{ $t('user_card.unmute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item="{item}">
|
|
||||||
<MuteCard :user-id="item" />
|
|
||||||
</template>
|
|
||||||
<template #empty>
|
|
||||||
{{ $t('settings.no_mutes') }}
|
|
||||||
</template>
|
|
||||||
</MuteList>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :label="$t('settings.domain_mutes')">
|
<div :label="$t('settings.domain_mutes2')">
|
||||||
<div class="domain-mute-form">
|
<div class="domain-mute-form">
|
||||||
<Autosuggest
|
<Autosuggest
|
||||||
:filter="filterUnMutedDomains"
|
:filter="filterUnMutedDomains"
|
||||||
:query="queryKnownDomains"
|
:query="queryKnownDomains"
|
||||||
:placeholder="$t('settings.type_domains_to_mute')"
|
:placeholder="$t('settings.type_domains_to_mute')"
|
||||||
|
>
|
||||||
|
<template #default="row">
|
||||||
|
<DomainMuteCard
|
||||||
|
:domain="row.item"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Autosuggest>
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
:get-key="i => i"
|
||||||
|
:external-items="domains"
|
||||||
|
:fetch-function="fetchItems('DomainMutes')"
|
||||||
|
scrollable
|
||||||
|
selectable
|
||||||
|
>
|
||||||
|
<template #header="{selected}">
|
||||||
|
<div class="bulk-actions">
|
||||||
|
<ProgressButton
|
||||||
|
v-if="selected.length > 0"
|
||||||
|
class="btn button-default"
|
||||||
|
:click="() => unmuteDomains(selected)"
|
||||||
>
|
>
|
||||||
<template #default="row">
|
{{ $t('domain_mute_card.unmute') }}
|
||||||
<DomainMuteCard
|
<template #progress>
|
||||||
:domain="row.item"
|
{{ $t('domain_mute_card.unmute_progress') }}
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Autosuggest>
|
</ProgressButton>
|
||||||
</div>
|
</div>
|
||||||
<DomainMuteList
|
</template>
|
||||||
:refresh="true"
|
<template #item="{item}">
|
||||||
:get-key="i => i"
|
{{ item }}
|
||||||
>
|
<DomainMuteCard :domain="item" />
|
||||||
<template #header="{selected}">
|
</template>
|
||||||
<div class="bulk-actions">
|
<template #empty>
|
||||||
<ProgressButton
|
{{ $t('settings.no_mutes') }}
|
||||||
v-if="selected.length > 0"
|
</template>
|
||||||
class="btn button-default"
|
</List>
|
||||||
:click="() => unmuteDomains(selected)"
|
|
||||||
>
|
|
||||||
{{ $t('domain_mute_card.unmute') }}
|
|
||||||
<template #progress>
|
|
||||||
{{ $t('domain_mute_card.unmute_progress') }}
|
|
||||||
</template>
|
|
||||||
</ProgressButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #item="{item}">
|
|
||||||
<DomainMuteCard :domain="item" />
|
|
||||||
</template>
|
|
||||||
<template #empty>
|
|
||||||
{{ $t('settings.no_mutes') }}
|
|
||||||
</template>
|
|
||||||
</DomainMuteList>
|
|
||||||
</div>
|
|
||||||
</tab-switcher>
|
|
||||||
</div>
|
</div>
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const NotificationsTab = {
|
||||||
if (!this.user) {
|
if (!this.user) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return this.user.privileges.includes('reports_manage_reports')
|
return this.user.privileges.has('reports_manage_reports')
|
||||||
},
|
},
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -131,40 +131,41 @@ const Status = {
|
||||||
Quote: defineAsyncComponent(() => import('src/components/quote/quote.vue')),
|
Quote: defineAsyncComponent(() => import('src/components/quote/quote.vue')),
|
||||||
StatusActionButtons,
|
StatusActionButtons,
|
||||||
},
|
},
|
||||||
props: [
|
props: {
|
||||||
'statusoid',
|
statusoid: Object,
|
||||||
'replies',
|
replies: Array,
|
||||||
|
|
||||||
'expandable',
|
expandable: Boolean,
|
||||||
'focused',
|
focused: Boolean,
|
||||||
'highlight',
|
highlight: Boolean,
|
||||||
'compact',
|
compact: Boolean,
|
||||||
'isPreview',
|
isPreview: Boolean,
|
||||||
'noHeading',
|
noHeading: Boolean,
|
||||||
'inlineExpanded',
|
inlineExpanded: Boolean,
|
||||||
'showPinned',
|
showPinned: Boolean,
|
||||||
'inProfile',
|
inProfile: Boolean,
|
||||||
'inConversation',
|
inConversation: Boolean,
|
||||||
'inQuote',
|
inQuote: Boolean,
|
||||||
'profileUserId',
|
|
||||||
'simpleTree',
|
|
||||||
'showOtherRepliesAsButton',
|
|
||||||
'dive',
|
|
||||||
'ignoreMute',
|
|
||||||
|
|
||||||
'controlledThreadDisplayStatus',
|
profileUserId: String,
|
||||||
'controlledToggleThreadDisplay',
|
simpleTree: Boolean,
|
||||||
'controlledShowingTall',
|
showOtherRepliesAsButton: Boolean,
|
||||||
'controlledToggleShowingTall',
|
dive: Function,
|
||||||
'controlledExpandingSubject',
|
ignoreMute: Boolean,
|
||||||
'controlledToggleExpandingSubject',
|
|
||||||
'controlledShowingLongSubject',
|
controlledThreadDisplayStatus: String,
|
||||||
'controlledToggleShowingLongSubject',
|
controlledToggleThreadDisplay: Function,
|
||||||
'controlledReplying',
|
controlledShowingTall: Boolean,
|
||||||
'controlledToggleReplying',
|
controlledToggleShowingTall: Function,
|
||||||
'controlledMediaPlaying',
|
controlledExpandingSubject: Boolean,
|
||||||
'controlledSetMediaPlaying',
|
controlledToggleExpandingSubject: Function,
|
||||||
],
|
controlledShowingLongSubject: Boolean,
|
||||||
|
controlledToggleShowingLongSubject: Function,
|
||||||
|
controlledReplying: Boolean,
|
||||||
|
controlledToggleReplying: Function,
|
||||||
|
controlledMediaPlaying: Boolean,
|
||||||
|
controlledSetMediaPlaying: Function,
|
||||||
|
},
|
||||||
emits: ['goto', 'toggleExpanded'],
|
emits: ['goto', 'toggleExpanded'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faExternalLinkAlt,
|
faExternalLinkAlt,
|
||||||
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faHistory,
|
faHistory,
|
||||||
faMinus,
|
faMinus,
|
||||||
|
|
@ -51,6 +52,7 @@ library.add(
|
||||||
faBookmark,
|
faBookmark,
|
||||||
faBookmarkRegular,
|
faBookmarkRegular,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
|
faEye,
|
||||||
faThumbtack,
|
faThumbtack,
|
||||||
faShareAlt,
|
faShareAlt,
|
||||||
faExternalLinkAlt,
|
faExternalLinkAlt,
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,30 @@ import { defineAsyncComponent } from 'vue'
|
||||||
import Popover from 'src/components/popover/popover.vue'
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
import ActionButton from './action_button.vue'
|
import ActionButton from './action_button.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
faEnvelope,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
faFolderTree,
|
faFolderTree,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
|
faLock,
|
||||||
|
faLockOpen,
|
||||||
faUser,
|
faUser,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(faUser, faGlobe, faFolderTree)
|
library.add(
|
||||||
|
faUser,
|
||||||
|
faGlobe,
|
||||||
|
faFolderTree,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faLock,
|
||||||
|
faLockOpen,
|
||||||
|
faEnvelope,
|
||||||
|
)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -61,8 +77,27 @@ export default {
|
||||||
this.domain,
|
this.domain,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
availableScopes() {
|
||||||
|
return ['private', 'unlisted', 'direct', 'public'].filter((scope) => {
|
||||||
|
return scope !== this.status.visibility
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
visibilityIcon(visibility) {
|
||||||
|
switch (visibility) {
|
||||||
|
case 'private':
|
||||||
|
return 'lock'
|
||||||
|
case 'unlisted':
|
||||||
|
return 'lock-open'
|
||||||
|
case 'direct':
|
||||||
|
return 'envelope'
|
||||||
|
case 'local':
|
||||||
|
return 'igloo'
|
||||||
|
default:
|
||||||
|
return 'globe'
|
||||||
|
}
|
||||||
|
},
|
||||||
unmuteUser() {
|
unmuteUser() {
|
||||||
return this.$store.dispatch('unmuteUser', this.user.id)
|
return this.$store.dispatch('unmuteUser', this.user.id)
|
||||||
},
|
},
|
||||||
|
|
@ -79,6 +114,18 @@ export default {
|
||||||
this.$refs.confirmUser.optionallyPrompt()
|
this.$refs.confirmUser.optionallyPrompt()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setScope(visibility) {
|
||||||
|
return useAdminSettingsStore().changeStatusScope({
|
||||||
|
id: this.status.id,
|
||||||
|
visibility,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setSensitive(sensitive) {
|
||||||
|
useAdminSettingsStore().changeStatusScope({
|
||||||
|
id: this.status.id,
|
||||||
|
sensitive,
|
||||||
|
})
|
||||||
|
},
|
||||||
toggleConversationMute() {
|
toggleConversationMute() {
|
||||||
if (this.conversationIsMuted) {
|
if (this.conversationIsMuted) {
|
||||||
this.unmuteConversation()
|
this.unmuteConversation()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,59 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
<div
|
||||||
|
v-if="button.name === 'changeScope'"
|
||||||
|
:id="`popup-menu-scope-${randomSeed}`"
|
||||||
|
class="dropdown-menu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="visibility in availableScopes"
|
||||||
|
:key="visibility"
|
||||||
|
class="menu-item dropdown-item extra-action -icon"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="main-button"
|
||||||
|
@click="() => setScope(visibility)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
:icon="visibilityIcon(visibility)"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
{{ $t('general.scope_in_timeline.' + visibility) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="status.nsfw"
|
||||||
|
class="menu-item dropdown-item extra-action -icon"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="main-button"
|
||||||
|
@click="() => setSensitive(false)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
icon="eye"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
{{ $t('status.mark_as_non-sensitive') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="menu-item dropdown-item extra-action -icon"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="main-button"
|
||||||
|
@click="() => setSensitive(true)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
icon="eye-slash"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
|
{{ $t('status.mark_as_sensitive') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="button.name === 'mute'"
|
v-if="button.name === 'mute'"
|
||||||
:id="`popup-menu-${randomSeed}`"
|
:id="`popup-menu-${randomSeed}`"
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ export const BUTTONS = [
|
||||||
return (
|
return (
|
||||||
loggedIn &&
|
loggedIn &&
|
||||||
(status.user.id === currentUser.id ||
|
(status.user.id === currentUser.id ||
|
||||||
currentUser.privileges.includes('messages_delete'))
|
currentUser.privileges.has('messages_delete'))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirm: ({ getters }) => useMergedConfigStore().mergedConfig.modalOnDelete,
|
confirm: ({ getters }) => useMergedConfigStore().mergedConfig.modalOnDelete,
|
||||||
|
|
@ -243,6 +243,26 @@ export const BUTTONS = [
|
||||||
return dispatch('deleteStatus', { id: status.id })
|
return dispatch('deleteStatus', { id: status.id })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// =========
|
||||||
|
// CHANGE SCOPE
|
||||||
|
// =========
|
||||||
|
name: 'changeScope',
|
||||||
|
icon: 'eye',
|
||||||
|
label: 'status.admin_change_scope',
|
||||||
|
if({ status, loggedIn, currentUser }) {
|
||||||
|
return (
|
||||||
|
loggedIn &&
|
||||||
|
(status.user.id === currentUser.id ||
|
||||||
|
currentUser.privileges.has('messages_delete'))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
toggleable: false,
|
||||||
|
dropdown: true,
|
||||||
|
action({ status, dispatch, emit }) {
|
||||||
|
/* prevent hiding */
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// =========
|
// =========
|
||||||
// SHARE/COPY
|
// SHARE/COPY
|
||||||
|
|
@ -279,10 +299,12 @@ export const BUTTONS = [
|
||||||
label: 'user_card.report',
|
label: 'user_card.report',
|
||||||
if: ({ loggedIn }) => loggedIn,
|
if: ({ loggedIn }) => loggedIn,
|
||||||
action({ status }) {
|
action({ status }) {
|
||||||
return useReportsStore().openUserReportingModal({
|
useReportsStore().openUserReportingModal({
|
||||||
userId: status.user.id,
|
userId: status.user.id,
|
||||||
statusIds: [status.id],
|
statusIds: [status.id],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
].map((button) => {
|
].map((button) => {
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,14 @@
|
||||||
|
|
||||||
.contents.scrollable-tabs {
|
.contents.scrollable-tabs {
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.contents {
|
.contents {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,12 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
// Hide action buttons
|
||||||
|
hideButtons: {
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
// default - open profile, 'zoom' - zoom, function - call function
|
// default - open profile, 'zoom' - zoom, function - call function
|
||||||
avatarAction: {
|
avatarAction: {
|
||||||
required: false,
|
required: false,
|
||||||
|
|
@ -280,7 +286,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visibleRole() {
|
visibleRole() {
|
||||||
if (!this.newShowRole) {
|
if (!this.user.show_role && !this.user.adminData) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const rights = this.user.rights
|
const rights = this.user.rights
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
&[open] summary {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input.bio {
|
.input.bio {
|
||||||
height: auto; // override settings default textarea size
|
height: auto; // override settings default textarea size
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +73,7 @@
|
||||||
|
|
||||||
.user-card-bio {
|
.user-card-bio {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0 0.6em;
|
margin: 0.6em;
|
||||||
|
|
||||||
&.input {
|
&.input {
|
||||||
margin: 0 1em;
|
margin: 0 1em;
|
||||||
|
|
@ -81,15 +98,18 @@
|
||||||
--_still-image-label-visibility: hidden;
|
--_still-image-label-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-data,
|
||||||
.personal-marks {
|
.personal-marks {
|
||||||
margin: 0.6em;
|
margin: 0 0.6em;
|
||||||
padding: 0.6em;
|
padding: 0 0.6em;
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
border-bottom: 1px dotted var(--border);
|
border-bottom: 1px dotted var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlighter {
|
.highlighter {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
margin-top: 0.6em;
|
margin-top: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<AccountActions
|
<AccountActions
|
||||||
v-if="isOtherUser && loggedIn"
|
v-if="isOtherUser && loggedIn && !hideButtons"
|
||||||
:user="user"
|
:user="user"
|
||||||
:relationship="relationship"
|
:relationship="relationship"
|
||||||
/>
|
/>
|
||||||
|
|
@ -228,7 +228,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="loggedIn && isOtherUser"
|
v-if="loggedIn && isOtherUser && !hideButtons"
|
||||||
class="user-interactions"
|
class="user-interactions"
|
||||||
>
|
>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
|
|
@ -291,8 +291,7 @@
|
||||||
</button>
|
</button>
|
||||||
<ModerationTools
|
<ModerationTools
|
||||||
v-if="showModerationMenu"
|
v-if="showModerationMenu"
|
||||||
class="moderation-menu"
|
:users="[user]"
|
||||||
:user="user"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -348,6 +347,122 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="user.adminData && !hideBio"
|
||||||
|
class="admin-data"
|
||||||
|
>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
{{ $t('user_card.admin_data.data') }}
|
||||||
|
</summary>
|
||||||
|
<div class="user-profile-fields">
|
||||||
|
<dl class="user-profile-field">
|
||||||
|
<dt class="user-profile-field-name">
|
||||||
|
{{ $t('admin_dash.users.local_id') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="user-profile-field-value">
|
||||||
|
{{ user.adminData.id }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl
|
||||||
|
v-if="user.is_local"
|
||||||
|
class="user-profile-field"
|
||||||
|
>
|
||||||
|
<dt class="user-profile-field-name">
|
||||||
|
{{ $t('admin_dash.users.labels.email') }}
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
class="user-profile-field-value"
|
||||||
|
:class="{ faint: user.adminData.email == null }"
|
||||||
|
>
|
||||||
|
{{ user.adminData.email == null ? $t('general.not_available') : user.adminData.email }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl
|
||||||
|
v-if="user.is_local"
|
||||||
|
class="user-profile-field"
|
||||||
|
>
|
||||||
|
<dt class="user-profile-field-name">
|
||||||
|
{{ $t('general.role.admin') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="user-profile-field-value">
|
||||||
|
{{ $t('general.' + (user.adminData.roles.admin ? 'yes' : 'no')) }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl
|
||||||
|
v-if="user.is_local"
|
||||||
|
class="user-profile-field"
|
||||||
|
>
|
||||||
|
<dt class="user-profile-field-name">
|
||||||
|
{{ $t('general.role.moderator') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="user-profile-field-value">
|
||||||
|
{{ $t('general.' + (user.adminData.roles.moderator ? 'yes' : 'no')) }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl
|
||||||
|
v-if="user.is_local"
|
||||||
|
class="user-profile-field"
|
||||||
|
>
|
||||||
|
<dt class="user-profile-field-name">
|
||||||
|
{{ $t('admin_dash.users.indicator.confirmed') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="user-profile-field-value">
|
||||||
|
{{ $t('general.' + (user.adminData.is_confirmed ? 'yes' : 'no')) }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl
|
||||||
|
v-if="user.is_local"
|
||||||
|
class="user-profile-field"
|
||||||
|
>
|
||||||
|
<dt class="user-profile-field-name">
|
||||||
|
{{ $t('admin_dash.users.indicator.approved') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="user-profile-field-value">
|
||||||
|
{{ $t('general.' + (user.adminData.is_approved ? 'yes' : 'no')) }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="user-profile-field">
|
||||||
|
<dt class="user-profile-field-name">
|
||||||
|
{{ $t('admin_dash.users.indicator.suggested') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="user-profile-field-value">
|
||||||
|
{{ $t('general.' + (user.adminData.is_suggested ? 'yes' : 'no')) }}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<details
|
||||||
|
v-if="user.is_local"
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<summary>
|
||||||
|
{{ $t('user_card.admin_data.registration_reason') }}
|
||||||
|
</summary>
|
||||||
|
<span>
|
||||||
|
{{ user.adminData.registration_reason == null ? $t('general.not_available') : user.adminData.registration_reason }}
|
||||||
|
</span>
|
||||||
|
</details>
|
||||||
|
<details open>
|
||||||
|
<summary>
|
||||||
|
{{ $t('user_card.admin_data.tags') }}
|
||||||
|
</summary>
|
||||||
|
<ul>
|
||||||
|
<li v-if="user.adminData.tags.length === 0">
|
||||||
|
{{ $t('general.none') }}
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="tag in user.adminData.tags"
|
||||||
|
:key="tag"
|
||||||
|
>
|
||||||
|
<code>
|
||||||
|
{{ tag }}
|
||||||
|
</code>
|
||||||
|
{{ ' ' }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
<h3 v-if="editable">
|
<h3 v-if="editable">
|
||||||
<span>
|
<span>
|
||||||
{{ $t('settings.bio') }}
|
{{ $t('settings.bio') }}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { get } from 'lodash'
|
import { get } from 'lodash'
|
||||||
import { mapState } from 'pinia'
|
import { mapState } from 'pinia'
|
||||||
|
|
||||||
import Conversation from 'src/components/conversation/conversation.vue'
|
|
||||||
import FollowCard from 'src/components/follow_card/follow_card.vue'
|
import FollowCard from 'src/components/follow_card/follow_card.vue'
|
||||||
import List from 'src/components/list/list.vue'
|
import List from 'src/components/list/list.vue'
|
||||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
import Timeline from 'src/components/timeline/timeline.vue'
|
import Timeline from 'src/components/timeline/timeline.vue'
|
||||||
import UserCard from 'src/components/user_card/user_card.vue'
|
import UserCard from 'src/components/user_card/user_card.vue'
|
||||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
|
||||||
|
|
||||||
import { useInstanceStore } from 'src/stores/instance.js'
|
import { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||||
|
|
@ -19,28 +17,6 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(faCircleNotch)
|
library.add(faCircleNotch)
|
||||||
|
|
||||||
const FollowerList = withLoadMore({
|
|
||||||
fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
|
|
||||||
select: (props, $store) =>
|
|
||||||
get($store.getters.findUser(props.userId), 'followerIds', []).map((id) =>
|
|
||||||
$store.getters.findUser(id),
|
|
||||||
),
|
|
||||||
destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
|
||||||
childPropName: 'items',
|
|
||||||
additionalPropNames: ['userId'],
|
|
||||||
})(List)
|
|
||||||
|
|
||||||
const FriendList = withLoadMore({
|
|
||||||
fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
|
|
||||||
select: (props, $store) =>
|
|
||||||
get($store.getters.findUser(props.userId), 'friendIds', []).map((id) =>
|
|
||||||
$store.getters.findUser(id),
|
|
||||||
),
|
|
||||||
destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
|
||||||
childPropName: 'items',
|
|
||||||
additionalPropNames: ['userId'],
|
|
||||||
})(List)
|
|
||||||
|
|
||||||
const defaultTabKey = 'statuses'
|
const defaultTabKey = 'statuses'
|
||||||
|
|
||||||
const UserProfile = {
|
const UserProfile = {
|
||||||
|
|
@ -64,6 +40,8 @@ const UserProfile = {
|
||||||
unmounted() {
|
unmounted() {
|
||||||
this.stopFetching()
|
this.stopFetching()
|
||||||
useInterfaceStore().setForeignProfileBackground(null)
|
useInterfaceStore().setForeignProfileBackground(null)
|
||||||
|
this.$store.dispatch('clearFollowers', this.userId)
|
||||||
|
this.$store.dispatch('clearFriends', this.userId)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
timeline() {
|
timeline() {
|
||||||
|
|
@ -104,11 +82,31 @@ const UserProfile = {
|
||||||
compactProfiles() {
|
compactProfiles() {
|
||||||
return useMergedConfigStore().mergedConfig.compactProfiles
|
return useMergedConfigStore().mergedConfig.compactProfiles
|
||||||
},
|
},
|
||||||
|
friends() {
|
||||||
|
return get(
|
||||||
|
this.$store.getters.findUser(this.userId),
|
||||||
|
'friendIds',
|
||||||
|
[],
|
||||||
|
).map((id) => this.$store.getters.findUser(id))
|
||||||
|
},
|
||||||
|
followers() {
|
||||||
|
return get(
|
||||||
|
this.$store.getters.findUser(this.userId),
|
||||||
|
'followerIds',
|
||||||
|
[],
|
||||||
|
).map((id) => this.$store.getters.findUser(id))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setFooterRef(el) {
|
setFooterRef(el) {
|
||||||
this.footerRef = el
|
this.footerRef = el
|
||||||
},
|
},
|
||||||
|
fetchUsers(group) {
|
||||||
|
return () =>
|
||||||
|
this.$store
|
||||||
|
.dispatch('fetch' + group, this.userId)
|
||||||
|
.then((result) => ({ items: result }))
|
||||||
|
},
|
||||||
load(userNameOrId) {
|
load(userNameOrId) {
|
||||||
const startFetchingTimeline = (timeline, userId) => {
|
const startFetchingTimeline = (timeline, userId) => {
|
||||||
// Clear timeline only if load another user's profile
|
// Clear timeline only if load another user's profile
|
||||||
|
|
@ -203,11 +201,9 @@ const UserProfile = {
|
||||||
components: {
|
components: {
|
||||||
UserCard,
|
UserCard,
|
||||||
Timeline,
|
Timeline,
|
||||||
FollowerList,
|
List,
|
||||||
FriendList,
|
|
||||||
FollowCard,
|
FollowCard,
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
Conversation,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
65
src/components/user_profile/user_profile.scss
Normal file
65
src/components/user_profile/user_profile.scss
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
.user-profile {
|
||||||
|
flex: 2;
|
||||||
|
|
||||||
|
.card-wrapper {
|
||||||
|
border-top-left-radius: var(--roundness);
|
||||||
|
border-top-right-radius: var(--roundness);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
border-bottom-left-radius: var(--roundness);
|
||||||
|
border-bottom-right-radius: var(--roundness);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No sticky header on user profile
|
||||||
|
--currentPanelStack: 0;
|
||||||
|
|
||||||
|
.userlist-placeholder {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
margin: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-admin-view {
|
||||||
|
.filter {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
|
.Status{
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-placeholder {
|
||||||
|
.panel-body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 0.75em 5em;
|
||||||
|
border-width: 2px;
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,14 +39,14 @@
|
||||||
:label="$t('user_card.followees')"
|
:label="$t('user_card.followees')"
|
||||||
:disabled="!user.friends_count"
|
:disabled="!user.friends_count"
|
||||||
>
|
>
|
||||||
<FriendList
|
<List
|
||||||
:user-id="userId"
|
:fetch-function="fetchUsers('Friends')"
|
||||||
:non-interactive="true"
|
:external-items="friends"
|
||||||
>
|
>
|
||||||
<template #item="{item}">
|
<template #item="{item}">
|
||||||
<FollowCard :user="item" />
|
<FollowCard :user="item" />
|
||||||
</template>
|
</template>
|
||||||
</FriendList>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="followersTabVisible"
|
v-if="followersTabVisible"
|
||||||
|
|
@ -55,9 +55,9 @@
|
||||||
:label="$t('user_card.followers')"
|
:label="$t('user_card.followers')"
|
||||||
:disabled="!user.followers_count"
|
:disabled="!user.followers_count"
|
||||||
>
|
>
|
||||||
<FollowerList
|
<List
|
||||||
:user-id="userId"
|
:fetch-function="fetchUsers('Followers')"
|
||||||
:non-interactive="true"
|
:external-items="followers"
|
||||||
>
|
>
|
||||||
<template #item="{item}">
|
<template #item="{item}">
|
||||||
<FollowCard
|
<FollowCard
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
:no-follows-you="isUs"
|
:no-follows-you="isUs"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FollowerList>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
<Timeline
|
<Timeline
|
||||||
key="media"
|
key="media"
|
||||||
|
|
@ -126,52 +126,4 @@
|
||||||
|
|
||||||
<script src="./user_profile.js"></script>
|
<script src="./user_profile.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style src="./user_profile.scss" lang="scss"></style>
|
||||||
.user-profile {
|
|
||||||
flex: 2;
|
|
||||||
|
|
||||||
.card-wrapper {
|
|
||||||
border-top-left-radius: var(--roundness);
|
|
||||||
border-top-right-radius: var(--roundness);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-footer {
|
|
||||||
border-bottom-left-radius: var(--roundness);
|
|
||||||
border-bottom-right-radius: var(--roundness);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No sticky header on user profile
|
|
||||||
--currentPanelStack: 0;
|
|
||||||
|
|
||||||
.userlist-placeholder {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
margin: 1.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-profile-placeholder {
|
|
||||||
.panel-body {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert {
|
|
||||||
padding: 0.75em 5em;
|
|
||||||
border-width: 2px;
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
71
src/components/user_profile/user_profile_admin_view.js
Normal file
71
src/components/user_profile/user_profile_admin_view.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { get } from 'lodash'
|
||||||
|
import { mapState } from 'pinia'
|
||||||
|
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
import List from 'src/components/list/list.vue'
|
||||||
|
import Status from 'src/components/status/status.vue'
|
||||||
|
import UserCard from 'src/components/user_card/user_card.vue'
|
||||||
|
|
||||||
|
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
|
||||||
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(faCircleNotch)
|
||||||
|
|
||||||
|
const UserProfileAdminView = {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
godmode: false,
|
||||||
|
showReblogs: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$store.dispatch('fetchUserIfMissing', this.userId)
|
||||||
|
useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
useInterfaceStore().setForeignProfileBackground(this.user?.background_image)
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
useInterfaceStore().setForeignProfileBackground(null)
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fetchOptions() {
|
||||||
|
return {
|
||||||
|
pageSize: 20,
|
||||||
|
godmode: this.godmode,
|
||||||
|
id: this.userId,
|
||||||
|
withReblogs: this.showReblogs,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.getters.findUser(this.userId)
|
||||||
|
},
|
||||||
|
userId() {
|
||||||
|
return this.$route.params.id
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchStatuses(page) {
|
||||||
|
return useAdminSettingsStore().fetchStatuses({
|
||||||
|
...this.fetchOptions,
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
UserCard,
|
||||||
|
List,
|
||||||
|
Status,
|
||||||
|
Checkbox,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
fetchOptions() {
|
||||||
|
this.$refs.list.reset()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserProfileAdminView
|
||||||
45
src/components/user_profile/user_profile_admin_view.vue
Normal file
45
src/components/user_profile/user_profile_admin_view.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="user"
|
||||||
|
class="user-profile -admin-view panel panel-default"
|
||||||
|
>
|
||||||
|
<div class="panel-body card-wrapper">
|
||||||
|
<UserCard
|
||||||
|
:user-id="userId"
|
||||||
|
:compact="true"
|
||||||
|
hide-bio
|
||||||
|
hide-buttons
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
v-model="godmode"
|
||||||
|
class="filter"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.filters.show_direct') }}
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox
|
||||||
|
v-model="showReblogs"
|
||||||
|
class="filter"
|
||||||
|
>
|
||||||
|
{{ $t('admin_dash.users.filters.show_reblogs') }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
ref="list"
|
||||||
|
:fetch-function="fetchStatuses"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<template #item="{item}">
|
||||||
|
<Status
|
||||||
|
:statusoid="item"
|
||||||
|
:in-conversation="false"
|
||||||
|
:focused="false"
|
||||||
|
ignore-mute
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./user_profile_admin_view.js"></script>
|
||||||
|
|
||||||
|
<style src="./user_profile.scss" lang="scss"></style>
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { mapState } from 'pinia'
|
||||||
|
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import List from 'src/components/list/list.vue'
|
import List from 'src/components/list/list.vue'
|
||||||
import Modal from 'src/components/modal/modal.vue'
|
import Modal from 'src/components/modal/modal.vue'
|
||||||
|
|
@ -16,15 +18,12 @@ const UserReportingModal = {
|
||||||
return {
|
return {
|
||||||
comment: '',
|
comment: '',
|
||||||
forward: false,
|
forward: false,
|
||||||
statusIdsToReport: [],
|
statusIdsToReport: new Set(),
|
||||||
processing: false,
|
processing: false,
|
||||||
error: false,
|
error: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
reportModal() {
|
|
||||||
return useReportsStore().reportModal
|
|
||||||
},
|
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
|
|
@ -43,31 +42,26 @@ const UserReportingModal = {
|
||||||
this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1)
|
this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
statuses() {
|
...mapState(useReportsStore, ['reportModal']),
|
||||||
return this.reportModal.statuses
|
|
||||||
},
|
|
||||||
preTickedIds() {
|
|
||||||
return this.reportModal.preTickedIds
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
userId: 'resetState',
|
userId: 'resetState',
|
||||||
preTickedIds(newValue) {
|
|
||||||
this.statusIdsToReport = newValue
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resetState() {
|
resetState() {
|
||||||
// Reset state
|
// Reset state
|
||||||
this.comment = ''
|
this.comment = ''
|
||||||
this.forward = false
|
this.forward = false
|
||||||
this.statusIdsToReport = this.preTickedIds
|
this.statusIdsToReport = new Set(this.reportModal.preTickedIds)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.error = false
|
this.error = false
|
||||||
},
|
},
|
||||||
closeModal() {
|
closeModal() {
|
||||||
useReportsStore().closeUserReportingModal()
|
useReportsStore().closeUserReportingModal()
|
||||||
},
|
},
|
||||||
|
onListSelect(selected) {
|
||||||
|
this.statusIdsToReport = selected
|
||||||
|
},
|
||||||
reportUser() {
|
reportUser() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.error = false
|
this.error = false
|
||||||
|
|
@ -75,7 +69,7 @@ const UserReportingModal = {
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
comment: this.comment,
|
comment: this.comment,
|
||||||
forward: this.forward,
|
forward: this.forward,
|
||||||
statusIds: this.statusIdsToReport,
|
statusIds: [...this.statusIdsToReport],
|
||||||
}
|
}
|
||||||
this.$store.state.api.backendInteractor
|
this.$store.state.api.backendInteractor
|
||||||
.reportUser({ ...params })
|
.reportUser({ ...params })
|
||||||
|
|
@ -92,23 +86,6 @@ const UserReportingModal = {
|
||||||
clearError() {
|
clearError() {
|
||||||
this.error = false
|
this.error = false
|
||||||
},
|
},
|
||||||
isChecked(statusId) {
|
|
||||||
return this.statusIdsToReport.indexOf(statusId) !== -1
|
|
||||||
},
|
|
||||||
toggleStatus(checked, statusId) {
|
|
||||||
if (checked === this.isChecked(statusId)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
this.statusIdsToReport.push(statusId)
|
|
||||||
} else {
|
|
||||||
this.statusIdsToReport.splice(
|
|
||||||
this.statusIdsToReport.indexOf(statusId),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resize(e) {
|
resize(e) {
|
||||||
const target = e.target || e
|
const target = e.target || e
|
||||||
if (!(target instanceof window.Element)) {
|
if (!(target instanceof window.Element)) {
|
||||||
|
|
|
||||||
|
|
@ -51,19 +51,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-reporting-panel-right">
|
<div class="user-reporting-panel-right">
|
||||||
<List :items="statuses">
|
<List
|
||||||
|
:external-items="reportModal.statuses"
|
||||||
|
:pre-select="reportModal.preTickedIds"
|
||||||
|
selectable
|
||||||
|
@select="onListSelect"
|
||||||
|
>
|
||||||
<template #item="{item}">
|
<template #item="{item}">
|
||||||
<div class="status-fadein user-reporting-panel-sitem">
|
<Status
|
||||||
<Status
|
:in-conversation="false"
|
||||||
:in-conversation="false"
|
:focused="false"
|
||||||
:focused="false"
|
:statusoid="item"
|
||||||
:statusoid="item"
|
/>
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
:model-value="isChecked(item.id)"
|
|
||||||
@update:model-value="checked => toggleStatus(checked, item.id)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,20 +135,6 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-sitem {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
/* TODO cleanup this */
|
|
||||||
> .Status {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .checkbox {
|
|
||||||
margin: 0.75em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (width >= 801px) {
|
@media all and (width >= 801px) {
|
||||||
.panel-body {
|
.panel-body {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
||||||
|
|
@ -11,62 +11,64 @@
|
||||||
<p>
|
<p>
|
||||||
{{ $t(isMute ? 'user_card.expire_mute_message' : 'user_card.expire_block_message', [user.screen_name]) }}
|
{{ $t(isMute ? 'user_card.expire_mute_message' : 'user_card.expire_block_message', [user.screen_name]) }}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<template #below>
|
||||||
{{ $t('user_card.expire_in') }}
|
<div>
|
||||||
<span class="expirationTime">
|
{{ $t('user_card.expire_in') }}
|
||||||
<input
|
<span class="expirationTime">
|
||||||
id="userFilterExpires"
|
<input
|
||||||
v-model="expiration"
|
id="userFilterExpires"
|
||||||
class="input input-expire-in"
|
v-model="expiration"
|
||||||
:class="{ disabled: forever }"
|
class="input input-expire-in"
|
||||||
:disabled="forever"
|
:class="{ disabled: forever }"
|
||||||
min="1"
|
:disabled="forever"
|
||||||
type="number"
|
min="1"
|
||||||
>
|
type="number"
|
||||||
<Select
|
|
||||||
id="userFilterExpiresUnit"
|
|
||||||
v-model="expirationUnit"
|
|
||||||
class="input unit-input unstyled"
|
|
||||||
:disabled="forever"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
key="s"
|
|
||||||
value="s"
|
|
||||||
>
|
>
|
||||||
{{ $t('time.unit.seconds_suffix') }}
|
<Select
|
||||||
</option>
|
id="userFilterExpiresUnit"
|
||||||
<option
|
v-model="expirationUnit"
|
||||||
key="m"
|
class="input unit-input unstyled"
|
||||||
value="m"
|
:disabled="forever"
|
||||||
>
|
>
|
||||||
{{ $t('time.unit.minutes_suffix') }}
|
<option
|
||||||
</option>
|
key="s"
|
||||||
<option
|
value="s"
|
||||||
key="h"
|
>
|
||||||
value="h"
|
{{ $t('time.unit.seconds_suffix') }}
|
||||||
>
|
</option>
|
||||||
{{ $t('time.unit.hours_suffix') }}
|
<option
|
||||||
</option>
|
key="m"
|
||||||
<option
|
value="m"
|
||||||
key="d"
|
>
|
||||||
value="d"
|
{{ $t('time.unit.minutes_suffix') }}
|
||||||
>
|
</option>
|
||||||
{{ $t('time.unit.days_suffix') }}
|
<option
|
||||||
</option>
|
key="h"
|
||||||
</Select>
|
value="h"
|
||||||
</span>
|
>
|
||||||
|
{{ $t('time.unit.hours_suffix') }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
key="d"
|
||||||
|
value="d"
|
||||||
|
>
|
||||||
|
{{ $t('time.unit.days_suffix') }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
</span>
|
||||||
|
|
||||||
{{ $t('user_card.mute_or') }}
|
{{ $t('user_card.mute_or') }}
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="forever"
|
id="forever"
|
||||||
v-model="forever"
|
v-model="forever"
|
||||||
name="forever"
|
name="forever"
|
||||||
class="input-forever"
|
class="input-forever"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.mute_block_never') }}
|
{{ $t('user_card.mute_block_never') }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #footerLeft>
|
<template #footerLeft>
|
||||||
<div class="footer-left-checkbox">
|
<div class="footer-left-checkbox">
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
// eslint-disable-next-line no-unused
|
|
||||||
|
|
||||||
import { isEmpty } from 'lodash'
|
|
||||||
import { h } from 'vue'
|
|
||||||
|
|
||||||
import { getComponentProps } from '../../services/component_utils/component_utils'
|
|
||||||
import './with_load_more.scss'
|
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
|
|
||||||
library.add(faCircleNotch)
|
|
||||||
|
|
||||||
const withLoadMore =
|
|
||||||
({
|
|
||||||
fetch, // function to fetch entries and return a promise
|
|
||||||
select, // function to select data from store
|
|
||||||
unmounted, // function called at "destroyed" lifecycle
|
|
||||||
childPropName = 'entries', // name of the prop to be passed into the wrapped component
|
|
||||||
additionalPropNames = [], // additional prop name list of the wrapper component
|
|
||||||
}) =>
|
|
||||||
(WrappedComponent) => {
|
|
||||||
const originalProps = Object.keys(getComponentProps(WrappedComponent))
|
|
||||||
const props = originalProps
|
|
||||||
.filter((v) => v !== childPropName)
|
|
||||||
.concat(additionalPropNames)
|
|
||||||
|
|
||||||
return {
|
|
||||||
props,
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
bottomedOut: false,
|
|
||||||
error: false,
|
|
||||||
entries: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
window.addEventListener('scroll', this.scrollLoad)
|
|
||||||
if (this.entries.length === 0) {
|
|
||||||
this.fetchEntries()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
window.removeEventListener('scroll', this.scrollLoad)
|
|
||||||
unmounted && unmounted(this.$props, this.$store)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Entries is not a computed because computed can't track the dynamic
|
|
||||||
// selector for changes and won't trigger after fetch.
|
|
||||||
updateEntries() {
|
|
||||||
this.entries = select(this.$props, this.$store) || []
|
|
||||||
},
|
|
||||||
fetchEntries() {
|
|
||||||
if (!this.loading) {
|
|
||||||
this.loading = true
|
|
||||||
this.error = false
|
|
||||||
fetch(this.$props, this.$store)
|
|
||||||
.then((newEntries) => {
|
|
||||||
this.loading = false
|
|
||||||
this.bottomedOut = isEmpty(newEntries)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.loading = false
|
|
||||||
this.error = true
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.updateEntries()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollLoad(e) {
|
|
||||||
const bodyBRect = document.body.getBoundingClientRect()
|
|
||||||
const height = Math.max(bodyBRect.height, -bodyBRect.y)
|
|
||||||
if (
|
|
||||||
this.loading === false &&
|
|
||||||
this.bottomedOut === false &&
|
|
||||||
this.$el.offsetHeight > 0 &&
|
|
||||||
window.innerHeight + window.pageYOffset >= height - 750
|
|
||||||
) {
|
|
||||||
this.fetchEntries()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
const props = {
|
|
||||||
...this.$props,
|
|
||||||
[childPropName]: this.entries,
|
|
||||||
}
|
|
||||||
const children = this.$slots
|
|
||||||
return (
|
|
||||||
<div class="with-load-more">
|
|
||||||
<WrappedComponent {...props}>{children}</WrappedComponent>
|
|
||||||
<div class="with-load-more-footer">
|
|
||||||
{this.error && (
|
|
||||||
<button
|
|
||||||
onClick={this.fetchEntries}
|
|
||||||
class="button-unstyled -link -fullwidth alert error"
|
|
||||||
>
|
|
||||||
{this.$t('general.generic_error')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!this.error && this.loading && (
|
|
||||||
<FAIcon spin icon="circle-notch" />
|
|
||||||
)}
|
|
||||||
{!this.error && !this.loading && !this.bottomedOut && (
|
|
||||||
<a onClick={this.fetchEntries} role="button" tabindex="0">
|
|
||||||
{this.$t('general.more')}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withLoadMore
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
.with-load-more {
|
|
||||||
&-footer {
|
|
||||||
padding: 0.9em;
|
|
||||||
text-align: center;
|
|
||||||
border-top: 1px solid;
|
|
||||||
border-top-color: var(--border);
|
|
||||||
|
|
||||||
.error {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
// eslint-disable-next-line no-unused
|
|
||||||
|
|
||||||
import { isEmpty } from 'lodash'
|
|
||||||
import { h } from 'vue'
|
|
||||||
|
|
||||||
import { getComponentProps } from '../../services/component_utils/component_utils'
|
|
||||||
import './with_subscription.scss'
|
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
|
|
||||||
library.add(faCircleNotch)
|
|
||||||
|
|
||||||
const withSubscription =
|
|
||||||
({
|
|
||||||
fetch, // function to fetch entries and return a promise
|
|
||||||
select, // function to select data from store
|
|
||||||
childPropName = 'content', // name of the prop to be passed into the wrapped component
|
|
||||||
additionalPropNames = [], // additional prop name list of the wrapper component
|
|
||||||
}) =>
|
|
||||||
(WrappedComponent) => {
|
|
||||||
const originalProps = Object.keys(getComponentProps(WrappedComponent))
|
|
||||||
const props = originalProps
|
|
||||||
.filter((v) => v !== childPropName)
|
|
||||||
.concat(additionalPropNames)
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: [
|
|
||||||
...props,
|
|
||||||
'refresh', // boolean saying to force-fetch data whenever created
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
error: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
fetchedData() {
|
|
||||||
return select(this.$props, this.$store)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
if (this.refresh || isEmpty(this.fetchedData)) {
|
|
||||||
this.fetchData()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchData() {
|
|
||||||
if (!this.loading) {
|
|
||||||
this.loading = true
|
|
||||||
this.error = false
|
|
||||||
fetch(this.$props, this.$store)
|
|
||||||
.then(() => {
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.error = true
|
|
||||||
this.loading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
if (!this.error && !this.loading) {
|
|
||||||
const props = {
|
|
||||||
...this.$props,
|
|
||||||
[childPropName]: this.fetchedData,
|
|
||||||
}
|
|
||||||
const children = this.$slots
|
|
||||||
return (
|
|
||||||
<div class="with-subscription">
|
|
||||||
<WrappedComponent {...props}>{children}</WrappedComponent>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div class="with-subscription-loading">
|
|
||||||
{this.error ? (
|
|
||||||
<a onClick={this.fetchData} class="alert error">
|
|
||||||
{this.$t('general.generic_error')}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<FAIcon spin icon="circle-notch" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withSubscription
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
.with-subscription {
|
|
||||||
&-loading {
|
|
||||||
padding: 0.7em;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.error {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
126
src/i18n/en.json
126
src/i18n/en.json
|
|
@ -86,6 +86,7 @@
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
|
"no_more": "No more items",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"generic_error": "An error occured",
|
"generic_error": "An error occured",
|
||||||
"generic_error_message": "An error occured: {0}",
|
"generic_error_message": "An error occured: {0}",
|
||||||
|
|
@ -105,6 +106,9 @@
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
|
"none": "None",
|
||||||
|
"not_applicable": "N/A",
|
||||||
|
"not_available": "N/A",
|
||||||
"peek": "Peek",
|
"peek": "Peek",
|
||||||
"scroll_to_top": "Scroll to top",
|
"scroll_to_top": "Scroll to top",
|
||||||
"role": {
|
"role": {
|
||||||
|
|
@ -584,6 +588,9 @@
|
||||||
"move_account_error": "Error moving account: {error}",
|
"move_account_error": "Error moving account: {error}",
|
||||||
"discoverable": "Allow discovery of this account in search results and other services",
|
"discoverable": "Allow discovery of this account in search results and other services",
|
||||||
"domain_mutes": "Domains",
|
"domain_mutes": "Domains",
|
||||||
|
"domain_mutes2": "Excluded domains",
|
||||||
|
"user_mutes2": "Muted users",
|
||||||
|
"user_blocks": "Blocked users",
|
||||||
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels. Recommended aspect ratio is 1:1",
|
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels. Recommended aspect ratio is 1:1",
|
||||||
"banner_size_instruction": "The recommended minimum size for banner images is 450x150 pixels. Recommended aspect ratio is 3:1",
|
"banner_size_instruction": "The recommended minimum size for banner images is 450x150 pixels. Recommended aspect ratio is 3:1",
|
||||||
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||||
|
|
@ -1156,6 +1163,7 @@
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"nodb": "No DB Config",
|
"nodb": "No DB Config",
|
||||||
"instance": "Instance",
|
"instance": "Instance",
|
||||||
|
"users": "Users",
|
||||||
"limits": "Limits",
|
"limits": "Limits",
|
||||||
"frontends": "Front-ends",
|
"frontends": "Front-ends",
|
||||||
"mailer": "EMails",
|
"mailer": "EMails",
|
||||||
|
|
@ -1284,6 +1292,56 @@
|
||||||
"adapter": "Mailing Adapter",
|
"adapter": "Mailing Adapter",
|
||||||
"auth": "Authentication"
|
"auth": "Authentication"
|
||||||
},
|
},
|
||||||
|
"users": {
|
||||||
|
"title": "Users",
|
||||||
|
"local_id": "Local ID",
|
||||||
|
"no_users_found": "No users found",
|
||||||
|
"labels": {
|
||||||
|
"query": "Search",
|
||||||
|
"nickname": "{'@'}handle",
|
||||||
|
"name": "Display Name",
|
||||||
|
"name_colon": "Name:",
|
||||||
|
"email": "Email",
|
||||||
|
"email_colon": "Email:",
|
||||||
|
"handle_colon": "Handle:",
|
||||||
|
"origin": "Origin",
|
||||||
|
"activity": "Activity",
|
||||||
|
"privileges": "Privileges"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"add_new": "Add New Tag",
|
||||||
|
"new_title": "Enter New Tag And Confirm",
|
||||||
|
"yes": "Add",
|
||||||
|
"no": "Abort"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"all": "All",
|
||||||
|
"only_local": "Only Local",
|
||||||
|
"only_external": "Only External",
|
||||||
|
"only_active": "Only Active",
|
||||||
|
"only_deactivated": "Only Deactivated",
|
||||||
|
"only_admins": "Only Admins",
|
||||||
|
"only_privileged": "Only Privileged",
|
||||||
|
"only_moderators": "Only Moderators",
|
||||||
|
"only_unapproved": "Exclude Approved",
|
||||||
|
"only_unconfirmed": "Exclude Confirmed"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"show_direct": "Show Direct Messages",
|
||||||
|
"show_reblogs": "Show Reblogs"
|
||||||
|
},
|
||||||
|
"indicator": {
|
||||||
|
"admin": "Admin",
|
||||||
|
"moderator": "Moderator",
|
||||||
|
"active": "Active",
|
||||||
|
"deactivated": "Deactivated",
|
||||||
|
"confirmed": "Confirmed",
|
||||||
|
"unconfirmed": "Pending confirmation",
|
||||||
|
"approved": "Approved",
|
||||||
|
"suggested": "Suggested",
|
||||||
|
"unapproved": "Pending approval"
|
||||||
|
}
|
||||||
|
},
|
||||||
"limits": {
|
"limits": {
|
||||||
"arbitrary_limits": "Arbitrary limits",
|
"arbitrary_limits": "Arbitrary limits",
|
||||||
"posts": "Post limits",
|
"posts": "Post limits",
|
||||||
|
|
@ -1628,7 +1686,10 @@
|
||||||
"invisible_quote": "Quoted status unavailable: {link}",
|
"invisible_quote": "Quoted status unavailable: {link}",
|
||||||
"more_actions": "More actions on this status",
|
"more_actions": "More actions on this status",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"load_error": "Unable to load status: {error}"
|
"load_error": "Unable to load status: {error}",
|
||||||
|
"admin_change_scope": "Change visibility",
|
||||||
|
"mark_as_sensitive": "Sensitive",
|
||||||
|
"mark_as_non-sensitive": "Non-sensitive"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
|
|
@ -1716,15 +1777,28 @@
|
||||||
"group": "Group",
|
"group": "Group",
|
||||||
"birthday": "Born {birthday}",
|
"birthday": "Born {birthday}",
|
||||||
"joined": "Joined",
|
"joined": "Joined",
|
||||||
|
"admin_data": {
|
||||||
|
"data": "Administrative info",
|
||||||
|
"registration_reason": "Registration reason",
|
||||||
|
"tags": "Tags"
|
||||||
|
},
|
||||||
"admin_menu": {
|
"admin_menu": {
|
||||||
"moderation": "Moderation",
|
"moderation": "Moderation",
|
||||||
"grant_admin": "Grant Admin",
|
"grant_admin": "Grant Admin",
|
||||||
"revoke_admin": "Revoke Admin",
|
"revoke_admin": "Revoke Admin",
|
||||||
"grant_moderator": "Grant Moderator",
|
"grant_moderator": "Grant Moderator",
|
||||||
"revoke_moderator": "Revoke Moderator",
|
"revoke_moderator": "Revoke Moderator",
|
||||||
"activate_account": "Activate account",
|
"activate_account": "Activate",
|
||||||
"deactivate_account": "Deactivate account",
|
"deactivate_account": "Deactivate",
|
||||||
"delete_account": "Delete account",
|
"delete_account": "Delete",
|
||||||
|
"suggest_account": "Add to suggested",
|
||||||
|
"remove_suggested_account": "Remove from suggested",
|
||||||
|
"approve_account": "Approve",
|
||||||
|
"confirm_account": "Confirm",
|
||||||
|
"show_statuses": "Show all posts",
|
||||||
|
|
||||||
|
"disable_mfa": "Disable MFA",
|
||||||
|
|
||||||
"force_nsfw": "Mark all posts as NSFW",
|
"force_nsfw": "Mark all posts as NSFW",
|
||||||
"strip_media": "Remove media from posts",
|
"strip_media": "Remove media from posts",
|
||||||
"force_unlisted": "Force posts to be unlisted",
|
"force_unlisted": "Force posts to be unlisted",
|
||||||
|
|
@ -1732,8 +1806,48 @@
|
||||||
"disable_remote_subscription": "Disallow following user from remote instances",
|
"disable_remote_subscription": "Disallow following user from remote instances",
|
||||||
"disable_any_subscription": "Disallow following user at all",
|
"disable_any_subscription": "Disallow following user at all",
|
||||||
"quarantine": "Disallow user posts from federating",
|
"quarantine": "Disallow user posts from federating",
|
||||||
"delete_user": "Delete user",
|
|
||||||
"delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?"
|
"require_password_change": "Require Password Change",
|
||||||
|
"resend_confirmation": "Resend Confirmation Email",
|
||||||
|
"confirm_modal": {
|
||||||
|
"delete_title": "User deletion",
|
||||||
|
"delete_content": "Delete user {user}? | Delete {count} users?",
|
||||||
|
"delete_content_2": "This will permanently delete the data from this accounts and deactivate it. Are you absolutely sure?",
|
||||||
|
"activate_title": "User activation",
|
||||||
|
"activate_content": "Activate user {user}? | Activate {count} users?",
|
||||||
|
"deactivate_content": "Dectivate user {user}? | Dectivate {count} users?",
|
||||||
|
"approval_title": "Approve users",
|
||||||
|
"approval_content": "Approve user {user}? | Approve {count} users?",
|
||||||
|
"confirm_title": "Confirm users",
|
||||||
|
"confirm_content": "Approve user {user}? | Approve {count} users?",
|
||||||
|
"suggest_title": "Suggest users",
|
||||||
|
"add_suggest_content": "Add user {user} to suggested users list? | Add {count} users to suggested users list?",
|
||||||
|
"remove_suggest_content": "Remove user {user} from suggested users list? | Add {count} users to suggested users list?",
|
||||||
|
"rights_title": "Promote users",
|
||||||
|
"grant_rights_content": "Grant user {user} {name} role? | Grant {count} users {name} role?",
|
||||||
|
"revoke_rights_content": "Revoke {name} role from user {user}? | Revoke {name} from {count} users?",
|
||||||
|
"tag_title": "Assign user policy",
|
||||||
|
"assign_tag_content": "Assign {user} a {name} policy? | Assign {name} policy to {count} users?",
|
||||||
|
"unassign_tag_content": "Unassign policy {name} from {user}? | Unassign policy {name} from {count} users?",
|
||||||
|
"resend_confirmation_title": "Email confirmation resend",
|
||||||
|
"resend_confirmation_content": "Resend confirmation email to {count} users?",
|
||||||
|
"disable_mfa_title": "Disable MFA",
|
||||||
|
"disable_mfa_content": "Disable Mult-Factor Authentication for {count} users?",
|
||||||
|
"require_password_change_title": "Force password change",
|
||||||
|
"require_password_change_content": "Force {count} users to change password on next login?",
|
||||||
|
"add": "Add",
|
||||||
|
"remove": "Remove",
|
||||||
|
"delete": "Delete",
|
||||||
|
"activate": "Activate",
|
||||||
|
"deactivate": "Deactivate",
|
||||||
|
"grant": "Grant",
|
||||||
|
"revoke": "Revoke",
|
||||||
|
"approve": "Approve",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"assign": "Assign",
|
||||||
|
"unassign": "Unassign",
|
||||||
|
"send": "Send"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"highlight_new": {
|
"highlight_new": {
|
||||||
"disabled": "Don't highlight",
|
"disabled": "Don't highlight",
|
||||||
|
|
|
||||||
|
|
@ -1,294 +0,0 @@
|
||||||
import { cloneDeep, differenceWith, flatten, get, isEqual, set } from 'lodash'
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
frontends: [],
|
|
||||||
loaded: false,
|
|
||||||
needsReboot: null,
|
|
||||||
config: null,
|
|
||||||
modifiedPaths: null,
|
|
||||||
descriptions: null,
|
|
||||||
draft: null,
|
|
||||||
dbConfigEnabled: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const newUserFlags = {
|
|
||||||
...defaultState.flagStorage,
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminSettingsStorage = {
|
|
||||||
state: {
|
|
||||||
...cloneDeep(defaultState),
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
setInstanceAdminNoDbConfig(state) {
|
|
||||||
state.loaded = false
|
|
||||||
state.dbConfigEnabled = false
|
|
||||||
},
|
|
||||||
setAvailableFrontends(state, { frontends }) {
|
|
||||||
state.frontends = frontends.map((f) => {
|
|
||||||
f.installedRefs = f.installed_refs
|
|
||||||
if (f.name === 'pleroma-fe') {
|
|
||||||
f.refs = ['master', 'develop']
|
|
||||||
} else {
|
|
||||||
f.refs = [f.ref]
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateAdminSettings(state, { config, modifiedPaths }) {
|
|
||||||
state.loaded = true
|
|
||||||
state.dbConfigEnabled = true
|
|
||||||
state.config = config
|
|
||||||
state.modifiedPaths = modifiedPaths
|
|
||||||
},
|
|
||||||
updateAdminDescriptions(state, { descriptions }) {
|
|
||||||
state.descriptions = descriptions
|
|
||||||
},
|
|
||||||
updateAdminDraft(state, { path, value }) {
|
|
||||||
const [group, key, subkey] = path
|
|
||||||
const parent = [group, key, subkey]
|
|
||||||
|
|
||||||
set(state.draft, path, value)
|
|
||||||
|
|
||||||
// force-updating grouped draft to trigger refresh of group settings
|
|
||||||
if (path.length > parent.length) {
|
|
||||||
set(state.draft, parent, cloneDeep(get(state.draft, parent)))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetAdminDraft(state) {
|
|
||||||
state.draft = cloneDeep(state.config)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
loadFrontendsStuff({ rootState, commit }) {
|
|
||||||
rootState.api.backendInteractor
|
|
||||||
.fetchAvailableFrontends()
|
|
||||||
.then((frontends) => commit('setAvailableFrontends', { frontends }))
|
|
||||||
},
|
|
||||||
loadAdminStuff({ state, rootState, dispatch, commit }) {
|
|
||||||
rootState.api.backendInteractor
|
|
||||||
.fetchInstanceDBConfig()
|
|
||||||
.then((backendDbConfig) => {
|
|
||||||
if (backendDbConfig.error) {
|
|
||||||
if (backendDbConfig.error.status === 400) {
|
|
||||||
backendDbConfig.error.json().then((errorJson) => {
|
|
||||||
if (/configurable_from_database/.test(errorJson.error)) {
|
|
||||||
commit('setInstanceAdminNoDbConfig')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dispatch('setInstanceAdminSettings', { backendDbConfig })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (state.descriptions === null) {
|
|
||||||
rootState.api.backendInteractor
|
|
||||||
.fetchInstanceConfigDescriptions()
|
|
||||||
.then((backendDescriptions) =>
|
|
||||||
dispatch('setInstanceAdminDescriptions', { backendDescriptions }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setInstanceAdminSettings({ state, commit }, { backendDbConfig }) {
|
|
||||||
const config = state.config || {}
|
|
||||||
const modifiedPaths = new Set()
|
|
||||||
|
|
||||||
backendDbConfig.configs.forEach((c) => {
|
|
||||||
const path = [c.group, c.key]
|
|
||||||
if (c.db) {
|
|
||||||
// Path elements can contain dot, therefore we use ' -> ' as a separator instead
|
|
||||||
// Using strings for modified paths for easier searching
|
|
||||||
c.db.forEach((x) => modifiedPaths.add([...path, x].join(' -> ')))
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to preserve tuples on second level only, possibly third
|
|
||||||
// but it's not a case right now.
|
|
||||||
const convert = (value, preserveTuples, preserveTuplesLv2) => {
|
|
||||||
if (Array.isArray(value) && value.length > 0 && value[0].tuple) {
|
|
||||||
if (!preserveTuples) {
|
|
||||||
return value.reduce((acc, c) => {
|
|
||||||
if (c.tuple == null) {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[c]: c,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[c.tuple[0]]: convert(c.tuple[1], preserveTuplesLv2),
|
|
||||||
}
|
|
||||||
}, {})
|
|
||||||
} else {
|
|
||||||
return value.map((x) => x.tuple)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!preserveTuples) {
|
|
||||||
return value
|
|
||||||
} else {
|
|
||||||
return value.tuple
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// for most stuff we want maps since those are more convenient
|
|
||||||
// however this doesn't allow for multiple values per same key
|
|
||||||
// so for those cases we want to preserve tuples as-is
|
|
||||||
// right now it's made exclusively for :pleroma.:rate_limit
|
|
||||||
// so it might not work properly elsewhere
|
|
||||||
const preserveTuples = path.find((x) => x === ':rate_limit')
|
|
||||||
set(config, path, convert(c.value, false, preserveTuples))
|
|
||||||
})
|
|
||||||
// patching http adapter config to be easier to handle
|
|
||||||
const adapter = config[':pleroma'][':http'][':adapter']
|
|
||||||
if (Array.isArray(adapter)) {
|
|
||||||
config[':pleroma'][':http'][':adapter'] = {
|
|
||||||
[':ssl_options']: {
|
|
||||||
[':versions']: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commit('updateAdminSettings', { config, modifiedPaths })
|
|
||||||
commit('resetAdminDraft')
|
|
||||||
},
|
|
||||||
setInstanceAdminDescriptions({ commit }, { backendDescriptions }) {
|
|
||||||
const convert = (
|
|
||||||
{ children, description, label, key = '<ROOT>', group, suggestions },
|
|
||||||
path,
|
|
||||||
acc,
|
|
||||||
) => {
|
|
||||||
const newPath = group ? [group, key] : [key]
|
|
||||||
const obj = { description, label, suggestions }
|
|
||||||
if (Array.isArray(children)) {
|
|
||||||
children.forEach((c) => {
|
|
||||||
convert(c, newPath, obj)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
set(acc, newPath, obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
const descriptions = {}
|
|
||||||
|
|
||||||
backendDescriptions.forEach((d) => convert(d, '', descriptions))
|
|
||||||
commit('updateAdminDescriptions', { descriptions })
|
|
||||||
},
|
|
||||||
|
|
||||||
// This action takes draft state, diffs it with live config state and then pushes
|
|
||||||
// only differences between the two. Difference detection only work up to subkey (third) level.
|
|
||||||
pushAdminDraft({ rootState, state, dispatch }) {
|
|
||||||
// TODO cleanup paths in modifiedPaths
|
|
||||||
const convert = (value) => {
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
return value
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
return value.map(convert)
|
|
||||||
} else {
|
|
||||||
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getting all group-keys used in config
|
|
||||||
const allGroupKeys = flatten(
|
|
||||||
Object.entries(state.config).map(([group, lv1data]) =>
|
|
||||||
Object.keys(lv1data).map((key) => ({ group, key })),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Only using group-keys where there are changes detected
|
|
||||||
const changedGroupKeys = allGroupKeys.filter(({ group, key }) => {
|
|
||||||
return !isEqual(state.config[group][key], state.draft[group][key])
|
|
||||||
})
|
|
||||||
|
|
||||||
// Here we take all changed group-keys and get all changed subkeys
|
|
||||||
const changed = changedGroupKeys.map(({ group, key }) => {
|
|
||||||
const config = state.config[group][key]
|
|
||||||
const draft = state.draft[group][key]
|
|
||||||
|
|
||||||
// We convert group-key value into entries arrays
|
|
||||||
const eConfig = Object.entries(config)
|
|
||||||
const eDraft = Object.entries(draft)
|
|
||||||
|
|
||||||
// Then those entries array we diff so only changed subkey entries remain
|
|
||||||
// We use the diffed array to reconstruct the object and then shove it into convert()
|
|
||||||
return {
|
|
||||||
group,
|
|
||||||
key,
|
|
||||||
value: convert(
|
|
||||||
Object.fromEntries(differenceWith(eDraft, eConfig, isEqual)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
rootState.api.backendInteractor
|
|
||||||
.pushInstanceDBConfig({
|
|
||||||
payload: {
|
|
||||||
configs: changed,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
|
|
||||||
.then((backendDbConfig) =>
|
|
||||||
dispatch('setInstanceAdminSettings', { backendDbConfig }),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
pushAdminSetting({ rootState, dispatch }, { path, value }) {
|
|
||||||
const [group, key, ...rest] = Array.isArray(path)
|
|
||||||
? path
|
|
||||||
: path.split(/\./g)
|
|
||||||
const clone = {} // not actually cloning the entire thing to avoid excessive writes
|
|
||||||
set(clone, rest, value)
|
|
||||||
|
|
||||||
// TODO cleanup paths in modifiedPaths
|
|
||||||
const convert = (value) => {
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
return value
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
return value.map(convert)
|
|
||||||
} else {
|
|
||||||
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootState.api.backendInteractor
|
|
||||||
.pushInstanceDBConfig({
|
|
||||||
payload: {
|
|
||||||
configs: [
|
|
||||||
{
|
|
||||||
group,
|
|
||||||
key,
|
|
||||||
value: convert(clone),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
|
|
||||||
.then((backendDbConfig) =>
|
|
||||||
dispatch('setInstanceAdminSettings', { backendDbConfig }),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
resetAdminSetting({ rootState, state, dispatch }, { path }) {
|
|
||||||
const [group, key, subkey] = Array.isArray(path)
|
|
||||||
? path
|
|
||||||
: path.split(/\./g)
|
|
||||||
|
|
||||||
state.modifiedPaths.delete(path)
|
|
||||||
|
|
||||||
return rootState.api.backendInteractor
|
|
||||||
.pushInstanceDBConfig({
|
|
||||||
payload: {
|
|
||||||
configs: [
|
|
||||||
{
|
|
||||||
group,
|
|
||||||
key,
|
|
||||||
delete: true,
|
|
||||||
subkeys: [subkey],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
|
|
||||||
.then((backendDbConfig) =>
|
|
||||||
dispatch('setInstanceAdminSettings', { backendDbConfig }),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default adminSettingsStorage
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import adminSettings from './adminSettings.js'
|
|
||||||
import api from './api.js'
|
import api from './api.js'
|
||||||
import chats from './chats.js'
|
import chats from './chats.js'
|
||||||
import drafts from './drafts.js'
|
import drafts from './drafts.js'
|
||||||
|
|
@ -13,7 +12,6 @@ export default {
|
||||||
users,
|
users,
|
||||||
api,
|
api,
|
||||||
profileConfig,
|
profileConfig,
|
||||||
adminSettings,
|
|
||||||
drafts,
|
drafts,
|
||||||
chats,
|
chats,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -591,7 +591,7 @@ const statuses = {
|
||||||
pagination,
|
pagination,
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
commit('addNewStatuses', {
|
return commit('addNewStatuses', {
|
||||||
statuses,
|
statuses,
|
||||||
showImmediately,
|
showImmediately,
|
||||||
timeline,
|
timeline,
|
||||||
|
|
@ -683,7 +683,7 @@ const statuses = {
|
||||||
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
|
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
|
||||||
},
|
},
|
||||||
unpinStatus({ rootState, dispatch }, statusId) {
|
unpinStatus({ rootState, dispatch }, statusId) {
|
||||||
rootState.api.backendInteractor
|
return rootState.api.backendInteractor
|
||||||
.unpinOwnStatus({ id: statusId })
|
.unpinOwnStatus({ id: statusId })
|
||||||
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
|
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
|
||||||
},
|
},
|
||||||
|
|
@ -822,7 +822,9 @@ const statuses = {
|
||||||
'addNewUsers',
|
'addNewUsers',
|
||||||
data.statuses.map((s) => s.user).filter((u) => u),
|
data.statuses.map((s) => s.user).filter((u) => u),
|
||||||
)
|
)
|
||||||
store.commit('addNewStatuses', { statuses: data.statuses })
|
data.statuses = store.commit('addNewStatuses', {
|
||||||
|
statuses: data.statuses,
|
||||||
|
})
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -170,15 +170,11 @@ const unmuteDomain = (store, domain) => {
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
tagUser(state, { user: { id }, tag }) {
|
tagUser(state, { user: { id }, tag }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
const tags = user.tags || []
|
user.tags.add(tag)
|
||||||
const newTags = tags.concat([tag])
|
|
||||||
user.tags = newTags
|
|
||||||
},
|
},
|
||||||
untagUser(state, { user: { id }, tag }) {
|
untagUser(state, { user: { id }, tag }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
const tags = user.tags || []
|
user.tags.delete(tag)
|
||||||
const newTags = tags.filter((t) => t !== tag)
|
|
||||||
user.tags = newTags
|
|
||||||
},
|
},
|
||||||
updateRight(state, { user: { id }, right, value }) {
|
updateRight(state, { user: { id }, right, value }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
|
|
@ -186,9 +182,12 @@ export const mutations = {
|
||||||
newRights[right] = value
|
newRights[right] = value
|
||||||
user.rights = newRights
|
user.rights = newRights
|
||||||
},
|
},
|
||||||
updateActivationStatus(state, { user: { id }, deactivated }) {
|
updateUserAdminData(state, { user }) {
|
||||||
const user = state.usersObject[id]
|
const { id } = user
|
||||||
user.deactivated = deactivated
|
const localUser = state.usersObject[id]
|
||||||
|
localUser.adminData = user
|
||||||
|
localUser.deactivated = !user.is_active
|
||||||
|
localUser.tags = new Set(user.tags)
|
||||||
},
|
},
|
||||||
setCurrentUser(state, user) {
|
setCurrentUser(state, user) {
|
||||||
state.lastLoginName = user.screen_name
|
state.lastLoginName = user.screen_name
|
||||||
|
|
@ -369,10 +368,22 @@ const users = {
|
||||||
getters,
|
getters,
|
||||||
actions: {
|
actions: {
|
||||||
fetchUserIfMissing(store, id) {
|
fetchUserIfMissing(store, id) {
|
||||||
if (!store.getters.findUser(id)) {
|
const user = store.getters.findUser(id)
|
||||||
store.dispatch('fetchUser', id)
|
if (!user) {
|
||||||
|
return store.dispatch('fetchUser', id)
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(user)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateUserAdminData(store, { userAdminData }) {
|
||||||
|
return store
|
||||||
|
.dispatch('fetchUserIfMissing', userAdminData.id)
|
||||||
|
.then((user) => {
|
||||||
|
user.adminData = userAdminData
|
||||||
|
store.commit('addNewUsers', [user])
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
},
|
||||||
fetchUser(store, id) {
|
fetchUser(store, id) {
|
||||||
return store.rootState.api.backendInteractor
|
return store.rootState.api.backendInteractor
|
||||||
.fetchUser({ id })
|
.fetchUser({ id })
|
||||||
|
|
@ -541,15 +552,6 @@ const users = {
|
||||||
commit('updateUserRelationship', [relationship]),
|
commit('updateUserRelationship', [relationship]),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
toggleActivationStatus({ rootState, commit }, { user }) {
|
|
||||||
const api = user.deactivated
|
|
||||||
? rootState.api.backendInteractor.activateUser
|
|
||||||
: rootState.api.backendInteractor.deactivateUser
|
|
||||||
api({ user }).then((user) => {
|
|
||||||
const deactivated = !user.is_active
|
|
||||||
commit('updateActivationStatus', { user, deactivated })
|
|
||||||
})
|
|
||||||
},
|
|
||||||
registerPushNotifications(store) {
|
registerPushNotifications(store) {
|
||||||
const token = store.state.currentUser.credentials
|
const token = store.state.currentUser.credentials
|
||||||
const vapidPublicKey = useInstanceStore().vapidPublicKey
|
const vapidPublicKey = useInstanceStore().vapidPublicKey
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { concat, each, get, last, map } from 'lodash'
|
import { concat, each, last, map } from 'lodash'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseAttachment,
|
parseAttachment,
|
||||||
|
|
@ -20,12 +20,6 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
|
||||||
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
||||||
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
|
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
|
||||||
const ALIASES_URL = '/api/pleroma/aliases'
|
const ALIASES_URL = '/api/pleroma/aliases'
|
||||||
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
|
|
||||||
const PERMISSION_GROUP_URL = (screenName, right) =>
|
|
||||||
`/api/pleroma/admin/users/${screenName}/permission_group/${right}`
|
|
||||||
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
|
|
||||||
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
|
|
||||||
const ADMIN_USERS_URL = '/api/v1/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'
|
||||||
|
|
@ -121,7 +115,6 @@ const PLEROMA_CHAT_MESSAGES_URL = (id) => `/api/v1/pleroma/chats/${id}/messages`
|
||||||
const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read`
|
const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read`
|
||||||
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) =>
|
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) =>
|
||||||
`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
||||||
const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
|
|
||||||
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
||||||
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
|
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
|
||||||
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
||||||
|
|
@ -138,6 +131,7 @@ const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders'
|
||||||
const PLEROMA_BOOKMARK_FOLDER_URL = (id) =>
|
const PLEROMA_BOOKMARK_FOLDER_URL = (id) =>
|
||||||
`/api/v1/pleroma/bookmark_folders/${id}`
|
`/api/v1/pleroma/bookmark_folders/${id}`
|
||||||
|
|
||||||
|
const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
|
||||||
const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config'
|
const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config'
|
||||||
const PLEROMA_ADMIN_DESCRIPTIONS_URL =
|
const PLEROMA_ADMIN_DESCRIPTIONS_URL =
|
||||||
'/api/v1/pleroma/admin/config/descriptions'
|
'/api/v1/pleroma/admin/config/descriptions'
|
||||||
|
|
@ -145,6 +139,66 @@ const PLEROMA_ADMIN_FRONTENDS_URL = '/api/v1/pleroma/admin/frontends'
|
||||||
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL =
|
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL =
|
||||||
'/api/v1/pleroma/admin/frontends/install'
|
'/api/v1/pleroma/admin/frontends/install'
|
||||||
|
|
||||||
|
const PLEROMA_ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
|
||||||
|
const PLEROMA_ADMIN_USERS_URL_SHOW = (nickname) =>
|
||||||
|
`/api/v1/pleroma/admin/users/${nickname}`
|
||||||
|
const PLEROMA_ADMIN_USERS_URL_LIST = ({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
filters = {},
|
||||||
|
query = '',
|
||||||
|
name = '',
|
||||||
|
email = '',
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
local = false,
|
||||||
|
external = false,
|
||||||
|
active = false,
|
||||||
|
needApproval = false,
|
||||||
|
unconfirmed = false,
|
||||||
|
deactivated = false,
|
||||||
|
isAdmin = true,
|
||||||
|
isModerator = true,
|
||||||
|
} = filters
|
||||||
|
const filters_str = [
|
||||||
|
local && 'local',
|
||||||
|
external && 'external',
|
||||||
|
active && 'active',
|
||||||
|
needApproval && 'need_approval',
|
||||||
|
unconfirmed && 'unconfirmed',
|
||||||
|
deactivated && 'deactivated',
|
||||||
|
isAdmin && 'is_admin',
|
||||||
|
isModerator && 'is_moderator',
|
||||||
|
]
|
||||||
|
.filter((x) => x)
|
||||||
|
.join(',')
|
||||||
|
return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}`
|
||||||
|
}
|
||||||
|
const PLEROMA_ADMIN_TAG_USER_URL = '/api/pleroma/admin/users/tag'
|
||||||
|
const PLEROMA_ADMIN_PERMISSION_GROUP_URL = (right) =>
|
||||||
|
`/api/pleroma/admin/users/permission_group/${right}`
|
||||||
|
const PLEROMA_ADMIN_ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate'
|
||||||
|
const PLEROMA_ADMIN_DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate'
|
||||||
|
const PLEROMA_ADMIN_SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest'
|
||||||
|
const PLEROMA_ADMIN_UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest'
|
||||||
|
const PLEROMA_ADMIN_APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve'
|
||||||
|
const PLEROMA_ADMIN_CONFIRM_USERS_URL =
|
||||||
|
'/api/v1/pleroma/admin/users/confirm_email'
|
||||||
|
const PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL =
|
||||||
|
'/api/v1/pleroma/admin/users/resend_confirmation_email'
|
||||||
|
const PLEROMA_ADMIN_LIST_STATUSES_URL = ({
|
||||||
|
id,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
godmode,
|
||||||
|
withReblogs,
|
||||||
|
}) =>
|
||||||
|
`/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&page=${page}&godmode=${godmode}&with_reblogs=${withReblogs}`
|
||||||
|
const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (id) =>
|
||||||
|
`/api/v1/pleroma/admin/statuses/${id}`
|
||||||
|
const PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL =
|
||||||
|
'/api/v1/pleroma/admin/users/force_password_reset'
|
||||||
|
const PLEROMA_ADMIN_DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa'
|
||||||
const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
|
const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
|
||||||
const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
|
const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
|
||||||
const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) =>
|
const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) =>
|
||||||
|
|
@ -205,8 +259,11 @@ const promisedRequest = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fetch(url, options).then((response) => {
|
return fetch(url, options).then((response) => {
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) => {
|
||||||
response
|
// 204 is "No content", which fails to parse json (as you'd might think)
|
||||||
|
if (response.ok && response.status === 204) resolve()
|
||||||
|
|
||||||
|
return response
|
||||||
.json()
|
.json()
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -230,8 +287,8 @@ const promisedRequest = ({
|
||||||
response,
|
response,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -723,89 +780,117 @@ const fetchStatusHistory = ({ status, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagUser = ({ tag, credentials, user }) => {
|
const adminSetUsersTags = ({
|
||||||
const screenName = user.screen_name
|
tags,
|
||||||
const form = {
|
credentials,
|
||||||
nicknames: [screenName],
|
value,
|
||||||
tags: [tag],
|
screen_names: nicknames,
|
||||||
}
|
}) => {
|
||||||
|
|
||||||
const headers = authHeaders(credentials)
|
|
||||||
headers['Content-Type'] = 'application/json'
|
|
||||||
|
|
||||||
return fetch(TAG_USER_URL, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(form),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const untagUser = ({ tag, credentials, user }) => {
|
|
||||||
const screenName = user.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,
|
|
||||||
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 activateUser = ({ credentials, user: { screen_name: nickname } }) => {
|
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: ACTIVATE_USER_URL,
|
url: PLEROMA_ADMIN_TAG_USER_URL,
|
||||||
|
method: value ? 'PUT' : 'DELETE',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
tags,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminSetUsersRight = ({
|
||||||
|
right,
|
||||||
|
credentials,
|
||||||
|
value,
|
||||||
|
screen_names: nicknames,
|
||||||
|
}) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_ADMIN_PERMISSION_GROUP_URL(right),
|
||||||
|
method: value ? 'POST' : 'DELETE',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminSetUsersActivationStatus = ({
|
||||||
|
credentials,
|
||||||
|
screen_names: nicknames,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: value
|
||||||
|
? PLEROMA_ADMIN_ACTIVATE_USERS_URL
|
||||||
|
: PLEROMA_ADMIN_DEACTIVATE_USERS_URL,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
credentials,
|
credentials,
|
||||||
payload: {
|
payload: {
|
||||||
nicknames: [nickname],
|
nicknames,
|
||||||
},
|
},
|
||||||
}).then((response) => get(response, 'users.0'))
|
}).then((response) => response.users)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => {
|
const adminSetUsersApprovalStatus = ({
|
||||||
|
credentials,
|
||||||
|
screen_names: nicknames,
|
||||||
|
}) => {
|
||||||
return promisedRequest({
|
return promisedRequest({
|
||||||
url: DEACTIVATE_USER_URL,
|
url: PLEROMA_ADMIN_APPROVE_USERS_URL,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
credentials,
|
credentials,
|
||||||
payload: {
|
payload: {
|
||||||
nicknames: [nickname],
|
nicknames,
|
||||||
},
|
},
|
||||||
}).then((response) => get(response, 'users.0'))
|
}).then((response) => response.users)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteUser = ({ credentials, user }) => {
|
const adminSetUsersConfirmationStatus = ({
|
||||||
const screenName = user.screen_name
|
credentials,
|
||||||
const headers = authHeaders(credentials)
|
screen_names: nicknames,
|
||||||
|
}) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_ADMIN_CONFIRM_USERS_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
}).then((response) => response.users)
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(`${ADMIN_USERS_URL}?nickname=${screenName}`, {
|
const adminSetUsersSuggestionStatus = ({
|
||||||
|
credentials,
|
||||||
|
screen_names: nicknames,
|
||||||
|
value,
|
||||||
|
}) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: value
|
||||||
|
? PLEROMA_ADMIN_SUGGEST_USERS_URL
|
||||||
|
: PLEROMA_ADMIN_UNSUGGEST_USERS_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
}).then((response) => response.users)
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminGetUserData = ({ credentials, screen_name: nickname }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_ADMIN_USERS_URL_SHOW(nickname),
|
||||||
|
method: 'GET',
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminDeleteAccounts = ({ credentials, screen_names: nicknames }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_ADMIN_USERS_URL,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers,
|
credentials,
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1617,6 +1702,90 @@ const dismissAnnouncement = ({ id, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminListUsers = ({ opts, credentials }) => {
|
||||||
|
// the reported list is hardly useful because standards are for dating i guess,
|
||||||
|
// so make sure to fetchIfMissing right afterward using this call
|
||||||
|
const url = PLEROMA_ADMIN_USERS_URL_LIST(opts)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url,
|
||||||
|
credentials,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminResendConfirmationEmail = ({
|
||||||
|
screen_names: nicknames,
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
const url = PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL
|
||||||
|
return promisedRequest({
|
||||||
|
url,
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminRequirePasswordChange = ({
|
||||||
|
screen_names: nicknames,
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
const url = PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL
|
||||||
|
return promisedRequest({
|
||||||
|
url,
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: {
|
||||||
|
nicknames,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminDisableMFA = ({ screen_name: nickname, credentials }) => {
|
||||||
|
const url = PLEROMA_ADMIN_DISABLE_MFA_URL
|
||||||
|
return promisedRequest({
|
||||||
|
url,
|
||||||
|
credentials,
|
||||||
|
method: 'PUT',
|
||||||
|
payload: {
|
||||||
|
nickname,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminListStatuses = ({ opts, credentials }) => {
|
||||||
|
const url = PLEROMA_ADMIN_LIST_STATUSES_URL(opts)
|
||||||
|
|
||||||
|
return promisedRequest({
|
||||||
|
url,
|
||||||
|
credentials,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminChangeStatusScope = ({
|
||||||
|
opts: { id, sensitive, visibility },
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
const url = PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL(id)
|
||||||
|
var payload = {}
|
||||||
|
if (typeof sensitive !== 'undefined') {
|
||||||
|
payload['sensitive'] = sensitive
|
||||||
|
}
|
||||||
|
if (typeof visibility !== 'undefined') {
|
||||||
|
payload['visibility'] = visibility
|
||||||
|
}
|
||||||
|
return promisedRequest({
|
||||||
|
url,
|
||||||
|
credentials,
|
||||||
|
method: 'PUT',
|
||||||
|
payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
|
const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
|
||||||
const payload = { content }
|
const payload = { content }
|
||||||
|
|
||||||
|
|
@ -2089,7 +2258,6 @@ const listEmojiPacks = ({ page, pageSize }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
|
const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
|
||||||
console.log(instance)
|
|
||||||
if (!instance.startsWith('http')) {
|
if (!instance.startsWith('http')) {
|
||||||
instance = 'https://' + instance
|
instance = 'https://' + instance
|
||||||
}
|
}
|
||||||
|
|
@ -2253,13 +2421,6 @@ const apiService = {
|
||||||
fetchBlocks,
|
fetchBlocks,
|
||||||
fetchOAuthTokens,
|
fetchOAuthTokens,
|
||||||
revokeOAuthToken,
|
revokeOAuthToken,
|
||||||
tagUser,
|
|
||||||
untagUser,
|
|
||||||
deleteUser,
|
|
||||||
addRight,
|
|
||||||
deleteRight,
|
|
||||||
activateUser,
|
|
||||||
deactivateUser,
|
|
||||||
register,
|
register,
|
||||||
getCaptcha,
|
getCaptcha,
|
||||||
updateProfileImages,
|
updateProfileImages,
|
||||||
|
|
@ -2347,6 +2508,20 @@ const apiService = {
|
||||||
createBookmarkFolder,
|
createBookmarkFolder,
|
||||||
updateBookmarkFolder,
|
updateBookmarkFolder,
|
||||||
deleteBookmarkFolder,
|
deleteBookmarkFolder,
|
||||||
|
adminListUsers,
|
||||||
|
adminGetUserData,
|
||||||
|
adminResendConfirmationEmail,
|
||||||
|
adminDeleteAccounts,
|
||||||
|
adminSetUsersRight,
|
||||||
|
adminSetUsersTags,
|
||||||
|
adminSetUsersApprovalStatus,
|
||||||
|
adminSetUsersConfirmationStatus,
|
||||||
|
adminSetUsersActivationStatus,
|
||||||
|
adminSetUsersSuggestionStatus,
|
||||||
|
adminListStatuses,
|
||||||
|
adminChangeStatusScope,
|
||||||
|
adminRequirePasswordChange,
|
||||||
|
adminDisableMFA,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
||||||
|
|
@ -15,234 +15,154 @@ import { isStatusNotification } from '../notification_utils/notification_utils.j
|
||||||
* it would be reverted back to []
|
* it would be reverted back to []
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const qvitterStatusType = (status) => {
|
|
||||||
if (status.is_post_verb) {
|
|
||||||
return 'status'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.retweeted_status) {
|
|
||||||
return 'retweet'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(typeof status.uri === 'string' &&
|
|
||||||
status.uri.match(/(fave|objectType=Favourite)/)) ||
|
|
||||||
(typeof status.text === 'string' && status.text.match(/favorited/))
|
|
||||||
) {
|
|
||||||
return 'favorite'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
status.text.match(/deleted notice {{tag/) ||
|
|
||||||
status.qvitter_delete_notice
|
|
||||||
) {
|
|
||||||
return 'deletion'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
status.text.match(/started following/) ||
|
|
||||||
status.activity_type === 'follow'
|
|
||||||
) {
|
|
||||||
return 'follow'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseUser = (data) => {
|
export const parseUser = (data) => {
|
||||||
const output = {}
|
const output = {}
|
||||||
const masto = Object.hasOwn(data, 'acct')
|
output._original = data // used for server-side settings
|
||||||
|
|
||||||
// case for users in "mentions" property for statuses in MastoAPI
|
// case for users in "mentions" property for statuses in MastoAPI
|
||||||
const mastoShort = masto && !Object.hasOwn(data, 'avatar')
|
const mastoShort = !Object.hasOwn(data, 'avatar')
|
||||||
|
|
||||||
output.inLists = null
|
output.inLists = null
|
||||||
output.id = String(data.id)
|
output.id = String(data.id)
|
||||||
output._original = data // used for server-side settings
|
|
||||||
|
|
||||||
if (masto) {
|
output.screen_name = data.acct
|
||||||
output.screen_name = data.acct
|
output.fqn = data.fqn
|
||||||
output.fqn = data.fqn
|
output.statusnet_profile_url = data.url
|
||||||
output.statusnet_profile_url = data.url
|
|
||||||
|
|
||||||
if (Object.hasOwn(data, 'mute_expires_at')) {
|
if (Object.hasOwn(data, 'mute_expires_at')) {
|
||||||
output.mute_expires_at =
|
output.mute_expires_at =
|
||||||
data.mute_expires_at == null ? false : data.mute_expires_at
|
data.mute_expires_at == null ? false : data.mute_expires_at
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.hasOwn(data, 'block_expires_at')) {
|
||||||
|
output.block_expires_at =
|
||||||
|
data.block_expires_at == null ? false : data.block_expires_at
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's nothing else to get
|
||||||
|
if (mastoShort) {
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
output.emoji = data.emojis
|
||||||
|
output.name = escapeHtml(data.display_name)
|
||||||
|
output.name_html = output.name
|
||||||
|
output.name_unescaped = data.display_name
|
||||||
|
|
||||||
|
output.description = data.note
|
||||||
|
// TODO cleanup this shit, output.description is overriden with source data
|
||||||
|
output.description_html = data.note
|
||||||
|
|
||||||
|
output.fields = data.fields
|
||||||
|
output.fields_html = data.fields.map((field) => {
|
||||||
|
return {
|
||||||
|
name: escapeHtml(field.name),
|
||||||
|
value: field.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
output.fields_text = data.fields.map((field) => {
|
||||||
|
return {
|
||||||
|
name: unescape(field.name.replace(/<[^>]*>/g, '')),
|
||||||
|
value: unescape(field.value.replace(/<[^>]*>/g, '')),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Utilize avatar_static for gif avatars?
|
||||||
|
output.profile_image_url = data.avatar
|
||||||
|
output.profile_image_url_original = data.avatar
|
||||||
|
|
||||||
|
// Same, utilize header_static?
|
||||||
|
output.cover_photo = data.header
|
||||||
|
|
||||||
|
output.friends_count = data.following_count
|
||||||
|
|
||||||
|
output.bot = data.bot
|
||||||
|
|
||||||
|
output.privileges = []
|
||||||
|
|
||||||
|
if (data.pleroma) {
|
||||||
|
if (data.pleroma.settings_store) {
|
||||||
|
output.storage = data.pleroma.settings_store['pleroma-fe']
|
||||||
|
output.user_highlight = data.pleroma.settings_store['user_highlight']
|
||||||
|
}
|
||||||
|
const relationship = data.pleroma.relationship
|
||||||
|
|
||||||
|
output.background_image = data.pleroma.background_image
|
||||||
|
output.favicon = data.pleroma.favicon
|
||||||
|
output.token = data.pleroma.chat_token
|
||||||
|
|
||||||
|
if (relationship) {
|
||||||
|
output.relationship = relationship
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.hasOwn(data, 'block_expires_at')) {
|
output.allow_following_move = data.pleroma.allow_following_move
|
||||||
output.block_expires_at =
|
|
||||||
data.block_expires_at == null ? false : data.block_expires_at
|
output.hide_favorites = data.pleroma.hide_favorites
|
||||||
|
output.hide_follows = data.pleroma.hide_follows
|
||||||
|
output.hide_followers = data.pleroma.hide_followers
|
||||||
|
output.hide_follows_count = data.pleroma.hide_follows_count
|
||||||
|
output.hide_followers_count = data.pleroma.hide_followers_count
|
||||||
|
|
||||||
|
output.rights = {
|
||||||
|
moderator: data.pleroma.is_moderator,
|
||||||
|
admin: data.pleroma.is_admin,
|
||||||
|
}
|
||||||
|
// TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
|
||||||
|
if (output.rights.admin) {
|
||||||
|
output.role = 'admin'
|
||||||
|
} else if (output.rights.moderator) {
|
||||||
|
output.role = 'moderator'
|
||||||
|
} else {
|
||||||
|
output.role = 'member'
|
||||||
}
|
}
|
||||||
|
|
||||||
// There's nothing else to get
|
output.birthday = data.pleroma.birthday
|
||||||
if (mastoShort) {
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
output.emoji = data.emojis
|
if (data.pleroma.privileges) {
|
||||||
output.name = escapeHtml(data.display_name)
|
output.privileges = new Set(data.pleroma.privileges)
|
||||||
output.name_html = output.name
|
} else if (data.pleroma.is_admin) {
|
||||||
output.name_unescaped = data.display_name
|
output.privileges = new Set([
|
||||||
|
'users_read',
|
||||||
output.description = data.note
|
'users_manage_invites',
|
||||||
// TODO cleanup this shit, output.description is overriden with source data
|
'users_manage_activation_state',
|
||||||
output.description_html = data.note
|
'users_manage_tags',
|
||||||
|
'users_manage_credentials',
|
||||||
output.fields = data.fields
|
'users_delete',
|
||||||
output.fields_html = data.fields.map((field) => {
|
'messages_read',
|
||||||
return {
|
'messages_delete',
|
||||||
name: escapeHtml(field.name),
|
'instances_delete',
|
||||||
value: field.value,
|
'reports_manage_reports',
|
||||||
}
|
'moderation_log_read',
|
||||||
})
|
'announcements_manage_announcements',
|
||||||
output.fields_text = data.fields.map((field) => {
|
'emoji_manage_emoji',
|
||||||
return {
|
'statistics_read',
|
||||||
name: unescape(field.name.replace(/<[^>]*>/g, '')),
|
])
|
||||||
value: unescape(field.value.replace(/<[^>]*>/g, '')),
|
} else if (data.pleroma.is_moderator) {
|
||||||
}
|
output.privileges = new Set(['messages_delete', 'reports_manage_reports'])
|
||||||
})
|
} else {
|
||||||
|
output.privileges = new Set()
|
||||||
// Utilize avatar_static for gif avatars?
|
|
||||||
output.profile_image_url = data.avatar
|
|
||||||
output.profile_image_url_original = data.avatar
|
|
||||||
|
|
||||||
// Same, utilize header_static?
|
|
||||||
output.cover_photo = data.header
|
|
||||||
|
|
||||||
output.friends_count = data.following_count
|
|
||||||
|
|
||||||
output.bot = data.bot
|
|
||||||
|
|
||||||
output.privileges = []
|
|
||||||
|
|
||||||
if (data.pleroma) {
|
|
||||||
if (data.pleroma.settings_store) {
|
|
||||||
output.storage = data.pleroma.settings_store['pleroma-fe']
|
|
||||||
output.user_highlight = data.pleroma.settings_store['user_highlight']
|
|
||||||
}
|
|
||||||
const relationship = data.pleroma.relationship
|
|
||||||
|
|
||||||
output.background_image = data.pleroma.background_image
|
|
||||||
output.favicon = data.pleroma.favicon
|
|
||||||
output.token = data.pleroma.chat_token
|
|
||||||
|
|
||||||
if (relationship) {
|
|
||||||
output.relationship = relationship
|
|
||||||
}
|
|
||||||
|
|
||||||
output.allow_following_move = data.pleroma.allow_following_move
|
|
||||||
|
|
||||||
output.hide_favorites = data.pleroma.hide_favorites
|
|
||||||
output.hide_follows = data.pleroma.hide_follows
|
|
||||||
output.hide_followers = data.pleroma.hide_followers
|
|
||||||
output.hide_follows_count = data.pleroma.hide_follows_count
|
|
||||||
output.hide_followers_count = data.pleroma.hide_followers_count
|
|
||||||
|
|
||||||
output.rights = {
|
|
||||||
moderator: data.pleroma.is_moderator,
|
|
||||||
admin: data.pleroma.is_admin,
|
|
||||||
}
|
|
||||||
// TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
|
|
||||||
if (output.rights.admin) {
|
|
||||||
output.role = 'admin'
|
|
||||||
} else if (output.rights.moderator) {
|
|
||||||
output.role = 'moderator'
|
|
||||||
} else {
|
|
||||||
output.role = 'member'
|
|
||||||
}
|
|
||||||
|
|
||||||
output.birthday = data.pleroma.birthday
|
|
||||||
|
|
||||||
if (data.pleroma.privileges) {
|
|
||||||
output.privileges = data.pleroma.privileges
|
|
||||||
} else if (data.pleroma.is_admin) {
|
|
||||||
output.privileges = [
|
|
||||||
'users_read',
|
|
||||||
'users_manage_invites',
|
|
||||||
'users_manage_activation_state',
|
|
||||||
'users_manage_tags',
|
|
||||||
'users_manage_credentials',
|
|
||||||
'users_delete',
|
|
||||||
'messages_read',
|
|
||||||
'messages_delete',
|
|
||||||
'instances_delete',
|
|
||||||
'reports_manage_reports',
|
|
||||||
'moderation_log_read',
|
|
||||||
'announcements_manage_announcements',
|
|
||||||
'emoji_manage_emoji',
|
|
||||||
'statistics_read',
|
|
||||||
]
|
|
||||||
} else if (data.pleroma.is_moderator) {
|
|
||||||
output.privileges = ['messages_delete', 'reports_manage_reports']
|
|
||||||
} else {
|
|
||||||
output.privileges = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.source) {
|
|
||||||
output.description = data.source.note
|
|
||||||
output.default_scope = data.source.privacy
|
|
||||||
output.fields = data.source.fields
|
|
||||||
if (data.source.pleroma) {
|
|
||||||
output.no_rich_text = data.source.pleroma.no_rich_text
|
|
||||||
output.show_role = data.source.pleroma.show_role
|
|
||||||
output.discoverable = data.source.pleroma.discoverable
|
|
||||||
output.show_birthday = data.pleroma.show_birthday
|
|
||||||
output.actor_type = data.source.pleroma.actor_type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: handle is_local
|
|
||||||
output.is_local = !output.screen_name.includes('@')
|
|
||||||
} else {
|
|
||||||
output.screen_name = data.screen_name
|
|
||||||
|
|
||||||
output.name = data.name
|
|
||||||
output.name_html = data.name_html
|
|
||||||
|
|
||||||
output.description = data.description
|
|
||||||
output.description_html = data.description_html
|
|
||||||
|
|
||||||
output.profile_image_url = data.profile_image_url
|
|
||||||
output.profile_image_url_original = data.profile_image_url_original
|
|
||||||
|
|
||||||
output.cover_photo = data.cover_photo
|
|
||||||
|
|
||||||
output.friends_count = data.friends_count
|
|
||||||
|
|
||||||
// output.bot = ??? missing
|
|
||||||
|
|
||||||
output.statusnet_profile_url = data.statusnet_profile_url
|
|
||||||
|
|
||||||
output.is_local = data.is_local
|
|
||||||
output.role = data.role
|
|
||||||
output.show_role = data.show_role
|
|
||||||
|
|
||||||
if (data.rights) {
|
|
||||||
output.rights = {
|
|
||||||
moderator: data.rights.delete_others_notice,
|
|
||||||
admin: data.rights.admin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.no_rich_text = data.no_rich_text
|
|
||||||
output.default_scope = data.default_scope
|
|
||||||
output.hide_follows = data.hide_follows
|
|
||||||
output.hide_followers = data.hide_followers
|
|
||||||
output.hide_follows_count = data.hide_follows_count
|
|
||||||
output.hide_followers_count = data.hide_followers_count
|
|
||||||
output.background_image = data.background_image
|
|
||||||
// Websocket token
|
|
||||||
output.token = data.token
|
|
||||||
|
|
||||||
// Convert relationsip data to expected format
|
|
||||||
output.relationship = {
|
|
||||||
muting: data.muted,
|
|
||||||
blocking: data.statusnet_blocking,
|
|
||||||
followed_by: data.follows_you,
|
|
||||||
following: data.following,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.source) {
|
||||||
|
output.description = data.source.note
|
||||||
|
output.default_scope = data.source.privacy
|
||||||
|
output.fields = data.source.fields
|
||||||
|
if (data.source.pleroma) {
|
||||||
|
output.no_rich_text = data.source.pleroma.no_rich_text
|
||||||
|
output.show_role =
|
||||||
|
typeof data.source.pleroma.show_role === 'boolean'
|
||||||
|
? data.source.pleroma.show_role
|
||||||
|
: true
|
||||||
|
output.discoverable = data.source.pleroma.discoverable
|
||||||
|
output.show_birthday = data.pleroma.show_birthday
|
||||||
|
output.actor_type = data.source.pleroma.actor_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle is_local
|
||||||
|
output.is_local = !output.screen_name.includes('@')
|
||||||
|
|
||||||
output.created_at = new Date(data.created_at)
|
output.created_at = new Date(data.created_at)
|
||||||
output.locked = data.locked
|
output.locked = data.locked
|
||||||
output.last_status_at = new Date(data.last_status_at)
|
output.last_status_at = new Date(data.last_status_at)
|
||||||
|
|
@ -252,7 +172,7 @@ export const parseUser = (data) => {
|
||||||
if (data.pleroma) {
|
if (data.pleroma) {
|
||||||
output.follow_request_count = data.pleroma.follow_request_count
|
output.follow_request_count = data.pleroma.follow_request_count
|
||||||
|
|
||||||
output.tags = data.pleroma.tags
|
output.tags = new Set(data.pleroma.tags)
|
||||||
|
|
||||||
// deactivated was changed to is_active in Pleroma 2.3.0
|
// deactivated was changed to is_active in Pleroma 2.3.0
|
||||||
// so check if is_active is present
|
// so check if is_active is present
|
||||||
|
|
@ -265,7 +185,7 @@ export const parseUser = (data) => {
|
||||||
output.unread_chat_count = data.pleroma.unread_chat_count
|
output.unread_chat_count = data.pleroma.unread_chat_count
|
||||||
}
|
}
|
||||||
|
|
||||||
output.tags = output.tags || []
|
output.tags = output.tags || new Set()
|
||||||
output.rights = output.rights || {}
|
output.rights = output.rights || {}
|
||||||
output.notification_settings = output.notification_settings || {}
|
output.notification_settings = output.notification_settings || {}
|
||||||
|
|
||||||
|
|
@ -289,17 +209,11 @@ export const parseUser = (data) => {
|
||||||
|
|
||||||
export const parseAttachment = (data) => {
|
export const parseAttachment = (data) => {
|
||||||
const output = {}
|
const output = {}
|
||||||
const masto = !Object.hasOwn(data, 'oembed')
|
|
||||||
|
|
||||||
if (masto) {
|
// Not exactly same...
|
||||||
// Not exactly same...
|
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
|
||||||
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
|
output.meta = data.meta // not present in BE yet
|
||||||
output.meta = data.meta // not present in BE yet
|
output.id = data.id
|
||||||
output.id = data.id
|
|
||||||
} else {
|
|
||||||
output.mimetype = data.mimetype
|
|
||||||
// output.meta = ??? missing
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type !== 'unknown') {
|
if (data.type !== 'unknown') {
|
||||||
// treat gifv like it is "video"
|
// treat gifv like it is "video"
|
||||||
|
|
@ -326,116 +240,76 @@ export const parseSource = (data) => {
|
||||||
|
|
||||||
export const parseStatus = (data) => {
|
export const parseStatus = (data) => {
|
||||||
const output = {}
|
const output = {}
|
||||||
const masto = Object.hasOwn(data, 'account')
|
|
||||||
|
|
||||||
if (masto) {
|
output.favorited = data.favourited
|
||||||
output.favorited = data.favourited
|
output.fave_num = data.favourites_count
|
||||||
output.fave_num = data.favourites_count
|
|
||||||
|
|
||||||
output.repeated = data.reblogged
|
output.repeated = data.reblogged
|
||||||
output.repeat_num = data.reblogs_count
|
output.repeat_num = data.reblogs_count
|
||||||
|
|
||||||
output.bookmarked = data.bookmarked
|
output.bookmarked = data.bookmarked
|
||||||
|
|
||||||
output.type = data.reblog ? 'retweet' : 'status'
|
output.type = data.reblog ? 'retweet' : 'status'
|
||||||
output.nsfw = data.sensitive
|
output.nsfw = data.sensitive
|
||||||
|
|
||||||
output.raw_html = data.content
|
output.raw_html = data.content
|
||||||
output.emojis = data.emojis
|
output.emojis = data.emojis
|
||||||
|
|
||||||
output.tags = data.tags
|
output.tags = data.tags
|
||||||
|
|
||||||
output.edited_at = data.edited_at
|
output.edited_at = data.edited_at
|
||||||
|
|
||||||
const { pleroma } = data
|
const { pleroma } = data
|
||||||
|
|
||||||
if (data.pleroma) {
|
if (data.pleroma) {
|
||||||
output.text = pleroma.content
|
output.text = pleroma.content
|
||||||
? data.pleroma.content['text/plain']
|
? data.pleroma.content['text/plain']
|
||||||
: data.content
|
: data.content
|
||||||
output.summary = pleroma.spoiler_text
|
output.summary = pleroma.spoiler_text
|
||||||
? data.pleroma.spoiler_text['text/plain']
|
? data.pleroma.spoiler_text['text/plain']
|
||||||
: data.spoiler_text
|
: data.spoiler_text
|
||||||
output.statusnet_conversation_id = data.pleroma.conversation_id
|
output.statusnet_conversation_id = data.pleroma.conversation_id
|
||||||
output.is_local = pleroma.local
|
output.is_local = pleroma.local
|
||||||
output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct
|
output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct
|
||||||
output.thread_muted = pleroma.thread_muted
|
output.thread_muted = pleroma.thread_muted
|
||||||
output.emoji_reactions = pleroma.emoji_reactions
|
output.emoji_reactions = pleroma.emoji_reactions
|
||||||
output.parent_visible =
|
output.parent_visible =
|
||||||
pleroma.parent_visible === undefined ? true : pleroma.parent_visible
|
pleroma.parent_visible === undefined ? true : pleroma.parent_visible
|
||||||
output.quote_visible = pleroma.quote_visible || true
|
output.quote_visible = pleroma.quote_visible || true
|
||||||
output.quotes_count = pleroma.quotes_count
|
output.quotes_count = pleroma.quotes_count
|
||||||
output.bookmark_folder_id = pleroma.bookmark_folder
|
output.bookmark_folder_id = pleroma.bookmark_folder
|
||||||
} else {
|
|
||||||
output.text = data.content
|
|
||||||
output.summary = data.spoiler_text
|
|
||||||
}
|
|
||||||
|
|
||||||
const quoteRaw = pleroma?.quote || data.quote
|
|
||||||
const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
|
|
||||||
output.quote = quoteData
|
|
||||||
output.quote_id =
|
|
||||||
data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma?.quote_id
|
|
||||||
output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma?.quote_url
|
|
||||||
|
|
||||||
output.in_reply_to_status_id = data.in_reply_to_id
|
|
||||||
output.in_reply_to_user_id = data.in_reply_to_account_id
|
|
||||||
output.replies_count = data.replies_count
|
|
||||||
|
|
||||||
if (output.type === 'retweet') {
|
|
||||||
output.retweeted_status = parseStatus(data.reblog)
|
|
||||||
}
|
|
||||||
|
|
||||||
output.summary_raw_html = escapeHtml(data.spoiler_text)
|
|
||||||
output.external_url = data.uri || data.url
|
|
||||||
output.poll = data.poll
|
|
||||||
if (output.poll) {
|
|
||||||
output.poll.options = (output.poll.options || []).map((field) => ({
|
|
||||||
...field,
|
|
||||||
title_html: escapeHtml(field.title),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
output.pinned = data.pinned
|
|
||||||
output.muted = data.muted
|
|
||||||
} else {
|
} else {
|
||||||
output.favorited = data.favorited
|
output.text = data.content
|
||||||
output.fave_num = data.fave_num
|
output.summary = data.spoiler_text
|
||||||
|
|
||||||
output.repeated = data.repeated
|
|
||||||
output.repeat_num = data.repeat_num
|
|
||||||
|
|
||||||
// catchall, temporary
|
|
||||||
// Object.assign(output, data)
|
|
||||||
|
|
||||||
output.type = qvitterStatusType(data)
|
|
||||||
|
|
||||||
if (data.nsfw === undefined) {
|
|
||||||
output.nsfw = isNsfw(data)
|
|
||||||
if (data.retweeted_status) {
|
|
||||||
output.nsfw = data.retweeted_status.nsfw
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.nsfw = data.nsfw
|
|
||||||
}
|
|
||||||
|
|
||||||
output.raw_html = data.statusnet_html
|
|
||||||
output.text = data.text
|
|
||||||
|
|
||||||
output.in_reply_to_status_id = data.in_reply_to_status_id
|
|
||||||
output.in_reply_to_user_id = data.in_reply_to_user_id
|
|
||||||
output.in_reply_to_screen_name = data.in_reply_to_screen_name
|
|
||||||
output.statusnet_conversation_id = data.statusnet_conversation_id
|
|
||||||
|
|
||||||
if (output.type === 'retweet') {
|
|
||||||
output.retweeted_status = parseStatus(data.retweeted_status)
|
|
||||||
}
|
|
||||||
|
|
||||||
output.summary = data.summary
|
|
||||||
output.summary_html = data.summary_html
|
|
||||||
output.external_url = data.external_url
|
|
||||||
output.is_local = data.is_local
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quoteRaw = pleroma?.quote || data.quote
|
||||||
|
const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
|
||||||
|
output.quote = quoteData
|
||||||
|
output.quote_id =
|
||||||
|
data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma?.quote_id
|
||||||
|
output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma?.quote_url
|
||||||
|
|
||||||
|
output.in_reply_to_status_id = data.in_reply_to_id
|
||||||
|
output.in_reply_to_user_id = data.in_reply_to_account_id
|
||||||
|
output.replies_count = data.replies_count
|
||||||
|
|
||||||
|
if (output.type === 'retweet') {
|
||||||
|
output.retweeted_status = parseStatus(data.reblog)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.summary_raw_html = escapeHtml(data.spoiler_text)
|
||||||
|
output.external_url = data.uri || data.url
|
||||||
|
output.poll = data.poll
|
||||||
|
if (output.poll) {
|
||||||
|
output.poll.options = (output.poll.options || []).map((field) => ({
|
||||||
|
...field,
|
||||||
|
title_html: escapeHtml(field.title),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
output.pinned = data.pinned
|
||||||
|
output.muted = data.muted
|
||||||
|
|
||||||
output.id = String(data.id)
|
output.id = String(data.id)
|
||||||
output.visibility = data.visibility
|
output.visibility = data.visibility
|
||||||
output.card = data.card
|
output.card = data.card
|
||||||
|
|
@ -449,17 +323,13 @@ export const parseStatus = (data) => {
|
||||||
? String(output.in_reply_to_user_id)
|
? String(output.in_reply_to_user_id)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
output.user = parseUser(masto ? data.account : data.user)
|
output.user = parseUser(data.account)
|
||||||
|
|
||||||
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(
|
output.attentions = (data.mentions || []).map(parseUser)
|
||||||
parseUser,
|
|
||||||
)
|
|
||||||
|
|
||||||
output.attachments = (
|
output.attachments = (data.media_attachments || []).map(parseAttachment)
|
||||||
(masto ? data.media_attachments : data.attachments) || []
|
|
||||||
).map(parseAttachment)
|
|
||||||
|
|
||||||
const retweetedStatus = masto ? data.reblog : data.retweeted_status
|
const retweetedStatus = data.reblog
|
||||||
if (retweetedStatus) {
|
if (retweetedStatus) {
|
||||||
output.retweeted_status = parseStatus(retweetedStatus)
|
output.retweeted_status = parseStatus(retweetedStatus)
|
||||||
}
|
}
|
||||||
|
|
@ -479,42 +349,26 @@ export const parseNotification = (data) => {
|
||||||
favourite: 'like',
|
favourite: 'like',
|
||||||
reblog: 'repeat',
|
reblog: 'repeat',
|
||||||
}
|
}
|
||||||
const masto = !Object.hasOwn(data, 'ntype')
|
|
||||||
const output = {}
|
const output = {}
|
||||||
|
|
||||||
if (masto) {
|
output.type = mastoDict[data.type] || data.type
|
||||||
output.type = mastoDict[data.type] || data.type
|
output.seen = data.pleroma.is_seen
|
||||||
output.seen = data.pleroma.is_seen
|
// TODO: null check should be a temporary fix, I guess.
|
||||||
// TODO: null check should be a temporary fix, I guess.
|
// Investigate why backend does this.
|
||||||
// Investigate why backend does this.
|
output.status =
|
||||||
output.status =
|
isStatusNotification(output.type) && data.status !== null
|
||||||
isStatusNotification(output.type) && data.status !== null
|
? parseStatus(data.status)
|
||||||
? parseStatus(data.status)
|
: null
|
||||||
: null
|
output.target = output.type !== 'move' ? null : parseUser(data.target)
|
||||||
output.target = output.type !== 'move' ? null : parseUser(data.target)
|
output.from_profile = parseUser(data.account)
|
||||||
output.from_profile = parseUser(data.account)
|
output.emoji = data.emoji
|
||||||
output.emoji = data.emoji
|
output.emoji_url = data.emoji_url
|
||||||
output.emoji_url = data.emoji_url
|
if (data.report) {
|
||||||
if (data.report) {
|
output.report = data.report
|
||||||
output.report = data.report
|
output.report.content = data.report.content
|
||||||
output.report.content = data.report.content
|
output.report.acct = parseUser(data.report.account)
|
||||||
output.report.acct = parseUser(data.report.account)
|
output.report.actor = parseUser(data.report.actor)
|
||||||
output.report.actor = parseUser(data.report.actor)
|
output.report.statuses = data.report.statuses.map(parseStatus)
|
||||||
output.report.statuses = data.report.statuses.map(parseStatus)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const parsedNotice = parseStatus(data.notice)
|
|
||||||
output.type = data.ntype
|
|
||||||
output.seen = Boolean(data.is_seen)
|
|
||||||
output.status =
|
|
||||||
output.type === 'like'
|
|
||||||
? parseStatus(data.notice.favorited_status)
|
|
||||||
: parsedNotice
|
|
||||||
output.action = parsedNotice
|
|
||||||
output.from_profile =
|
|
||||||
output.type === 'pleroma:chat_mention'
|
|
||||||
? parseUser(data.account)
|
|
||||||
: parseUser(data.from_profile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output.created_at = new Date(data.created_at)
|
output.created_at = new Date(data.created_at)
|
||||||
|
|
@ -523,14 +377,6 @@ export const parseNotification = (data) => {
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNsfw = (status) => {
|
|
||||||
const nsfwRegex = /#nsfw/i
|
|
||||||
return (
|
|
||||||
(status.tags || []).includes('nsfw') ||
|
|
||||||
!!(status.text || '').match(nsfwRegex)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
|
export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
|
||||||
const flakeId = opts.flakeId
|
const flakeId = opts.flakeId
|
||||||
const parsedLinkHeader = parseLinkHeader(linkHeader)
|
const parsedLinkHeader = parseLinkHeader(linkHeader)
|
||||||
|
|
|
||||||
466
src/stores/admin_settings.js
Normal file
466
src/stores/admin_settings.js
Normal file
|
|
@ -0,0 +1,466 @@
|
||||||
|
import { cloneDeep, differenceWith, flatten, get, isEqual, set } from 'lodash'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
import { parseStatus } from 'src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
frontends: [],
|
||||||
|
loaded: false,
|
||||||
|
needsReboot: null,
|
||||||
|
config: null,
|
||||||
|
modifiedPaths: null,
|
||||||
|
descriptions: null,
|
||||||
|
draft: null,
|
||||||
|
dbConfigEnabled: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newUserFlags = {
|
||||||
|
...defaultState.flagStorage,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAdminSettingsStore = defineStore('adminSettings', {
|
||||||
|
state: () => ({
|
||||||
|
...cloneDeep(defaultState),
|
||||||
|
backendInteractor: window.vuex.state.api.backendInteractor,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
// Configuration Stuff
|
||||||
|
setInstanceAdminNoDbConfig() {
|
||||||
|
this.loaded = false
|
||||||
|
this.dbConfigEnabled = false
|
||||||
|
},
|
||||||
|
updateAdminSettings({ config, modifiedPaths }) {
|
||||||
|
this.loaded = true
|
||||||
|
this.dbConfigEnabled = true
|
||||||
|
this.config = config
|
||||||
|
this.modifiedPaths = modifiedPaths
|
||||||
|
},
|
||||||
|
updateAdminDescriptions({ descriptions }) {
|
||||||
|
this.descriptions = descriptions
|
||||||
|
},
|
||||||
|
updateAdminDraft({ path, value }) {
|
||||||
|
const [group, key, subkey] = path
|
||||||
|
const parent = [group, key, subkey]
|
||||||
|
|
||||||
|
set(this.draft, path, value)
|
||||||
|
|
||||||
|
// force-updating grouped draft to trigger refresh of group settings
|
||||||
|
if (path.length > parent.length) {
|
||||||
|
set(this.draft, parent, cloneDeep(get(this.draft, parent)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetAdminDraft() {
|
||||||
|
this.draft = cloneDeep(this.config)
|
||||||
|
},
|
||||||
|
|
||||||
|
loadAdminStuff() {
|
||||||
|
this.backendInteractor.fetchInstanceDBConfig().then((backendDbConfig) => {
|
||||||
|
if (backendDbConfig.error) {
|
||||||
|
if (backendDbConfig.error.status === 400) {
|
||||||
|
backendDbConfig.error.json().then((errorJson) => {
|
||||||
|
if (/configurable_from_database/.test(errorJson.error)) {
|
||||||
|
this.setInstanceAdminNoDbConfig()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setInstanceAdminSettings({ backendDbConfig })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (this.descriptions === null) {
|
||||||
|
this.backendInteractor
|
||||||
|
.fetchInstanceConfigDescriptions()
|
||||||
|
.then((backendDescriptions) =>
|
||||||
|
this.setInstanceAdminDescriptions({ backendDescriptions }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setInstanceAdminSettings({ backendDbConfig }) {
|
||||||
|
const config = this.config || {}
|
||||||
|
const modifiedPaths = new Set()
|
||||||
|
|
||||||
|
backendDbConfig.configs.forEach((c) => {
|
||||||
|
const path = [c.group, c.key]
|
||||||
|
if (c.db) {
|
||||||
|
// Path elements can contain dot, therefore we use ' -> ' as a separator instead
|
||||||
|
// Using strings for modified paths for easier searching
|
||||||
|
c.db.forEach((x) => modifiedPaths.add([...path, x].join(' -> ')))
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to preserve tuples on second level only, possibly third
|
||||||
|
// but it's not a case right now.
|
||||||
|
const convert = (value, preserveTuples, preserveTuplesLv2) => {
|
||||||
|
if (Array.isArray(value) && value.length > 0 && value[0].tuple) {
|
||||||
|
if (!preserveTuples) {
|
||||||
|
return value.reduce((acc, c) => {
|
||||||
|
if (c.tuple == null) {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[c]: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[c.tuple[0]]: convert(c.tuple[1], preserveTuplesLv2),
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
} else {
|
||||||
|
return value.map((x) => x.tuple)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!preserveTuples) {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
return value.tuple
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// for most stuff we want maps since those are more convenient
|
||||||
|
// however this doesn't allow for multiple values per same key
|
||||||
|
// so for those cases we want to preserve tuples as-is
|
||||||
|
// right now it's made exclusively for :pleroma.:rate_limit
|
||||||
|
// so it might not work properly elsewhere
|
||||||
|
const preserveTuples = path.find((x) => x === ':rate_limit')
|
||||||
|
set(config, path, convert(c.value, false, preserveTuples))
|
||||||
|
})
|
||||||
|
// patching http adapter config to be easier to handle
|
||||||
|
const adapter = config[':pleroma'][':http'][':adapter']
|
||||||
|
if (Array.isArray(adapter)) {
|
||||||
|
config[':pleroma'][':http'][':adapter'] = {
|
||||||
|
[':ssl_options']: {
|
||||||
|
[':versions']: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateAdminSettings({ config, modifiedPaths })
|
||||||
|
this.resetAdminDraft()
|
||||||
|
},
|
||||||
|
setInstanceAdminDescriptions({ backendDescriptions }) {
|
||||||
|
const convert = (
|
||||||
|
{ children, description, label, key = '<ROOT>', group, suggestions },
|
||||||
|
path,
|
||||||
|
acc,
|
||||||
|
) => {
|
||||||
|
const newPath = group ? [group, key] : [key]
|
||||||
|
const obj = { description, label, suggestions }
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
children.forEach((c) => {
|
||||||
|
convert(c, newPath, obj)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
set(acc, newPath, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptions = {}
|
||||||
|
|
||||||
|
backendDescriptions.forEach((d) => convert(d, '', descriptions))
|
||||||
|
this.updateAdminDescriptions({ descriptions })
|
||||||
|
},
|
||||||
|
|
||||||
|
// This action takes draft state, diffs it with live config state and then pushes
|
||||||
|
// only differences between the two. Difference detection only work up to subkey (third) level.
|
||||||
|
pushAdminDraft() {
|
||||||
|
// TODO cleanup paths in modifiedPaths
|
||||||
|
const convert = (value) => {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return value
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
return value.map(convert)
|
||||||
|
} else {
|
||||||
|
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting all group-keys used in config
|
||||||
|
const allGroupKeys = flatten(
|
||||||
|
Object.entries(this.config).map(([group, lv1data]) =>
|
||||||
|
Object.keys(lv1data).map((key) => ({ group, key })),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Only using group-keys where there are changes detected
|
||||||
|
const changedGroupKeys = allGroupKeys.filter(({ group, key }) => {
|
||||||
|
return !isEqual(this.config[group][key], this.draft[group][key])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Here we take all changed group-keys and get all changed subkeys
|
||||||
|
const changed = changedGroupKeys.map(({ group, key }) => {
|
||||||
|
const config = this.config[group][key]
|
||||||
|
const draft = this.draft[group][key]
|
||||||
|
|
||||||
|
// We convert group-key value into entries arrays
|
||||||
|
const eConfig = Object.entries(config)
|
||||||
|
const eDraft = Object.entries(draft)
|
||||||
|
|
||||||
|
// Then those entries array we diff so only changed subkey entries remain
|
||||||
|
// We use the diffed array to reconstruct the object and then shove it into convert()
|
||||||
|
return {
|
||||||
|
group,
|
||||||
|
key,
|
||||||
|
value: convert(
|
||||||
|
Object.fromEntries(differenceWith(eDraft, eConfig, isEqual)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.vuex.state.api.backendInteractor
|
||||||
|
.pushInstanceDBConfig({
|
||||||
|
payload: {
|
||||||
|
configs: changed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
|
||||||
|
)
|
||||||
|
.then((backendDbConfig) =>
|
||||||
|
this.setInstanceAdminSettings({ backendDbConfig }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pushAdminSetting({ path, value }) {
|
||||||
|
const [group, key, ...rest] = Array.isArray(path)
|
||||||
|
? path
|
||||||
|
: path.split(/\./g)
|
||||||
|
const clone = {} // not actually cloning the entire thing to avoid excessive writes
|
||||||
|
set(clone, rest, value)
|
||||||
|
|
||||||
|
// TODO cleanup paths in modifiedPaths
|
||||||
|
const convert = (value) => {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return value
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
return value.map(convert)
|
||||||
|
} else {
|
||||||
|
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.vuex.state.api.backendInteractor
|
||||||
|
.pushInstanceDBConfig({
|
||||||
|
payload: {
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
group,
|
||||||
|
key,
|
||||||
|
value: convert(clone),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
|
||||||
|
)
|
||||||
|
.then((backendDbConfig) =>
|
||||||
|
this.setInstanceAdminSettings({ backendDbConfig }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
resetAdminSetting({ path }) {
|
||||||
|
const [group, key, subkey] = Array.isArray(path)
|
||||||
|
? path
|
||||||
|
: path.split(/\./g)
|
||||||
|
|
||||||
|
this.modifiedPaths.delete(path)
|
||||||
|
|
||||||
|
return window.vuex.state.api.backendInteractor
|
||||||
|
.pushInstanceDBConfig({
|
||||||
|
payload: {
|
||||||
|
configs: [
|
||||||
|
{
|
||||||
|
group,
|
||||||
|
key,
|
||||||
|
delete: true,
|
||||||
|
subkeys: [subkey],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() =>
|
||||||
|
window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
|
||||||
|
)
|
||||||
|
.then((backendDbConfig) =>
|
||||||
|
this.setInstanceAdminSettings({ backendDbConfig }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Frontends Stuff
|
||||||
|
loadFrontendsStuff() {
|
||||||
|
this.backendInteractor
|
||||||
|
.fetchAvailableFrontends()
|
||||||
|
.then((frontends) => this.setAvailableFrontends({ frontends }))
|
||||||
|
},
|
||||||
|
|
||||||
|
setAvailableFrontends({ frontends }) {
|
||||||
|
this.frontends = frontends.map((f) => {
|
||||||
|
f.installedRefs = f.installed_refs
|
||||||
|
if (f.name === 'pleroma-fe') {
|
||||||
|
f.refs = ['master', 'develop']
|
||||||
|
} else {
|
||||||
|
f.refs = [f.ref]
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Statuses stuff
|
||||||
|
async fetchStatuses(opts) {
|
||||||
|
const { total, activities } =
|
||||||
|
await this.backendInteractor.adminListStatuses({
|
||||||
|
opts,
|
||||||
|
})
|
||||||
|
|
||||||
|
const statuses = activities.map(parseStatus)
|
||||||
|
|
||||||
|
await window.vuex.dispatch('addNewStatuses', { statuses })
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: statuses,
|
||||||
|
count: total,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async changeStatusScope(opts) {
|
||||||
|
const raw = await this.backendInteractor.adminChangeStatusScope({
|
||||||
|
opts,
|
||||||
|
})
|
||||||
|
const status = parseStatus(raw)
|
||||||
|
|
||||||
|
await window.vuex.dispatch('addNewStatuses', { statuses: [status] })
|
||||||
|
},
|
||||||
|
|
||||||
|
// Users stuff
|
||||||
|
async fetchUsers(opts) {
|
||||||
|
const { users, count } = await this.backendInteractor.adminListUsers({
|
||||||
|
opts,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: await Promise.all(
|
||||||
|
users.map(
|
||||||
|
async (userAdminData) =>
|
||||||
|
await window.vuex.dispatch('updateUserAdminData', {
|
||||||
|
userAdminData,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
count,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getUserData({ user }) {
|
||||||
|
const api = this.backendInteractor.adminGetUserData
|
||||||
|
const { screen_name } = user
|
||||||
|
|
||||||
|
const result = await api({ screen_name })
|
||||||
|
window.vuex.commit('updateUserAdminData', { user: result })
|
||||||
|
},
|
||||||
|
async deleteUsers({ users }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
const api = this.backendInteractor.adminDeleteAccounts
|
||||||
|
|
||||||
|
const resultUserIds = await api({ screen_names })
|
||||||
|
|
||||||
|
resultUserIds.forEach((userId) => {
|
||||||
|
window.vuex.dispatch(
|
||||||
|
'markStatusesAsDeleted',
|
||||||
|
(status) => userId === status.user.id,
|
||||||
|
)
|
||||||
|
// TODO when migrated to pinia, also remove user
|
||||||
|
})
|
||||||
|
|
||||||
|
return resultUserIds
|
||||||
|
},
|
||||||
|
resendConfirmationEmail({ users }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
|
||||||
|
return this.backendInteractor.adminResendConfirmationEmail({
|
||||||
|
screen_names,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
requirePasswordChange({ users }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
|
||||||
|
return this.backendInteractor.adminRequirePasswordChange({
|
||||||
|
screen_names,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// Singular only!
|
||||||
|
disableMFA({ user }) {
|
||||||
|
const { screen_name } = user
|
||||||
|
|
||||||
|
return this.backendInteractor.adminDisableMFA({ screen_name })
|
||||||
|
},
|
||||||
|
async setUsersTags({ users, tags, value }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
const api = this.backendInteractor.adminSetUsersTags
|
||||||
|
|
||||||
|
await api({
|
||||||
|
screen_names,
|
||||||
|
tags,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
|
||||||
|
users.forEach((user) => {
|
||||||
|
this.getUserData({ user })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async setUsersRight({ users, right, value }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
const api = this.backendInteractor.adminSetUsersRight
|
||||||
|
|
||||||
|
await api({
|
||||||
|
screen_names,
|
||||||
|
right,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
|
||||||
|
users.forEach((user) => {
|
||||||
|
window.vuex.commit('updateRight', { user, right, value })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async setUsersActivationStatus({ users, value }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
const api = this.backendInteractor.adminSetUsersActivationStatus
|
||||||
|
|
||||||
|
const resultUsers = await api({
|
||||||
|
screen_names,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
|
||||||
|
resultUsers.forEach((user) => {
|
||||||
|
window.vuex.commit('updateUserAdminData', { user })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async setUsersSuggestionStatus({ users, value }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
const api = this.backendInteractor.adminSetUsersSuggestionStatus
|
||||||
|
|
||||||
|
const resultUsers = await api({
|
||||||
|
screen_names,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
|
||||||
|
resultUsers.forEach((user) => {
|
||||||
|
window.vuex.commit('updateUserAdminData', { user })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async setUsersConfirmationStatus({ users }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
const api = this.backendInteractor.adminSetUsersConfirmationStatus
|
||||||
|
|
||||||
|
await api({ screen_names })
|
||||||
|
|
||||||
|
users.forEach((user) => {
|
||||||
|
this.getUserData({ user })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async setUsersApprovalStatus({ users }) {
|
||||||
|
const screen_names = users.map((u) => u.screen_name)
|
||||||
|
const api = this.backendInteractor.adminSetUsersApprovalStatus
|
||||||
|
|
||||||
|
const resultUsers = await api({
|
||||||
|
screen_names,
|
||||||
|
})
|
||||||
|
|
||||||
|
resultUsers.forEach((user) => {
|
||||||
|
window.vuex.commit('updateUserAdminData', { user })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -29,7 +29,7 @@ export const useAnnouncementsStore = defineStore('announcements', {
|
||||||
const currentUser = window.vuex.state.users.currentUser
|
const currentUser = window.vuex.state.users.currentUser
|
||||||
const isAdmin =
|
const isAdmin =
|
||||||
currentUser &&
|
currentUser &&
|
||||||
currentUser.privileges.includes('announcements_manage_announcements')
|
currentUser.privileges.has('announcements_manage_announcements')
|
||||||
|
|
||||||
const getAnnouncements = async () => {
|
const getAnnouncements = async () => {
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,24 @@ export const useInterfaceStore = defineStore('interface', {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
togglePeekSettingsModal() {
|
setSettingsModalState(newState) {
|
||||||
|
const oldState = this.settingsModalState
|
||||||
|
const legal = (() => {
|
||||||
|
switch (oldState) {
|
||||||
|
case 'minimized':
|
||||||
|
return true
|
||||||
|
case 'visible':
|
||||||
|
return true
|
||||||
|
case 'hidden':
|
||||||
|
return newState === 'visible'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (legal) {
|
||||||
|
this.settingsModalState = newState
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleMinimizeSettingsModal() {
|
||||||
switch (this.settingsModalState) {
|
switch (this.settingsModalState) {
|
||||||
case 'minimized':
|
case 'minimized':
|
||||||
this.settingsModalState = 'visible'
|
this.settingsModalState = 'visible'
|
||||||
|
|
@ -141,8 +158,12 @@ export const useInterfaceStore = defineStore('interface', {
|
||||||
case 'visible':
|
case 'visible':
|
||||||
this.settingsModalState = 'minimized'
|
this.settingsModalState = 'minimized'
|
||||||
return
|
return
|
||||||
|
case 'hidden':
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
throw new Error('Illegal minimization state of settings modal')
|
throw new Error(
|
||||||
|
`Illegal minimization state of settings modal: ${this.settingsModalState}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearSettingsModalTargetTab() {
|
clearSettingsModalTargetTab() {
|
||||||
|
|
|
||||||
1640
test/fixtures/statuses.json
vendored
1640
test/fixtures/statuses.json
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -5,76 +5,6 @@ import {
|
||||||
parseUser,
|
parseUser,
|
||||||
} from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
} from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
import mastoapidata from '../../../../fixtures/mastoapi.json'
|
import mastoapidata from '../../../../fixtures/mastoapi.json'
|
||||||
import qvitterapidata from '../../../../fixtures/statuses.json'
|
|
||||||
|
|
||||||
const makeMockStatusQvitter = (overrides = {}) => {
|
|
||||||
return Object.assign(
|
|
||||||
{
|
|
||||||
activity_type: 'post',
|
|
||||||
attachments: [],
|
|
||||||
attentions: [],
|
|
||||||
created_at: 'Tue Jan 15 13:57:56 +0000 2019',
|
|
||||||
external_url: 'https://ap.example/whatever',
|
|
||||||
fave_num: 1,
|
|
||||||
favorited: false,
|
|
||||||
id: '10335970',
|
|
||||||
in_reply_to_ostatus_uri: null,
|
|
||||||
in_reply_to_profileurl: null,
|
|
||||||
in_reply_to_screen_name: null,
|
|
||||||
in_reply_to_status_id: null,
|
|
||||||
in_reply_to_user_id: null,
|
|
||||||
is_local: false,
|
|
||||||
is_post_verb: true,
|
|
||||||
possibly_sensitive: false,
|
|
||||||
repeat_num: 0,
|
|
||||||
repeated: false,
|
|
||||||
statusnet_conversation_id: '16300488',
|
|
||||||
summary: null,
|
|
||||||
tags: [],
|
|
||||||
text: 'haha benis',
|
|
||||||
uri: 'https://ap.example/whatever',
|
|
||||||
user: makeMockUserQvitter(),
|
|
||||||
visibility: 'public',
|
|
||||||
},
|
|
||||||
overrides,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMockUserQvitter = (overrides = {}) => {
|
|
||||||
return Object.assign(
|
|
||||||
{
|
|
||||||
background_image: null,
|
|
||||||
cover_photo: '',
|
|
||||||
created_at: 'Mon Jan 14 13:56:51 +0000 2019',
|
|
||||||
default_scope: 'public',
|
|
||||||
description: 'ebin',
|
|
||||||
description_html: '<p>ebin</p>',
|
|
||||||
favourites_count: 0,
|
|
||||||
fields: [],
|
|
||||||
followers_count: 1,
|
|
||||||
following: true,
|
|
||||||
follows_you: true,
|
|
||||||
friends_count: 1,
|
|
||||||
id: '60717',
|
|
||||||
is_local: false,
|
|
||||||
locked: false,
|
|
||||||
name: 'Spurdo :ebin:',
|
|
||||||
name_html: 'Spurdo <img src="whatever">',
|
|
||||||
no_rich_text: false,
|
|
||||||
pleroma: { confirmation_pending: false, tags: [] },
|
|
||||||
profile_image_url: 'https://ap.example/whatever',
|
|
||||||
profile_image_url_https: 'https://ap.example/whatever',
|
|
||||||
profile_image_url_original: 'https://ap.example/whatever',
|
|
||||||
profile_image_url_profile_size: 'https://ap.example/whatever',
|
|
||||||
rights: { delete_others_notice: false },
|
|
||||||
screen_name: 'spurdo@ap.example',
|
|
||||||
statuses_count: 46,
|
|
||||||
statusnet_blocking: false,
|
|
||||||
statusnet_profile_url: '',
|
|
||||||
},
|
|
||||||
overrides,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMockUserMasto = (overrides = {}) => {
|
const makeMockUserMasto = (overrides = {}) => {
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
|
|
@ -151,19 +81,6 @@ const makeMockStatusMasto = (overrides = {}) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeMockNotificationQvitter = (overrides = {}) => {
|
|
||||||
return Object.assign(
|
|
||||||
{
|
|
||||||
notice: makeMockStatusQvitter(),
|
|
||||||
ntype: 'follow',
|
|
||||||
from_profile: makeMockUserQvitter(),
|
|
||||||
is_seen: 0,
|
|
||||||
id: 123,
|
|
||||||
},
|
|
||||||
overrides,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMockEmojiMasto = (overrides = [{}]) => {
|
const makeMockEmojiMasto = (overrides = [{}]) => {
|
||||||
return [
|
return [
|
||||||
Object.assign(
|
Object.assign(
|
||||||
|
|
@ -189,78 +106,6 @@ const makeMockEmojiMasto = (overrides = [{}]) => {
|
||||||
|
|
||||||
describe('API Entities normalizer', () => {
|
describe('API Entities normalizer', () => {
|
||||||
describe('parseStatus', () => {
|
describe('parseStatus', () => {
|
||||||
describe('QVitter preprocessing', () => {
|
|
||||||
it("doesn't blow up", () => {
|
|
||||||
const parsed = qvitterapidata.map(parseStatus)
|
|
||||||
expect(parsed.length).to.eq(qvitterapidata.length)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('identifies favorites', () => {
|
|
||||||
const fav = {
|
|
||||||
uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00',
|
|
||||||
is_post_verb: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
const mastoFav = {
|
|
||||||
uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite',
|
|
||||||
is_post_verb: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property(
|
|
||||||
'type',
|
|
||||||
'favorite',
|
|
||||||
)
|
|
||||||
expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property(
|
|
||||||
'type',
|
|
||||||
'favorite',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('processes repeats correctly', () => {
|
|
||||||
const post = makeMockStatusQvitter({
|
|
||||||
retweeted_status: null,
|
|
||||||
id: 'deadbeef',
|
|
||||||
})
|
|
||||||
const repeat = makeMockStatusQvitter({
|
|
||||||
retweeted_status: post,
|
|
||||||
is_post_verb: false,
|
|
||||||
id: 'foobar',
|
|
||||||
})
|
|
||||||
|
|
||||||
const parsedPost = parseStatus(post)
|
|
||||||
const parsedRepeat = parseStatus(repeat)
|
|
||||||
|
|
||||||
expect(parsedPost).to.have.property('type', 'status')
|
|
||||||
expect(parsedRepeat).to.have.property('type', 'retweet')
|
|
||||||
expect(parsedRepeat).to.have.property('retweeted_status')
|
|
||||||
expect(parsedRepeat).to.have.nested.property(
|
|
||||||
'retweeted_status.id',
|
|
||||||
'deadbeef',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sets nsfw for statuses with the #nsfw tag', () => {
|
|
||||||
const safe = makeMockStatusQvitter({ id: '1', text: 'Hello oniichan' })
|
|
||||||
const nsfw = makeMockStatusQvitter({
|
|
||||||
id: '1',
|
|
||||||
text: 'Hello oniichan #nsfw',
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(parseStatus(safe).nsfw).to.eq(false)
|
|
||||||
expect(parseStatus(nsfw).nsfw).to.eq(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('leaves existing nsfw settings alone', () => {
|
|
||||||
const nsfw = makeMockStatusQvitter({
|
|
||||||
id: '1',
|
|
||||||
text: 'Hello oniichan #nsfw',
|
|
||||||
nsfw: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(parseStatus(nsfw).nsfw).to.eq(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Mastoapi preprocessing and converting', () => {
|
describe('Mastoapi preprocessing and converting', () => {
|
||||||
it("doesn't blow up", () => {
|
it("doesn't blow up", () => {
|
||||||
const parsed = mastoapidata.map(parseStatus)
|
const parsed = mastoapidata.map(parseStatus)
|
||||||
|
|
@ -344,60 +189,6 @@ describe('API Entities normalizer', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI
|
|
||||||
// is more of an afterthought
|
|
||||||
describe('parseNotifications (QvitterAPI)', () => {
|
|
||||||
it("correctly normalizes data to FE's format", () => {
|
|
||||||
const notif = makeMockNotificationQvitter({
|
|
||||||
id: 123,
|
|
||||||
notice: makeMockStatusQvitter({ id: 444 }),
|
|
||||||
from_profile: makeMockUserQvitter({ id: 'spurdo' }),
|
|
||||||
})
|
|
||||||
expect(parseNotification(notif)).to.have.property('id', 123)
|
|
||||||
expect(parseNotification(notif)).to.have.property('seen', false)
|
|
||||||
expect(parseNotification(notif)).to.have.nested.property(
|
|
||||||
'status.id',
|
|
||||||
'444',
|
|
||||||
)
|
|
||||||
expect(parseNotification(notif)).to.have.nested.property(
|
|
||||||
'action.id',
|
|
||||||
'444',
|
|
||||||
)
|
|
||||||
expect(parseNotification(notif)).to.have.nested.property(
|
|
||||||
'from_profile.id',
|
|
||||||
'spurdo',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('correctly normalizes favorite notifications', () => {
|
|
||||||
const notif = makeMockNotificationQvitter({
|
|
||||||
id: 123,
|
|
||||||
ntype: 'like',
|
|
||||||
notice: makeMockStatusQvitter({
|
|
||||||
id: 444,
|
|
||||||
favorited_status: makeMockStatusQvitter({ id: 4412 }),
|
|
||||||
}),
|
|
||||||
is_seen: 1,
|
|
||||||
from_profile: makeMockUserQvitter({ id: 'spurdo' }),
|
|
||||||
})
|
|
||||||
expect(parseNotification(notif)).to.have.property('id', 123)
|
|
||||||
expect(parseNotification(notif)).to.have.property('type', 'like')
|
|
||||||
expect(parseNotification(notif)).to.have.property('seen', true)
|
|
||||||
expect(parseNotification(notif)).to.have.nested.property(
|
|
||||||
'status.id',
|
|
||||||
'4412',
|
|
||||||
)
|
|
||||||
expect(parseNotification(notif)).to.have.nested.property(
|
|
||||||
'action.id',
|
|
||||||
'444',
|
|
||||||
)
|
|
||||||
expect(parseNotification(notif)).to.have.nested.property(
|
|
||||||
'from_profile.id',
|
|
||||||
'spurdo',
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Link header pagination', () => {
|
describe('Link header pagination', () => {
|
||||||
it('Parses min and max ids as integers', () => {
|
it('Parses min and max ids as integers', () => {
|
||||||
const linkHeader =
|
const linkHeader =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue