diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index 3a9516f..fcf91fa 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -1746,6 +1746,9 @@ html[data-files="1"] #files-btn{color:var(--accent)} color:var(--dim);letter-spacing:-.01em;overflow:visible} .side .preset:hover .conv-when{opacity:0} .conv-empty{font-size:12px;color:var(--dim);padding:8px 11px} +.conv-more{display:block;width:100%;font:inherit;font-size:12px;color:var(--dim);background:none; + border:0;padding:8px 11px;text-align:left;cursor:pointer} +.conv-more:hover{color:var(--text)} /* Discussion au travail : un anneau qui tourne, à gauche du titre. C'est le seul élément animé de la liste — il désigne sans ambiguïté le fil sur lequel un agent est en train de travailler, y compris depuis un autre appareil. */ @@ -9127,6 +9130,12 @@ let CONVS = [], CONV_ACTIVE = ''; // ligne doit déjà s'animer si un agent travaille (page rechargée en cours de // route, ou ouverte depuis un autre appareil). let CONV_BUSY = false; +// Liste paginée (AJEAN 0.15.6) : avec des centaines de discussions, construire +// toutes les lignes d'un coup figeait l'ouverture du panneau. On en dessine +// CONV_PAGE, puis la suite quand le bas de la liste devient visible. La +// recherche, elle, filtre toujours TOUTE la liste (CONVS est en mémoire). +const CONV_PAGE = 40; +let CONV_SHOWN = CONV_PAGE, CONV_LAST_Q = '', convMoreObs = null; async function loadConversations(){ let r; @@ -9149,6 +9158,7 @@ function renderConversations(){ if(!cont) return; const q = (document.getElementById('conv-search')?.value || '').trim().toLowerCase(); cont.textContent = ''; + if(q !== CONV_LAST_Q){ CONV_LAST_Q = q; CONV_SHOWN = CONV_PAGE; } const list = q ? CONVS.filter(c => (c.title || 'Nouvelle discussion').toLowerCase().includes(q)) : CONVS; @@ -9161,7 +9171,10 @@ function renderConversations(){ convSyncBusy(); return; } - for(const c of list){ + // La discussion active reste toujours visible, même loin dans la liste. + const ai = list.findIndex(c => c.id === CONV_ACTIVE); + const shown = Math.max(CONV_SHOWN, ai + 1); + for(const c of list.slice(0, shown)){ const row = document.createElement('div'); row.className = 'preset' + (c.id === CONV_ACTIVE ? ' active' : ''); // L'identifiant est porté par la ligne : convSyncBusy retrouve ainsi CELLE @@ -9199,10 +9212,28 @@ function renderConversations(){ row.append(info, when, acts); cont.appendChild(row); } + if(list.length > shown) convMoreRow(cont, list.length - shown); syncTopbarTitle(); convSyncBusy(); } +// Ligne « N de plus » en bas de la liste : un clic, ou son arrivée à l'écran +// (IntersectionObserver), dessine la page suivante. +function convMoreRow(cont, left){ + const more = document.createElement('button'); + more.type = 'button'; + more.className = 'conv-more'; + more.textContent = 'afficher ' + Math.min(left, CONV_PAGE) + ' de plus (' + left + ' restantes)'; + const next = () => { CONV_SHOWN += CONV_PAGE; renderConversations(); }; + more.onclick = next; + cont.appendChild(more); + if(convMoreObs) convMoreObs.disconnect(); + if(window.IntersectionObserver){ + convMoreObs = new IntersectionObserver(es => { if(es.some(e => e.isIntersecting)){ convMoreObs.disconnect(); next(); } }); + convMoreObs.observe(more); + } +} + // --- Discussion au travail : anneau qui tourne ------------------------------ // Une seule génération tourne à la fois, et c'est forcément sur la discussion // OUVERTE (le serveur n'a qu'un fil en mémoire) : l'indicateur va donc sur la diff --git a/internal/loki/ui/src/js/16-conversations.js b/internal/loki/ui/src/js/16-conversations.js index f5103ae..48dd777 100644 --- a/internal/loki/ui/src/js/16-conversations.js +++ b/internal/loki/ui/src/js/16-conversations.js @@ -19,6 +19,12 @@ let CONVS = [], CONV_ACTIVE = ''; // ligne doit déjà s'animer si un agent travaille (page rechargée en cours de // route, ou ouverte depuis un autre appareil). let CONV_BUSY = false; +// Liste paginée (AJEAN 0.15.6) : avec des centaines de discussions, construire +// toutes les lignes d'un coup figeait l'ouverture du panneau. On en dessine +// CONV_PAGE, puis la suite quand le bas de la liste devient visible. La +// recherche, elle, filtre toujours TOUTE la liste (CONVS est en mémoire). +const CONV_PAGE = 40; +let CONV_SHOWN = CONV_PAGE, CONV_LAST_Q = '', convMoreObs = null; async function loadConversations(){ let r; @@ -41,6 +47,7 @@ function renderConversations(){ if(!cont) return; const q = (document.getElementById('conv-search')?.value || '').trim().toLowerCase(); cont.textContent = ''; + if(q !== CONV_LAST_Q){ CONV_LAST_Q = q; CONV_SHOWN = CONV_PAGE; } const list = q ? CONVS.filter(c => (c.title || 'Nouvelle discussion').toLowerCase().includes(q)) : CONVS; @@ -53,7 +60,10 @@ function renderConversations(){ convSyncBusy(); return; } - for(const c of list){ + // La discussion active reste toujours visible, même loin dans la liste. + const ai = list.findIndex(c => c.id === CONV_ACTIVE); + const shown = Math.max(CONV_SHOWN, ai + 1); + for(const c of list.slice(0, shown)){ const row = document.createElement('div'); row.className = 'preset' + (c.id === CONV_ACTIVE ? ' active' : ''); // L'identifiant est porté par la ligne : convSyncBusy retrouve ainsi CELLE @@ -91,10 +101,28 @@ function renderConversations(){ row.append(info, when, acts); cont.appendChild(row); } + if(list.length > shown) convMoreRow(cont, list.length - shown); syncTopbarTitle(); convSyncBusy(); } +// Ligne « N de plus » en bas de la liste : un clic, ou son arrivée à l'écran +// (IntersectionObserver), dessine la page suivante. +function convMoreRow(cont, left){ + const more = document.createElement('button'); + more.type = 'button'; + more.className = 'conv-more'; + more.textContent = 'afficher ' + Math.min(left, CONV_PAGE) + ' de plus (' + left + ' restantes)'; + const next = () => { CONV_SHOWN += CONV_PAGE; renderConversations(); }; + more.onclick = next; + cont.appendChild(more); + if(convMoreObs) convMoreObs.disconnect(); + if(window.IntersectionObserver){ + convMoreObs = new IntersectionObserver(es => { if(es.some(e => e.isIntersecting)){ convMoreObs.disconnect(); next(); } }); + convMoreObs.observe(more); + } +} + // --- Discussion au travail : anneau qui tourne ------------------------------ // Une seule génération tourne à la fois, et c'est forcément sur la discussion // OUVERTE (le serveur n'a qu'un fil en mémoire) : l'indicateur va donc sur la diff --git a/internal/loki/ui/src/styles.css b/internal/loki/ui/src/styles.css index 6128a48..22c6fde 100644 --- a/internal/loki/ui/src/styles.css +++ b/internal/loki/ui/src/styles.css @@ -1711,6 +1711,9 @@ html[data-files="1"] #files-btn{color:var(--accent)} color:var(--dim);letter-spacing:-.01em;overflow:visible} .side .preset:hover .conv-when{opacity:0} .conv-empty{font-size:12px;color:var(--dim);padding:8px 11px} +.conv-more{display:block;width:100%;font:inherit;font-size:12px;color:var(--dim);background:none; + border:0;padding:8px 11px;text-align:left;cursor:pointer} +.conv-more:hover{color:var(--text)} /* Discussion au travail : un anneau qui tourne, à gauche du titre. C'est le seul élément animé de la liste — il désigne sans ambiguïté le fil sur lequel un agent est en train de travailler, y compris depuis un autre appareil. */