Merge pull request #7 from NabilOuldHamou/features/createMessage

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:02:12 +01:00 committed by GitHub
commit 0e806a7428
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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)
});
});
} }
} }