mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-11-04 02:12:05 +01:00 
			
		
		
		
	Minify CSS and JavaScript files
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
		
							
								
								
									
										912
									
								
								chat/static/tfjm/js/chat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										912
									
								
								chat/static/tfjm/js/chat.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,912 @@
 | 
			
		||||
(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 <ul> correspondant à la liste des messages affichés
 | 
			
		||||
    let messageList = document.getElementById('message-list')
 | 
			
		||||
    // On commence par le vider
 | 
			
		||||
    messageList.innerHTML = ''
 | 
			
		||||
 | 
			
		||||
    let lastMessage = null
 | 
			
		||||
    let lastContentDiv = null
 | 
			
		||||
 | 
			
		||||
    for (let message of messages[selected_channel_id].values()) {
 | 
			
		||||
        if (lastMessage && lastMessage.author === message.author) {
 | 
			
		||||
            // Si le message est écrit par læ même auteur⋅rice que le message précédent,
 | 
			
		||||
            // alors on les groupe ensemble
 | 
			
		||||
            let lastTimestamp = new Date(lastMessage.timestamp)
 | 
			
		||||
            let newTimestamp = new Date(message.timestamp)
 | 
			
		||||
            if ((newTimestamp - lastTimestamp) / 1000 < 60 * 10) {
 | 
			
		||||
                // Les messages sont groupés uniquement s'il y a une différence maximale de 10 minutes
 | 
			
		||||
                // entre le premier message du groupe et celui en étude
 | 
			
		||||
                // On ajoute alors le contenu du message en cours dans le dernier div de message
 | 
			
		||||
                let messageContentDiv = document.createElement('div')
 | 
			
		||||
                messageContentDiv.classList.add('message')
 | 
			
		||||
                messageContentDiv.setAttribute('data-message-id', message.id)
 | 
			
		||||
                lastContentDiv.appendChild(messageContentDiv)
 | 
			
		||||
                let messageContentSpan = document.createElement('span')
 | 
			
		||||
                messageContentSpan.innerHTML = markdownToHTML(message.content)
 | 
			
		||||
                messageContentDiv.appendChild(messageContentSpan)
 | 
			
		||||
 | 
			
		||||
                // Enregistrement du menu contextuel pour le message permettant la modification, la suppression
 | 
			
		||||
                // et l'envoi de messages privés
 | 
			
		||||
                registerMessageContextMenu(message, messageContentDiv, messageContentSpan)
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Création de l'élément <li> pour le bloc de messages
 | 
			
		||||
        let messageElement = document.createElement('li')
 | 
			
		||||
        messageElement.classList.add('list-group-item')
 | 
			
		||||
        messageList.appendChild(messageElement)
 | 
			
		||||
 | 
			
		||||
        // Ajout d'un div contenant le nom de l'auteur⋅rice du message ainsi que la date et heure d'envoi
 | 
			
		||||
        let authorDiv = document.createElement('div')
 | 
			
		||||
        messageElement.appendChild(authorDiv)
 | 
			
		||||
 | 
			
		||||
        // Ajout du nom de l'auteur⋅rice du message
 | 
			
		||||
        let authorSpan = document.createElement('span')
 | 
			
		||||
        authorSpan.classList.add('text-muted', 'fw-bold')
 | 
			
		||||
        authorSpan.innerText = message.author
 | 
			
		||||
        authorDiv.appendChild(authorSpan)
 | 
			
		||||
 | 
			
		||||
        // Ajout de la date du message
 | 
			
		||||
        let dateSpan = document.createElement('span')
 | 
			
		||||
        dateSpan.classList.add('text-muted', 'float-end')
 | 
			
		||||
        dateSpan.innerText = new Date(message.timestamp).toLocaleString()
 | 
			
		||||
        authorDiv.appendChild(dateSpan)
 | 
			
		||||
 | 
			
		||||
        // Enregistrement du menu contextuel pour le message permettant l'envoi de messages privés à l'auteur⋅rice
 | 
			
		||||
        registerSendPrivateMessageContextMenu(message, authorDiv, authorSpan)
 | 
			
		||||
 | 
			
		||||
        let contentDiv = document.createElement('div')
 | 
			
		||||
        messageElement.appendChild(contentDiv)
 | 
			
		||||
 | 
			
		||||
        // Ajout du contenu du message
 | 
			
		||||
        // Le contenu est mis dans un span lui-même inclus dans un div,
 | 
			
		||||
        let messageContentDiv = document.createElement('div')
 | 
			
		||||
        messageContentDiv.classList.add('message')
 | 
			
		||||
        messageContentDiv.setAttribute('data-message-id', message.id)
 | 
			
		||||
        contentDiv.appendChild(messageContentDiv)
 | 
			
		||||
        let messageContentSpan = document.createElement('span')
 | 
			
		||||
        messageContentSpan.innerHTML = markdownToHTML(message.content)
 | 
			
		||||
        messageContentDiv.appendChild(messageContentSpan)
 | 
			
		||||
 | 
			
		||||
        // Enregistrement du menu contextuel pour le message permettant la modification, la suppression
 | 
			
		||||
        // et l'envoi de messages privés
 | 
			
		||||
        registerMessageContextMenu(message, messageContentDiv, messageContentSpan)
 | 
			
		||||
 | 
			
		||||
        lastMessage = message
 | 
			
		||||
        lastContentDiv = contentDiv
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Le bouton « Afficher les messages précédents » est affiché si et seulement si
 | 
			
		||||
    // il y a des messages à récupérer (c'est-à-dire si le nombre de messages récupérés est un multiple de MAX_MESSAGES)
 | 
			
		||||
    let fetchMoreButton = document.getElementById('fetch-previous-messages')
 | 
			
		||||
    if (!messages[selected_channel_id].size || messages[selected_channel_id].size % MAX_MESSAGES !== 0)
 | 
			
		||||
        fetchMoreButton.classList.add('d-none')
 | 
			
		||||
    else
 | 
			
		||||
        fetchMoreButton.classList.remove('d-none')
 | 
			
		||||
 | 
			
		||||
    // On envoie un événement personnalisé pour indiquer que les messages ont été mis à jour
 | 
			
		||||
    // Permettant entre autres de marquer les messages visibles comme lus si c'est le cas
 | 
			
		||||
    messageList.dispatchEvent(new CustomEvent('updatemessages'))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Convertit un texte écrit en Markdown en HTML.
 | 
			
		||||
 * Les balises Markdown suivantes sont supportées :
 | 
			
		||||
 * - Souligné : `_texte_`
 | 
			
		||||
 * - Gras : `**texte**`
 | 
			
		||||
 * - Italique : `*texte*`
 | 
			
		||||
 * - Code : `` `texte` ``
 | 
			
		||||
 * - Les liens sont automatiquement convertis
 | 
			
		||||
 * - Les esperluettes, guillemets et chevrons sont échappés.
 | 
			
		||||
 * @param text Le texte écrit en Markdown.
 | 
			
		||||
 * @return {string} Le texte converti en HTML.
 | 
			
		||||
 */
 | 
			
		||||
function markdownToHTML(text) {
 | 
			
		||||
    // On échape certains caractères spéciaux (esperluettes, chevrons, guillemets)
 | 
			
		||||
    let safeText = text.replace(/&/g, "&")
 | 
			
		||||
         .replace(/</g, "<")
 | 
			
		||||
         .replace(/>/g, ">")
 | 
			
		||||
         .replace(/"/g, """)
 | 
			
		||||
         .replace(/'/g, "'")
 | 
			
		||||
    let lines = safeText.split('\n')
 | 
			
		||||
    let htmlLines = []
 | 
			
		||||
    for (let line of lines) {
 | 
			
		||||
        // Pour chaque ligne, on remplace le Markdown par un équivalent HTML (pour ce qui est supporté)
 | 
			
		||||
        let htmlLine = line
 | 
			
		||||
            .replaceAll(/_(.*)_/gim, '<span class="text-decoration-underline">$1</span>')  // Souligné
 | 
			
		||||
            .replaceAll(/\*\*(.*)\*\*/gim, '<span class="fw-bold">$1</span>')  // Gras
 | 
			
		||||
            .replaceAll(/\*(.*)\*/gim, '<span class="fst-italic">$1</span>')  // Italique
 | 
			
		||||
            .replaceAll(/`(.*)`/gim, '<pre>$1</pre>')  // Code
 | 
			
		||||
            .replaceAll(/(https?:\/\/\S+)/g, '<a href="$1" target="_blank">$1</a>')  // Liens
 | 
			
		||||
        htmlLines.push(htmlLine)
 | 
			
		||||
    }
 | 
			
		||||
    // On joint enfin toutes les lignes par des balises de saut de ligne
 | 
			
		||||
    return htmlLines.join('<br>')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Ferme toutes les popovers ouvertes.
 | 
			
		||||
 */
 | 
			
		||||
function removeAllPopovers() {
 | 
			
		||||
    for (let popover of document.querySelectorAll('*[aria-describedby*="popover"]')) {
 | 
			
		||||
        let instance = bootstrap.Popover.getInstance(popover)
 | 
			
		||||
        if (instance)
 | 
			
		||||
            instance.dispose()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Enregistrement du menu contextuel pour un⋅e auteur⋅rice de message,
 | 
			
		||||
 * donnant la possibilité d'envoyer un message privé.
 | 
			
		||||
 * @param message Le message écrit par l'auteur⋅rice du bloc en question.
 | 
			
		||||
 * @param div Le bloc contenant le nom de l'auteur⋅rice et de la date d'envoi du message.
 | 
			
		||||
 *            Un clic droit sur lui affichera le menu contextuel.
 | 
			
		||||
 * @param span Le span contenant le nom de l'auteur⋅rice.
 | 
			
		||||
 *             Il désignera l'emplacement d'affichage du popover.
 | 
			
		||||
 */
 | 
			
		||||
function registerSendPrivateMessageContextMenu(message, div, span) {
 | 
			
		||||
    // Enregistrement de l'écouteur d'événement pour le clic droit
 | 
			
		||||
    div.addEventListener('contextmenu', (menu_event) => {
 | 
			
		||||
        // On empêche le menu traditionnel de s'afficher
 | 
			
		||||
        menu_event.preventDefault()
 | 
			
		||||
        // On retire toutes les popovers déjà ouvertes
 | 
			
		||||
        removeAllPopovers()
 | 
			
		||||
 | 
			
		||||
        // On crée le popover contenant le lien pour envoyer un message privé, puis on l'affiche
 | 
			
		||||
        const popover = bootstrap.Popover.getOrCreateInstance(span, {
 | 
			
		||||
            'title': message.author,
 | 
			
		||||
            'content': `<a id="send-private-message-link-${message.id}" class="nav-link" href="#" tabindex="0">Envoyer un message privé</a>`,
 | 
			
		||||
            'html': true,
 | 
			
		||||
        })
 | 
			
		||||
        popover.show()
 | 
			
		||||
 | 
			
		||||
        // Lorsqu'on clique sur le lien, on ferme le popover
 | 
			
		||||
        // et on demande à ouvrir le canal privé avec l'auteur⋅rice du message
 | 
			
		||||
        document.getElementById('send-private-message-link-' + message.id).addEventListener('click', event => {
 | 
			
		||||
            event.preventDefault()
 | 
			
		||||
            popover.dispose()
 | 
			
		||||
            socket.send(JSON.stringify({
 | 
			
		||||
                'type': 'start_private_chat',
 | 
			
		||||
                'user_id': message.author_id,
 | 
			
		||||
            }))
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Enregistrement du menu contextuel pour un message,
 | 
			
		||||
 * donnant la possibilité de modifier ou de supprimer le message, ou d'envoyer un message privé à l'auteur⋅rice.
 | 
			
		||||
 * @param message Le message en question.
 | 
			
		||||
 * @param div Le bloc contenant le contenu du message.
 | 
			
		||||
 *            Un clic droit sur lui affichera le menu contextuel.
 | 
			
		||||
 * @param span Le span contenant le contenu du message.
 | 
			
		||||
 *             Il désignera l'emplacement d'affichage du popover.
 | 
			
		||||
 */
 | 
			
		||||
function registerMessageContextMenu(message, div, span) {
 | 
			
		||||
    // Enregistrement de l'écouteur d'événement pour le clic droit
 | 
			
		||||
    div.addEventListener('contextmenu', (menu_event) => {
 | 
			
		||||
        // On empêche le menu traditionnel de s'afficher
 | 
			
		||||
        menu_event.preventDefault()
 | 
			
		||||
        // On retire toutes les popovers déjà ouvertes
 | 
			
		||||
        removeAllPopovers()
 | 
			
		||||
 | 
			
		||||
        // On crée le popover contenant les liens pour modifier, supprimer le message ou envoyer un message privé.
 | 
			
		||||
        let content = `<a id="send-private-message-link-msg-${message.id}" class="nav-link" href="#" tabindex="0">Envoyer un message privé</a>`
 | 
			
		||||
 | 
			
		||||
        // On ne peut modifier ou supprimer un message que si on est l'auteur⋅rice ou que l'on est administrateur⋅rice.
 | 
			
		||||
        let has_right_to_edit = message.author_id === USER_ID || IS_ADMIN
 | 
			
		||||
        if (has_right_to_edit) {
 | 
			
		||||
            content += `<hr class="my-1">`
 | 
			
		||||
            content += `<a id="edit-message-${message.id}" class="nav-link" href="#" tabindex="0">Modifier</a>`
 | 
			
		||||
            content += `<a id="delete-message-${message.id}" class="nav-link" href="#" tabindex="0">Supprimer</a>`
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const popover = bootstrap.Popover.getOrCreateInstance(span, {
 | 
			
		||||
            'content': content,
 | 
			
		||||
            'html': true,
 | 
			
		||||
            'placement': 'bottom',
 | 
			
		||||
        })
 | 
			
		||||
        popover.show()
 | 
			
		||||
 | 
			
		||||
        // Lorsqu'on clique sur le lien d'envoi de message privé, on ferme le popover
 | 
			
		||||
        // et on demande à ouvrir le canal privé avec l'auteur⋅rice du message
 | 
			
		||||
        document.getElementById('send-private-message-link-msg-' + message.id).addEventListener('click', event => {
 | 
			
		||||
            event.preventDefault()
 | 
			
		||||
            popover.dispose()
 | 
			
		||||
            socket.send(JSON.stringify({
 | 
			
		||||
                'type': 'start_private_chat',
 | 
			
		||||
                'user_id': message.author_id,
 | 
			
		||||
            }))
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (has_right_to_edit) {
 | 
			
		||||
            // Si on a le droit de modifier ou supprimer le message, on enregistre les écouteurs d'événements
 | 
			
		||||
            // Le bouton de modification de message ouvre une boîte de dialogue pour modifier le message
 | 
			
		||||
            document.getElementById('edit-message-' + message.id).addEventListener('click', event => {
 | 
			
		||||
                event.preventDefault()
 | 
			
		||||
                // Fermeture du popover
 | 
			
		||||
                popover.dispose()
 | 
			
		||||
 | 
			
		||||
                // Ouverture d'une boîte de diaologue afin de modifier le message
 | 
			
		||||
                let new_message = prompt("Modifier le message", message.content)
 | 
			
		||||
                if (new_message) {
 | 
			
		||||
                    // Si le message a été modifié, on envoie la demande de modification au serveur
 | 
			
		||||
                    socket.send(JSON.stringify({
 | 
			
		||||
                        'type': 'edit_message',
 | 
			
		||||
                        'message_id': message.id,
 | 
			
		||||
                        'content': new_message,
 | 
			
		||||
                    }))
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            // Le bouton de suppression de message demande une confirmation avant de supprimer le message
 | 
			
		||||
            document.getElementById('delete-message-' + message.id).addEventListener('click', event => {
 | 
			
		||||
                event.preventDefault()
 | 
			
		||||
                // Fermeture du popover
 | 
			
		||||
                popover.dispose()
 | 
			
		||||
 | 
			
		||||
                // Demande de confirmation avant de supprimer le message
 | 
			
		||||
                if (confirm(`Supprimer le message ?\n${message.content}`)) {
 | 
			
		||||
                    socket.send(JSON.stringify({
 | 
			
		||||
                        'type': 'delete_message',
 | 
			
		||||
                        'message_id': message.id,
 | 
			
		||||
                    }))
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Passe le chat en version plein écran, ou l'inverse si c'est déjà le cas.
 | 
			
		||||
 */
 | 
			
		||||
function toggleFullscreen() {
 | 
			
		||||
    let chatContainer = document.getElementById('chat-container')
 | 
			
		||||
    if (!chatContainer.getAttribute('data-fullscreen')) {
 | 
			
		||||
        // Le chat n'est pas en plein écran.
 | 
			
		||||
        // On le passe en plein écran en le plaçant en avant plan en position absolue
 | 
			
		||||
        // prenant toute la hauteur et toute la largeur
 | 
			
		||||
        chatContainer.setAttribute('data-fullscreen', 'true')
 | 
			
		||||
        chatContainer.classList.add('position-absolute', 'top-0', 'start-0', 'vh-100', 'z-3')
 | 
			
		||||
        window.history.replaceState({}, null, `?fullscreen=1`)
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        // Le chat est déjà en plein écran. On retire les tags CSS correspondant au plein écran.
 | 
			
		||||
        chatContainer.removeAttribute('data-fullscreen')
 | 
			
		||||
        chatContainer.classList.remove('position-absolute', 'top-0', 'start-0', 'vh-100', 'z-3')
 | 
			
		||||
        window.history.replaceState({}, null, `?fullscreen=0`)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    // Lorsqu'on effectue le moindre clic, on ferme les éventuelles popovers ouvertes
 | 
			
		||||
    document.addEventListener('click', removeAllPopovers)
 | 
			
		||||
 | 
			
		||||
    // Lorsqu'on change entre le tri des canaux par ordre alphabétique et par nombre de messages non lus,
 | 
			
		||||
    // on met à jour l'ordre des canaux
 | 
			
		||||
    document.getElementById('sort-by-unread-switch').addEventListener('change', event => {
 | 
			
		||||
        const sortByUnread = event.target.checked
 | 
			
		||||
        for (let channel of Object.values(channels)) {
 | 
			
		||||
            let item = document.getElementById(`tab-channel-${channel.id}`)
 | 
			
		||||
            if (sortByUnread)
 | 
			
		||||
                // Si on trie par nombre de messages non lus,
 | 
			
		||||
                // on définit l'ordre de l'élément en fonction du nombre de messages non lus
 | 
			
		||||
                // à l'aide d'une propriété CSS
 | 
			
		||||
                item.style.order = `${-channel.unread_messages}`
 | 
			
		||||
            else
 | 
			
		||||
                // Sinon, les canaux sont de base triés par ordre alphabétique
 | 
			
		||||
                item.style.removeProperty('order')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // On stocke le mode de tri dans le stockage local
 | 
			
		||||
        localStorage.setItem('chat.sort-by-unread', sortByUnread)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // On récupère le mode de tri des canaux depuis le stockage local
 | 
			
		||||
    if (localStorage.getItem('chat.sort-by-unread') === 'true') {
 | 
			
		||||
        document.getElementById('sort-by-unread-switch').checked = true
 | 
			
		||||
        document.getElementById('sort-by-unread-switch').dispatchEvent(new Event('change'))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Des données sont reçues depuis le serveur. Elles sont traitées dans cette fonction,
 | 
			
		||||
     * qui a pour but de trier et de répartir dans d'autres sous-fonctions.
 | 
			
		||||
     * @param data Le message reçu.
 | 
			
		||||
     */
 | 
			
		||||
    function processMessage(data) {
 | 
			
		||||
        // On traite le message en fonction de son type
 | 
			
		||||
        switch (data.type) {
 | 
			
		||||
            case 'fetch_channels':
 | 
			
		||||
                setChannels(data.channels)
 | 
			
		||||
                break
 | 
			
		||||
            case 'send_message':
 | 
			
		||||
                receiveMessage(data)
 | 
			
		||||
                break
 | 
			
		||||
            case 'edit_message':
 | 
			
		||||
                editMessage(data)
 | 
			
		||||
                break
 | 
			
		||||
            case 'delete_message':
 | 
			
		||||
                deleteMessage(data)
 | 
			
		||||
                break
 | 
			
		||||
            case 'fetch_messages':
 | 
			
		||||
                receiveFetchedMessages(data)
 | 
			
		||||
                break
 | 
			
		||||
            case 'mark_read':
 | 
			
		||||
                markMessageAsRead(data)
 | 
			
		||||
                break
 | 
			
		||||
            case 'start_private_chat':
 | 
			
		||||
                startPrivateChat(data)
 | 
			
		||||
                break
 | 
			
		||||
            default:
 | 
			
		||||
                // Le type de message est inconnu. On affiche une erreur dans la console.
 | 
			
		||||
                console.log(data)
 | 
			
		||||
                console.error('Unknown message type:', data.type)
 | 
			
		||||
                break
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Configuration du socket de chat, permettant de communiquer avec le serveur.
 | 
			
		||||
     * @param nextDelay Correspond au délai de reconnexion en cas d'erreur.
 | 
			
		||||
     *                  Augmente exponentiellement en cas d'erreurs répétées,
 | 
			
		||||
     *                  et se réinitialise à 1s en cas de connexion réussie.
 | 
			
		||||
     */
 | 
			
		||||
    function setupSocket(nextDelay = 1000) {
 | 
			
		||||
        // Ouverture du socket
 | 
			
		||||
        socket = new WebSocket(
 | 
			
		||||
            (document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws/chat/'
 | 
			
		||||
        )
 | 
			
		||||
        let socketOpen = false
 | 
			
		||||
 | 
			
		||||
        // Écoute des messages reçus depuis le serveur
 | 
			
		||||
        socket.addEventListener('message', e => {
 | 
			
		||||
            // Analyse du message reçu en tant que JSON
 | 
			
		||||
            const data = JSON.parse(e.data)
 | 
			
		||||
 | 
			
		||||
            // Traite le message reçu
 | 
			
		||||
            processMessage(data)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // En cas d'erreur, on affiche un message et on réessaie de se connecter après un certain délai
 | 
			
		||||
        // Ce délai double après chaque erreur répétée, jusqu'à un maximum de 2 minutes
 | 
			
		||||
        socket.addEventListener('close', e => {
 | 
			
		||||
            console.error('Chat socket closed unexpectedly, restarting…')
 | 
			
		||||
            setTimeout(() => setupSocket(Math.max(socketOpen ? 1000 : 2 * nextDelay, 120000)), nextDelay)
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // En cas de connexion réussie, on demande au serveur les derniers messages pour chaque canal
 | 
			
		||||
        socket.addEventListener('open', e => {
 | 
			
		||||
            socketOpen = true
 | 
			
		||||
            socket.send(JSON.stringify({
 | 
			
		||||
                'type': 'fetch_channels',
 | 
			
		||||
            }))
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Configuration du swipe pour ouvrir et fermer le sélecteur de canaux.
 | 
			
		||||
     * Fonctionne a priori uniquement sur les écrans tactiles.
 | 
			
		||||
     * Lorsqu'on swipe de la gauche vers la droite, depuis le côté gauche de l'écran, on ouvre le sélecteur de canaux.
 | 
			
		||||
     * Quand on swipe de la droite vers la gauche, on ferme le sélecteur de canaux.
 | 
			
		||||
     */
 | 
			
		||||
    function setupSwipeOffscreen() {
 | 
			
		||||
        // Récupération du sélecteur de canaux
 | 
			
		||||
        const offcanvas = new bootstrap.Offcanvas(document.getElementById('channelSelector'))
 | 
			
		||||
 | 
			
		||||
        // L'écran a été touché. On récupère la coordonnée X de l'emplacement touché.
 | 
			
		||||
        let lastX = null
 | 
			
		||||
        document.addEventListener('touchstart', (event) => {
 | 
			
		||||
            if (event.touches.length === 1)
 | 
			
		||||
                lastX = event.touches[0].clientX
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // Le doigt a été déplacé. Selon le nouvel emplacement du doigt, on ouvre ou on ferme le sélecteur de canaux.
 | 
			
		||||
        document.addEventListener('touchmove', (event) => {
 | 
			
		||||
            if (event.touches.length === 1 && lastX !== null) {
 | 
			
		||||
                // L'écran a été touché à un seul doigt, et on a déjà récupéré la coordonnée X touchée.
 | 
			
		||||
                const diff = event.touches[0].clientX - lastX
 | 
			
		||||
                if (diff > window.innerWidth / 10 && lastX < window.innerWidth / 4) {
 | 
			
		||||
                    // Si le déplacement correspond à au moins 10 % de la largeur de l'écran vers la droite
 | 
			
		||||
                    // et que le point de départ se trouve dans le quart gauche de l'écran, alors on ouvre le sélecteur
 | 
			
		||||
                    offcanvas.show()
 | 
			
		||||
                    lastX = null
 | 
			
		||||
                }
 | 
			
		||||
                else if (diff < -window.innerWidth / 10) {
 | 
			
		||||
                    // Si le déplacement correspond à au moins 10 % de la largeur de l'écran vers la gauche,
 | 
			
		||||
                    // alors on ferme le sélecteur
 | 
			
		||||
                    offcanvas.hide()
 | 
			
		||||
                    lastX = null
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // Le doigt a été relâché. On réinitialise la coordonnée X touchée.
 | 
			
		||||
        document.addEventListener('touchend', () => {
 | 
			
		||||
            lastX = null
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Configuration du suivi de lecture des messages.
 | 
			
		||||
     * Lorsque l'utilisateur⋅rice scrolle dans la fenêtre de chat, on vérifie quels sont les messages qui sont
 | 
			
		||||
     * visibles à l'écran, et on les marque comme lus.
 | 
			
		||||
     */
 | 
			
		||||
    function setupReadTracker() {
 | 
			
		||||
        // Récupération du conteneur de messages
 | 
			
		||||
        const scrollableContent = document.getElementById('chat-messages')
 | 
			
		||||
        const messagesList = document.getElementById('message-list')
 | 
			
		||||
        let markReadBuffer = []
 | 
			
		||||
        let markReadTimeout = null
 | 
			
		||||
 | 
			
		||||
        // Lorsqu'on scrolle, on récupère les anciens messages si on est tout en haut,
 | 
			
		||||
        // et on marque les messages visibles comme lus
 | 
			
		||||
        scrollableContent.addEventListener('scroll', () => {
 | 
			
		||||
            if (scrollableContent.clientHeight - scrollableContent.scrollTop === scrollableContent.scrollHeight
 | 
			
		||||
                && !document.getElementById('fetch-previous-messages').classList.contains('d-none')) {
 | 
			
		||||
                // Si l'utilisateur⋅rice est en haut du chat, on récupère la liste des anciens messages
 | 
			
		||||
                fetchPreviousMessages()}
 | 
			
		||||
 | 
			
		||||
            // On marque les messages visibles comme lus
 | 
			
		||||
            markVisibleMessagesAsRead()
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // Lorsque les messages stockés sont mis à jour, on vérifie quels sont les messages visibles à marquer comme lus
 | 
			
		||||
        messagesList.addEventListener('updatemessages', () => markVisibleMessagesAsRead())
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Marque les messages visibles à l'écran comme lus.
 | 
			
		||||
         * On récupère pour cela les coordonnées du conteneur de messages ainsi que les coordonnées de chaque message
 | 
			
		||||
         * et on vérifie si le message est visible à l'écran. Si c'est le cas, on le marque comme lu.
 | 
			
		||||
         * Après 3 secondes d'attente après qu'aucun message n'ait été lu,
 | 
			
		||||
         * on envoie la liste des messages lus au serveur.
 | 
			
		||||
         */
 | 
			
		||||
        function markVisibleMessagesAsRead() {
 | 
			
		||||
            // Récupération des coordonnées visibles du conteneur de messages
 | 
			
		||||
            let viewport = scrollableContent.getBoundingClientRect()
 | 
			
		||||
 | 
			
		||||
            for (let item of messagesList.querySelectorAll('.message')) {
 | 
			
		||||
                let message = messages[selected_channel_id].get(parseInt(item.getAttribute('data-message-id')))
 | 
			
		||||
                if (!message.read) {
 | 
			
		||||
                    // Si le message n'a pas déjà été lu, on récupère ses coordonnées
 | 
			
		||||
                    let rect = item.getBoundingClientRect()
 | 
			
		||||
                    if (rect.top >= viewport.top && rect.bottom <= viewport.bottom) {
 | 
			
		||||
                        // Si les coordonnées sont entièrement incluses dans le rectangle visible, on le marque comme lu
 | 
			
		||||
                        // et comme étant à envoyer au serveur
 | 
			
		||||
                        message.read = true
 | 
			
		||||
                        markReadBuffer.push(message.id)
 | 
			
		||||
                        if (markReadTimeout)
 | 
			
		||||
                            clearTimeout(markReadTimeout)
 | 
			
		||||
                        // 3 secondes après qu'aucun nouveau message n'ait été rajouté, on envoie la liste des messages
 | 
			
		||||
                        // lus au serveur
 | 
			
		||||
                        markReadTimeout = setTimeout(() => {
 | 
			
		||||
                            socket.send(JSON.stringify({
 | 
			
		||||
                                'type': 'mark_read',
 | 
			
		||||
                                'message_ids': markReadBuffer,
 | 
			
		||||
                            }))
 | 
			
		||||
                            markReadBuffer = []
 | 
			
		||||
                            markReadTimeout = null
 | 
			
		||||
                        }, 3000)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // On considère les messages d'ores-et-déjà visibles comme lus
 | 
			
		||||
        markVisibleMessagesAsRead()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Configuration de la demande d'installation de l'application en tant qu'application web progressive (PWA).
 | 
			
		||||
     * Lorsque l'utilisateur⋅rice arrive sur la page, on lui propose de télécharger l'application
 | 
			
		||||
     * pour l'ajouter à son écran d'accueil.
 | 
			
		||||
     * Fonctionne uniquement sur les navigateurs compatibles.
 | 
			
		||||
     */
 | 
			
		||||
    function setupPWAPrompt() {
 | 
			
		||||
        let deferredPrompt = null
 | 
			
		||||
 | 
			
		||||
        window.addEventListener("beforeinstallprompt", (e) => {
 | 
			
		||||
            // Une demande d'installation a été faite. On commence par empêcher l'action par défaut.
 | 
			
		||||
            e.preventDefault()
 | 
			
		||||
            deferredPrompt = e
 | 
			
		||||
 | 
			
		||||
            // L'installation est possible, on rend visible le bouton de téléchargement
 | 
			
		||||
            // ainsi que le message qui indique c'est possible.
 | 
			
		||||
            let btn = document.getElementById('install-app-home-screen')
 | 
			
		||||
            let alert = document.getElementById('alert-download-chat-app')
 | 
			
		||||
            btn.classList.remove('d-none')
 | 
			
		||||
            alert.classList.remove('d-none')
 | 
			
		||||
            btn.onclick = function () {
 | 
			
		||||
                // Lorsque le bouton de téléchargement est cliqué, on lance l'installation du PWA.
 | 
			
		||||
                deferredPrompt.prompt()
 | 
			
		||||
                deferredPrompt.userChoice.then((choiceResult) => {
 | 
			
		||||
                    if (choiceResult.outcome === 'accepted') {
 | 
			
		||||
                        // Si l'installation a été acceptée, on masque le bouton de téléchargement.
 | 
			
		||||
                        deferredPrompt = null
 | 
			
		||||
                        btn.classList.add('d-none')
 | 
			
		||||
                        alert.classList.add('d-none')
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setupSocket()  // Configuration du Websocket
 | 
			
		||||
    setupSwipeOffscreen()  // Configuration du swipe sur les écrans tactiles pour le sélecteur de canaux
 | 
			
		||||
    setupReadTracker()  // Configuration du suivi de lecture des messages
 | 
			
		||||
    setupPWAPrompt()  // Configuration de l'installateur d'application en tant qu'application web progressive
 | 
			
		||||
})
 | 
			
		||||
		Reference in New Issue
	
	Block a user