Merge branch 'feat/report-notification' into 'develop'

#949 Feat/report notification

See merge request pleroma/pleroma-fe!1322
This commit is contained in:
HJ 2022-08-09 21:56:15 +00:00
commit 750696643f
18 changed files with 318 additions and 19 deletions

View File

@ -5,6 +5,8 @@ const tabModeDict = {
mentions: ['mention'], mentions: ['mention'],
'likes+repeats': ['repeat', 'like'], 'likes+repeats': ['repeat', 'like'],
follows: ['follow'], follows: ['follow'],
reactions: ['pleroma:emoji_reaction'],
reports: ['pleroma:report'],
moves: ['move'] moves: ['move']
} }
@ -12,7 +14,8 @@ const Interactions = {
data () { data () {
return { return {
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move, 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: { methods: {

View File

@ -21,6 +21,15 @@
key="follows" key="follows"
:label="$t('interactions.follows')" :label="$t('interactions.follows')"
/> />
<span
key="reactions"
:label="$t('interactions.emoji_reactions')"
/>
<span
v-if="canSeeReports"
key="reports"
:label="$t('interactions.reports')"
/>
<span <span
v-if="!allowFollowingMove" v-if="!allowFollowingMove"
key="moves" key="moves"

View File

@ -4,6 +4,7 @@ import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import Report from '../report/report.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserPopover from '../user_popover/user_popover.vue' import UserPopover from '../user_popover/user_popover.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
@ -47,6 +48,7 @@ const Notification = {
UserCard, UserCard,
Timeago, Timeago,
Status, Status,
Report,
RichContent, RichContent,
UserPopover UserPopover
}, },

View File

@ -121,6 +121,9 @@
</i18n-t> </i18n-t>
</small> </small>
</span> </span>
<span v-if="notification.type === 'pleroma:report'">
<small>{{ $t('notifications.submitted_report') }}</small>
</span>
<span v-if="notification.type === 'poll'"> <span v-if="notification.type === 'poll'">
<FAIcon <FAIcon
class="type-icon" class="type-icon"
@ -211,6 +214,10 @@
@{{ notification.target.screen_name_ui }} @{{ notification.target.screen_name_ui }}
</router-link> </router-link>
</div> </div>
<Report
v-else-if="notification.type === 'pleroma:report'"
:report-id="notification.report.id"
/>
<template v-else> <template v-else>
<StatusContent <StatusContent
class="faint" class="faint"

View File

@ -59,9 +59,11 @@
height: 32px; height: 32px;
} }
.faint {
--link: var(--faintLink); --link: var(--faintLink);
--text: var(--faint); --text: var(--faint);
} }
}
.follow-request-accept { .follow-request-accept {
&:hover { &:hover {

View File

@ -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

View File

@ -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;
}
}

View File

@ -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>

View File

@ -1,4 +1,3 @@
import Status from '../status/status.vue' import Status from '../status/status.vue'
import List from '../list/list.vue' import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
@ -21,14 +20,17 @@ const UserReportingModal = {
} }
}, },
computed: { computed: {
reportModal () {
return this.$store.state.reports.reportModal
},
isLoggedIn () { isLoggedIn () {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
}, },
isOpen () { isOpen () {
return this.isLoggedIn && this.$store.state.reports.modalActivated return this.isLoggedIn && this.reportModal.activated
}, },
userId () { userId () {
return this.$store.state.reports.userId return this.reportModal.userId
}, },
user () { user () {
return this.$store.getters.findUser(this.userId) 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) return !this.user.is_local && this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1)
}, },
statuses () { statuses () {
return this.$store.state.reports.statuses return this.reportModal.statuses
}, },
preTickedIds () { preTickedIds () {
return this.$store.state.reports.preTickedIds return this.reportModal.preTickedIds
} }
}, },
watch: { watch: {

View File

@ -66,6 +66,7 @@
"more": "More", "more": "More",
"loading": "Loading…", "loading": "Loading…",
"generic_error": "An error occured", "generic_error": "An error occured",
"generic_error_message": "An error occured: {0}",
"error_retry": "Please try again", "error_retry": "Please try again",
"retry": "Try again", "retry": "Try again",
"optional": "optional", "optional": "optional",
@ -163,6 +164,7 @@
"no_more_notifications": "No more notifications", "no_more_notifications": "No more notifications",
"migrated_to": "migrated to", "migrated_to": "migrated to",
"reacted_with": "reacted with {0}", "reacted_with": "reacted with {0}",
"submitted_report": "submitted a report",
"poll_ended": "poll has ended" "poll_ended": "poll has ended"
}, },
"polls": { "polls": {
@ -198,6 +200,8 @@
"interactions": { "interactions": {
"favs_repeats": "Repeats and favorites", "favs_repeats": "Repeats and favorites",
"follows": "New follows", "follows": "New follows",
"emoji_reactions": "Emoji Reactions",
"reports": "Reports",
"moves": "User migrates", "moves": "User migrates",
"load_older": "Load older interactions" "load_older": "Load older interactions"
}, },
@ -266,6 +270,16 @@
"searching_for": "Searching for", "searching_for": "Searching for",
"error": "Not found." "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": { "selectable_list": {
"select_all": "Select all" "select_all": "Select all"
}, },

View File

@ -744,6 +744,8 @@
"favs_repeats": "Herhalingen en favorieten", "favs_repeats": "Herhalingen en favorieten",
"follows": "Nieuwe gevolgden", "follows": "Nieuwe gevolgden",
"moves": "Gebruikermigraties", "moves": "Gebruikermigraties",
"emoji_reactions": "Emoji Reacties",
"reports": "Rapportages",
"load_older": "Oudere interacties laden" "load_older": "Oudere interacties laden"
}, },
"remote_user_resolver": { "remote_user_resolver": {
@ -751,6 +753,17 @@
"error": "Niet gevonden.", "error": "Niet gevonden.",
"remote_user_resolver": "Externe gebruikers-zoeker" "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": { "selectable_list": {
"select_all": "Alles selecteren" "select_all": "Alles selecteren"
}, },

View File

@ -59,6 +59,7 @@ export const defaultState = {
moves: true, moves: true,
emojiReactions: true, emojiReactions: true,
followRequest: true, followRequest: true,
reports: true,
chatMention: true, chatMention: true,
polls: true polls: true
}, },

View File

@ -2,20 +2,29 @@ import filter from 'lodash/filter'
const reports = { const reports = {
state: { state: {
reportModal: {
userId: null, userId: null,
statuses: [], statuses: [],
preTickedIds: [], preTickedIds: [],
modalActivated: false activated: false
},
reports: {}
}, },
mutations: { mutations: {
openUserReportingModal (state, { userId, statuses, preTickedIds }) { openUserReportingModal (state, { userId, statuses, preTickedIds }) {
state.userId = userId state.reportModal.userId = userId
state.statuses = statuses state.reportModal.statuses = statuses
state.preTickedIds = preTickedIds state.reportModal.preTickedIds = preTickedIds
state.modalActivated = true state.reportModal.activated = true
}, },
closeUserReportingModal (state) { 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: { actions: {
@ -31,6 +40,23 @@ const reports = {
}, },
closeUserReportingModal ({ commit }) { closeUserReportingModal ({ commit }) {
commit('closeUserReportingModal') 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)
} }
} }
} }

View File

@ -337,6 +337,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
} }
if (notification.type === 'pleroma:report') {
dispatch('addReport', notification.report)
}
if (notification.type === 'pleroma:emoji_reaction') { if (notification.type === 'pleroma:emoji_reaction') {
dispatch('fetchEmojiReactionsBy', notification.status.id) dispatch('fetchEmojiReactionsBy', notification.status.id)
} }

View File

@ -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_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read` 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_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 PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const oldfetch = window.fetch const oldfetch = window.fetch
@ -588,7 +589,8 @@ const fetchTimeline = ({
listId = false, listId = false,
tag = false, tag = false,
withMuted = false, withMuted = false,
replyVisibility = 'all' replyVisibility = 'all',
includeTypes = []
}) => { }) => {
const timelineUrls = { const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE, public: MASTODON_PUBLIC_TIMELINE,
@ -640,6 +642,11 @@ const fetchTimeline = ({
if (replyVisibility !== 'all') { if (replyVisibility !== 'all') {
params.push(['reply_visibility', replyVisibility]) params.push(['reply_visibility', replyVisibility])
} }
if (includeTypes.length > 0) {
includeTypes.forEach(type => {
params.push(['include_types[]', type])
})
}
params.push(['limit', 20]) 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 = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -1523,7 +1562,8 @@ const apiService = {
chatMessages, chatMessages,
sendChatMessage, sendChatMessage,
readChat, readChat,
deleteChatMessage deleteChatMessage,
setReportState
} }
export default apiService export default apiService

View File

@ -390,6 +390,13 @@ export const parseNotification = (data) => {
: parseUser(data.target) : parseUser(data.target)
output.from_profile = parseUser(data.account) output.from_profile = parseUser(data.account)
output.emoji = data.emoji 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 { } else {
const parsedNotice = parseStatus(data.notice) const parsedNotice = parseStatus(data.notice)
output.type = data.ntype output.type = data.ntype

View File

@ -15,6 +15,7 @@ export const visibleTypes = store => {
rootState.config.notificationVisibility.followRequest && 'follow_request', rootState.config.notificationVisibility.followRequest && 'follow_request',
rootState.config.notificationVisibility.moves && 'move', 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',
rootState.config.notificationVisibility.polls && 'poll' rootState.config.notificationVisibility.polls && 'poll'
].filter(_ => _)) ].filter(_ => _))
} }
@ -99,6 +100,9 @@ export const prepareNotificationObject = (notification, i18n) => {
case 'follow_request': case 'follow_request':
i18nString = 'follow_request' i18nString = 'follow_request'
break break
case 'pleroma:report':
i18nString = 'submitted_report'
break
case 'poll': case 'poll':
i18nString = 'poll_ended' i18nString = 'poll_ended'
break break

View File

@ -1,6 +1,18 @@
import apiService from '../api/api.service.js' import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.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 }) => { const update = ({ store, notifications, older }) => {
store.dispatch('addNewNotifications', { notifications, older }) store.dispatch('addNewNotifications', { notifications, older })
} }
@ -12,6 +24,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const timelineData = rootState.statuses.notifications const timelineData = rootState.statuses.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts const hideMutedPosts = getters.mergedConfig.hideMutedPosts
args.includeTypes = mastoApiNotificationTypes
args.withMuted = !hideMutedPosts args.withMuted = !hideMutedPosts
args.timeline = 'notifications' args.timeline = 'notifications'
@ -63,6 +76,7 @@ const fetchNotifications = ({ store, args, older }) => {
messageArgs: [error.message], messageArgs: [error.message],
timeout: 5000 timeout: 5000
}) })
console.error(error)
}) })
} }