Ajout des sockets et du chargement des channels avec les sockets, de la deconnexion et resolution bug de gestion du sort des date des channels

This commit is contained in:
Bilal Dieumegard 2024-12-04 00:01:51 +01:00
parent 98c70f062a
commit 5f0be72d71
14 changed files with 151 additions and 40 deletions

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import Alert from "$lib/components/ui/Alert.svelte"; // Importer le composant Alert import Alert from "$lib/components/ui/Alert.svelte"; // Importer le composant Alert
export let show = false; export let show = false;
@ -9,6 +8,8 @@
let showAlert = false; let showAlert = false;
let alertMessage = ""; let alertMessage = "";
export let socket;
let chatName = ""; let chatName = "";
const createChat = async () => { const createChat = async () => {
@ -27,12 +28,15 @@
const data = await response.json(); const data = await response.json();
alertMessage = `Le chat "${data.name}" a été créé avec succès.`; alertMessage = `Le chat "${data.name}" a été créé avec succès.`;
chatName = ""; // Réinitialiser chatName = ""; // Réinitialiser
socket.emit("new-channel", data);
onClose?.(); // Fermer le composant après création onClose?.(); // Fermer le composant après création
} else { } else {
alertMessage = "Une erreur est survenue lors de la création du chat."; response.json().then(error => {
alertMessage = error.error;
});
} }
} catch (err) { } catch (err) {
alertMessage = "Erreur réseau ou serveur."; alertMessage = err;
} }
showAlert = true; showAlert = true;
@ -42,10 +46,6 @@
} }
}; };
const closeAlert = () => {
showAlert = false;
};
// Fonction pour détecter le clic en dehors // Fonction pour détecter le clic en dehors
let createChatRef: HTMLElement | null = null; let createChatRef: HTMLElement | null = null;
@ -75,7 +75,7 @@
</div> </div>
{/if} {/if}
<Alert show={showAlert} message={alertMessage} onClose={closeAlert} /> <Alert show={showAlert} message={alertMessage} onClose={() => (showAlert = false)} />
<style> <style>
.fixed { .fixed {

View file

@ -1,4 +1,6 @@
<script> <script>
import Button from '$lib/components/ui/button/button.svelte';
export let user = { export let user = {
pseudo: '', pseudo: '',
prenom: '', prenom: '',
@ -7,8 +9,24 @@
profilePictureUrl: '', // Ajouter l'URL de l'image de profil profilePictureUrl: '', // Ajouter l'URL de l'image de profil
}; // Infos utilisateur }; // Infos utilisateur
export let show = false; // Contrôle si la carte est visible export let show = false; // Contrôle si la carte est visible
export let onClose = () => { export let onClose = () => {}; // Fonction pour fermer la carte
}; // Fonction pour fermer la carte
const disconnect = async () => {
try {
// Envoyer une requête POST à l'endpoint /disconnect
const response = await fetch('/disconnect', {
method: 'POST',
});
// Vérifier si la déconnexion a réussi (ici, on se base sur le code de redirection)
if (response.redirected) {
// Si la redirection est effectuée, vous pouvez rediriger manuellement côté client
window.location.href = response.url;
}
} catch (error) {
console.error('Erreur lors de la déconnexion:', error);
}
};
</script> </script>
{#if show} {#if show}
@ -21,6 +39,7 @@
</div> </div>
<p>{user.prenom} {user.nom}</p> <p>{user.prenom} {user.nom}</p>
<p>{user.description}</p> <p>{user.description}</p>
<Button on:click={disconnect}>Deconnecter</Button>
</div> </div>
</div> </div>
{/if} {/if}

17
src/lib/stores/socket.ts Normal file
View file

@ -0,0 +1,17 @@
import { io } from "socket.io-client";
// Initialisation de la socket
export const initSocket = () => {
const socketInstance = io("http://localhost:5173");
// Événements globaux de connexion
socketInstance.on("connect", () => {
console.log("Connected to Socket.IO server:", socketInstance.id);
});
socketInstance.on("disconnect", () => {
console.log("Disconnected from Socket.IO server");
});
return socketInstance;
}

View file

@ -1,6 +1,9 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { initSocket } from "$lib/stores/socket";
export async function load({ locals, url }) { export async function load({ locals, url }) {
const token = locals.token; const token = locals.token;
if (token == undefined && url.pathname !== "/") { if (token == undefined && url.pathname !== "/") {

View file

@ -1,5 +1,5 @@
import { type Actions } from '@sveltejs/kit'; import { type Actions } from '@sveltejs/kit';
import { redirect, error } from '@sveltejs/kit'; import { redirect, error, fail } from '@sveltejs/kit';
import logger from '$lib/logger'; import logger from '$lib/logger';
export async function load({locals}) { export async function load({locals}) {
@ -9,7 +9,7 @@ export async function load({locals}) {
} }
export const actions: Actions = { export const actions: Actions = {
login: async ({request, fetch, cookies, locals}) => { login: async ({request, fetch, cookies}) => {
const formData = await request.formData(); const formData = await request.formData();
const response = await fetch('/api/auth/login', { const response = await fetch('/api/auth/login', {
@ -38,8 +38,7 @@ export const actions: Actions = {
return redirect(302, "/chats"); return redirect(302, "/chats");
} else { } else {
return fail(400, { error: data.message });
return error(400, data.message);
} }
}, },
@ -65,8 +64,7 @@ export const actions: Actions = {
return redirect(302, "/chats"); return redirect(302, "/chats");
} else { } else {
return fail(400, { error: data.message });
return error(400, data.message);
} }
} }
} }

View file

@ -1,10 +1,27 @@
<script lang="ts"> <script lang="ts">
import Alert from "$lib/components/ui/Alert.svelte";
import { Label } from "$lib/components/ui/label"; import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input"; import { Input } from "$lib/components/ui/input";
import * as Card from "$lib/components/ui/card"; import * as Card from "$lib/components/ui/card";
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import * as Tabs from "$lib/components/ui/tabs"; import * as Tabs from "$lib/components/ui/tabs";
let { data, form } = $props();
import { writable } from 'svelte/store';
const showAlert = writable(false);
const alertMessage = writable("");
$effect(() => {
// Manipuler l'état via les stores, ce qui est plus réactif
if (form?.error) {
alertMessage.set(form.error);
showAlert.set(true);
} else {
showAlert.set(false);
}
});
</script> </script>
@ -69,4 +86,6 @@
</Tabs.Content> </Tabs.Content>
</Tabs.Root> </Tabs.Root>
</div> </div>
<Alert message={$alertMessage} show={$showAlert} onClose={() => ($showAlert = false)} />

View file

@ -26,7 +26,7 @@ export async function POST({request}) {
logger.debug(`Found user with email (${email}) in database`); logger.debug(`Found user with email (${email}) in database`);
try { try {
if (await argon2.verify(user.password, password)) { if (await argon2.verify(user.password, password)) {
logger.debug(`Password for user ${user.email} is correct.`);
// @ts-ignore // @ts-ignore
const token = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: "1h" }); const token = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: "1h" });
logger.debug(`Generated a JWT token for user ${user.email}.`) logger.debug(`Generated a JWT token for user ${user.email}.`)
@ -39,7 +39,7 @@ export async function POST({request}) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) { } catch (e) {
logger.error(e); logger.error(e);
return error(500, {message: "Erreur interne."}); return error(500, {message: e.body.message});
} }

View file

@ -18,12 +18,24 @@ export async function GET({ params, url }) {
include: { include: {
messages: { messages: {
take: 1, // Récupère le dernier message take: 1, // Récupère le dernier message
orderBy: { createdAt: 'desc' }, // Trie par date décroissante orderBy: { createdAt: 'desc' },// Trie par date décroissante
// as lastMessage not list last message
}, },
}, },
}); });
console.log(canaux);
canaux = canaux.map((canaux) => {
return {
...canaux,
lastMessage: canaux.messages.length > 0 ? canaux.messages[0] : null,
messages: undefined
};
});
canaux = sortChannels(canaux); canaux = sortChannels(canaux);
console.log(canaux);
return json(canaux); return json(canaux);
@ -50,7 +62,16 @@ export async function GET({ params, url }) {
}, },
}); });
canaux = canaux.map((canaux) => {
return {
...canaux,
lastMessage: canaux.messages.length > 0 ? canaux.messages[0] : null,
messages: undefined
};
});
canaux = sortChannels(canaux); canaux = sortChannels(canaux);
console.log(canaux);
logger.debug('Caching channels with EX of 3600 secs'); logger.debug('Caching channels with EX of 3600 secs');
await redisClient.set('channels', JSON.stringify(canaux), { EX: 3600 }); await redisClient.set('channels', JSON.stringify(canaux), { EX: 3600 });
@ -69,7 +90,7 @@ export async function POST({ request }) {
const { name } = await request.json(); const { name } = await request.json();
try { try {
const canal = await prisma.channel.create({ let canal = await prisma.channel.create({
data: { data: {
name name
}, },
@ -79,13 +100,16 @@ export async function POST({ request }) {
const cachedChanels = await redisClient.get('channels'); const cachedChanels = await redisClient.get('channels');
let channels = cachedChanels != null ? JSON.parse(cachedChanels) : []; let channels = cachedChanels != null ? JSON.parse(cachedChanels) : [];
console.log(channels);
console.log(canal); canal = {
...canal,
lastMessage: null,
messages: undefined
}
channels.push(canal); channels.push(canal);
channels = sortChannels(channels); channels = sortChannels(channels);
console.log(channels);
logger.debug(`Added channel (${canal.id}) to channels cache.`); logger.debug(`Added channel (${canal.id}) to channels cache.`);
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 }); await redisClient.set('channels', JSON.stringify(channels), { EX: 600 });
@ -100,11 +124,16 @@ export async function POST({ request }) {
} }
function sortChannels(channels) { function sortChannels(channels) {
return channels.sort((a, b) => { channels = channels.map((channel) => {
// Vérifie si 'a.messages' existe et est un tableau, sinon utilise la date de création du canal return {
const lastMessageA = Array.isArray(a.messages) && a.messages.length > 0 ? a.messages[0]?.createdAt : a.createdAt; ...channel,
const lastMessageB = Array.isArray(b.messages) && b.messages.length > 0 ? b.messages[0]?.createdAt : b.createdAt; lastUpdate : channel.lastMessage != null ? channel.lastMessage.createdAt : channel.createdAt
};
});
return new Date(lastMessageB).getTime() - new Date(lastMessageA).getTime(); return channels.sort((a, b) => {
return new Date(b.lastUpdate) - new Date(a.lastUpdate);
}); });
} }

View file

@ -73,7 +73,6 @@ export async function POST({ params, request }) {
channelId, channelId,
text, text,
}, },
include: { user: { select: { id: true, username: true } } },
}); });
updateCaches(); // Mettre à jour les caches après la création dun nouveau message updateCaches(); // Mettre à jour les caches après la création dun nouveau message

View file

@ -6,7 +6,7 @@
import ProfileCard from "$lib/components/ui/ProfileCard.svelte"; // Importer le composant ProfileCard import ProfileCard from "$lib/components/ui/ProfileCard.svelte"; // Importer le composant ProfileCard
import CreateChat from "$lib/components/ui/CreateChat.svelte"; // Importer le composant CreateChat import CreateChat from "$lib/components/ui/CreateChat.svelte"; // Importer le composant CreateChat
import { formatDistanceToNow } from "$lib/utils/date.js"; import { formatDistanceToNow } from "$lib/utils/date.js";
import { io } from 'socket.io-client'; import { initSocket } from "$lib/stores/socket";
let showProfileCard = false; // État pour afficher ou masquer le ProfileCard let showProfileCard = false; // État pour afficher ou masquer le ProfileCard
let showCreateChat = false; // État pour afficher ou masquer CreateChat let showCreateChat = false; // État pour afficher ou masquer CreateChat
@ -18,6 +18,12 @@
profilePictureUrl: 'path/to/profile-picture.jpg', // URL de l'image de profil profilePictureUrl: 'path/to/profile-picture.jpg', // URL de l'image de profil
}; };
let socket = initSocket(); // Initialiser le socket
socket.on("new-channel", (channel) => {
channels = [channel, ...channels];
});
function openProfileCard() { function openProfileCard() {
console.log('openProfileCard'); console.log('openProfileCard');
showProfileCard = true; // Inverser l'état pour afficher/masquer le ProfilCard showProfileCard = true; // Inverser l'état pour afficher/masquer le ProfilCard
@ -85,12 +91,17 @@
<div class="flex flex-col gap-4 overflow-y-auto"> <div class="flex flex-col gap-4 overflow-y-auto">
{#each channels as channel} {#each channels as channel}
<ChatItem id={channel.id} title={channel.name} lastMessage={channel.lastMessage} time={formatDistanceToNow(channel.createdAt)} /> <ChatItem
id={channel.id}
title={channel.name}
lastMessage={channel.lastMessage ? channel.lastMessage.text : "Ecrire le premier message"}
time={formatDistanceToNow(channel.lastUpdate)}
/>
{/each} {/each}
</div> </div>
</div> </div>
<CreateChat show={showCreateChat} onClose={closeCreateChat} /> <CreateChat show={showCreateChat} socket={socket} onClose={closeCreateChat} />
<ProfileCard {user} show={showProfileCard} onClose={closeProfileCard} /> <ProfileCard {user} show={showProfileCard} onClose={closeProfileCard} />
<style> <style>

View file

@ -1,4 +1,4 @@
export async function load({ fetch, params }) { export async function load({ fetch, params, locals }) {
try { try {
const res = await fetch(`/api/channels/${params.id}/messages?page=1`, { const res = await fetch(`/api/channels/${params.id}/messages?page=1`, {
method: 'GET', method: 'GET',
@ -7,15 +7,18 @@ export async function load({ fetch, params }) {
} }
}); });
const messages = await res.json(); const messages = await res.json();
console.log(messages);
return { return {
messages, messages,
channelId: params.id, channelId: params.id,
userId: locals.userId
} }
}catch (error) { }catch (error) {
console.error('Erreur lors du chargement des messages:', error); console.error('Erreur lors du chargement des messages:', error);
return { return {
messages: [], messages: [],
channelId: params.id, channelId: params.id,
userId: locals.userId
}; };
} }
} }

View file

@ -6,7 +6,7 @@
import UserChat from '$lib/components/ui/UserChat.svelte'; import UserChat from '$lib/components/ui/UserChat.svelte';
export let data; export let data;
export let messages = data.messages; export let messages = data.messages.messages;
export let users = data.users; export let users = data.users;
let messageText = ''; let messageText = '';
@ -18,11 +18,12 @@
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ user: data.userId, text: messageText }), body: JSON.stringify({ userId: data.userId, text: messageText }),
}); });
if (response.ok) { if (response.ok) {
messageText = ''; messageText = '';
// Envoyer le message avec les sockets (à implémenter)
console.log('Message envoyé avec succès'); console.log('Message envoyé avec succès');
}else{ }else{
console.log('Erreur lors de l\'envoi du message'); console.log('Erreur lors de l\'envoi du message');
@ -53,7 +54,7 @@
<!-- Afficher les messages (mock d'un utilisateur sélectionné ou aucun message par défaut) --> <!-- Afficher les messages (mock d'un utilisateur sélectionné ou aucun message par défaut) -->
{#if messages.length > 0} {#if messages.length > 0}
{#each messages as message} {#each messages as message}
<Message username={message.username} messageContent={message.messageContent} /> <Message username={message.user.username} messageContent={message.text} />
{/each} {/each}
{:else} {:else}
<div class="text-center text-gray-500 mt-10">Sélectionnez un message le chat est vide.</div> <div class="text-center text-gray-500 mt-10">Sélectionnez un message le chat est vide.</div>
@ -62,7 +63,7 @@
<!-- Input pour envoyer un message --> <!-- Input pour envoyer un message -->
<div class="px-10 py-5 w-full flex gap-2 border-t"> <div class="px-10 py-5 w-full flex gap-2 border-t">
<Textarea class="h-16 resize-none flex-grow" placeholder="Écrivez un message..." value={messageText}/> <Textarea class="h-16 resize-none flex-grow" placeholder="Écrivez un message..." bind:value={messageText}/>
<Button size="icon" class="h-16 w-16 bg-blue-500 hover:bg-blue-600 h-full" on:click={sendMessage}> <Button size="icon" class="h-16 w-16 bg-blue-500 hover:bg-blue-600 h-full" on:click={sendMessage}>
<PaperPlane class="h-6 w-6" /> <PaperPlane class="h-6 w-6" />
</Button> </Button>

View file

@ -0,0 +1,10 @@
import { redirect } from '@sveltejs/kit';
export async function POST({ cookies }) {
// Supprimer les cookies "token" et "UID"
cookies.delete("token", { path: '/' });
cookies.delete("UID", { path: '/' });
// Rediriger vers la page d'accueil ou la page de connexion après déconnexion
throw redirect(303, '/');
}

View file

@ -11,8 +11,10 @@ const webSocketServer = {
const io = new Server(server.httpServer) const io = new Server(server.httpServer)
io.on('connection', (socket) => { io.on('connection', (socket) => {
socket.emit('eventFromServer', 'Hello, World 👋') socket.on('new-channel', (channel) => {
}) io.emit('new-channel', channel)
});
});
} }
} }