diff --git a/src/App.js b/src/App.js index 46145b16a35b02417dc39b44c10207f81f403c65..e72c73e35c413ceb3a6523aae2885ec496206dd0 100644 --- 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.vue b/src/App.vue index 3b8623ad77195bcef3bb00d8f20bd15318475bce..cb7e8d785ea23ca937eef3fd96e5a4ad82b85919 100644 --- 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/checkbox/checkbox.js b/src/components/checkbox/checkbox.js new file mode 100644 index 0000000000000000000000000000000000000000..324a7597d6cc7998bbb7b473277ff7b5db9f22b9 --- /dev/null +++ b/src/components/checkbox/checkbox.js @@ -0,0 +1,44 @@ +// TODO: Template-based functional component is supported in vue-loader 13.3.0+. +// Also, somehow, props are not provided through 'context' even though they are defined. +// Need to upgrade vue-loader + +import './checkbox.scss' + +export default { + functional: true, + name: 'Checkbox', + model: { + prop: 'checked', + event: 'change' + }, + render (createElement, { data, children }) { + const { props = {}, attrs = {}, on = {}, ...rest } = data + const { name, checked, disabled, readonly, ...restAttrs } = attrs + const { change, ...restListeners } = on + const wrapperProps = { + attrs: restAttrs, + on: restListeners, + ...rest + } + const inputProps = { + attrs: { + name, + checked, + disabled, + readonly, + ...props + }, + on: {} + } + if (change) { + inputProps.on.change = e => change(e.target.checked) + } + return ( + <label class="checkbox" {...wrapperProps}> + <input type="checkbox" {...inputProps} /> + <i /> + {children && <span>{children}</span>} + </label> + ) + } +} diff --git a/src/components/checkbox/checkbox.scss b/src/components/checkbox/checkbox.scss new file mode 100644 index 0000000000000000000000000000000000000000..07b3f39e1ddaea547fdb615ec2dbe93f9b58be59 --- /dev/null +++ b/src/components/checkbox/checkbox.scss @@ -0,0 +1,48 @@ +@import '../../_variables.scss'; + +.checkbox { + position: relative; + display: inline-block; + padding-left: 1.2em; + + input[type=checkbox] { + display: none; + + & + i::before { + position: absolute; + left: 0; + top: 0; + display: block; + content: '✔'; + transition: color 200ms; + width: 1.1em; + height: 1.1em; + border-radius: $fallback--checkboxRadius; + border-radius: var(--checkboxRadius, $fallback--checkboxRadius); + box-shadow: 0px 0px 2px black inset; + box-shadow: var(--inputShadow); + background-color: $fallback--fg; + background-color: var(--input, $fallback--fg); + vertical-align: top; + text-align: center; + line-height: 1.1em; + font-size: 1.1em; + color: transparent; + overflow: hidden; + box-sizing: border-box; + } + + &:checked + i::before { + color: $fallback--text; + color: var(--text, $fallback--text); + } + + &:disabled + i::before { + opacity: .5; + } + } + + & > span { + margin-left: .5em; + } +} \ No newline at end of file diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 1a100de398ba3715157ca35a32645027f7738416..7c6ffa8986fbbf2bbf59bbe428e845a1f39b4bf5 100644 --- 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 index e62b384d201555330f04654bd615e6843017ac9c..2d02ca0300e6c459804d5b5add55057cdae65edb 100644 --- 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 new file mode 100644 index 0000000000000000000000000000000000000000..fb9ea16da8ccc271cf861c81274bbc3c8994bb98 --- /dev/null +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -0,0 +1,81 @@ + +import Status from '../status/status.vue' +import Checkbox from '../checkbox/checkbox.js' + +const UserReportingModal = { + components: { + Status, + Checkbox + }, + data () { + return { + comment: '', + forward: false, + statusIdsToReport: [] + } + }, + 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 (value) { + this.statusIdsToReport = [] + } + }, + methods: { + closeModal () { + this.$store.dispatch('closeUserReportingModal') + }, + reportUser () { + const payload = { + comment: this.comment, + forward: this.forward, + statusIdsToReport: this.statusIdsToReport + } + this.$store.dispatch('reportUser', payload) + }, + 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 new file mode 100644 index 0000000000000000000000000000000000000000..49839da3a4174d93338d3cca167fd69ff73b5d93 --- /dev/null +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -0,0 +1,111 @@ +<template> +<div class="modal-view" @click="closeModal" v-if="isOpen"> + <div class="user-reporting-panel panel" @click.stop=""> + <div class="panel-heading">Reporting {{user.screen_name}}</div> + <div class="panel-body"> + <div class="user-reporting-panel-left"> + <div> + <p>The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:</p> + <textarea + v-model="comment" + class="form-control" + placeholder="Additional comments" + rows="1" + @input="resize" + /> + </div> + <div v-if="!user.is_local"> + <p>The account is from another server. Send an anonymized copy of the report there as well?</p> + <Checkbox v-model="forward">Forward to {{remoteInstance}}</Checkbox> + </div> + <div> + <button class="btn btn-default" @click="reportUser">Submit</button> + </div> + </div> + <div class="user-reporting-panel-right"> + <div v-for="status in statuses" :key="status.id" class="status-fadein"> + <Status :inConversation="false" :focused="false" :statusoid="status" /> + <Checkbox :checked="isChecked(status.id)" @change="checked => toggleStatus(checked, status.id)" /> + </div> + </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; + + .panel-body { + display: flex; + border-top: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + } + + &-left { + width: 50%; + padding: 1.1em; + border-right: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + max-width: 320px; + line-height: 1.4em; + box-sizing: border-box; + + > div { + margin-bottom: 2em; + + &: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; + } + } + + &-right { + width: 50%; + flex: 1 1 auto; + min-height: 20vh; + max-height: 80vh; + overflow-y: auto; + overflow-x: hidden; + + > div { + display: flex; + justify-content: space-between; + border-bottom-width: 1px; + border-bottom-style: solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + + .checkbox { + margin: 0.75em; + } + } + } +} +</style> diff --git a/src/i18n/en.json b/src/i18n/en.json index b07af5e5d9acb7f0f4a4571fd9caab293fcbc3b4..8292c921b94048d916234081d6dcfafc113d5f42 100644 --- 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...", diff --git a/src/main.js b/src/main.js index 725f5806583e392dab1e4f2ab15a73e393edb049..92f843b1b221d3fa40d972e5df1e8a64b4f83161 100644 --- 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 new file mode 100644 index 0000000000000000000000000000000000000000..b712cfebf046134d730fac36e8b3009f6c96621b --- /dev/null +++ b/src/modules/reports.js @@ -0,0 +1,33 @@ +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') + }, + reportUser ({ commit }, payload) { + console.log('payload', payload) + } + } +} + +export default reports