From 15bac1e4019fb3f05a643af3890a6e3f627e11d8 Mon Sep 17 00:00:00 2001 From: sfr Date: Sun, 6 Nov 2022 21:26:05 +0000 Subject: [PATCH] Add reports management (#186) implements part of #178, other parts will come later Co-authored-by: Sol Fisher Romanoff Reviewed-on: https://akkoma.dev/AkkomaGang/pleroma-fe/pulls/186 Co-authored-by: sfr Co-committed-by: sfr --- src/App.js | 2 + src/App.vue | 1 + src/boot/after_store.js | 1 + src/components/desktop_nav/desktop_nav.js | 9 +- src/components/desktop_nav/desktop_nav.vue | 12 ++ src/components/mod_modal/mod_modal.js | 58 +++++ src/components/mod_modal/mod_modal.scss | 44 ++++ src/components/mod_modal/mod_modal.vue | 43 ++++ src/components/mod_modal/mod_modal_content.js | 63 ++++++ .../mod_modal/mod_modal_content.scss | 21 ++ .../mod_modal/mod_modal_content.vue | 20 ++ .../mod_modal/tabs/reports_tab/report_card.js | 124 +++++++++++ .../tabs/reports_tab/report_card.vue | 202 ++++++++++++++++++ .../mod_modal/tabs/reports_tab/report_note.js | 37 ++++ .../tabs/reports_tab/report_note.vue | 43 ++++ .../mod_modal/tabs/reports_tab/reports_tab.js | 26 +++ .../tabs/reports_tab/reports_tab.scss | 83 +++++++ .../tabs/reports_tab/reports_tab.vue | 20 ++ src/components/side_drawer/side_drawer.js | 9 +- src/components/side_drawer/side_drawer.vue | 15 ++ src/components/tab_switcher/tab_switcher.jsx | 6 +- src/i18n/en.json | 26 +++ src/modules/api.js | 14 ++ src/modules/interface.js | 39 ++++ src/modules/reports.js | 58 ++++- src/services/api/api.service.js | 64 +++++- .../backend_interactor_service.js | 5 + .../entity_normalizer.service.js | 18 ++ .../reports_fetcher.service.js | 20 ++ yarn.lock | 31 +-- 30 files changed, 1082 insertions(+), 32 deletions(-) create mode 100644 src/components/mod_modal/mod_modal.js create mode 100644 src/components/mod_modal/mod_modal.scss create mode 100644 src/components/mod_modal/mod_modal.vue create mode 100644 src/components/mod_modal/mod_modal_content.js create mode 100644 src/components/mod_modal/mod_modal_content.scss create mode 100644 src/components/mod_modal/mod_modal_content.vue create mode 100644 src/components/mod_modal/tabs/reports_tab/report_card.js create mode 100644 src/components/mod_modal/tabs/reports_tab/report_card.vue create mode 100644 src/components/mod_modal/tabs/reports_tab/report_note.js create mode 100644 src/components/mod_modal/tabs/reports_tab/report_note.vue create mode 100644 src/components/mod_modal/tabs/reports_tab/reports_tab.js create mode 100644 src/components/mod_modal/tabs/reports_tab/reports_tab.scss create mode 100644 src/components/mod_modal/tabs/reports_tab/reports_tab.vue create mode 100644 src/services/reports_fetcher/reports_fetcher.service.js diff --git a/src/App.js b/src/App.js index 3690b944..d4b3b41a 100644 --- a/src/App.js +++ b/src/App.js @@ -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, diff --git a/src/App.vue b/src/App.vue index ca114c89..80ebb525 100644 --- a/src/App.vue +++ b/src/App.vue @@ -61,6 +61,7 @@ + diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 41c7b73e..986cd356 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -397,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 }) diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index 9ba5abc4..78e93f0e 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -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') } } } diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 9dc02e68..0c592326 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -151,6 +151,18 @@ :title="$t('nav.preferences')" /> + 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 diff --git a/src/components/mod_modal/mod_modal.scss b/src/components/mod_modal/mod_modal.scss new file mode 100644 index 00000000..4821df74 --- /dev/null +++ b/src/components/mod_modal/mod_modal.scss @@ -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; + } + } +} diff --git a/src/components/mod_modal/mod_modal.vue b/src/components/mod_modal/mod_modal.vue new file mode 100644 index 00000000..64bbf021 --- /dev/null +++ b/src/components/mod_modal/mod_modal.vue @@ -0,0 +1,43 @@ + + + + diff --git a/src/components/mod_modal/mod_modal_content.js b/src/components/mod_modal/mod_modal_content.js new file mode 100644 index 00000000..e0ba6259 --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.js @@ -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 diff --git a/src/components/mod_modal/mod_modal_content.scss b/src/components/mod_modal/mod_modal_content.scss new file mode 100644 index 00000000..b1aeba38 --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.scss @@ -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; + } + } +} diff --git a/src/components/mod_modal/mod_modal_content.vue b/src/components/mod_modal/mod_modal_content.vue new file mode 100644 index 00000000..6fa32be1 --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.vue @@ -0,0 +1,20 @@ + + + + diff --git a/src/components/mod_modal/tabs/reports_tab/report_card.js b/src/components/mod_modal/tabs/reports_tab/report_card.js new file mode 100644 index 00000000..6e6bfdae --- /dev/null +++ b/src/components/mod_modal/tabs/reports_tab/report_card.js @@ -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('
', '\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 diff --git a/src/components/mod_modal/tabs/reports_tab/report_card.vue b/src/components/mod_modal/tabs/reports_tab/report_card.vue new file mode 100644 index 00000000..6cc034b1 --- /dev/null +++ b/src/components/mod_modal/tabs/reports_tab/report_card.vue @@ -0,0 +1,202 @@ +