feat(Chat): implement chat incident reporting feature

- Added reportChatIncident method in ChatController to handle reporting of chat incidents.
- Introduced a new API route for reporting incidents in chatRouter.
- Implemented chatService methods to ensure the chat report table is created and to handle incident data storage.
- Enhanced frontend components to allow users to report incidents in both multi and random chat dialogs.
- Updated internationalization files to include new strings for reporting functionality in multiple languages.
This commit is contained in:
Torsten Schulz (local)
2026-04-27 15:00:52 +02:00
parent a21f0628b6
commit 278ed5992d
12 changed files with 297 additions and 8 deletions

View File

@@ -197,6 +197,9 @@
<img @click="action" src="/images/icons/activity.png" class="icon-btn"
:class="{ disabled: !selectedTargetUser }" :alt="$t('chat.multichat.action')"
:title="selectedTargetUser ? $t('chat.multichat.action_to', { to: selectedTargetUser }) : $t('chat.multichat.action_select_user')" />
<button type="button" class="send-btn report-btn" :disabled="!selectedTargetUser" @click="reportSelectedUser">
{{ $t('chat.multichat.report') }}
</button>
<img @click="roll" src="/images/icons/dice24.png" class="icon-btn" alt="Würfeln" title="Würfeln" />
<img @click="openColorPicker" src="/images/icons/colorpicker.png" class="icon-btn"
:alt="$t('chat.multichat.colorpicker')" :title="$t('chat.multichat.colorpicker')" />
@@ -236,7 +239,7 @@
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import { fetchPublicRooms, fetchRoomCreateOptions, fetchOwnRooms } from '@/api/chatApi.js';
import { fetchPublicRooms, fetchRoomCreateOptions, fetchOwnRooms, reportChatIncident } from '@/api/chatApi.js';
import { mapGetters } from 'vuex';
import { getChatWsUrl, getChatWsCandidates, getChatWsProtocols } from '@/services/chatWs.js';
import { confirmAction } from '@/utils/feedback.js';
@@ -1305,6 +1308,41 @@ export default {
this.sendWithToken(payload);
this.input = '';
},
async reportSelectedUser() {
if (!this.selectedTargetUser || !this.messages.length) return;
const confirmed = await confirmAction(this, {
title: this.$t('chat.multichat.reportConfirmTitle'),
message: this.$t('chat.multichat.reportConfirmMessage', { user: this.selectedTargetUser })
});
if (!confirmed) return;
const payload = {
context: 'multi_chat',
reporterHashedId: this.user?.hashedId || null,
reporterUsername: this.user?.username || null,
offenderUsername: this.selectedTargetUser,
incidentAt: new Date().toISOString(),
chatHistory: this.messages.map((entry) => ({
type: entry?.type || 'message',
user: entry?.user || '',
text: entry?.action
? `${entry.action} ${entry.to || ''}`.trim()
: (entry?.text || ''),
timestamp: Date.now(),
})),
metadata: {
roomId: this.selectedRoom || null,
roomName: this.getSelectedRoomName() || null,
usersInRoom: this.usersInRoom.map((u) => u.name),
}
};
try {
await reportChatIncident(payload);
this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.reportSent') });
} catch (_) {
this.messages.push({ id: Date.now(), user: 'System', text: this.$t('chat.multichat.reportError') });
}
},
roll() {
const payload = { type: 'dice', message: '' };
if (this.debug) console.log('[Chat WS >>]', payload);

View File

@@ -32,6 +32,14 @@
<input type="text" v-model="inputtext" @keyup.enter="sendMessage" />
</label>
<img src="/images/icons/enter16.png" @click="sendMessage" />
<button
v-if="partner"
type="button"
class="report-btn"
@click="reportCurrentChat"
>
{{ $t('chat.randomchat.report') }}
</button>
<img src="/images/icons/dice16.png" />
</div>
</div>
@@ -61,6 +69,7 @@ import DialogWidget from '@/components/DialogWidget.vue';
import { mapGetters } from 'vuex';
import axios from 'axios';
import DOMPurify from 'dompurify';
import { reportChatIncident } from '@/api/chatApi.js';
export default {
name: 'RandomChatDialog',
@@ -245,6 +254,37 @@ export default {
this.messages.push({ type: 'system', tr: 'chat.randomchat.waitingForMatch' });
}
},
async reportCurrentChat() {
if (!this.partner || !this.messages.length) return;
const confirmed = window.confirm(this.$t('chat.randomchat.reportConfirm'));
if (!confirmed) return;
const payload = {
context: 'random_chat',
reporterHashedId: this.user?.hashedId || null,
reporterRandomId: this.userId || null,
reporterUsername: this.user?.username || null,
offenderRandomId: this.partner?.id || null,
offenderUsername: this.$t('chat.randomchat.partner'),
incidentAt: new Date().toISOString(),
chatHistory: this.messages.map((entry) => ({
type: entry?.type || 'message',
user: entry?.type === 'self' ? (this.user?.username || 'self') : (entry?.user || 'partner'),
text: entry?.tr ? this.$t(entry.tr) : (entry?.text || ''),
timestamp: Date.now(),
})),
metadata: {
mode: 'random_chat',
partnerAge: this.partner?.age || null,
partnerGender: this.partner?.gender || null
}
};
try {
await reportChatIncident(payload);
this.messages.push({ type: 'system', text: this.$t('chat.randomchat.reportSent') });
} catch (_) {
this.messages.push({ type: 'system', text: this.$t('chat.randomchat.reportError') });
}
},
renderMessage(message) {
if (message.type === 'system') {
@@ -318,4 +358,8 @@ export default {
.inputline>label>input {
flex: 1;
}
.report-btn {
margin-left: 0.4em;
}
</style>