forked from AkkomaGang/akkoma-fe
Merge pull request 'develop update' (#3) from AkkomaGang/pleroma-fe:develop into develop
Reviewed-on: #3
This commit is contained in:
commit
8693a39477
53 changed files with 1595 additions and 94 deletions
|
@ -5,6 +5,7 @@ import FeaturesPanel from './components/features_panel/features_panel.vue'
|
|||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import 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,
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
<EditStatusModal v-if="editingAvailable" />
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<ModModal />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -148,6 +148,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('showWiderShortcuts')
|
||||
copyInstanceOption('showNavShortcuts')
|
||||
copyInstanceOption('showPanelNavShortcuts')
|
||||
copyInstanceOption('stopGifs')
|
||||
copyInstanceOption('logo')
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
|
@ -396,6 +397,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
store.dispatch('startFetchingAnnouncements')
|
||||
store.dispatch('startFetchingReports')
|
||||
getTOS({ store })
|
||||
getStickers({ store })
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
display: flex;
|
||||
padding-top: 0.5em;
|
||||
z-index: 1;
|
||||
max-height: 50%;
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
|
@ -36,7 +37,7 @@
|
|||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
&.-static {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -98,6 +100,9 @@ export default {
|
|||
privateMode () { return this.$store.state.instance.private },
|
||||
shouldConfirmLogout () {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
},
|
||||
showBubbleTimeline () {
|
||||
return this.$store.state.instance.localBubbleInstances.length > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -109,6 +114,9 @@ export default {
|
|||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
openModModal () {
|
||||
this.$store.dispatch('openModModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="currentUser"
|
||||
v-if="currentUser && showBubbleTimeline"
|
||||
:to="{ name: 'bubble-timeline' }"
|
||||
class="nav-icon"
|
||||
>
|
||||
|
@ -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"
|
||||
|
|
|
@ -178,7 +178,7 @@ const EmojiInput = {
|
|||
textAtCaret: async function (newWord) {
|
||||
const firstchar = newWord.charAt(0)
|
||||
this.suggestions = []
|
||||
if (newWord === firstchar) return
|
||||
if (newWord === firstchar && firstchar !== '$') return
|
||||
const matchedSuggestions = await this.suggest(newWord)
|
||||
// Async: cancel if textAtCaret has changed during wait
|
||||
if (this.textAtCaret !== newWord) return
|
||||
|
@ -277,7 +277,6 @@ const EmojiInput = {
|
|||
},
|
||||
replaceText (e, suggestion) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (this.textAtCaret.length === 1) { return }
|
||||
if (len > 0 || suggestion) {
|
||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
||||
const replacement = chosenSuggestion.replacement
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
:class="{ highlighted: index === highlighted }"
|
||||
@click.stop.prevent="onClick($event, suggestion)"
|
||||
>
|
||||
<span class="image">
|
||||
<span v-if="!suggestion.mfm" class="image">
|
||||
<img
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
|
||||
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
|
||||
|
||||
/**
|
||||
* suggest - generates a suggestor function to be used by emoji-input
|
||||
* data: object providing source information for specific types of suggestions:
|
||||
|
@ -21,6 +24,10 @@ export default data => {
|
|||
if (firstChar === '@' && usersCurry) {
|
||||
return usersCurry(input)
|
||||
}
|
||||
if (firstChar === '$') {
|
||||
return MFM_TAGS
|
||||
.filter(({ replacement }) => replacement.toLowerCase().indexOf(input) !== -1)
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.Notification {
|
||||
.emoji-picker {
|
||||
min-width: 160%;
|
||||
width: 150%;
|
||||
overflow: hidden;
|
||||
left: -70%;
|
||||
max-width: 100%;
|
||||
@media (min-width: 800px) and (max-width: 1300px) {
|
||||
left: -50%;
|
||||
min-width: 50%;
|
||||
max-width: 130%;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
left: -10%;
|
||||
min-width: 50%;
|
||||
max-width: 130%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.emoji-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.picked-reaction {
|
||||
.button-default.picked-reaction {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
|
|
58
src/components/mod_modal/mod_modal.js
Normal file
58
src/components/mod_modal/mod_modal.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import Modal from 'src/components/modal/modal.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
faChevronDown
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faWindowMinimize
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
faWindowMinimize,
|
||||
faChevronDown
|
||||
)
|
||||
|
||||
const ModModal = {
|
||||
components: {
|
||||
Modal,
|
||||
ModModalContent: getResettableAsyncComponent(
|
||||
() => import('./mod_modal_content.vue'),
|
||||
{
|
||||
loadingComponent: PanelLoading,
|
||||
errorComponent: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeModModal')
|
||||
},
|
||||
peekModal () {
|
||||
this.$store.dispatch('togglePeekModModal')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
moderator () {
|
||||
return this.$store.state.users.currentUser &&
|
||||
(this.$store.state.users.currentUser.role === 'admin' ||
|
||||
this.$store.state.users.currentUser.role === 'moderator')
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.interface.modModalState !== 'hidden'
|
||||
},
|
||||
modalOpenedOnce () {
|
||||
return this.$store.state.interface.modModalLoaded
|
||||
},
|
||||
modalPeeked () {
|
||||
return this.$store.state.interface.modModalState === 'minimized'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ModModal
|
44
src/components/mod_modal/mod_modal.scss
Normal file
44
src/components/mod_modal/mod_modal.scss
Normal file
|
@ -0,0 +1,44 @@
|
|||
@import 'src/_variables.scss';
|
||||
.mod-modal {
|
||||
overflow: hidden;
|
||||
|
||||
&.peek {
|
||||
.mod-modal-panel {
|
||||
/* Explanation:
|
||||
* Modal is positioned vertically centered.
|
||||
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
|
||||
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
|
||||
* + 100% - we move modal completely off-screen, it's top boundary touches
|
||||
* bottom of the screen
|
||||
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
|
||||
*/
|
||||
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
/* For mobile, the modal takes 100% of the available screen.
|
||||
This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
|
||||
*/
|
||||
transform: translateY(calc(100% - 50px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mod-modal-panel {
|
||||
overflow: hidden;
|
||||
transition: transform;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 300ms;
|
||||
width: 1000px;
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
max-width: 100vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
43
src/components/mod_modal/mod_modal.vue
Normal file
43
src/components/mod_modal/mod_modal.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<Modal
|
||||
v-if="moderator"
|
||||
:is-open="modalActivated"
|
||||
class="mod-modal"
|
||||
:class="{ peek: modalPeeked }"
|
||||
:no-background="modalPeeked"
|
||||
>
|
||||
<div class="mod-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
<span class="title">
|
||||
{{ $t('moderation.moderation') }}
|
||||
</span>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:title="$t('general.peek')"
|
||||
@click="peekModal"
|
||||
>
|
||||
<FAIcon
|
||||
:icon="['far', 'window-minimize']"
|
||||
fixed-width
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:title="$t('general.close')"
|
||||
@click="closeModal"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
fixed-width
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ModModalContent v-if="modalOpenedOnce" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script src="./mod_modal.js"></script>
|
||||
<style src="./mod_modal.scss" lang="scss"></style>
|
63
src/components/mod_modal/mod_modal_content.js
Normal file
63
src/components/mod_modal/mod_modal_content.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
|
||||
import ReportsTab from './tabs/reports_tab/reports_tab.vue'
|
||||
// import StatusesTab from './tabs/statuses_tab.vue'
|
||||
// import UsersTab from './tabs/users_tab.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faFlag,
|
||||
faMessage,
|
||||
faUsers
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faFlag,
|
||||
faMessage,
|
||||
faUsers
|
||||
)
|
||||
|
||||
const ModModalContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
|
||||
ReportsTab
|
||||
// StatusesTab,
|
||||
// UsersTab
|
||||
},
|
||||
computed: {
|
||||
open () {
|
||||
return this.$store.state.interface.modModalState !== 'hidden'
|
||||
},
|
||||
bodyLock () {
|
||||
return this.$store.state.interface.modModalState === 'visible'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen () {
|
||||
const targetTab = this.$store.state.interface.modModalTargetTab
|
||||
// We're being told to open in specific tab
|
||||
if (targetTab) {
|
||||
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
|
||||
return elm.props && elm.props['data-tab-name'] === targetTab
|
||||
})
|
||||
if (tabIndex >= 0) {
|
||||
this.$refs.tabSwitcher.setTab(tabIndex)
|
||||
}
|
||||
}
|
||||
// Clear the state of target tab, so that next time moderation is opened
|
||||
// it doesn't force it.
|
||||
this.$store.dispatch('clearModModalTargetTab')
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.onOpen()
|
||||
},
|
||||
watch: {
|
||||
open: function (value) {
|
||||
if (value) this.onOpen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ModModalContent
|
21
src/components/mod_modal/mod_modal_content.scss
Normal file
21
src/components/mod_modal/mod_modal_content.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
@import 'src/_variables.scss';
|
||||
.mod_tab-switcher {
|
||||
height: 100%;
|
||||
|
||||
.content {
|
||||
margin: 1em 1em 1.4em;
|
||||
|
||||
> div {
|
||||
margin-bottom: .5em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
20
src/components/mod_modal/mod_modal_content.vue
Normal file
20
src/components/mod_modal/mod_modal_content.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="mod_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
:body-scroll-lock="bodyLock"
|
||||
>
|
||||
<div
|
||||
:label="$t('moderation.reports.reports')"
|
||||
icon="flag"
|
||||
data-tab-name="reports"
|
||||
>
|
||||
<ReportsTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./mod_modal_content.js"></script>
|
||||
<style src="./mod_modal_content.scss" lang="scss"></style>
|
124
src/components/mod_modal/tabs/reports_tab/report_card.js
Normal file
124
src/components/mod_modal/tabs/reports_tab/report_card.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
import Popover from 'src/components/popover/popover.vue'
|
||||
import Status from 'src/components/status/status.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import ReportNote from './report_note.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronUp
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faChevronDown,
|
||||
faChevronUp
|
||||
)
|
||||
|
||||
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||
const STRIP_MEDIA = 'mrf_tag:media-strip'
|
||||
const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
|
||||
const SANDBOX = 'mrf_tag:sandbox'
|
||||
|
||||
const ReportCard = {
|
||||
data () {
|
||||
return {
|
||||
hidden: true,
|
||||
statusesHidden: true,
|
||||
notesHidden: true,
|
||||
note: null,
|
||||
tags: {
|
||||
FORCE_NSFW,
|
||||
STRIP_MEDIA,
|
||||
FORCE_UNLISTED,
|
||||
SANDBOX
|
||||
}
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'account',
|
||||
'actor',
|
||||
'content',
|
||||
'id',
|
||||
'notes',
|
||||
'state',
|
||||
'statuses'
|
||||
],
|
||||
components: {
|
||||
ReportNote,
|
||||
Popover,
|
||||
Status,
|
||||
UserAvatar
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchUser', this.account.id)
|
||||
},
|
||||
computed: {
|
||||
isOpen () {
|
||||
return this.state === 'open'
|
||||
},
|
||||
tagPolicyEnabled () {
|
||||
return this.$store.state.instance.federationPolicy.mrf_policies.includes('TagPolicy')
|
||||
},
|
||||
user () {
|
||||
return this.$store.getters.findUser(this.account.id)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleHidden () {
|
||||
this.hidden = !this.hidden
|
||||
},
|
||||
decode (content) {
|
||||
content = content.replaceAll('<br/>', '\n')
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.innerHTML = content
|
||||
return textarea.value
|
||||
},
|
||||
updateReportState (state) {
|
||||
this.$store.dispatch('updateReportStates', { reports: [{ id: this.id, state }] })
|
||||
},
|
||||
toggleNotes () {
|
||||
this.notesHidden = !this.notesHidden
|
||||
},
|
||||
addNoteToReport () {
|
||||
if (this.note.length > 0) {
|
||||
this.$store.dispatch('addNoteToReport', { id: this.id, note: this.note })
|
||||
this.note = null
|
||||
}
|
||||
},
|
||||
toggleStatuses () {
|
||||
this.statusesHidden = !this.statusesHidden
|
||||
},
|
||||
hasTag (tag) {
|
||||
return this.user.tags.includes(tag)
|
||||
},
|
||||
toggleTag (tag) {
|
||||
if (this.hasTag(tag)) {
|
||||
this.$store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
this.$store.commit('untagUser', { user: this.user, tag })
|
||||
})
|
||||
} else {
|
||||
this.$store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
this.$store.commit('tagUser', { user: this.user, tag })
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleActivationStatus () {
|
||||
this.$store.dispatch('toggleActivationStatus', { user: this.user })
|
||||
},
|
||||
deleteUser () {
|
||||
this.$store.state.backendInteractor.deleteUser({ user: this.user })
|
||||
.then(e => {
|
||||
this.$store.dispatch('markStatusesAsDeleted', status => this.user.id === status.user.id)
|
||||
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
|
||||
const isTargetUser = this.$route.params.name === this.user.name || this.$route.params.id === this.user.id
|
||||
if (isProfile && isTargetUser) {
|
||||
window.history.back()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ReportCard
|
202
src/components/mod_modal/tabs/reports_tab/report_card.vue
Normal file
202
src/components/mod_modal/tabs/reports_tab/report_card.vue
Normal file
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<div class="report-card panel">
|
||||
<div
|
||||
class="panel-heading"
|
||||
@click="toggleHidden"
|
||||
>
|
||||
<h4>{{ $t('moderation.reports.report') + ' ' + this.account.screen_name }}</h4>
|
||||
<button
|
||||
v-if="isOpen"
|
||||
class="button-default"
|
||||
@click.stop="updateReportState('closed')"
|
||||
>
|
||||
{{ $t('moderation.reports.close') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="isOpen"
|
||||
class="button-default"
|
||||
@click.stop="updateReportState('resolved')"
|
||||
>
|
||||
{{ $t('moderation.reports.resolve') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="button-default"
|
||||
@click.stop="updateReportState('open')"
|
||||
>
|
||||
{{ $t('moderation.reports.reopen') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hidden"
|
||||
class="panel-body report-body"
|
||||
>
|
||||
<div class="report-content">
|
||||
<div v-if="content">
|
||||
{{ decode(content) }}
|
||||
</div>
|
||||
<i v-else class="faint">
|
||||
{{ $t('moderation.reports.no_content') }}
|
||||
</i>
|
||||
<div class="report-author">
|
||||
<UserAvatar
|
||||
class="small-avatar"
|
||||
:user="actor"
|
||||
/>
|
||||
{{ this.actor.screen_name }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown"
|
||||
v-if="!hidden && this.statuses.length > 0"
|
||||
>
|
||||
<button
|
||||
class="button button-unstyled dropdown-header"
|
||||
@click="toggleStatuses"
|
||||
>
|
||||
{{ $tc('moderation.reports.statuses', statuses.length - 1, { count: statuses.length }) }}
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="statusesHidden ? 'chevron-down' : 'chevron-up'"
|
||||
/>
|
||||
</button>
|
||||
<div v-if="!statusesHidden">
|
||||
<Status
|
||||
v-for="status in statuses"
|
||||
:key="status.id"
|
||||
:collapsable="false"
|
||||
:expandable="false"
|
||||
:compact="false"
|
||||
:statusoid="status"
|
||||
:no-heading="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown"
|
||||
v-if="!hidden && this.notes.length > 0"
|
||||
>
|
||||
<button
|
||||
class="button button-unstyled dropdown-header"
|
||||
@click="toggleNotes"
|
||||
>
|
||||
{{ $tc('moderation.reports.notes', notes.length - 1, { count: notes.length }) }}
|
||||
<FAIcon
|
||||
class="timelines-chevron"
|
||||
fixed-width
|
||||
:icon="notesHidden ? 'chevron-down' : 'chevron-up'"
|
||||
/>
|
||||
</button>
|
||||
<div v-if="!notesHidden">
|
||||
<ReportNote
|
||||
v-for="note in notes"
|
||||
:key="note.id"
|
||||
:report_id="id"
|
||||
v-bind="note"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-add-note">
|
||||
<textarea
|
||||
rows="1"
|
||||
cols="1"
|
||||
v-model.trim="note"
|
||||
:placeholder="$t('moderation.reports.note_placeholder')"
|
||||
/>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.stop="addNoteToReport"
|
||||
>
|
||||
{{ $t('moderation.reports.add_note') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hidden"
|
||||
class="panel-footer"
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.stop="toggleActivationStatus"
|
||||
>
|
||||
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.stop="deleteUser"
|
||||
>
|
||||
{{ $t('user_card.admin_menu.delete_account') }}
|
||||
</button>
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
:offset="{ y: 5 }"
|
||||
remove-padding
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="!tagPolicyEnabled"
|
||||
:title="tagPolicyEnabled ? '' : $t('moderation.reports.account.tag_policy_notice')"
|
||||
>
|
||||
<span>{{ $t("moderation.reports.tags") }}</span>
|
||||
{{ ' ' }}
|
||||
<FAIcon
|
||||
icon="chevron-down"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<template v-slot:content="{close}">
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
:disabled="!tagPolicyEnabled"
|
||||
>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="toggleTag(tags.FORCE_NSFW)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
|
||||
/>
|
||||
{{ $t('user_card.admin_menu.force_nsfw') }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="toggleTag(tags.STRIP_MEDIA)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
|
||||
/>
|
||||
{{ $t('user_card.admin_menu.strip_media') }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="toggleTag(tags.FORCE_UNLISTED)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
|
||||
/>
|
||||
{{ $t('user_card.admin_menu.force_unlisted') }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="toggleTag(tags.SANDBOX)"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
|
||||
/>
|
||||
{{ $t('user_card.admin_menu.sandbox') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./report_card.js"></script>
|
37
src/components/mod_modal/tabs/reports_tab/report_note.js
Normal file
37
src/components/mod_modal/tabs/reports_tab/report_note.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
import Timeago from 'src/components/timeago/timeago.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
|
||||
const ReportNote = {
|
||||
data () {
|
||||
return {
|
||||
showingDeleteDialog: false
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'content',
|
||||
'created_at',
|
||||
'user',
|
||||
'report_id',
|
||||
'id'
|
||||
],
|
||||
components: {
|
||||
ConfirmModal,
|
||||
Timeago,
|
||||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
deleteNoteFromReport () {
|
||||
this.$store.dispatch('deleteNoteFromReport', { id: this.report_id, note: this.id })
|
||||
this.showingDeleteDialog = false
|
||||
},
|
||||
showDeleteDialog () {
|
||||
this.showingDeleteDialog = true
|
||||
},
|
||||
hideDeleteDialog () {
|
||||
this.showingDeleteDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ReportNote
|
43
src/components/mod_modal/tabs/reports_tab/report_note.vue
Normal file
43
src/components/mod_modal/tabs/reports_tab/report_note.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="report-note">
|
||||
<div class="note-header">
|
||||
<div class="note-author">
|
||||
<UserAvatar
|
||||
class="small-avatar"
|
||||
:user="user"
|
||||
/>
|
||||
{{ this.user.screen_name }}
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<Timeago
|
||||
class="faint"
|
||||
:time="created_at"
|
||||
:auto-update="60"
|
||||
:long-format="true"
|
||||
:with-direction="true"
|
||||
/>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.stop="showDeleteDialog"
|
||||
>
|
||||
{{ $t('moderation.reports.delete_note') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note-content">
|
||||
{{ content }}
|
||||
</div>
|
||||
<confirm-modal
|
||||
v-if="showingDeleteDialog"
|
||||
:title="$t('moderation.reports.delete_note_title')"
|
||||
:confirm-text="$t('moderation.reports.delete_note_accept')"
|
||||
:cancel-text="$t('moderation.reports.delete_note_cancel')"
|
||||
@accepted="deleteNoteFromReport"
|
||||
@cancelled="hideDeleteDialog"
|
||||
>
|
||||
{{ $t('moderation.reports.delete_note_confirm') }}
|
||||
</confirm-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./report_note.js"></script>
|
26
src/components/mod_modal/tabs/reports_tab/reports_tab.js
Normal file
26
src/components/mod_modal/tabs/reports_tab/reports_tab.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { filter } from 'lodash'
|
||||
|
||||
import ReportCard from './report_card.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const ReportsTab = {
|
||||
data () {
|
||||
return {
|
||||
showClosed: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
ReportCard
|
||||
},
|
||||
computed: {
|
||||
reports () {
|
||||
return this.$store.state.reports.reports
|
||||
},
|
||||
openReports () {
|
||||
return filter(this.reports, { state: 'open' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ReportsTab
|
83
src/components/mod_modal/tabs/reports_tab/reports_tab.scss
Normal file
83
src/components/mod_modal/tabs/reports_tab/reports_tab.scss
Normal file
|
@ -0,0 +1,83 @@
|
|||
@import '../../../../_variables.scss';
|
||||
.report-card {
|
||||
.report-body {
|
||||
& > * {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
& > :not(:last-child) {
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: var(--border, #222);
|
||||
}
|
||||
|
||||
.report-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.report-author {
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
.small-avatar {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
padding-right: 0.4em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
|
||||
.dropdown-header {
|
||||
padding: 1em;
|
||||
color: var(--link, $fallback--link);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--selectedMenu, $fallback--lightBg);
|
||||
color: var(--selectedMenuText, $fallback--link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-note {
|
||||
padding: 1em;
|
||||
|
||||
.note-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.report-add-note {
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 2em;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
display: flex;
|
||||
& > * {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reports-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
25
src/components/mod_modal/tabs/reports_tab/reports_tab.vue
Normal file
25
src/components/mod_modal/tabs/reports_tab/reports_tab.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div :label="$t('moderation.reports.reports')">
|
||||
<div class="content">
|
||||
<div class="reports-header">
|
||||
<h2>{{ $t('moderation.reports.reports') }}</h2>
|
||||
<Checkbox v-model="showClosed">
|
||||
{{ $t('moderation.reports.show_closed') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="reports">
|
||||
<div v-if="(openReports.length === 0 && !showClosed) || reports.length === 0">
|
||||
<p>{{ $t('moderation.reports.no_reports') }}</p>
|
||||
</div>
|
||||
<ReportCard
|
||||
v-for="report in (showClosed ? reports : openReports)"
|
||||
:key="report.id"
|
||||
v-bind="report"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./reports_tab.js"></script>
|
||||
<style src="./reports_tab.scss" lang="scss"></style>
|
|
@ -148,7 +148,7 @@ const PostStatusForm = {
|
|||
spoilerText: this.subject || '',
|
||||
status: this.statusText || '',
|
||||
sensitiveIfSubject,
|
||||
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
|
||||
nsfw: this.statusIsSensitive || (sensitiveIfSubject && this.subject) || !!sensitiveByDefault,
|
||||
files: this.statusFiles || [],
|
||||
poll: this.statusPoll || {},
|
||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||
|
@ -418,7 +418,7 @@ const PostStatusForm = {
|
|||
addMediaFile (fileInfo) {
|
||||
this.newStatus.files.push(fileInfo)
|
||||
|
||||
if (this.newStatus.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
|
||||
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
|
||||
this.newStatus.nsfw = true
|
||||
}
|
||||
this.$emit('resize', { delayed: true })
|
||||
|
@ -498,7 +498,7 @@ const PostStatusForm = {
|
|||
})
|
||||
},
|
||||
onSubjectInput (e) {
|
||||
if (this.newStatus.sensitiveIfSubject) {
|
||||
if (this.$store.getters.mergedConfig.sensitiveIfSubject) {
|
||||
this.newStatus.nsfw = true
|
||||
}
|
||||
},
|
||||
|
|
|
@ -130,11 +130,11 @@ export default {
|
|||
codeblocks.forEach((pre) => {
|
||||
content = content.replace(pre,
|
||||
pre.replaceAll('<br/>', '\n')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ const SearchBar = {
|
|||
this.$emit('toggled', this.hidden)
|
||||
this.$nextTick(() => {
|
||||
if (!this.hidden) {
|
||||
this.searchTerm = undefined
|
||||
this.$refs.searchInput.focus()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { filter, trim } from 'lodash'
|
||||
import { filter, trim, debounce } from 'lodash'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
|
@ -27,13 +27,13 @@ const FilteringTab = {
|
|||
get () {
|
||||
return this.muteWordsStringLocal
|
||||
},
|
||||
set (value) {
|
||||
set: debounce(function (value) {
|
||||
this.muteWordsStringLocal = value
|
||||
this.$store.dispatch('setOption', {
|
||||
name: 'muteWords',
|
||||
value: filter(value.split('\n'), (word) => trim(word).length > 0)
|
||||
})
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
// Updating nested properties
|
||||
|
|
|
@ -105,8 +105,12 @@ const GeneralTab = {
|
|||
return this.$store.getters.mergedConfig.profileVersion
|
||||
},
|
||||
translationLanguages () {
|
||||
const langs = this.$store.state.instance.translationLanguages || []
|
||||
return (langs || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
|
||||
const langs = this.$store.state.instance.supportedTranslationLanguages
|
||||
if (langs && langs.source) {
|
||||
return langs.source.map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
translationLanguage: {
|
||||
get: function () { return this.$store.getters.mergedConfig.translationLanguage },
|
||||
|
|
|
@ -151,7 +151,7 @@ const ProfileTab = {
|
|||
return false
|
||||
},
|
||||
deleteField (index, event) {
|
||||
this.$delete(this.newFields, index)
|
||||
this.newFields.splice(index, 1)
|
||||
},
|
||||
uploadFile (slot, e) {
|
||||
const file = e.target.files[0]
|
||||
|
|
|
@ -15,7 +15,8 @@ import {
|
|||
faTachometerAlt,
|
||||
faCog,
|
||||
faInfoCircle,
|
||||
faList
|
||||
faList,
|
||||
faUserTie
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
|
@ -30,7 +31,8 @@ library.add(
|
|||
faTachometerAlt,
|
||||
faCog,
|
||||
faInfoCircle,
|
||||
faList
|
||||
faList,
|
||||
faUserTie
|
||||
)
|
||||
|
||||
const SideDrawer = {
|
||||
|
@ -102,6 +104,9 @@ const SideDrawer = {
|
|||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
openModModal () {
|
||||
this.$store.dispatch('openModModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,6 +143,21 @@
|
|||
/> {{ $t("nav.about") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled -link -fullwidth"
|
||||
@click="openModModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="user-tie"
|
||||
/> {{ $t("nav.moderation") }}
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
@click="toggleDrawer"
|
||||
|
|
|
@ -460,6 +460,16 @@ const Status = {
|
|||
return 'globe'
|
||||
}
|
||||
},
|
||||
faviconAlt (status) {
|
||||
if (!status.user.instance) {
|
||||
return ''
|
||||
}
|
||||
const software = ((status.user.instance) && (status.user.instance.nodeinfo) && (status.user.instance.nodeinfo.software)) || {}
|
||||
if (software.name) {
|
||||
return `${status.user.instance.name} (${software.name || ''} ${software.version || ''})`
|
||||
}
|
||||
return ''
|
||||
},
|
||||
showError (error) {
|
||||
this.error = error
|
||||
},
|
||||
|
|
|
@ -177,6 +177,7 @@
|
|||
v-if="!!(status.user && status.user.favicon)"
|
||||
class="status-favicon"
|
||||
:src="status.user.favicon"
|
||||
:title="faviconAlt(status)"
|
||||
>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="StatusBody"
|
||||
:class="{ '-compact': compact }"
|
||||
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }"
|
||||
>
|
||||
<div class="body">
|
||||
<div
|
||||
|
|
|
@ -82,9 +82,16 @@
|
|||
}
|
||||
}
|
||||
&.mfm-disabled {
|
||||
span {
|
||||
font-size: 100% !important;
|
||||
}
|
||||
.mfm {
|
||||
animation: none !important;
|
||||
}
|
||||
.emoji {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -235,7 +235,7 @@
|
|||
line-height: 22px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.following {
|
||||
.following, .requested_by {
|
||||
flex: 1 0 auto;
|
||||
margin: 0;
|
||||
margin-bottom: .25em;
|
||||
|
|
|
@ -122,6 +122,12 @@
|
|||
>
|
||||
{{ $t('user_card.follows_you') }}
|
||||
</div>
|
||||