improved error handling and added retry buttons in chat page

This commit is contained in:
2026-01-15 22:57:44 +01:00
parent 376243f64c
commit 7e2bf22e7b
5 changed files with 179 additions and 29 deletions

View File

@@ -1,5 +1,6 @@
<template>
<InvitePeopleModal v-if="showInviteModal" :room_uuid=props.uuid @close="showInviteModal = false" />
<InvitePeopleModal v-if="showInviteModal && isSocketConnected" :room_uuid=props.uuid
@close="showInviteModal = false" />
<div v-if="uuid === 'none'" class="no-room">
<div class="empty-state">
@@ -12,16 +13,25 @@
<h2 class="room-name">{{ currentRoom?.name }}</h2>
<div class="messages-container" ref="messageListRef" @scroll="handleScroll">
<MessageList :messages="messages" />
<MessageList v-if="isSocketConnected" :messages="messages" />
</div>
<p v-if="!isSocketConnected" class="wait-msg">{{ $t('chat-connecting') }}</p>
<div class="input-container">
<button v-if="isOwner && !currentRoom?.global" class="invite-btn" @click="showInviteModal = true"
:title="$t('chat-invite-title')">
<i class="fa-solid fa-users"></i>
</button>
<MessageInput ref="messageInputRef" @send="onSend" />
<div v-if="connectionError" class="connection-error">
<p>{{ connectionError }}</p>
<button class="retry-btn" @click="initializeRoom">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<MessageInput v-if="isSocketConnected" ref="messageInputRef" @send="onSend" />
</div>
</div>
</template>
@@ -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<Message[]>([]);
const messageListRef = ref<HTMLElement | null>(null);
const messageInputRef = ref<InstanceType<typeof MessageInput> | null>(null);
const currentUser = ref<User | null>(null);
const currentRoom = ref<Room | null>(null);
const connectionError = ref<string | null>(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();
});
</script>
<style scoped>
.chat-container {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
@@ -207,6 +271,19 @@ onUnmounted(async () => {
scroll-behavior: auto;
}
.wait-msg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
color: var(--muted);
font-size: 1.1rem;
z-index: 10;
pointer-events: none;
text-align: center;
}
.input-container {
display: flex;
gap: 10px;
@@ -247,6 +324,23 @@ onUnmounted(async () => {
justify-content: center;
}
.connection-error {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 10px;
}
.retry-btn {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.retry-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
.no-room {
height: 100%;

View File

@@ -1,4 +1,8 @@
<template>
<Teleport to="body">
<CreateRoomModal v-if="showCreate" @close="showCreate = false" @created="rooms.push($event)" />
</Teleport>
<div class="room-list">
<header class="rooms-header">
<h2>{{ $t('chat-room-list-title') }}</h2>
@@ -7,9 +11,12 @@
</button>
</header>
<Teleport to="body">
<CreateRoomModal v-if="showCreate" @close="showCreate = false" @created="rooms.push($event)" />
</Teleport>
<div class="wait-container" v-if="!rooms || rooms.length === 0">
<p class="wait-msg">{{ $t('chat-room-list-connecting') }}</p>
<button class="retry-btn" @click="refreshRooms()">
<i class="fa-solid fa-rotate-right"></i>
</button>
</div>
<div class="scroll-area">
<router-link v-for="room in rooms" :key="room.uuid" :to="`/rooms/${room.uuid}`" class="btn room-item"
@@ -36,6 +43,10 @@ const rooms = ref<Room[]>([]);
const emit = defineEmits(['select-room']);
async function refreshRooms() {
rooms.value = await fetchRooms();
}
onMounted(async () => {
rooms.value = await fetchRooms();
});
@@ -43,6 +54,7 @@ onMounted(async () => {
<style scoped>
.room-list {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
@@ -50,6 +62,37 @@ onMounted(async () => {
-webkit-user-select: none;
}
.wait-container {
display: flex;
flex-direction: column;
gap: 1.2rem;
align-items: center;
justify-content: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
}
.wait-msg {
color: var(--muted);
font-size: 1.1rem;
z-index: 5;
pointer-events: none;
text-align: center;
}
.retry-btn {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.retry-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
.rooms-header {
padding: 15px;
display: flex;

View File

@@ -46,6 +46,9 @@ import { readFile } from '@tauri-apps/plugin-fs';
import { refreshLocalUser } from '../store.ts';
import { getAuthData } from '../store.ts';
import { refreshAvatar } from '../store.ts';
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const emit = defineEmits(['close', 'updated']);
@@ -110,7 +113,7 @@ async function setFile(path: string) {
errorMessage.value = '';
} catch (e) {
console.error("Error reading file:", e);
errorMessage.value = "Could not read image file.";
errorMessage.value = $t('settings-error-upload-avatar-failed-read')
}
}
@@ -144,7 +147,7 @@ async function handleUpload() {
emit('close');
} catch (err: any) {
console.error("Upload failed:", err);
errorMessage.value = err.message || 'Failed to upload avatar';
errorMessage.value = $t('settings-error-upload-avatar-failed-upload');
isSubmitting.value = false;
}
}

View File

@@ -32,12 +32,15 @@ chat-invite-friend-too = Also send a friend request
chat-invite-send = Send
chat-invite-username-placeholder = username
chat-room-list-title = Rooms
chat-room-list-connecting = Connecting...
chat-room-owner = by {$owner}
chat-create-title = Create room
chat-create-name = Room name
chat-create-name-placeholder = room name
chat-create-global = Global room
chat-create-submit = Create
chat-connecting = Connecting to room...
chat-connecting-failed = Could not connect. Check internet connection.
## Friends page
friends-title = Your friends
@@ -82,6 +85,8 @@ settings-update-save = Save Changes
settings-updating = Updating...
settings-error-required = Username and Email are required.
settings-error-failed = Update failed
settings-error-upload-avatar-failed-read = Failed to read image
settings-error-upload-avatar-failed-upload = Failed to upload image
## Warning
warning-wrongversion-title = Wrong app version

View File

@@ -32,12 +32,15 @@ chat-invite-friend-too = Envoyer aussi une demande d'ami
chat-invite-send = Envoyer
chat-invite-username-placeholder = nom d'utilisateur
chat-room-list-title = Salons
chat-room-list-connecting = Connexion...
chat-room-owner = par {$owner}
chat-create-title = Créer un salon
chat-create-name = Nom du salon
chat-create-name-placeholder = nom du salon
chat-create-global = Salon public
chat-create-submit = Créer
chat-connecting = Connexion au salon...
chat-connecting-failed = Impossible d'établir la connexion. Vérifiez votre internet.
## Friends page
friends-title = Vos amis
@@ -80,6 +83,8 @@ settings-upload-avatar-btn = Importer un avatar
settings-upload-avatar-title = Importer un avatar
settings-error-required = Le nom d'utilisateur et l'email sont requis.
settings-error-failed = Échec de la mise à jour
settings-error-upload-avatar-failed-read = Erreur de lecture de l'image
settings-error-upload-avatar-failed-upload = Erreur d'envoi de l'image
## Warning
warning-wrongversion-title = Mauvaise version de l'application