diff --git a/src/components/ChatWindow.vue b/src/components/ChatWindow.vue index 0c66008..3389f32 100644 --- a/src/components/ChatWindow.vue +++ b/src/components/ChatWindow.vue @@ -1,5 +1,6 @@ @@ -37,37 +47,71 @@ import InvitePeopleModal from './InvitePeopleModal.vue'; import WebSocket from '@tauri-apps/plugin-websocket'; import { getAuthData } from "../store.ts"; import { fetchRoomInfo } from "../api/rooms.ts"; +import { useFluent } from 'fluent-vue'; + +const { $t } = useFluent(); const props = defineProps<{ uuid: string }>(); + +// UI State const messages = ref([]); const messageListRef = ref(null); const messageInputRef = ref | null>(null); const currentUser = ref(null); const currentRoom = ref(null); +const connectionError = ref(null); // Pagination State const isLoadingMore = ref(false); -const hasMore = ref(true); // Assume there are more until API returns empty +const hasMore = ref(true); const showInviteModal = ref(false); + +// WebSocket State let socket: WebSocket | null = null; +const isSocketConnected = ref(false); +let unlistenSocket: (() => void) | null = null; const isOwner = computed(() => { if (!currentUser.value || !currentRoom.value) return false; return currentUser.value.uuid === currentRoom.value.owner_uuid; }); +// Detaches listeners and attempts to close the socket. +async function cleanupWebSocket() { + isSocketConnected.value = false; + + if (unlistenSocket) { + unlistenSocket(); + unlistenSocket = null; + } + + if (socket) { + const tempSocket = socket; + socket = null; + + try { + await tempSocket.disconnect(); + } catch (err) { + console.warn("Socket cleanup warning (non-fatal):", err); + } + } +} + async function initializeRoom() { - if (socket) { await socket.disconnect(); socket = null; } + await cleanupWebSocket(); messages.value = []; - hasMore.value = true; currentRoom.value = null; + hasMore.value = true; + connectionError.value = null; + + isSocketConnected.value = false; if (props.uuid === 'none') return; try { const [msgs, roomInfo, auth] = await Promise.all([ - fetchMessages(props.uuid, undefined, 40), // Load first 40 + fetchMessages(props.uuid, undefined, 40), fetchRoomInfo(props.uuid), getAuthData() ]); @@ -80,21 +124,45 @@ async function initializeRoom() { await nextTick(); scrollToBottom(); + await connectWebSocket(); + + } catch (err) { + console.error("Room initialization failed:", err); + connectionError.value = $t('chat-connecting-failed'); + } +} + +async function connectWebSocket() { + try { const wsToken = await getWsToken(props.uuid); const url = `${API_WS}/rooms/${props.uuid}?token=${wsToken}`; + socket = await WebSocket.connect(url); - socket.addListener((msg) => { + isSocketConnected.value = true; + + unlistenSocket = socket.addListener((msg) => { if (msg.type === 'Text') { - const data: Message = JSON.parse(msg.data); - if (!messages.value.some(m => m.uuid === data.uuid)) { - messages.value.push(data); - nextTick().then(scrollToBottomIfAtEnd); + try { + const data: Message = JSON.parse(msg.data); + + // if (props.uuid !== 'none' && data.room_uuid && data.room_uuid !== props.uuid) { + // return; + // } + + if (!messages.value.some(m => m.uuid === data.uuid)) { + messages.value.push(data); + nextTick().then(scrollToBottomIfAtEnd); + } + } catch (e) { + console.error("Error parsing message:", e); } } }); } catch (err) { - console.error("Room initialization failed:", err); + console.error("WS Connect failed:", err); + isSocketConnected.value = false; + connectionError.value = "Live chat disconnected."; } } @@ -102,7 +170,6 @@ async function handleScroll() { const el = messageListRef.value; if (!el) return; - // If user scrolls to the top, is not already loading, and there's more data if (el.scrollTop < 50 && !isLoadingMore.value && hasMore.value) { await loadMore(); } @@ -122,7 +189,6 @@ async function loadMore() { return; } - // Capture height before adding messages to maintain scroll position const el = messageListRef.value; const previousScrollHeight = el?.scrollHeight || 0; @@ -130,7 +196,6 @@ async function loadMore() { await nextTick(); - // Restore scroll position so the view doesn't jump if (el) { el.scrollTop = el.scrollHeight - previousScrollHeight; } @@ -149,7 +214,6 @@ function scrollToBottom() { } } -// Only scroll to bottom for new messages if the user is already near the bottom function scrollToBottomIfAtEnd() { const el = messageListRef.value; if (!el) return; @@ -158,8 +222,6 @@ function scrollToBottomIfAtEnd() { if (isAtBottom) scrollToBottom(); } - - const handleGlobalKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter') { const active = document.activeElement?.tagName.toLowerCase(); @@ -176,8 +238,11 @@ async function onSend(content: string) { await sendMessage(props.uuid, content); } -watch(() => props.uuid, () => { - initializeRoom(); +// Watch for room changes +watch(() => props.uuid, (newUuid, oldUuid) => { + if (newUuid !== oldUuid) { + initializeRoom(); + } }); onMounted(() => { @@ -186,15 +251,14 @@ onMounted(() => { }); onUnmounted(async () => { - if (socket) { - await socket.disconnect(); - } window.removeEventListener('keydown', handleGlobalKeyDown); + await cleanupWebSocket(); });