410 lines
9.2 KiB
Vue
Executable File
410 lines
9.2 KiB
Vue
Executable File
<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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
|
||
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>
|