Add moderation modal

This commit is contained in:
Sol Fisher Romanoff 2022-10-20 12:59:00 +03:00
parent 2e00c19074
commit 4fe11ee378
Signed by untrusted user who does not match committer: nbsp
GPG Key ID: 9D3F2B64F2341B62
14 changed files with 396 additions and 25 deletions

View File

@ -5,6 +5,7 @@ import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue'
import ModModal from './components/mod_modal/mod_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue'
@ -33,6 +34,7 @@ export default {
MobileNav,
DesktopNav,
SettingsModal,
ModModal,
UserReportingModal,
PostStatusModal,
EditStatusModal,

View File

@ -61,6 +61,7 @@
<EditStatusModal v-if="editingAvailable" />
<StatusHistoryModal v-if="editingAvailable" />
<SettingsModal />
<ModModal />
<GlobalNoticeList />
</div>
</template>

View File

@ -16,7 +16,8 @@ import {
faUsers,
faCommentMedical,
faBookmark,
faInfoCircle
faInfoCircle,
faUserTie
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -34,7 +35,8 @@ library.add(
faUsers,
faCommentMedical,
faBookmark,
faInfoCircle
faInfoCircle,
faUserTie
)
export default {
@ -109,6 +111,9 @@ export default {
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
},
openModModal () {
this.$store.dispatch('openModModal')
}
}
}

View File

@ -151,6 +151,18 @@
:title="$t('nav.preferences')"
/>
</button>
<button
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
class="button-unstyled nav-icon"
@click.stop="openModModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="user-tie"
:title="$t('nav.moderation')"
/>
</button>
<a
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"

View File

@ -0,0 +1,58 @@
import Modal from 'src/components/modal/modal.vue'
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faTimes,
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
import {
faWindowMinimize
} from '@fortawesome/free-regular-svg-icons'
library.add(
faTimes,
faWindowMinimize,
faChevronDown
)
const ModModal = {
components: {
Modal,
ModModalContent: getResettableAsyncComponent(
() => import('./mod_modal_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
delay: 0
}
)
},
methods: {
closeModal () {
this.$store.dispatch('closeModModal')
},
peekModal () {
this.$store.dispatch('togglePeekModModal')
}
},
computed: {
moderator () {
return this.$store.state.users.currentUser &&
(this.$store.state.users.currentUser.role === 'admin' ||
this.$store.state.users.currentUser.role === 'moderator')
},
modalActivated () {
return this.$store.state.interface.modModalState !== 'hidden'
},
modalOpenedOnce () {
return this.$store.state.interface.modModalLoaded
},
modalPeeked () {
return this.$store.state.interface.modModalState === 'minimized'
}
}
}
export default ModModal

View File

@ -0,0 +1,63 @@
@import 'src/_variables.scss';
.mod-modal {
overflow: hidden;
.setting-list,
.option-list {
list-style-type: none;
padding-left: 2em;
li {
margin-bottom: 0.5em;
}
.suboptions {
margin-top: 0.3em
}
}
&.peek {
.mod-modal-panel {
/* Explanation:
* Modal is positioned vertically centered.
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
* + 100% - we move modal completely off-screen, it's top boundary touches
* bottom of the screen
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
*/
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
@media all and (max-width: 800px) {
/* For mobile, the modal takes 100% of the available screen.
This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
*/
transform: translateY(calc(100% - 50px));
}
}
}
.mod-modal-panel {
overflow: hidden;
transition: transform;
transition-timing-function: ease-in-out;
transition-duration: 300ms;
width: 1000px;
max-width: 90vw;
height: 90vh;
@media all and (max-width: 800px) {
max-width: 100vw;
height: 100%;
}
>.panel-body {
height: 100%;
overflow-y: hidden;
.btn {
min-height: 2em;
min-width: 10em;
padding: 0 2em;
}
}
}
}

View File

@ -0,0 +1,43 @@
<template>
<Modal
v-if="moderator"
:is-open="modalActivated"
class="mod-modal"
:class="{ peek: modalPeeked }"
:no-background="modalPeeked"
>
<div class="mod-modal-panel panel">
<div class="panel-heading">
<span class="title">
{{ $t('moderation.moderation') }}
</span>
<button
class="btn button-default"
:title="$t('general.peek')"
@click="peekModal"
>
<FAIcon
:icon="['far', 'window-minimize']"
fixed-width
/>
</button>
<button
class="btn button-default"
:title="$t('general.close')"
@click="closeModal"
>
<FAIcon
icon="times"
fixed-width
/>
</button>
</div>
<div class="panel-body">
<ModModalContent v-if="modalOpenedOnce" />
</div>
</div>
</Modal>
</template>
<script src="./mod_modal.js"></script>
<style src="./mod_modal.scss" lang="scss"></style>

View File

@ -0,0 +1,63 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import ReportsTab from './tabs/reports_tab.vue'
import StatusesTab from './tabs/statuses_tab.vue'
import UsersTab from './tabs/users_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFlag,
faMessage,
faUsers
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFlag,
faMessage,
faUsers
)
const ModModalContent = {
components: {
TabSwitcher,
ReportsTab,
StatusesTab,
UsersTab
},
computed: {
open () {
return this.$store.state.interface.modModalState !== 'hidden'
},
bodyLock () {
return this.$store.state.interface.modModalState === 'visible'
}
},
methods: {
onOpen () {
const targetTab = this.$store.state.interface.modModalTargetTab
// We're being told to open in specific tab
if (targetTab) {
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
return elm.props && elm.props['data-tab-name'] === targetTab
})
if (tabIndex >= 0) {
this.$refs.tabSwitcher.setTab(tabIndex)
}
}
// Clear the state of target tab, so that next time moderation is opened
// it doesn't force it.
this.$store.dispatch('clearModModalTargetTab')
}
},
mounted () {
this.onOpen()
},
watch: {
open: function (value) {
if (value) this.onOpen()
}
}
}
export default ModModalContent

View File

@ -0,0 +1,54 @@
@import 'src/_variables.scss';
.mod_tab-switcher {
height: 100%;
.setting-item {
border-bottom: 2px solid var(--fg, $fallback--fg);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div,
> label {
display: block;
margin-bottom: .5em;
&:last-child {
margin-bottom: 0;
}
}
.select-multiple {
display: flex;
.option-list {
margin: 0;
padding-left: .5em;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable svg {
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
.number-input {
max-width: 6em;
}
}
}

View File

@ -0,0 +1,34 @@
<template>
<tab-switcher
ref="tabSwitcher"
class="mod_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:body-scroll-lock="bodyLock"
>
<div
:label="$t('moderation.reports')"
icon="flag"
data-tab-name="reports"
>
<ReportsTab />
</div>
<div
:label="$t('moderation.users')"
icon="users"
data-tab-name="users"
>
<UsersTab />
</div>
<div
:label="$t('moderation.statuses')"
icon="message"
data-tab-name="statuses"
>
<StatusesTab />
</div>
</tab-switcher>
</template>
<script src="./mod_modal_content.js"></script>
<style src="./mod_modal_content.scss" lang="scss"></style>

View File

@ -64,8 +64,12 @@ export default {
settingsModalVisible () {
return this.settingsModalState === 'visible'
},
modModalVisible () {
return this.modModalState === 'visible'
},
...mapState({
settingsModalState: state => state.interface.settingsModalState
settingsModalState: state => state.interface.settingsModalState,
modModalState: state => state.interface.modModalState
})
},
beforeUpdate () {

View File

@ -266,6 +266,12 @@
"next": "Next",
"previous": "Previous"
},
"moderation": {
"moderation": "Moderation",
"reports": "Reports",
"statuses": "Statuses",
"users": "Users"
},
"nav": {
"about": "About",
"administration": "Administration",

View File

@ -2,6 +2,9 @@ const defaultState = {
settingsModalState: 'hidden',
settingsModalLoaded: false,
settingsModalTargetTab: null,
modModalState: 'hidden',
modModalLoaded: false,
modModalTargetTab: null,
settings: {
currentSaveStateNotice: null,
noticeClearTimeout: null,
@ -63,6 +66,30 @@ const interfaceMod = {
setSettingsModalTargetTab (state, value) {
state.settingsModalTargetTab = value
},
closeModModal (state) {
state.modModalState = 'hidden'
},
togglePeekModModal (state) {
switch (state.modModalState) {
case 'minimized':
state.modModalState = 'visible'
return
case 'visible':
state.modModalState = 'minimized'
return
default:
throw new Error('Illegal minimization state of mod modal')
}
},
openModModal (state) {
state.modModalState = 'visible'
if (!state.modModalLoaded) {
state.modModalLoaded = true
}
},
setModModalTargetTab (state, value) {
state.modModalTargetTab = value
},
pushGlobalNotice (state, notice) {
state.globalNotices.push(notice)
},
@ -105,6 +132,18 @@ const interfaceMod = {
commit('setSettingsModalTargetTab', value)
commit('openSettingsModal')
},
closeModModal ({ commit }) {
commit('closeModModal')
},
openModModal ({ commit }) {
commit('openModModal')
},
togglePeekModModal ({ commit }) {
commit('togglePeekModModal')
},
clearModModalTargetTab ({ commit }) {
commit('setModModalTargetTab', null)
},
pushGlobalNotice (
{ commit, dispatch, state },
{

View File

@ -1092,12 +1092,12 @@
"@fortawesome/fontawesome-common-types@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz#949995a05c0d8801be7e0a594f775f1dbaa0d893"
integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w==
"@fortawesome/fontawesome-svg-core@1.3.0":
version "1.3.0"
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz#343fac91fa87daa630d26420bfedfba560f85885"
integrity sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.3.0"
@ -1141,9 +1141,9 @@
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@intlify/bundle-utils@next":
version "3.1.2"
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.1.2.tgz"
integrity sha512-amgSo0NN5OKWYdcgFmfJqo2tcUcZ6C66Bxm5ALQnB0m3MUQtS9aJzKoIo+EU9XQiOVmlBFxRtNoZm+psHa5FNA==
version "3.2.1"
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.2.1.tgz"
integrity sha512-rf4cLBOnbqmpXVcCdcYHilZpMt1m82syh3WLBJlZvGxN2KkH9HeHVH4+bnibF/SDXCHNh6lM6wTpS/qw+PkcMg==
dependencies:
"@intlify/message-compiler" next
"@intlify/shared" next
@ -1168,7 +1168,7 @@
dependencies:
"@intlify/shared" "9.2.2"
"@intlify/message-compiler@9.2.2":
"@intlify/message-compiler@9.2.2", "@intlify/message-compiler@next":
version "9.2.2"
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz"
integrity sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==
@ -1176,24 +1176,11 @@
"@intlify/shared" "9.2.2"
source-map "0.6.1"
"@intlify/message-compiler@next":
version "9.3.0-beta.3"
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.3.0-beta.3.tgz"
integrity sha512-j8OwToBQgs01RBMX4GCDNQfcnmw3AiDG3moKIONTrfXcf+1yt/rWznLTYH/DXbKcFMAFijFpCzMYjUmH1jVFYA==
dependencies:
"@intlify/shared" "9.3.0-beta.3"
source-map "0.6.1"
"@intlify/shared@9.2.2", "@intlify/shared@next":
version "9.2.2"
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz"
integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==
"@intlify/shared@9.3.0-beta.3":
version "9.3.0-beta.3"
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.3.0-beta.3.tgz"
integrity sha512-Z/0TU4GhFKRxKh+0RbwJExik9zz57gXYgxSYaPn7YQdkQ/pabSioCY/SXnYxQHL6HzULF5tmqarFm6glbGqKhw==
"@intlify/vue-devtools@9.2.2":
version "9.2.2"
resolved "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz"
@ -8095,9 +8082,9 @@ mute-stream@0.0.7:
integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==
nan@^2.12.1:
version "2.16.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nanoid@^3.3.4:
version "3.3.4"