(async () => { // Vérification de la permission pour envoyer des notifications // C'est utile pour prévenir les utilisateur⋅rices de l'arrivée de nouveaux messages les mentionnant await Notification.requestPermission() })() const MAX_MESSAGES = 50 // Nombre maximal de messages à charger à la fois const channel_categories = ['general', 'tournament', 'team', 'private'] // Liste des catégories de canaux let channels = {} // Liste des canaux disponibles let messages = {} // Liste des messages reçus par canal let selected_channel_id = null // Canal courant /** * Affiche une nouvelle notification avec le titre donné et le contenu donné. * @param title Le titre de la notification * @param body Le contenu de la notification * @param timeout La durée (en millisecondes) après laquelle la notification se ferme automatiquement. * Définir à 0 (défaut) pour la rendre infinie. * @return Notification */ function showNotification(title, body, timeout = 0) { Notification.requestPermission().then((status) => { if (status === 'granted') { // On envoie la notification que si la permission a été donnée let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm-192.png"}) if (timeout > 0) setTimeout(() => notif.close(), timeout) return notif } }) } /** * Sélectionne le canal courant à afficher sur l'interface de chat. * Va alors définir le canal courant et mettre à jour les messages affichés. * @param channel_id L'identifiant du canal à afficher. */ function selectChannel(channel_id) { let channel = channels[channel_id] if (!channel) { // Le canal n'existe pas console.error('Channel not found:', channel_id) return } selected_channel_id = channel_id // On stocke dans le stockage local l'identifiant du canal // pour pouvoir rouvrir le dernier canal ouvert dans le futur localStorage.setItem('chat.last-channel-id', channel_id) // Définition du titre du contenu let channelTitle = document.getElementById('channel-title') channelTitle.innerText = channel.name // Si on a pas le droit d'écrire dans le canal, on désactive l'input de message // On l'active sinon let messageInput = document.getElementById('input-message') messageInput.disabled = !channel.write_access // On redessine la liste des messages à partir des messages stockés redrawMessages() } /** * On récupère le message écrit par l'utilisateur⋅rice dans le champ de texte idoine, * et on le transmet ensuite au serveur. * Il ne s'affiche pas instantanément sur l'interface, * mais seulement une fois que le serveur aura validé et retransmis le message. */ function sendMessage() { // Récupération du message à envoyer let messageInput = document.getElementById('input-message') let message = messageInput.value // On efface le champ de texte après avoir récupéré le message messageInput.value = '' if (!message) { return } // Envoi du message au serveur socket.send(JSON.stringify({ 'type': 'send_message', 'channel_id': selected_channel_id, 'content': message, })) } /** * Met à jour la liste des canaux disponibles, à partir de la liste récupérée du serveur. * @param new_channels La liste des canaux à afficher. * Chaque canal doit être un objet avec les clés `id`, `name`, `category` * `read_access`, `write_access` et `unread_messages`, correspondant à l'identifiant du canal, * son nom, sa catégorie, la permission de lecture, d'écriture et le nombre de messages non lus. */ function setChannels(new_channels) { channels = {} for (let category of channel_categories) { // On commence par vider la liste des canaux sélectionnables let categoryList = document.getElementById(`nav-${category}-channels-tab`) categoryList.innerHTML = '' categoryList.parentElement.classList.add('d-none') } for (let channel of new_channels) // On ajoute chaque canal à la liste des canaux addChannel(channel) if (new_channels && (!selected_channel_id || !channels[selected_channel_id])) { // Si aucun canal n'a encore été sélectionné et qu'il y a des canaux disponibles, // on commence par vérifier si on a stocké un canal précédemment sélectionné et on l'affiche si c'est le cas // Sinon, on affiche le premier canal disponible let last_channel_id = parseInt(localStorage.getItem('chat.last-channel-id')) if (last_channel_id && channels[last_channel_id]) selectChannel(last_channel_id) else selectChannel(Object.keys(channels)[0]) } } /** * Ajoute un canal à la liste des canaux disponibles. * @param channel Le canal à ajouter. Doit être un objet avec les clés `id`, `name`, `category`, * `read_access`, `write_access` et `unread_messages`, correspondant à l'identifiant du canal, * son nom, sa catégorie, la permission de lecture, d'écriture et le nombre de messages non lus. */ async function addChannel(channel) { channels[channel.id] = channel if (!messages[channel.id]) messages[channel.id] = new Map() // On récupère la liste des canaux de la catégorie concernée let categoryList = document.getElementById(`nav-${channel.category}-channels-tab`) // On la rend visible si elle ne l'était pas déjà categoryList.parentElement.classList.remove('d-none') // On crée un nouvel élément de liste pour la catégorie concernant le canal let navItem = document.createElement('li') navItem.classList.add('list-group-item', 'tab-channel') navItem.id = `tab-channel-${channel.id}` navItem.setAttribute('data-bs-dismiss', 'offcanvas') navItem.onclick = () => selectChannel(channel.id) categoryList.appendChild(navItem) // L'élément est cliquable afin de sélectionner le canal let channelButton = document.createElement('button') channelButton.classList.add('nav-link') channelButton.type = 'button' channelButton.innerText = channel.name navItem.appendChild(channelButton) // Affichage du nombre de messages non lus let unreadBadge = document.createElement('span') unreadBadge.classList.add('badge', 'rounded-pill', 'text-bg-light', 'ms-2') unreadBadge.id = `unread-messages-${channel.id}` unreadBadge.innerText = channel.unread_messages || 0 if (!channel.unread_messages) unreadBadge.classList.add('d-none') channelButton.appendChild(unreadBadge) // Si on veut trier les canaux par nombre décroissant de messages non lus, // on définit l'ordre de l'élément (propriété CSS) en fonction du nombre de messages non lus if (document.getElementById('sort-by-unread-switch').checked) navItem.style.order = `${-channel.unread_messages}` // On demande enfin à récupérer les derniers messages du canal en question afin de les stocker / afficher fetchMessages(channel.id) } /** * Un⋅e utilisateur⋅rice a envoyé un message, qui a été retransmis par le serveur. * On le stocke alors et on l'affiche sur l'interface si nécessaire. * On affiche également une notification si le message contient une mention pour tout le monde. * @param message Le message qui a été transmis. Doit être un objet avec * les clés `id`, `channel_id`, `author`, `author_id`, `content` et `timestamp`, * correspondant à l'identifiant du message, du canal, le nom de l'auteur⋅rice et l'heure d'envoi. */ function receiveMessage(message) { // On vérifie si la barre de défilement est tout en bas let scrollableContent = document.getElementById('chat-messages') let isScrolledToBottom = scrollableContent.scrollHeight - scrollableContent.clientHeight <= scrollableContent.scrollTop + 1 // On stocke le message dans la liste des messages du canal concerné // et on redessine les messages affichés si on est dans le canal concerné messages[message.channel_id].set(message.id, message) if (message.channel_id === selected_channel_id) redrawMessages() // Si la barre de défilement était tout en bas, alors on la remet tout en bas après avoir redessiné les messages if (isScrolledToBottom) scrollableContent.scrollTop = scrollableContent.scrollHeight - scrollableContent.clientHeight // On ajoute un à la liste des messages non lus du canal (il pourra être lu plus tard) updateUnreadBadge(message.channel_id, channels[message.channel_id].unread_messages + 1) // Si le message contient une mention à @everyone, alors on envoie une notification (si la permission est donnée) if (message.content.includes("@everyone")) showNotification(channels[message.channel_id].name, `${message.author} : ${message.content}`) // On envoie un événement personnalisé pour indiquer que les messages ont été mis à jour // Permettant entre autres de marquer le message comme lu si c'est le cas document.getElementById('message-list').dispatchEvent(new CustomEvent('updatemessages')) } /** * Un message a été modifié, et le serveur nous a transmis les nouvelles informations. * @param data Le nouveau message qui a été modifié. */ function editMessage(data) { // On met à jour le contenu du message messages[data.channel_id].get(data.id).content = data.content // Si le message appartient au canal courant, on redessine les messages if (data.channel_id === selected_channel_id) redrawMessages() } /** * Un message a été supprimé, et le serveur nous a transmis les informations. * @param data Le message qui a été supprimé. */ function deleteMessage(data) { // On supprime le message de la liste des messages du canal concerné messages[data.channel_id].delete(data.id) // Si le message appartient au canal courant, on redessine les messages if (data.channel_id === selected_channel_id) redrawMessages() } /** * Demande au serveur de récupérer les messages du canal donné. * @param channel_id L'identifiant du canal dont on veut récupérer les messages. * @param offset Le décalage à partir duquel on veut récupérer les messages, * correspond au nombre de messages en mémoire. * @param limit Le nombre maximal de messages à récupérer. */ function fetchMessages(channel_id, offset = 0, limit = MAX_MESSAGES) { // Envoi de la requête au serveur avec les différents paramètres socket.send(JSON.stringify({ 'type': 'fetch_messages', 'channel_id': channel_id, 'offset': offset, 'limit': limit, })) } /** * Demande au serveur de récupérer les messages précédents du canal courant. * Par défaut, on récupère `MAX_MESSAGES` messages avant tous ceux qui ont été reçus sur ce canal. */ function fetchPreviousMessages() { let channel_id = selected_channel_id let offset = messages[channel_id].size fetchMessages(channel_id, offset, MAX_MESSAGES) } /** * L'utilisateur⋅rice a demandé à récupérer une partie des messages d'un canal. * Cette fonction est alors appelée lors du retour du serveur. * @param data Dictionnaire contenant l'identifiant du canal concerné, et la liste des messages récupérés. */ function receiveFetchedMessages(data) { // Récupération du canal concerné ainsi que des nouveaux messages à mémoriser let channel_id = data.channel_id let new_messages = data.messages if (!messages[channel_id]) messages[channel_id] = new Map() // Ajout des nouveaux messages à la liste des messages du canal for (let message of new_messages) messages[channel_id].set(message.id, message) // On trie les messages reçus par date et heure d'envoi messages[channel_id] = new Map([...messages[channel_id].values()] .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)) .map(message => [message.id, message])) // Enfin, si le canal concerné est le canal courant, on redessine les messages if (channel_id === selected_channel_id) redrawMessages() } /** * L'utilisateur⋅rice a indiqué au serveur que des messages ont été lus. * Cette fonction est appelée en retour, pour confirmer, et stocke quels messages ont été lus * et combien de messages sont non lus par canal. * @param data Dictionnaire contenant une clé `read`, contenant la liste des identifiants des messages * marqués comme lus avec leur canal respectif, et une clé `unread_messages` contenant le nombre * de messages non lus par canal. */ function markMessageAsRead(data) { for (let message of data.messages) { // Récupération du message à marquer comme lu let stored_message = messages[message.channel_id].get(message.id) // Marquage du message comme lu if (stored_message) stored_message.read = true } // Actualisation des badges contenant le nombre de messages non lus par canal updateUnreadBadges(data.unread_messages) } /** * Mise à jour des badges contenant le nombre de messages non lus par canal. * @param unreadMessages Dictionnaire des nombres de messages non lus par canal (identifiés par leurs identifiants) */ function updateUnreadBadges(unreadMessages) { for (let channel of Object.values(channels)) { // Récupération du nombre de messages non lus pour le canal en question et mise à jour du badge pour ce canal updateUnreadBadge(channel.id, unreadMessages[channel.id] || 0) } } /** * Mise à jour du badge du nombre de messages non lus d'un canal. * Actualise sa visibilité. * @param channel_id Identifiant du canal concerné. * @param unreadMessagesCount Nombre de messages non lus du canal. */ function updateUnreadBadge(channel_id, unreadMessagesCount = 0) { // Vaut true si on veut trier les canaux par nombre de messages non lus ou non const sortByUnread = document.getElementById('sort-by-unread-switch').checked // Récupération du canal concerné let channel = channels[channel_id] // Récupération du nombre de messages non lus pour le canal en question, que l'on stocke channel.unread_messages = unreadMessagesCount // On met à jour le badge du canal contenant le nombre de messages non lus let unreadBadge = document.getElementById(`unread-messages-${channel.id}`) unreadBadge.innerText = unreadMessagesCount.toString() // Le badge est visible si et seulement si il y a au moins un message non lu if (unreadMessagesCount) unreadBadge.classList.remove('d-none') else unreadBadge.classList.add('d-none') // S'il faut trier les canaux par nombre de messages non lus, on ajoute la propriété CSS correspondante if (sortByUnread) document.getElementById(`tab-channel-${channel.id}`).style.order = `${-unreadMessagesCount}` } /** * La création d'un canal privé entre deux personnes a été demandée. * Cette fonction est appelée en réponse du serveur. * Le canal est ajouté à la liste s'il est nouveau, et automatiquement sélectionné. * @param data Dictionnaire contenant une unique clé `channel` correspondant aux informations du canal privé. */ function startPrivateChat(data) { // Récupération du canal let channel = data.channel if (!channel) { console.error('Private chat not found:', data) return } if (!channels[channel.id]) { // Si le canal n'est pas récupéré, on l'ajoute à la liste channels[channel.id] = channel messages[channel.id] = new Map() addChannel(channel) } // Sélection immédiate du canal privé selectChannel(channel.id) } /** * Met à jour le composant correspondant à la liste des messages du canal sélectionné. * Le conteneur est d'abord réinitialisé, puis les messages sont affichés un à un à partir de ceux stockés. */ function redrawMessages() { // Récupération du composant HTML