- 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.
366 lines
13 KiB
Vue
366 lines
13 KiB
Vue
<template>
|
|
<DialogWidget ref="dialog" title="chat.randomchat.title" icon="dice24.png" :show-close="true" :buttons="buttons"
|
|
:modal="false" :isTitleTranslated="true" @close="closeDialog" name="RandomChat">
|
|
<div v-if="chatIsRunning" class="randomchat">
|
|
<div class="headline">
|
|
{{ $t("chat.randomchat.agerange") }}
|
|
<input type="number" v-model="agefromsearch" min="18" max="150" size="5" />
|
|
-
|
|
<input type="number" v-model="agetosearch" min="18" max="150" size="5" />
|
|
<span class="multiselect">
|
|
{{ $t("chat.randomchat.gendersearch") }}
|
|
<div>
|
|
<label><input type="checkbox" v-model="searchmale" />{{ $t("chat.randomchat.gender.male") }}</label>
|
|
<label><input type="checkbox" v-model="searchfemale" />{{ $t("chat.randomchat.gender.female")
|
|
}}</label>
|
|
</div>
|
|
</span>
|
|
<label><input type="checkbox" v-model="camonlysearch" />{{ $t("chat.randomchat.camonly") }}</label>
|
|
<label><input type="checkbox" v-model="showcam" />{{ $t("chat.randomchat.showcam") }}</label>
|
|
<img v-if="isLoggedIn" src="/images/icons/friendsadd16.png" :tooltip="$t('chat.randomchat.addfriend')" />
|
|
<label><input type="checkbox" v-model="autosearch" />{{ $t("chat.randomchat.autosearch") }}</label>
|
|
<button @click="nextUser" v-if="partner != null">{{ $t("chat.randomchat.jumptonext") }}</button>
|
|
<button @click="startSearch" v-if="partner == null && !searching">{{ $t("chat.randomchat.startsearch")
|
|
}}</button>
|
|
</div>
|
|
<div class="output">
|
|
<div v-for="message in messages" v-html="renderMessage(message)" />
|
|
</div>
|
|
<div class="inputline">
|
|
<label>
|
|
{{ $t("chat.randomchat.input") }}
|
|
<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>
|
|
<div v-else>
|
|
<div>
|
|
<label>{{ $t("chat.randomchat.age") }}
|
|
<input type="number" v-model="age" min="18" max="150" value="18" />
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<label>{{ $t("chat.randomchat.gender.title") }}
|
|
<select v-model="gender">
|
|
<option value="f">{{ $t("chat.randomchat.gender.female") }}</option>
|
|
<option value="m">{{ $t("chat.randomchat.gender.male") }}</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<div>
|
|
<button @click="startRandomChat()">{{ $t("chat.randomchat.start") }}</button>
|
|
</div>
|
|
</div>
|
|
</DialogWidget>
|
|
</template>
|
|
|
|
<script>
|
|
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',
|
|
components: {
|
|
DialogWidget,
|
|
},
|
|
computed: {
|
|
...mapGetters(['isLoggedIn', 'user']),
|
|
buttons() {
|
|
// Use translation key; DialogWidget will translate when isTitleTranslated=true
|
|
return [{ text: 'chat.randomchat.close' }];
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
chatIsRunning: false,
|
|
age: 18,
|
|
gender: 'f',
|
|
agefromsearch: 18,
|
|
agetosearch: 150,
|
|
searchmale: true,
|
|
searchfemale: true,
|
|
camonlysearch: false,
|
|
showcam: false,
|
|
autosearch: false,
|
|
inputtext: '',
|
|
searching: false,
|
|
userId: null, // ID, die vom Chat-Backend beim Register zurückkommt
|
|
searchInterval: null,
|
|
messages: [],
|
|
partner: null,
|
|
messagesInterval: null,
|
|
};
|
|
},
|
|
methods: {
|
|
async open() {
|
|
if (this.user && Array.isArray(this.user.param)) {
|
|
const bdParam = this.user.param.find(p => p.name === 'birthdate')?.value;
|
|
if (bdParam) {
|
|
const birth = new Date(bdParam);
|
|
const ageYears = Math.floor(
|
|
(Date.now() - birth.getTime()) / (365.25 * 24 * 60 * 60 * 1000)
|
|
);
|
|
this.age = ageYears;
|
|
}
|
|
const gParam = this.user.param.find(p => p.name === 'gender')?.value;
|
|
if (gParam != null) {
|
|
// value==='1' ⇒ männlich
|
|
this.gender = gParam === '1' ? 'm' : 'f';
|
|
}
|
|
}
|
|
this.$refs.dialog.open();
|
|
},
|
|
|
|
async registerUser() {
|
|
try {
|
|
const response = await axios.post('/api/chat/register', {
|
|
gender: this.gender,
|
|
age: this.age,
|
|
});
|
|
this.userId = response.data.id;
|
|
} catch (error) {
|
|
console.error('Error registering user:', error);
|
|
}
|
|
},
|
|
|
|
async closeDialog() {
|
|
// Stop intervals first
|
|
if (this.searchInterval) {
|
|
clearInterval(this.searchInterval);
|
|
this.searchInterval = null;
|
|
}
|
|
if (this.messagesInterval) {
|
|
clearInterval(this.messagesInterval);
|
|
this.messagesInterval = null;
|
|
}
|
|
// Inform backend and cleanup
|
|
try {
|
|
if (this.userId) {
|
|
await axios.post('/api/chat/exit', { id: this.userId });
|
|
await this.removeUserFromChat();
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
// Reset state and close widget
|
|
this.chatIsRunning = false;
|
|
this.partner = null;
|
|
this.messages = [];
|
|
this.$refs.dialog.close();
|
|
},
|
|
|
|
async startRandomChat() {
|
|
this.chatIsRunning = true;
|
|
await this.registerUser();
|
|
await this.startSearch();
|
|
},
|
|
|
|
async startSearch() {
|
|
this.searching = true;
|
|
await this.findMatch();
|
|
this.messages.push({ type: 'system', tr: 'chat.randomchat.waitingForMatch' });
|
|
this.searchInterval = setInterval(this.findMatch, 500);
|
|
},
|
|
|
|
async findMatch() {
|
|
try {
|
|
const response = await axios.post('/api/chat/findMatch', {
|
|
genders: this.getSearchGenders(),
|
|
age: { min: this.agefromsearch, max: this.agetosearch },
|
|
id: this.userId,
|
|
});
|
|
if (response.data.status === 'matched') {
|
|
this.searching = false;
|
|
clearInterval(this.searchInterval);
|
|
const initText = this.$t('chat.randomchat.chatpartner')
|
|
.replace(
|
|
'<gender>',
|
|
this.$t(`chat.randomchat.partnergender${response.data.user.gender}`)
|
|
)
|
|
.replace('<age>', response.data.user.age);
|
|
this.messages = [{ type: 'system', text: initText }];
|
|
this.partner = response.data.user;
|
|
this.messagesInterval = setInterval(this.getNewMessages, 250);
|
|
} else if (this.autosearch && !this.searchInterval) {
|
|
this.searchInterval = setInterval(this.findMatch, 500);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error finding match:', error);
|
|
}
|
|
},
|
|
|
|
getSearchGenders() {
|
|
const genders = [];
|
|
if (this.searchmale) genders.push('m');
|
|
if (this.searchfemale) genders.push('f');
|
|
return genders;
|
|
},
|
|
|
|
async sendMessage() {
|
|
if (!this.inputtext.trim()) return;
|
|
const response = await axios.post('/api/chat/sendMessage', {
|
|
from: this.userId,
|
|
to: this.partner.id,
|
|
text: this.inputtext,
|
|
});
|
|
this.messages.push({ type: 'self', text: response.data.text });
|
|
this.inputtext = '';
|
|
},
|
|
|
|
async getNewMessages() {
|
|
if (!this.partner) return;
|
|
const response = await axios.post('/api/chat/getMessages', {
|
|
to: this.userId,
|
|
from: this.partner.id,
|
|
});
|
|
const newMsgs = response.data.filter((m) => !m.activity);
|
|
const activities = response.data.filter((m) => m.activity);
|
|
activities.forEach((act) => {
|
|
if (act.activity === 'otheruserleft') {
|
|
this.partner = null;
|
|
this.messages.push({ type: 'system', tr: 'chat.randomchat.userleftchat' });
|
|
}
|
|
});
|
|
this.messages.push(...newMsgs);
|
|
},
|
|
|
|
async removeUserFromChat() {
|
|
try {
|
|
await axios.post('/api/chat/remove', { id: this.userId });
|
|
} catch (error) {
|
|
console.error('Error removing user from chat:', error);
|
|
}
|
|
},
|
|
|
|
async nextUser() {
|
|
await axios.post('/api/chat/leave', { id: this.userId });
|
|
this.partner = null;
|
|
this.messages.push({ type: 'system', tr: 'chat.randomchat.selfstopped' });
|
|
if (this.autosearch) {
|
|
this.searchInterval = setInterval(this.findMatch, 500);
|
|
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') {
|
|
const txt = message.tr ? this.$t(message.tr) : message.text;
|
|
return `<span class="rc-system">${txt}</span>`;
|
|
}
|
|
const cls = message.type === 'self' ? 'rc-self' : 'rc-partner';
|
|
const who = message.type === 'self' ? this.$t('chat.randomchat.self') : this.$t('chat.randomchat.partner');
|
|
return `<span class="${cls}">${who}: </span>${message.text}`;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.randomchat {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
gap: 0.5em;
|
|
}
|
|
|
|
.randomchat>div {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
|
|
.randomchat>.headline {
|
|
gap: 1.5em;
|
|
}
|
|
|
|
.multiselect {
|
|
border: 1px solid black;
|
|
position: relative;
|
|
padding: 0.1em;
|
|
}
|
|
|
|
.multiselect>div {
|
|
border: 1px solid black;
|
|
position: absolute;
|
|
top: 1.5em;
|
|
left: -1px;
|
|
text-align: left;
|
|
background-color: rgba(256, 256, 256, 0.9);
|
|
display: none;
|
|
}
|
|
|
|
.multiselect:hover>div {
|
|
display: inline-block;
|
|
}
|
|
|
|
.output {
|
|
border: 1px solid #909090;
|
|
padding: 0.2em;
|
|
overflow: auto;
|
|
flex: 1;
|
|
flex-direction: column !important;
|
|
text-align: left;
|
|
}
|
|
|
|
.inputline {
|
|
display: flex;
|
|
gap: 0.5em;
|
|
}
|
|
|
|
.inputline>label {
|
|
flex: 1;
|
|
display: flex;
|
|
}
|
|
|
|
.inputline>label>input {
|
|
flex: 1;
|
|
}
|
|
|
|
.report-btn {
|
|
margin-left: 0.4em;
|
|
}
|
|
</style>
|