import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { io } from 'socket.io-client'; export const useChatStore = defineStore('chat', () => { // State const isLoggedIn = ref(false); const userName = ref(''); const gender = ref(''); const age = ref(0); const country = ref(''); const isoCountryCode = ref(''); const sessionId = ref(''); const socket = ref(null); const users = ref([]); const currentConversation = ref(null); const messages = ref([]); const currentView = ref('chat'); const searchResults = ref([]); const inboxResults = ref([]); const historyResults = ref([]); const unreadChatsCount = ref(0); const errorMessage = ref(null); const commandTable = ref(null); const remainingSecondsToTimeout = ref(1800); const awaitingLoginUsername = ref(false); const awaitingLoginPassword = ref(false); const searchData = ref({ nameIncludes: '', minAge: null, maxAge: null, genders: [], selectedCountries: [], // Übersetzte Namen (für UI) selectedCountriesEnglish: [] // Englische Namen (für Server) }); let timeoutTimer = null; const TIMEOUT_SECONDS = 1800; // 30 Minuten // Computed const currentConversationWith = computed(() => { if (!currentConversation.value) return null; return currentConversation.value; }); // Actions function connectWebSocket() { return new Promise((resolve, reject) => { // Schließe alte Verbindung, falls vorhanden if (socket.value) { try { socket.value.disconnect(); } catch (e) { // Ignoriere Fehler beim Schließen } socket.value = null; } let url; if (import.meta.env.DEV) { // Socket.IO läuft jetzt auf dem gleichen Port wie Express url = 'http://localhost:3300'; } else { // In Production: Socket.IO läuft über den gleichen Host/Port wie die Webseite // Apache leitet alles an den Backend-Server weiter url = window.location.origin; } console.log('=== Socket.IO-Verbindung ==='); console.log('Versuche Socket.IO-Verbindung zu:', url); console.log('Aktuelle Seite:', window.location.href); console.log('DEV-Modus:', import.meta.env.DEV); let timeoutId; let resolved = false; try { const socketInstance = io(url, { transports: ['polling'], // Nur Polling verwenden, um WebSocket-Probleme zu vermeiden reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000, upgrade: false, // Kein Upgrade zu WebSocket rememberUpgrade: false, withCredentials: true // Wichtig für Cookies/Sessions }); // Timeout nach 5 Sekunden timeoutId = setTimeout(() => { if (!resolved) { resolved = true; socketInstance.disconnect(); reject(new Error('Socket.IO-Verbindung-Timeout: Server antwortet nicht. Bitte stelle sicher, dass der Server auf Port 3300 läuft.')); } }, 5000); socketInstance.on('connect', async () => { if (!resolved) { resolved = true; clearTimeout(timeoutId); console.log('Socket.IO-Verbindung erfolgreich'); socket.value = socketInstance; // Hole Express-Session-ID und sende sie an den Server try { const response = await fetch('/api/session', { credentials: 'include' }); if (response.ok) { const data = await response.json(); if (data.sessionId) { console.log('Socket.IO Connect - Sende Express-Session-ID:', data.sessionId); socketInstance.emit('setSessionId', { expressSessionId: data.sessionId }); } } } catch (error) { console.error('Fehler beim Abrufen der Session-ID:', error); } resolve(socketInstance); } }); socketInstance.on('connected', (data) => { console.log('Connected-Nachricht empfangen:', data); sessionId.value = data.sessionId; // Wenn bereits eingeloggt, Login-Status wiederherstellen if (data.loggedIn && data.user) { isLoggedIn.value = true; userName.value = data.user.userName; gender.value = data.user.gender; age.value = data.user.age; country.value = data.user.country; isoCountryCode.value = data.user.isoCountryCode; startTimeoutTimer(); } }); socketInstance.on('disconnect', (reason) => { console.log('Socket.IO-Verbindung getrennt:', reason); socket.value = null; }); socketInstance.on('connect_error', (error) => { console.error('Socket.IO Verbindungsfehler:', error); if (!resolved) { resolved = true; clearTimeout(timeoutId); reject(new Error('Socket.IO-Verbindung fehlgeschlagen: ' + error.message)); } }); // Event-Handler für verschiedene Nachrichtentypen socketInstance.on('loginSuccess', (data) => { handleWebSocketMessage({ type: 'loginSuccess', ...data }); }); socketInstance.on('userList', (data) => { handleWebSocketMessage({ type: 'userList', ...data }); }); socketInstance.on('message', (data) => { handleWebSocketMessage({ type: 'message', ...data }); }); socketInstance.on('messageSent', (data) => { handleWebSocketMessage({ type: 'messageSent', ...data }); }); socketInstance.on('messageSent', (data) => { handleWebSocketMessage({ type: 'messageSent', ...data }); }); socketInstance.on('conversation', (data) => { handleWebSocketMessage({ type: 'conversation', ...data }); }); socketInstance.on('searchResults', (data) => { handleWebSocketMessage({ type: 'searchResults', ...data }); }); socketInstance.on('inboxResults', (data) => { handleWebSocketMessage({ type: 'inboxResults', ...data }); }); socketInstance.on('historyResults', (data) => { handleWebSocketMessage({ type: 'historyResults', ...data }); }); socketInstance.on('commandResult', (data) => { handleWebSocketMessage({ type: 'commandResult', ...data }); }); socketInstance.on('commandTable', (data) => { handleWebSocketMessage({ type: 'commandTable', ...data }); }); socketInstance.on('unreadChats', (data) => { handleWebSocketMessage({ type: 'unreadChats', ...data }); }); socketInstance.on('error', (data) => { handleWebSocketMessage({ type: 'error', ...data }); }); console.log('Socket.IO-Objekt erstellt'); } catch (error) { if (timeoutId) clearTimeout(timeoutId); console.error('Fehler beim Erstellen der Socket.IO-Verbindung:', error); reject(new Error('Fehler beim Erstellen der Socket.IO-Verbindung: ' + error.message)); } }); } function handleWebSocketMessage(data) { console.log('WebSocket-Nachricht empfangen:', data.type); switch (data.type) { case 'connected': sessionId.value = data.sessionId; break; case 'loginSuccess': isLoggedIn.value = true; userName.value = data.user.userName; gender.value = data.user.gender; age.value = data.user.age; country.value = data.user.country; isoCountryCode.value = data.user.isoCountryCode; sessionId.value = data.sessionId; break; case 'userList': users.value = data.users; // Aktualisiere Suchergebnisse, falls eine Suche aktiv ist updateSearchResults(); break; case 'message': // Debug-Logging für empfangene Nachrichten if (data.isImage) { console.log('[Bild empfangen] Von:', data.from, 'URL:', data.imageUrl || data.message); } // Sound abspielen bei neuer Nachricht (nur wenn nicht selbst gesendet) if (!data.self) { try { const audio = new Audio('/static/newmessage.mp3'); audio.play().catch(err => { // Ignoriere Fehler (z.B. wenn Browser Auto-Play blockiert) console.log('Sound konnte nicht abgespielt werden:', err); }); } catch (error) { console.log('Fehler beim Abspielen des Sounds:', error); } } if (currentConversation.value === data.from) { const newMessage = { from: data.from, message: data.imageUrl || data.message, // Verwende URL für Bilder timestamp: data.timestamp, self: false, isImage: data.isImage || false, imageType: data.imageType || null, imageUrl: data.imageUrl || null, imageCode: data.imageCode || null }; console.log('[Nachricht hinzugefügt]', newMessage); messages.value.push(newMessage); } else { console.log('[Nachricht ignoriert] Aktuelle Konversation:', currentConversation.value, 'Nachricht von:', data.from); } // Timeout zurücksetzen bei empfangener Nachricht resetTimeoutTimer(); break; case 'messageSent': // Bestätigung, dass Nachricht gesendet wurde break; case 'conversation': currentConversation.value = data.with; messages.value = data.messages.map(msg => ({ from: msg.from, message: msg.imageUrl || msg.message, // Verwende URL für Bilder timestamp: msg.timestamp, self: msg.from === userName.value, isImage: msg.isImage || false, imageType: msg.imageType || null, imageUrl: msg.imageUrl || null, imageCode: msg.imageCode || null })); break; case 'searchResults': searchResults.value = data.results; break; case 'inboxResults': inboxResults.value = data.results; break; case 'historyResults': historyResults.value = data.results; break; case 'commandResult': { const lines = Array.isArray(data.lines) ? data.lines : []; const kind = data.kind || 'info'; if (kind === 'loginPromptUsername') { awaitingLoginUsername.value = true; awaitingLoginPassword.value = false; } else if (kind === 'loginPromptPassword') { awaitingLoginUsername.value = false; awaitingLoginPassword.value = true; } else if (kind === 'loginSuccess' || kind === 'loginError' || kind === 'loginAbort' || kind === 'loginLogout') { awaitingLoginUsername.value = false; awaitingLoginPassword.value = false; } // Command-Ausgaben immer global anzeigen, nicht im Chatverlauf. errorMessage.value = lines.join(' | '); setTimeout(() => { errorMessage.value = null; }, 5000); break; } case 'commandTable': { const title = data.title || 'Ausgabe'; const columns = Array.isArray(data.columns) ? data.columns : []; const rows = Array.isArray(data.rows) ? data.rows : []; commandTable.value = { title, columns, rows }; // Tabelle ist persistent; temporäre Fehlermeldung löschen errorMessage.value = null; break; } case 'unreadChats': unreadChatsCount.value = data.count || 0; break; case 'error': console.error('Server-Fehler:', data.message); errorMessage.value = data.message; // Fehlermeldung nach 5 Sekunden automatisch entfernen setTimeout(() => { errorMessage.value = null; }, 5000); break; } } async function login(userNameVal, genderVal, ageVal, countryVal) { // Stelle sicher, dass Socket.IO verbunden ist if (!socket.value || !socket.value.connected) { console.log('Socket.IO nicht verbunden, versuche Verbindung herzustellen...'); try { await connectWebSocket(); // Warte kurz, damit die Verbindung vollständig hergestellt ist await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { console.error('Fehler beim Verbinden mit Socket.IO:', error); alert('Verbindung zum Server fehlgeschlagen. Bitte stelle sicher, dass der Server läuft.'); return; } } // Prüfe nochmal, ob die Verbindung jetzt besteht if (!socket.value || !socket.value.connected) { console.error('Socket.IO-Verbindung konnte nicht hergestellt werden'); alert('Verbindung zum Server fehlgeschlagen. Bitte stelle sicher, dass der Server läuft.'); return; } // Hole Express-Session-ID vom Server let expressSessionId = null; try { const response = await fetch('/api/session', { credentials: 'include' }); if (response.ok) { const data = await response.json(); expressSessionId = data.sessionId; console.log('Login - Express-Session-ID erhalten:', expressSessionId); } } catch (error) { console.error('Fehler beim Abrufen der Session-ID:', error); } socket.value.emit('login', { userName: userNameVal, gender: genderVal, age: ageVal, country: countryVal, expressSessionId: expressSessionId }); } function sendMessage(toUserName, message, options = {}) { if (!socket.value || !socket.value.connected) { console.error('Socket.IO nicht verbunden'); return; } const messageId = Date.now().toString(); const trimmed = message.trim(); const isCommand = trimmed.startsWith('/'); socket.value.emit('message', { toUserName, message: trimmed, messageId }); const suppressLocal = !!options.suppressLocal || isCommand || awaitingLoginUsername.value || awaitingLoginPassword.value; // Lokal hinzufügen (außer bei Commands oder sensiblen Eingaben wie Login) if (!isCommand && !suppressLocal) { messages.value.push({ from: userName.value, message: trimmed, timestamp: new Date().toISOString(), self: true }); } // Timeout zurücksetzen bei Aktivität resetTimeoutTimer(); } function sendImage(toUserName, imageCode, imageUrl) { if (!socket.value || !socket.value.connected) { console.error('Socket.IO nicht verbunden'); return; } if (!toUserName) { console.error('Empfänger fehlt'); return; } const messageId = Date.now().toString(); socket.value.emit('message', { toUserName, message: imageCode, // Nur der Code, nicht das gesamte Bild messageId, isImage: true, imageUrl: imageUrl // URL für das Bild }); // Lokal hinzufügen (mit URL) messages.value.push({ from: userName.value, message: imageUrl, // Verwende URL statt Code für lokale Anzeige timestamp: new Date().toISOString(), self: true, isImage: true, imageCode: imageCode }); // Timeout zurücksetzen bei Aktivität resetTimeoutTimer(); } function requestConversation(withUserName) { if (!socket.value || !socket.value.connected) { console.error('Socket.IO nicht verbunden'); errorMessage.value = 'Socket.IO nicht verbunden'; setTimeout(() => { errorMessage.value = null; }, 5000); return; } // Fehlermeldung zurücksetzen errorMessage.value = null; socket.value.emit('requestConversation', { withUserName }); currentConversation.value = withUserName; currentView.value = 'chat'; } function userSearch(searchDataPayload) { if (!socket.value || !socket.value.connected) { console.error('Socket.IO nicht verbunden'); errorMessage.value = 'Socket.IO nicht verbunden'; setTimeout(() => { errorMessage.value = null; }, 5000); return; } // Fehlermeldung zurücksetzen errorMessage.value = null; // Speichere Suchparameter für spätere Aktualisierungen // Hinweis: selectedCountries wird in SearchView.vue verwaltet und enthält übersetzte Namen // Für updateSearchResults speichern wir auch die englischen Länder-Namen searchData.value.nameIncludes = searchDataPayload.nameIncludes || ''; searchData.value.minAge = searchDataPayload.minAge || null; searchData.value.maxAge = searchDataPayload.maxAge || null; searchData.value.genders = searchDataPayload.genders || []; // Speichere die englischen Länder-Namen für spätere Aktualisierungen if (searchDataPayload.countries) { searchData.value.selectedCountriesEnglish = searchDataPayload.countries; } socket.value.emit('userSearch', searchDataPayload); resetTimeoutTimer(); // Aktivität zurücksetzen } function updateSearchResults() { // Aktualisiere Suchergebnisse nur, wenn eine Suche aktiv ist // Prüfe, ob der Benutzer auf der Suchseite ist UND ob es bereits Suchergebnisse gibt const hasSearchResults = searchResults.value && searchResults.value.length > 0; const hasSearchParams = searchData.value.nameIncludes || searchData.value.minAge || searchData.value.maxAge || (searchData.value.selectedCountries && searchData.value.selectedCountries.length > 0) || (searchData.value.genders && searchData.value.genders.length > 0); // Aktualisiere nur, wenn der Benutzer auf der Suchseite ist UND (Ergebnisse vorhanden ODER Parameter gesetzt) if (currentView.value !== 'search' || (!hasSearchResults && !hasSearchParams)) { return; } // Führe die Suche erneut aus, um aktuelle Ergebnisse zu erhalten // Verwende die englischen Länder-Namen, die beim letzten Suchvorgang gespeichert wurden const searchPayload = { nameIncludes: searchData.value.nameIncludes || null, minAge: searchData.value.minAge || null, maxAge: searchData.value.maxAge || null, countries: searchData.value.selectedCountriesEnglish && searchData.value.selectedCountriesEnglish.length > 0 ? searchData.value.selectedCountriesEnglish : null, genders: searchData.value.genders && searchData.value.genders.length > 0 ? searchData.value.genders : null }; // Sende Suchanfrage erneut an den Server if (socket.value && socket.value.connected) { socket.value.emit('userSearch', searchPayload); } } function requestHistory() { if (!socket.value || !socket.value.connected) { console.error('Socket.IO nicht verbunden'); return; } socket.value.emit('requestHistory'); } function requestOpenConversations() { if (!socket.value || !socket.value.connected) { console.error('Socket.IO nicht verbunden'); return; } socket.value.emit('requestOpenConversations'); } function clearCommandTable() { commandTable.value = null; } function setView(view) { currentView.value = view; // Search-Ergebnisse NICHT zurücksetzen, damit sie beim Zurückkehren erhalten bleiben if (view === 'search') { // Wenn zur Suchseite zurückgekehrt wird, aktualisiere die Suchergebnisse // falls bereits Suchparameter vorhanden sind updateSearchResults(); } else if (view === 'inbox') { requestOpenConversations(); } else if (view === 'history') { requestHistory(); } } function logout() { stopTimeoutTimer(); isLoggedIn.value = false; userName.value = ''; gender.value = ''; age.value = 0; country.value = ''; isoCountryCode.value = ''; sessionId.value = ''; users.value = []; currentConversation.value = null; messages.value = []; currentView.value = 'chat'; searchResults.value = []; inboxResults.value = []; historyResults.value = []; commandTable.value = null; searchData.value = { nameIncludes: '', minAge: null, maxAge: null, genders: [], selectedCountries: [], selectedCountriesEnglish: [] }; if (socket.value) { socket.value.disconnect(); socket.value = null; } } function startTimeoutTimer() { stopTimeoutTimer(); // Stoppe alten Timer, falls vorhanden remainingSecondsToTimeout.value = TIMEOUT_SECONDS; timeoutTimer = setInterval(() => { remainingSecondsToTimeout.value--; if (remainingSecondsToTimeout.value <= 0) { stopTimeoutTimer(); // Auto-Logout console.log('Timeout erreicht - automatischer Logout'); logout(); } }, 1000); // Jede Sekunde aktualisieren } function resetTimeoutTimer() { if (isLoggedIn.value && timeoutTimer) { remainingSecondsToTimeout.value = TIMEOUT_SECONDS; } } function stopTimeoutTimer() { if (timeoutTimer) { clearInterval(timeoutTimer); timeoutTimer = null; } remainingSecondsToTimeout.value = TIMEOUT_SECONDS; } async function restoreSession() { try { console.log('restoreSession: Starte Session-Wiederherstellung...'); const response = await fetch('/api/session', { credentials: 'include' // Wichtig für Cookies }); if (!response.ok) { console.log('restoreSession: Response nicht OK:', response.status); return false; } const data = await response.json(); console.log('restoreSession: Antwort vom Server:', data); if (data.loggedIn && data.user) { console.log('restoreSession: Session gefunden, stelle Login-Status wieder her...'); // Session wiederherstellen isLoggedIn.value = true; userName.value = data.user.userName; gender.value = data.user.gender; age.value = data.user.age; country.value = data.user.country; isoCountryCode.value = data.user.isoCountryCode; sessionId.value = data.user.sessionId; console.log('restoreSession: Login-Status wiederhergestellt:', { userName: userName.value, sessionId: sessionId.value }); // WebSocket-Verbindung herstellen try { await connectWebSocket(); startTimeoutTimer(); } catch (error) { console.error('Fehler beim Wiederherstellen der WebSocket-Verbindung:', error); } return true; } console.log('restoreSession: Keine gültige Session gefunden'); return false; } catch (error) { console.error('Fehler beim Wiederherstellen der Session:', error); return false; } } return { // State isLoggedIn, userName, gender, age, country, isoCountryCode, sessionId, socket, users, currentConversation, messages, currentView, searchResults, inboxResults, historyResults, unreadChatsCount, remainingSecondsToTimeout, errorMessage, commandTable, searchData, awaitingLoginUsername, awaitingLoginPassword, // Computed currentConversationWith, // Actions connectWebSocket, login, sendMessage, sendImage, requestConversation, userSearch, requestHistory, requestOpenConversations, clearCommandTable, setView, logout, restoreSession }; });