commit: 750696643ffde27c1e8ae5a50253bbee5bfc4249
parent 7773e7de6454a7ecd79b695ba27cccabeaca184e
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Tue, 9 Aug 2022 21:56:15 +0000
Merge branch 'feat/report-notification' into 'develop'
#949 Feat/report notification
See merge request pleroma/pleroma-fe!1322
Diffstat:
18 files changed, 318 insertions(+), 19 deletions(-)
diff --git 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
@@ -22,6 +22,15 @@
: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"
:label="$t('interactions.moves')"
diff --git 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 UserPopover from '../user_popover/user_popover.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
@@ -47,6 +48,7 @@ const Notification = {
UserCard,
Timeago,
Status,
+ Report,
RichContent,
UserPopover
},
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
@@ -121,6 +121,9 @@
</i18n-t>
</small>
</span>
+ <span v-if="notification.type === 'pleroma:report'">
+ <small>{{ $t('notifications.submitted_report') }}</small>
+ </span>
<span v-if="notification.type === 'poll'">
<FAIcon
class="type-icon"
@@ -211,6 +214,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
@@ -163,6 +164,7 @@
"no_more_notifications": "No more notifications",
"migrated_to": "migrated to",
"reacted_with": "reacted with {0}",
+ "submitted_report": "submitted a report",
"poll_ended": "poll has ended"
},
"polls": {
@@ -198,6 +200,8 @@
"interactions": {
"favs_repeats": "Repeats and favorites",
"follows": "New follows",
+ "emoji_reactions": "Emoji Reactions",
+ "reports": "Reports",
"moves": "User migrates",
"load_older": "Load older interactions"
},
@@ -266,6 +270,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
@@ -744,6 +744,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": {
@@ -751,6 +753,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
@@ -59,6 +59,7 @@ export const defaultState = {
moves: true,
emojiReactions: true,
followRequest: true,
+ reports: true,
chatMention: true,
polls: true
},
diff --git 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
@@ -337,6 +337,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
@@ -93,6 +93,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 PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const oldfetch = window.fetch
@@ -588,7 +589,8 @@ const fetchTimeline = ({
listId = false,
tag = false,
withMuted = false,
- replyVisibility = 'all'
+ replyVisibility = 'all',
+ includeTypes = []
}) => {
const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE,
@@ -640,6 +642,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])
@@ -1424,6 +1431,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,
@@ -1523,7 +1562,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
@@ -390,6 +390,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
@@ -15,6 +15,7 @@ export const visibleTypes = store => {
rootState.config.notificationVisibility.followRequest && 'follow_request',
rootState.config.notificationVisibility.moves && 'move',
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
+ rootState.config.notificationVisibility.reports && 'pleroma:report',
rootState.config.notificationVisibility.polls && 'poll'
].filter(_ => _))
}
@@ -99,6 +100,9 @@ export const prepareNotificationObject = (notification, i18n) => {
case 'follow_request':
i18nString = 'follow_request'
break
+ case 'pleroma:report':
+ i18nString = 'submitted_report'
+ break
case 'poll':
i18nString = 'poll_ended'
break
diff --git 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)
})
}