Files
singlechat/client/src/components/ChatWindow.vue
Torsten Schulz (local) bfe28a25fa
All checks were successful
Deploy SingleChat / deploy (push) Successful in 40s
android app, extension of stats
2026-08-10 14:03:34 +02:00

410 lines
9.2 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="chat-window">
<div v-if="!chatStore.currentConversation" class="no-conversation">
<div class="empty-icon">C</div>
<h2>Unterhaltung starten</h2>
<p>Wähle eine Person aus der aktiven Liste oder suche gezielt nach jemandem.</p>
<div class="empty-actions">
<button type="button" @click="chatStore.setView('search')">Zur Suche</button>
<button type="button" class="secondary" @click="chatStore.setView('history')">Verlauf ansehen</button>
</div>
<div class="empty-stats">
<span>
<strong>Sicherer Chat</strong>
Privater Austausch
</span>
<span>
<strong>{{ chatStore.users.length }} online</strong>
Aktive Kontakte
</span>
<span>
<strong>Profile</strong>
Direkt erreichbar
</span>
</div>
</div>
<div v-else class="messages-container">
<div v-if="currentVideoSession" class="video-call-banner">
<div class="video-call-banner-copy">
<strong>{{ currentVideoSession.withUserName }}</strong>
<span>{{ statusLabel(currentVideoSession.status) }} · {{ currentVideoSession.remoteMuted ? 'Mikro aus' : 'Mikro an' }}</span>
</div>
<div class="video-call-banner-actions">
<button
v-if="currentVideoSession.status === 'ringing' && currentVideoSession.initiatedBy !== chatStore.userName"
type="button"
@click="chatStore.acceptVideoCall(currentVideoSession.callId)"
>
Annehmen
</button>
<button
v-if="currentVideoSession.status === 'ringing' && currentVideoSession.initiatedBy !== chatStore.userName"
type="button"
class="danger"
@click="chatStore.rejectVideoCall(currentVideoSession.callId)"
>
Ablehnen
</button>
<button
v-if="currentVideoSession.status === 'ringing' && currentVideoSession.initiatedBy === chatStore.userName"
type="button"
class="secondary"
@click="chatStore.cancelVideoCall(currentVideoSession.callId)"
>
Abbrechen
</button>
<button
v-if="currentVideoSession.status === 'connecting' || currentVideoSession.status === 'active'"
type="button"
class="secondary"
@click="chatStore.bringVideoSessionToFront(currentVideoSession.callId)"
>
Vordergrund
</button>
</div>
</div>
<div
v-for="(message, index) in chatStore.messages"
:key="index"
:class="['output-box-format', message.self ? 'ouput-box-format-self' : 'output-box-format-other']"
:title="formatTime(message.timestamp)"
>
<strong>{{ message.from }}:</strong>
<span v-if="message.isImage" class="image-message">
<img
:src="message.message"
:alt="'Bild von ' + message.from"
class="chat-image"
@click="openImageModal(message.message)"
/>
</span>
<span v-else v-html="replaceSmileys(message.message)"></span>
</div>
<div v-if="selectedImage" class="image-modal-overlay" @click="closeImageModal">
<div class="image-modal-content" @click.stop>
<button class="image-modal-close" @click="closeImageModal" title="Schließen">×</button>
<img :src="selectedImage" alt="Vergrößertes Bild" class="image-modal-image" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import { useChatStore } from '../stores/chat';
const chatStore = useChatStore();
const selectedImage = ref(null);
const currentVideoSession = computed(() => chatStore.currentConversationVideoSession);
function openImageModal(imageSrc) {
selectedImage.value = imageSrc;
}
function closeImageModal() {
selectedImage.value = null;
}
const smileys = {
':)': { code: '1F642' },
':D': { code: '1F600' },
':(': { code: '1F641' },
';)': { code: '1F609' },
':p': { code: '1F60B' },
';p': { code: '1F61C' },
'O)': { code: '1F607' },
':*': { code: '1F617' },
'(h)': { code: '1FA77' },
'xD': { code: '1F602' },
':@': { code: '1F635' },
':O': { code: '1F632' },
':3': { code: '1F63A' },
':|': { code: '1F610' },
':/': { code: '1FAE4' },
':#': { code: '1F912' },
'#)': { code: '1F973' },
'%)': { code: '1F974' },
'(t)': { code: '1F44D' },
":'(": { code: '1F622' }
};
function replaceSmileys(text) {
if (!text) return '';
let outputText = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
const sortedCodes = Object.keys(smileys).sort((a, b) => b.length - a.length);
for (const code of sortedCodes) {
const regex = new RegExp(escapeRegex(code), 'g');
outputText = outputText.replace(regex, `&#x${smileys[code].code};`);
}
return outputText;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
function statusLabel(status) {
switch (status) {
case 'ringing':
return 'Videoanruf klingelt';
case 'connecting':
return 'Videoanruf verbindet';
case 'active':
return 'Videoanruf aktiv';
default:
return status;
}
}
</script>
<style scoped>
.no-conversation {
min-height: 100%;
padding: 60px 24px;
text-align: center;
color: #4f5c54;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-icon {
width: 74px;
height: 74px;
border-radius: 8px;
display: grid;
place-items: center;
background: #ffffff;
color: #1e6840;
font-size: 24px;
font-weight: 900;
box-shadow: 0 18px 34px rgba(29, 45, 36, 0.08);
}
.no-conversation h2 {
margin: 24px 0 12px;
color: #202720;
font-size: 25px;
line-height: 1.15;
}
.no-conversation p {
max-width: 430px;
margin: 0;
font-size: 15px;
line-height: 1.55;
}
.empty-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
margin-top: 28px;
}
.empty-actions button {
min-height: 46px;
border: 0;
border-radius: 8px;
padding: 0 24px;
background: #1f6e43;
color: #ffffff;
font-weight: 800;
}
.empty-actions button.secondary {
background: #ebeeec;
color: #2c5f40;
}
.empty-stats {
display: grid;
grid-template-columns: repeat(3, minmax(120px, 1fr));
gap: 12px;
width: min(520px, 100%);
margin-top: 48px;
}
.empty-stats span {
min-height: 58px;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
justify-content: center;
background: rgba(255, 255, 255, 0.72);
color: #707c73;
font-size: 10px;
line-height: 1.2;
}
.empty-stats strong {
margin-bottom: 3px;
color: #253027;
font-size: 11px;
}
.messages-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.video-call-banner {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid #d8e1da;
border-radius: 10px;
background: #f7fbf8;
}
.video-call-banner-copy {
display: flex;
flex-direction: column;
gap: 3px;
}
.video-call-banner-copy strong {
color: #223026;
}
.video-call-banner-copy span {
color: #58685d;
font-size: 13px;
}
.video-call-banner-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.video-call-banner-actions button {
min-height: 34px;
border: 0;
border-radius: 8px;
padding: 0 12px;
background: #1d6a42;
color: #ffffff;
font-weight: 700;
cursor: pointer;
}
.video-call-banner-actions button.secondary {
background: #edf2ee;
color: #1d6a42;
border: 1px solid #ccd9cf;
}
.video-call-banner-actions button.danger {
background: #b03737;
}
.chat-image {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
margin-top: 0.5em;
display: block;
cursor: pointer;
transition: opacity 0.2s;
}
.chat-image:hover {
opacity: 0.8;
}
.image-message {
display: block;
}
.image-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.image-modal-content {
position: relative;
width: 80%;
height: 80%;
background-color: #fff;
border-radius: 14px;
border: 1px solid #d7dfd9;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.image-modal-close {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
border: none;
border-radius: 10px;
width: 40px;
height: 40px;
font-size: 28px;
line-height: 1;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
z-index: 1001;
transition: background-color 0.2s;
}
.image-modal-close:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.image-modal-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 4px;
}
@media (max-width: 620px) {
.empty-stats {
grid-template-columns: 1fr;
}
.video-call-banner {
flex-direction: column;
align-items: stretch;
}
}
</style>