logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe
commit: 8e1c5841e98094c9801f3dc378195af9e3541493
parent: 0438031da44a70816716de40625541d569a49c85
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date:   Sat,  4 May 2019 13:59:27 +0000

Merge branch '441-reporting' into 'develop'

Reporting

Closes #441

See merge request pleroma/pleroma-fe!695

Diffstat:

Msrc/App.js4+++-
Msrc/App.scss4++--
Msrc/App.vue1+
Msrc/components/user_card/user_card.js3+++
Msrc/components/user_card/user_card.vue10++++++++--
Asrc/components/user_reporting_modal/user_reporting_modal.js106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/user_reporting_modal/user_reporting_modal.vue157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/i18n/en.json10++++++++++
Msrc/main.js4+++-
Asrc/modules/reports.js30++++++++++++++++++++++++++++++
Msrc/services/api/api.service.js68++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/services/backend_interactor_service/backend_interactor_service.js4+++-
12 files changed, 372 insertions(+), 29 deletions(-)

diff --git a/src/App.js b/src/App.js @@ -10,6 +10,7 @@ import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue' import MobileNav from './components/mobile_nav/mobile_nav.vue' +import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' import { windowWidth } from './services/window_utils/window_utils' export default { @@ -26,7 +27,8 @@ export default { MediaModal, SideDrawer, MobilePostStatusModal, - MobileNav + MobileNav, + UserReportingModal }, data: () => ({ mobileActivePanel: 'timeline', diff --git a/src/App.scss b/src/App.scss @@ -379,6 +379,7 @@ main-router { .panel-heading { display: flex; + flex: none; border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; background-size: cover; @@ -793,4 +794,4 @@ nav { background-color: var(--lightBg, $fallback--fg); } } -}- \ No newline at end of file +} diff --git a/src/App.vue b/src/App.vue @@ -46,6 +46,7 @@ <media-modal></media-modal> </div> <chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel> + <UserReportingModal /> </div> </template> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js @@ -151,6 +151,9 @@ export default { }, userProfileLink (user) { return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) + }, + reportUser () { + this.$store.dispatch('openUserReportingModal', this.user.id) } } } diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -99,8 +99,14 @@ </button> </span> </div> - <ModerationTools :user='user' v-if='loggedIn.role === "admin"'> - </ModerationTools> + <div class='block' v-if='isOtherUser && loggedIn'> + <span> + <button @click="reportUser"> + {{ $t('user_card.report') }} + </button> + </span> + </div> + <ModerationTools :user='user' v-if='loggedIn.role === "admin"'/> </div> </div> </div> diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js @@ -0,0 +1,106 @@ + +import Status from '../status/status.vue' +import List from '../list/list.vue' +import Checkbox from '../checkbox/checkbox.vue' + +const UserReportingModal = { + components: { + Status, + List, + Checkbox + }, + data () { + return { + comment: '', + forward: false, + statusIdsToReport: [], + processing: false, + error: false + } + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + isOpen () { + return this.isLoggedIn && this.$store.state.reports.modalActivated + }, + userId () { + return this.$store.state.reports.userId + }, + user () { + return this.$store.getters.findUser(this.userId) + }, + remoteInstance () { + return !this.user.is_local && this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1) + }, + statuses () { + return this.$store.state.reports.statuses + } + }, + watch: { + userId: 'resetState' + }, + methods: { + resetState () { + // Reset state + this.comment = '' + this.forward = false + this.statusIdsToReport = [] + this.processing = false + this.error = false + }, + closeModal () { + this.$store.dispatch('closeUserReportingModal') + }, + reportUser () { + this.processing = true + this.error = false + const params = { + userId: this.userId, + comment: this.comment, + forward: this.forward, + statusIds: this.statusIdsToReport + } + this.$store.state.api.backendInteractor.reportUser(params) + .then(() => { + this.processing = false + this.resetState() + this.closeModal() + }) + .catch(() => { + this.processing = false + this.error = true + }) + }, + clearError () { + this.error = false + }, + isChecked (statusId) { + return this.statusIdsToReport.indexOf(statusId) !== -1 + }, + toggleStatus (checked, statusId) { + if (checked === this.isChecked(statusId)) { + return + } + + if (checked) { + this.statusIdsToReport.push(statusId) + } else { + this.statusIdsToReport.splice(this.statusIdsToReport.indexOf(statusId), 1) + } + }, + resize (e) { + const target = e.target || e + if (!(target instanceof window.Element)) { return } + // Auto is needed to make textbox shrink when removing lines + target.style.height = 'auto' + target.style.height = `${target.scrollHeight}px` + if (target.value === '') { + target.style.height = null + } + } + } +} + +export default UserReportingModal diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -0,0 +1,157 @@ +<template> +<div class="modal-view" @click="closeModal" v-if="isOpen"> + <div class="user-reporting-panel panel" @click.stop=""> + <div class="panel-heading"> + <div class="title">{{$t('user_reporting.title', [user.screen_name])}}</div> + </div> + <div class="panel-body"> + <div class="user-reporting-panel-left"> + <div> + <p>{{$t('user_reporting.add_comment_description')}}</p> + <textarea + v-model="comment" + class="form-control" + :placeholder="$t('user_reporting.additional_comments')" + rows="1" + @input="resize" + /> + </div> + <div v-if="!user.is_local"> + <p>{{$t('user_reporting.forward_description')}}</p> + <Checkbox v-model="forward">{{$t('user_reporting.forward_to', [remoteInstance])}}</Checkbox> + </div> + <div> + <button class="btn btn-default" @click="reportUser" :disabled="processing">{{$t('user_reporting.submit')}}</button> + <div class="alert error" v-if="error"> + {{$t('user_reporting.generic_error')}} + </div> + </div> + </div> + <div class="user-reporting-panel-right"> + <List :items="statuses"> + <template slot="item" slot-scope="{item}"> + <div class="status-fadein user-reporting-panel-sitem"> + <Status :inConversation="false" :focused="false" :statusoid="item" /> + <Checkbox :checked="isChecked(item.id)" @change="checked => toggleStatus(checked, item.id)" /> + </div> + </template> + </List> + </div> + </div> + </div> +</div> +</template> + +<script src="./user_reporting_modal.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.user-reporting-panel { + width: 90vw; + max-width: 700px; + min-height: 20vh; + max-height: 80vh; + + .panel-heading { + .title { + text-align: center; + // TODO: Consider making these as default of panel + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .panel-body { + display: flex; + flex-direction: column-reverse; + border-top: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + overflow: hidden; + } + + &-left { + padding: 1.1em 0.7em 0.7em; + line-height: 1.4em; + box-sizing: border-box; + + > div { + margin-bottom: 1em; + + &:last-child { + margin-bottom: 0; + } + } + + p { + margin-top: 0; + } + + textarea.form-control { + line-height: 16px; + resize: none; + overflow: hidden; + transition: min-height 200ms 100ms; + min-height: 44px; + width: 100%; + } + + .btn { + min-width: 10em; + padding: 0 2em; + } + + .alert { + margin: 1em 0 0 0; + line-height: 1.3em; + } + } + + &-right { + display: flex; + flex-direction: column; + overflow-y: auto; + } + + &-sitem { + display: flex; + justify-content: space-between; + + > .status-el { + flex: 1; + } + + > .checkbox { + margin: 0.75em; + } + } + + @media all and (min-width: 801px) { + .panel-body { + flex-direction: row; + } + + &-left { + width: 50%; + max-width: 320px; + border-right: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + padding: 1.1em; + + > div { + margin-bottom: 2em; + } + } + + &-right { + width: 50%; + flex: 1 1 auto; + margin-bottom: 12px; + } + } +} +</style> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -420,6 +420,7 @@ "muted": "Muted", "per_day": "per day", "remote_follow": "Remote follow", + "report": "Report", "statuses": "Statuses", "unblock": "Unblock", "unblock_progress": "Unblocking...", @@ -452,6 +453,15 @@ "profile_does_not_exist": "Sorry, this profile does not exist.", "profile_loading_error": "Sorry, there was an error loading this profile." }, + "user_reporting": { + "title": "Reporting {0}", + "add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "additional_comments": "Additional comments", + "forward_description": "The account is from another server. Send a copy of the report there as well?", + "forward_to": "Forward to {0}", + "submit": "Submit", + "generic_error": "An error occurred while processing your request." + }, "who_to_follow": { "more": "More", "who_to_follow": "Who to follow" diff --git a/src/main.js b/src/main.js @@ -12,6 +12,7 @@ import chatModule from './modules/chat.js' import oauthModule from './modules/oauth.js' import mediaViewerModule from './modules/media_viewer.js' import oauthTokensModule from './modules/oauth_tokens.js' +import reportsModule from './modules/reports.js' import VueTimeago from 'vue-timeago' import VueI18n from 'vue-i18n' @@ -75,7 +76,8 @@ const persistedStateOptions = { chat: chatModule, oauth: oauthModule, mediaViewer: mediaViewerModule, - oauthTokens: oauthTokensModule + oauthTokens: oauthTokensModule, + reports: reportsModule }, plugins: [persistedState, pushNotifications], strict: false // Socket modifies itself, let's ignore this for now. diff --git a/src/modules/reports.js b/src/modules/reports.js @@ -0,0 +1,30 @@ +import filter from 'lodash/filter' + +const reports = { + state: { + userId: null, + statuses: [], + modalActivated: false + }, + mutations: { + openUserReportingModal (state, { userId, statuses }) { + state.userId = userId + state.statuses = statuses + state.modalActivated = true + }, + closeUserReportingModal (state) { + state.modalActivated = false + } + }, + actions: { + openUserReportingModal ({ rootState, commit }, userId) { + const statuses = filter(rootState.statuses.allStatuses, status => status.user.id === userId) + commit('openUserReportingModal', { userId, statuses }) + }, + closeUserReportingModal ({ commit }) { + commit('closeUserReportingModal') + } + } +} + +export default reports diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -50,6 +50,7 @@ const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' const MASTODON_STATUS_FAVORITEDBY_URL = id => `/api/v1/statuses/${id}/favourited_by` const MASTODON_STATUS_REBLOGGEDBY_URL = id => `/api/v1/statuses/${id}/reblogged_by` const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials' +const MASTODON_REPORT_USER_URL = '/api/v1/reports' import { each, map, concat, last } from 'lodash' import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' @@ -66,7 +67,24 @@ let fetch = (url, options) => { return oldfetch(fullUrl, options) } -const promisedRequest = (url, options) => { +const promisedRequest = ({ method, url, payload, credentials, headers = {} }) => { + const options = { + method, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + ...headers + } + } + if (payload) { + options.body = JSON.stringify(payload) + } + if (credentials) { + options.headers = { + ...options.headers, + ...authHeaders(credentials) + } + } return fetch(url, options) .then((response) => { return new Promise((resolve, reject) => response.json() @@ -122,14 +140,11 @@ const updateBanner = ({credentials, banner}) => { } const updateProfile = ({credentials, params}) => { - return promisedRequest(MASTODON_PROFILE_UPDATE_URL, { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - ...authHeaders(credentials) - }, + return promisedRequest({ + url: MASTODON_PROFILE_UPDATE_URL, method: 'PATCH', - body: JSON.stringify(params) + payload: params, + credentials }) .then((data) => parseUser(data)) } @@ -227,7 +242,7 @@ const denyUser = ({id, credentials}) => { const fetchUser = ({id, credentials}) => { let url = `${MASTODON_USER_URL}/${id}` - return promisedRequest(url, { headers: authHeaders(credentials) }) + return promisedRequest({ url, credentials }) .then((data) => parseUser(data)) } @@ -651,26 +666,20 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma } const fetchMutes = ({credentials}) => { - return promisedRequest(MASTODON_USER_MUTES_URL, { headers: authHeaders(credentials) }) + return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials }) .then((users) => users.map(parseUser)) } const muteUser = ({id, credentials}) => { - return promisedRequest(MASTODON_MUTE_USER_URL(id), { - headers: authHeaders(credentials), - method: 'POST' - }) + return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' }) } const unmuteUser = ({id, credentials}) => { - return promisedRequest(MASTODON_UNMUTE_USER_URL(id), { - headers: authHeaders(credentials), - method: 'POST' - }) + return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' }) } const fetchBlocks = ({credentials}) => { - return promisedRequest(MASTODON_USER_BLOCKS_URL, { headers: authHeaders(credentials) }) + return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials }) .then((users) => users.map(parseUser)) } @@ -715,11 +724,25 @@ const markNotificationsAsSeen = ({id, credentials}) => { } const fetchFavoritedByUsers = ({id}) => { - return promisedRequest(MASTODON_STATUS_FAVORITEDBY_URL(id)).then((users) => users.map(parseUser)) + return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser)) } const fetchRebloggedByUsers = ({id}) => { - return promisedRequest(MASTODON_STATUS_REBLOGGEDBY_URL(id)).then((users) => users.map(parseUser)) + return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser)) +} + +const reportUser = ({credentials, userId, statusIds, comment, forward}) => { + return promisedRequest({ + url: MASTODON_REPORT_USER_URL, + method: 'POST', + payload: { + 'account_id': userId, + 'status_ids': statusIds, + comment, + forward + }, + credentials + }) } const apiService = { @@ -773,7 +796,8 @@ const apiService = { suggestions, markNotificationsAsSeen, fetchFavoritedByUsers, - fetchRebloggedByUsers + fetchRebloggedByUsers, + reportUser } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js @@ -115,6 +115,7 @@ const backendInteractorService = (credentials) => { const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({id}) const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({id}) + const reportUser = (params) => apiService.reportUser({credentials, ...params}) const backendInteractorServiceInstance = { fetchStatus, @@ -159,7 +160,8 @@ const backendInteractorService = (credentials) => { approveUser, denyUser, fetchFavoritedByUsers, - fetchRebloggedByUsers + fetchRebloggedByUsers, + reportUser } return backendInteractorServiceInstance