diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js index c5ceb63d..95087eac 100644 --- a/src/components/interactions/interactions.js +++ b/src/components/interactions/interactions.js @@ -5,6 +5,8 @@ const tabModeDict = { mentions: ['mention'], 'likes+repeats': ['repeat', 'like'], follows: ['follow'], + reactions: ['pleroma:emoji_reaction'], + reports: ['pleroma:report'], moves: ['move'] } @@ -12,7 +14,8 @@ const Interactions = { data () { return { allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, - filterMode: tabModeDict['mentions'] + filterMode: tabModeDict['mentions'], + canSeeReports: ['moderator', 'admin'].includes(this.$store.state.users.currentUser.role) } }, methods: { diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue index 57d5d87c..b7291c02 100644 --- a/src/components/interactions/interactions.vue +++ b/src/components/interactions/interactions.vue @@ -21,6 +21,15 @@ key="follows" :label="$t('interactions.follows')" /> + <span + key="reactions" + :label="$t('interactions.emoji_reactions')" + /> + <span + v-if="canSeeReports" + key="reports" + :label="$t('interactions.reports')" + /> <span v-if="!allowFollowingMove" key="moves" diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 398bb7a9..8f74c0e6 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -4,6 +4,7 @@ import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' +import Report from '../report/report.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' @@ -46,6 +47,7 @@ const Notification = { UserCard, Timeago, Status, + Report, RichContent }, methods: { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 9ecb034f..1ababfc0 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -120,6 +120,9 @@ </i18n-t> </small> </span> + <span v-if="notification.type === 'pleroma:report'"> + <small>{{ $t('notifications.submitted_report') }}</small> + </span> </div> <div v-if="isStatusNotification" @@ -202,6 +205,10 @@ @{{ notification.target.screen_name_ui }} </router-link> </div> + <Report + v-else-if="notification.type === 'pleroma:report'" + :report-id="notification.report.id" + /> <template v-else> <StatusContent class="faint" diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index a285027d..c3ceb89a 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -59,8 +59,10 @@ height: 32px; } - --link: var(--faintLink); - --text: var(--faint); + .faint { + --link: var(--faintLink); + --text: var(--faint); + } } .follow-request-accept { diff --git a/src/components/report/report.js b/src/components/report/report.js new file mode 100644 index 00000000..76055764 --- /dev/null +++ b/src/components/report/report.js @@ -0,0 +1,34 @@ +import Select from '../select/select.vue' +import StatusContent from '../status_content/status_content.vue' +import Timeago from '../timeago/timeago.vue' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' + +const Report = { + props: [ + 'reportId' + ], + components: { + Select, + StatusContent, + Timeago + }, + computed: { + report () { + return this.$store.state.reports.reports[this.reportId] || {} + }, + state: { + get: function () { return this.report.state }, + set: function (val) { this.setReportState(val) } + } + }, + methods: { + generateUserProfileLink (user) { + return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) + }, + setReportState (state) { + return this.$store.dispatch('setReportState', { id: this.report.id, state }) + } + } +} + +export default Report diff --git a/src/components/report/report.scss b/src/components/report/report.scss new file mode 100644 index 00000000..578b4eb1 --- /dev/null +++ b/src/components/report/report.scss @@ -0,0 +1,43 @@ +@import '../../_variables.scss'; + +.Report { + .report-content { + margin: 0.5em 0 1em; + } + + .report-state { + margin: 0.5em 0 1em; + } + + .reported-status { + border: 1px solid $fallback--faint; + border-color: var(--faint, $fallback--faint); + border-radius: $fallback--inputRadius; + border-radius: var(--inputRadius, $fallback--inputRadius); + color: $fallback--text; + color: var(--text, $fallback--text); + display: block; + padding: 0.5em; + margin: 0.5em 0; + + .status-content { + pointer-events: none; + } + + .reported-status-heading { + display: flex; + width: 100%; + justify-content: space-between; + margin-bottom: 0.2em; + } + + .reported-status-name { + font-weight: bold; + } + } + + .note { + width: 100%; + margin-bottom: 0.5em; + } +} diff --git a/src/components/report/report.vue b/src/components/report/report.vue new file mode 100644 index 00000000..1f19cc25 --- /dev/null +++ b/src/components/report/report.vue @@ -0,0 +1,74 @@ +<template> + <div class="Report"> + <div class="reported-user"> + <span>{{ $t('report.reported_user') }}</span> + <router-link :to="generateUserProfileLink(report.acct)"> + @{{ report.acct.screen_name }} + </router-link> + </div> + <div class="reporter"> + <span>{{ $t('report.reporter') }}</span> + <router-link :to="generateUserProfileLink(report.actor)"> + @{{ report.actor.screen_name }} + </router-link> + </div> + <div class="report-state"> + <span>{{ $t('report.state') }}</span> + <Select + :id="report-state" + v-model="state" + class="form-control" + > + <option + v-for="state in ['open', 'closed', 'resolved']" + :key="state" + :value="state" + > + {{ $t('report.state_' + state) }} + </option> + </Select> + </div> + <RichContent + class="report-content" + :html="report.content" + :emoji="[]" + /> + <div v-if="report.statuses.length"> + <small>{{ $t('report.reported_statuses') }}</small> + <router-link + v-for="status in report.statuses" + :key="status.id" + :to="{ name: 'conversation', params: { id: status.id } }" + class="reported-status" + > + <div class="reported-status-heading"> + <span class="reported-status-name">{{ status.user.name }}</span> + <Timeago + :time="status.created_at" + :auto-update="240" + class="faint" + /> + </div> + <status-content :status="status" /> + </router-link> + </div> + <div v-if="report.notes.length"> + <small>{{ $t('report.notes') }}</small> + <div + v-for="note in report.notes" + :key="note.id" + class="note" + > + <span>{{ note.content }}</span> + <Timeago + :time="note.created_at" + :auto-update="240" + class="faint" + /> + </div> + </div> + </div> +</template> + +<script src="./report.js"></script> +<style src="./report.scss" lang="scss"></style> diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js index 8d171b2d..85ffc661 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.js +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -1,4 +1,3 @@ - import Status from '../status/status.vue' import List from '../list/list.vue' import Checkbox from '../checkbox/checkbox.vue' @@ -21,14 +20,17 @@ const UserReportingModal = { } }, computed: { + reportModal () { + return this.$store.state.reports.reportModal + }, isLoggedIn () { return !!this.$store.state.users.currentUser }, isOpen () { - return this.isLoggedIn && this.$store.state.reports.modalActivated + return this.isLoggedIn && this.reportModal.activated }, userId () { - return this.$store.state.reports.userId + return this.reportModal.userId }, user () { return this.$store.getters.findUser(this.userId) @@ -37,10 +39,10 @@ const UserReportingModal = { return !this.user.is_local && this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1) }, statuses () { - return this.$store.state.reports.statuses + return this.reportModal.statuses }, preTickedIds () { - return this.$store.state.reports.preTickedIds + return this.reportModal.preTickedIds } }, watch: { diff --git a/src/i18n/en.json b/src/i18n/en.json index f8336e5c..5e7ee494 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -66,6 +66,7 @@ "more": "More", "loading": "Loading…", "generic_error": "An error occured", + "generic_error_message": "An error occured: {0}", "error_retry": "Please try again", "retry": "Try again", "optional": "optional", @@ -160,7 +161,8 @@ "repeated_you": "repeated your status", "no_more_notifications": "No more notifications", "migrated_to": "migrated to", - "reacted_with": "reacted with {0}" + "reacted_with": "reacted with {0}", + "submitted_report": "submitted a report" }, "polls": { "add_poll": "Add poll", @@ -195,6 +197,8 @@ "interactions": { "favs_repeats": "Repeats and favorites", "follows": "New follows", + "emoji_reactions": "Emoji Reactions", + "reports": "Reports", "moves": "User migrates", "load_older": "Load older interactions" }, @@ -261,6 +265,16 @@ "searching_for": "Searching for", "error": "Not found." }, + "report": { + "reporter": "Reporter:", + "reported_user": "Reported user:", + "reported_statuses": "Reported statuses:", + "notes": "Notes:", + "state": "State:", + "state_open": "Open", + "state_closed": "Closed", + "state_resolved": "Resolved" + }, "selectable_list": { "select_all": "Select all" }, diff --git a/src/i18n/nl.json b/src/i18n/nl.json index b113ffe4..fd61572c 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -745,6 +745,8 @@ "favs_repeats": "Herhalingen en favorieten", "follows": "Nieuwe gevolgden", "moves": "Gebruikermigraties", + "emoji_reactions": "Emoji Reacties", + "reports": "Rapportages", "load_older": "Oudere interacties laden" }, "remote_user_resolver": { @@ -752,6 +754,17 @@ "error": "Niet gevonden.", "remote_user_resolver": "Externe gebruikers-zoeker" }, + "report": { + "reporter": "Reporteerder:", + "reported_user": "Gerapporteerde gebruiker:", + "reported_statuses": "Gerapporteerde statussen:", + "notes": "Notas:", + "state": "Status:", + "state_open": "Open", + "state_closed": "Gesloten", + "state_resolved": "Opgelost" + }, + "selectable_list": { "select_all": "Alles selecteren" }, diff --git a/src/modules/config.js b/src/modules/config.js index ff5ef270..561cacd4 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -54,6 +54,7 @@ export const defaultState = { moves: true, emojiReactions: true, followRequest: true, + reports: true, chatMention: true }, webPushNotifications: false, diff --git a/src/modules/reports.js b/src/modules/reports.js index fea83e5f..925792c0 100644 --- a/src/modules/reports.js +++ b/src/modules/reports.js @@ -2,20 +2,29 @@ import filter from 'lodash/filter' const reports = { state: { - userId: null, - statuses: [], - preTickedIds: [], - modalActivated: false + reportModal: { + userId: null, + statuses: [], + preTickedIds: [], + activated: false + }, + reports: {} }, mutations: { openUserReportingModal (state, { userId, statuses, preTickedIds }) { - state.userId = userId - state.statuses = statuses - state.preTickedIds = preTickedIds - state.modalActivated = true + state.reportModal.userId = userId + state.reportModal.statuses = statuses + state.reportModal.preTickedIds = preTickedIds + state.reportModal.activated = true }, closeUserReportingModal (state) { - state.modalActivated = false + state.reportModal.activated = false + }, + setReportState (reportsState, { id, state }) { + reportsState.reports[id].state = state + }, + addReport (state, report) { + state.reports[report.id] = report } }, actions: { @@ -31,6 +40,23 @@ const reports = { }, closeUserReportingModal ({ commit }) { commit('closeUserReportingModal') + }, + setReportState ({ commit, dispatch, rootState }, { id, state }) { + const oldState = rootState.reports.reports[id].state + commit('setReportState', { id, state }) + rootState.api.backendInteractor.setReportState({ id, state }).catch(e => { + console.error('Failed to set report state', e) + dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'general.generic_error_message', + messageArgs: [e.message], + timeout: 5000 + }) + commit('setReportState', { id, state: oldState }) + }) + }, + addReport ({ commit }, report) { + commit('addReport', report) } } } diff --git a/src/modules/statuses.js b/src/modules/statuses.js index a13930e9..66cc82bc 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -336,6 +336,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item } + if (notification.type === 'pleroma:report') { + dispatch('addReport', notification.report) + } + if (notification.type === 'pleroma:emoji_reaction') { dispatch('fetchEmojiReactionsBy', notification.status.id) } diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 436b8b0a..c17d0476 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -87,6 +87,7 @@ const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}` const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages` const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read` const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}` +const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports' const oldfetch = window.fetch @@ -497,7 +498,8 @@ const fetchTimeline = ({ userId = false, tag = false, withMuted = false, - replyVisibility = 'all' + replyVisibility = 'all', + includeTypes = [] }) => { const timelineUrls = { public: MASTODON_PUBLIC_TIMELINE, @@ -544,6 +546,11 @@ const fetchTimeline = ({ if (replyVisibility !== 'all') { params.push(['reply_visibility', replyVisibility]) } + if (includeTypes.length > 0) { + includeTypes.forEach(type => { + params.push(['include_types[]', type]) + }) + } params.push(['limit', 20]) @@ -1266,6 +1273,38 @@ const deleteChatMessage = ({ chatId, messageId, credentials }) => { }) } +const setReportState = ({ id, state, credentials }) => { + // TODO: Can't use promisedRequest because on OK this does not return json + // See https://git.pleroma.social/pleroma/pleroma-fe/-/merge_requests/1322 + return fetch(PLEROMA_ADMIN_REPORTS, { + headers: { + ...authHeaders(credentials), + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + method: 'PATCH', + body: JSON.stringify({ + reports: [{ + id, + state + }] + }) + }) + .then(data => { + if (data.status >= 500) { + throw Error(data.statusText) + } else if (data.status >= 400) { + return data.json() + } + return data + }) + .then(data => { + if (data.errors) { + throw Error(data.errors[0].message) + } + }) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -1351,7 +1390,8 @@ const apiService = { chatMessages, sendChatMessage, readChat, - deleteChatMessage + deleteChatMessage, + setReportState } export default apiService diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index f219c161..0005e95a 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -387,6 +387,13 @@ export const parseNotification = (data) => { : parseUser(data.target) output.from_profile = parseUser(data.account) output.emoji = data.emoji + if (data.report) { + output.report = data.report + output.report.content = data.report.content + output.report.acct = parseUser(data.report.account) + output.report.actor = parseUser(data.report.actor) + output.report.statuses = data.report.statuses.map(parseStatus) + } } else { const parsedNotice = parseStatus(data.notice) output.type = data.ntype diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 6fef1022..8c70ea53 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -14,7 +14,8 @@ export const visibleTypes = store => { rootState.config.notificationVisibility.follows && 'follow', rootState.config.notificationVisibility.followRequest && 'follow_request', rootState.config.notificationVisibility.moves && 'move', - rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction' + rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction', + rootState.config.notificationVisibility.reports && 'pleroma:report' ].filter(_ => _)) } @@ -98,6 +99,9 @@ export const prepareNotificationObject = (notification, i18n) => { case 'follow_request': i18nString = 'follow_request' break + case 'pleroma:report': + i18nString = 'submitted_report' + break } if (notification.type === 'pleroma:emoji_reaction') { diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index f83f871e..cb241e33 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -1,6 +1,18 @@ import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' +// For using include_types when fetching notifications. +// Note: chat_mention excluded as pleroma-fe polls them separately +const mastoApiNotificationTypes = [ + 'mention', + 'favourite', + 'reblog', + 'follow', + 'move', + 'pleroma:emoji_reaction', + 'pleroma:report' +] + const update = ({ store, notifications, older }) => { store.dispatch('addNewNotifications', { notifications, older }) } @@ -12,6 +24,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { const timelineData = rootState.statuses.notifications const hideMutedPosts = getters.mergedConfig.hideMutedPosts + args['includeTypes'] = mastoApiNotificationTypes args['withMuted'] = !hideMutedPosts args['timeline'] = 'notifications' @@ -63,6 +76,7 @@ const fetchNotifications = ({ store, args, older }) => { messageArgs: [error.message], timeout: 5000 }) + console.error(error) }) }