From 4fe11ee378398049982b1cccfa21fc994da0b4bb Mon Sep 17 00:00:00 2001 From: Sol Fisher Romanoff Date: Thu, 20 Oct 2022 12:59:00 +0300 Subject: [PATCH 1/5] Add moderation modal --- src/App.js | 2 + src/App.vue | 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 | 63 +++++++++++++++++++ src/components/mod_modal/mod_modal.vue | 43 +++++++++++++ src/components/mod_modal/mod_modal_content.js | 63 +++++++++++++++++++ .../mod_modal/mod_modal_content.scss | 54 ++++++++++++++++ .../mod_modal/mod_modal_content.vue | 34 ++++++++++ src/components/tab_switcher/tab_switcher.jsx | 6 +- src/i18n/en.json | 6 ++ src/modules/interface.js | 39 ++++++++++++ yarn.lock | 31 +++------ 14 files changed, 396 insertions(+), 25 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 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/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..d80205c9 --- /dev/null +++ b/src/components/mod_modal/mod_modal.scss @@ -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; + } + } + } +} 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..6330f162 --- /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.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..99fc2d2a --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.scss @@ -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; + } + } +} 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..bb31bc19 --- /dev/null +++ b/src/components/mod_modal/mod_modal_content.vue @@ -0,0 +1,34 @@ + + + + diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index c8d390bc..84fc14da 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -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 () { diff --git a/src/i18n/en.json b/src/i18n/en.json index e920cf11..2ef4ff5a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -266,6 +266,12 @@ "next": "Next", "previous": "Previous" }, + "moderation": { + "moderation": "Moderation", + "reports": "Reports", + "statuses": "Statuses", + "users": "Users" + }, "nav": { "about": "About", "administration": "Administration", diff --git a/src/modules/interface.js b/src/modules/interface.js index a86193ea..ae1a31c3 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -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 }, { diff --git a/yarn.lock b/yarn.lock index 86ae6b85..b8642176 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" -- 2.34.1 From b490ade544d83339e84ee00c98c04f94791f03cc Mon Sep 17 00:00:00 2001 From: Sol Fisher Romanoff Date: Fri, 21 Oct 2022 17:56:32 +0300 Subject: [PATCH 2/5] Add API endpoints for reports --- src/modules/reports.js | 62 +++++++++++++++++- src/services/api/api.service.js | 64 ++++++++++++++++++- .../entity_normalizer.service.js | 18 ++++++ 3 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/modules/reports.js b/src/modules/reports.js index fea83e5f..6048d719 100644 --- a/src/modules/reports.js +++ b/src/modules/reports.js @@ -1,11 +1,17 @@ -import filter from 'lodash/filter' +import { filter, find, forEach, remove } from 'lodash' + +const getReport = (state, id) => find(state.reports, { id }) +const updateReport = (state, { report, param, value }) => { + getReport(state, report.id)[param] = value +} const reports = { state: { userId: null, statuses: [], preTickedIds: [], - modalActivated: false + modalActivated: false, + reports: [] }, mutations: { openUserReportingModal (state, { userId, statuses, preTickedIds }) { @@ -16,6 +22,38 @@ const reports = { }, closeUserReportingModal (state) { state.modalActivated = false + }, + setReport (state, { report }) { + let existing = getReport(state, report.id) + if (existing) { + existing = report + } else { + state.reports.push(report) + } + }, + updateReportStates (state, { reports }) { + forEach(reports, (report) => { + updateReport(state, { report, param: 'state', value: report.state }) + }) + }, + addNoteToReport (state, { id, note, user }) { + // akkoma doesn't return the note from this API endpoint, and there's no + // good way to get it. the note data is spoofed in the frontend until + // reload. + // definitely worth adding this to the backend at some point + const report = getReport(state, id) + const date = new Date() + + report.notes.push({ + content: note, + user, + created_at: date.toISOString(), + id: date.getTime() + }) + }, + deleteNoteFromReport (state, { id, note }) { + const report = getReport(state, id) + remove(report.notes, { id: note }) } }, actions: { @@ -31,6 +69,26 @@ const reports = { }, closeUserReportingModal ({ commit }) { commit('closeUserReportingModal') + }, + getReports ({ rootState, commit }, params) { + return rootState.api.backendInteractor.getReports(params) + .then(reports => forEach(reports, report => commit('setReport', { report }))) + }, + updateReportStates ({ rootState, commit }, { reports }) { + commit('updateReportStates', { reports }) + return rootState.api.backendInteractor.updateReportStates({ reports }) + }, + getReport ({ rootState, commit }, { id }) { + return rootState.api.backendInteractor.getReport({ id }) + .then(report => commit('setReport', { report })) + }, + addNoteToReport ({ rootState, commit }, { id, note }) { + commit('addNoteToReport', { id, note, user: rootState.users.currentUser }) + return rootState.api.backendInteractor.addNoteToReport({ id, note }) + }, + deleteNoteFromReport ({ rootState, commit }, { id, note }) { + commit('deleteNoteFromReport', { id, note }) + return rootState.api.backendInteractor.deleteNoteFromReport({ id, note }) } } } diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 0cacf251..4e3e1ced 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,5 +1,5 @@ import { each, map, concat, last, get } from 'lodash' -import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js' import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ @@ -19,6 +19,9 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users' const SUGGESTIONS_URL = '/api/v1/suggestions' const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' +const ADMIN_REPORTS_URL = '/api/v1/pleroma/admin/reports' +const ADMIN_REPORT_NOTES_URL = id => `/api/v1/pleroma/admin/reports/${id}/notes` +const ADMIN_REPORT_NOTE_URL = (report, note) => `/api/v1/pleroma/admin/reports/${report}/notes/${note}` const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' @@ -342,7 +345,7 @@ const fetchUserRelationship = ({ id, credentials }) => { return new Promise((resolve, reject) => response.json() .then((json) => { if (!response.ok) { - return reject(new StatusCodeError(response.status, json, { url }, response)) + return reject(new StatusCodeError(400, json, { url }, response)) } return resolve(json) })) @@ -635,6 +638,57 @@ const deleteUser = ({ credentials, user }) => { }) } +const getReports = ({ state, limit, page, pageSize, credentials }) => { + let url = ADMIN_REPORTS_URL + const args = [ + state && `state=${state}`, + limit && `limit=${limit}`, + page && `page=${page}`, + pageSize && `page_size=${pageSize}` + ].filter(_ => _).join('&') + + url = url + (args ? '?' + args : '') + return fetch(url, { headers: authHeaders(credentials) }) + .then((data) => data.json()) + .then((data) => data.reports.map(parseReport)) +} + +const updateReportStates = ({ credentials, reports }) => { + // reports syntax: [{ id: int, state: string }...] + const updates = { + reports: reports.map(report => { + return { + id: report.id.toString(), + state: report.state + } + }) + } + + return promisedRequest({ + url: ADMIN_REPORTS_URL, + method: 'PATCH', + payload: updates, + credentials + }) +} + +const addNoteToReport = ({ id, note, credentials }) => { + return promisedRequest({ + url: ADMIN_REPORT_NOTES_URL(id), + method: 'POST', + payload: { content: note }, + credentials + }) +} + +const deleteNoteFromReport = ({ report, note, credentials }) => { + return promisedRequest({ + url: ADMIN_REPORT_NOTE_URL(report, note), + method: 'DELETE', + credentials + }) +} + const fetchTimeline = ({ timeline, credentials, @@ -1726,7 +1780,11 @@ const apiService = { getSettingsProfile, saveSettingsProfile, listSettingsProfiles, - deleteSettingsProfile + deleteSettingsProfile, + getReports, + updateReportStates, + addNoteToReport, + deleteNoteFromReport } export default apiService diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index b1aded33..a2fa741f 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -429,6 +429,24 @@ export const parseNotification = (data) => { return output } +export const parseReport = (data) => { + const report = {} + + report.account = parseUser(data.account) + report.actor = parseUser(data.actor) + report.statuses = data.statuses.map(parseStatus) + report.notes = data.notes.map(note => { + note.user = parseUser(note.user) + return note + }) + report.state = data.state + report.content = data.content + report.created_at = data.created_at + report.id = data.id + + return report +} + const isNsfw = (status) => { const nsfwRegex = /#nsfw/i return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex) -- 2.34.1 From 405a60d249cd60909c3b9d999f9b48ff07b8a19b Mon Sep 17 00:00:00 2001 From: Sol Fisher Romanoff Date: Fri, 21 Oct 2022 20:44:54 +0300 Subject: [PATCH 3/5] Add reports tab to moderation modal --- src/components/mod_modal/mod_modal.scss | 23 +- src/components/mod_modal/mod_modal_content.js | 12 +- .../mod_modal/mod_modal_content.scss | 37 +--- .../mod_modal/mod_modal_content.vue | 30 +-- .../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 | 29 +++ .../tabs/reports_tab/reports_tab.scss | 83 +++++++ .../tabs/reports_tab/reports_tab.vue | 20 ++ src/i18n/en.json | 21 +- 12 files changed, 583 insertions(+), 78 deletions(-) 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 diff --git a/src/components/mod_modal/mod_modal.scss b/src/components/mod_modal/mod_modal.scss index d80205c9..4821df74 100644 --- a/src/components/mod_modal/mod_modal.scss +++ b/src/components/mod_modal/mod_modal.scss @@ -2,18 +2,6 @@ .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: @@ -49,15 +37,8 @@ height: 100%; } - >.panel-body { - height: 100%; - overflow-y: hidden; - - .btn { - min-height: 2em; - min-width: 10em; - padding: 0 2em; - } + .panel-body { + height: inherit; } } } diff --git a/src/components/mod_modal/mod_modal_content.js b/src/components/mod_modal/mod_modal_content.js index 6330f162..e0ba6259 100644 --- a/src/components/mod_modal/mod_modal_content.js +++ b/src/components/mod_modal/mod_modal_content.js @@ -1,8 +1,8 @@ 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 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 { @@ -21,9 +21,9 @@ const ModModalContent = { components: { TabSwitcher, - ReportsTab, - StatusesTab, - UsersTab + ReportsTab + // StatusesTab, + // UsersTab }, computed: { open () { diff --git a/src/components/mod_modal/mod_modal_content.scss b/src/components/mod_modal/mod_modal_content.scss index 99fc2d2a..b1aeba38 100644 --- a/src/components/mod_modal/mod_modal_content.scss +++ b/src/components/mod_modal/mod_modal_content.scss @@ -2,53 +2,20 @@ .mod_tab-switcher { height: 100%; - .setting-item { - border-bottom: 2px solid var(--fg, $fallback--fg); + .content { margin: 1em 1em 1.4em; - padding-bottom: 1.4em; - > div, - > label { - display: block; + > div { 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; - } } } diff --git a/src/components/mod_modal/mod_modal_content.vue b/src/components/mod_modal/mod_modal_content.vue index bb31bc19..1c11cb33 100644 --- a/src/components/mod_modal/mod_modal_content.vue +++ b/src/components/mod_modal/mod_modal_content.vue @@ -7,26 +7,26 @@ :body-scroll-lock="bodyLock" >
-
- -
-
- -
+ + + + + + + + + + + + + + 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 @@ +