Recent replies takes 2 seconds to display

Is there any way to display the latest replies instantly? Currently, I use this code:

  api.onPageChange(() => {
    if (window.location.pathname === "/") {
      const container = document.querySelector(".latest-topic-list");
      if (!container || container.dataset.modified === "true") return;

      fetch("/posts.json?order=created")
        .then(res => res.json())
        .then(data => {
          const replies = data.latest_posts
            .filter(p => p.post_number > 1 && !p.topic_slug.includes("private-message"))
            .slice(0, 15);

          const topicFetches = replies.map(post =>
            fetch(`/t/${post.topic_id}.json`)
              .then(res => res.json())
              .then(topic => {
                return {
                  post,
                  category: topic.category_id ? topic.category_name : null,
                  tags: topic.tags || []
                };
              })
          );

          Promise.all(topicFetches).then(results => {
            const rows = results.map(({ post, category, tags }) => {
              const url = `/t/${post.topic_slug}/${post.topic_id}/${post.post_number}`;
              const avatarUrl = post.avatar_template.replace("{size}", "45");
              const excerpt = post.excerpt?.replace(/<\/?[^>]+(>|$)/g, "")?.slice(0, 120) + (post.excerpt?.length > 120 ? '...' : '') || '';

              const categoryHtml = category
                ? `<span style="font-size: 0.85em; color: #666;">Categoria: <strong>${category}</strong></span><br>`
                : '';

              const tagsHtml = tags.length
                ? `<span style="font-size: 0.85em; color: #666;">Tags: ${tags.map(tag => `<span style="background:#eee; padding:2px 6px; border-radius:3px; margin-right:4px;">${tag}</span>`).join("")}</span>`
                : '';

              return `
                <tr class="topic-list-item">
                  <td class="main-link clearfix">
                    <div style="display: flex; align-items: center; gap: 16px; padding: 8px 0;">
                      <div style="flex-shrink: 0;">
                        <a class="avatar-link" href="/u/${post.username}">
                          <img loading="lazy" width="45" height="45" src="${avatarUrl}" class="avatar" alt="${post.username}">
                        </a>
                      </div>
                      <div style="display: flex; flex-direction: column; justify-content: center; padding-top: 8px; padding-bottom: 8px;">
                        <span class="link-top-line" style="margin-bottom: 6px;">
                          <a href="${url}" class="title raw-link">${excerpt}</a>
                        </span>
                        <div class="link-bottom-line">
                          ${categoryHtml}
                          ${tagsHtml}
                        </div>
                      </div>
                    </div>
                  </td>
                </tr>
              `;
            }).join("");

            // Cria o contêiner da seção de últimos comentários
            const latestRepliesContainer = document.createElement("div");
            latestRepliesContainer.className = "latest-replies-container";
            latestRepliesContainer.style.marginTop = "2em";

            latestRepliesContainer.innerHTML = `
              <table class="topic-list latest-topic-list">
                <thead>
                  <tr>
                    <th class="default">Últimos Comentários</th>
                  </tr>
                </thead>
                <tbody>
                  ${rows}
                </tbody>
              </table>
            `;

            container.parentNode.insertBefore(latestRepliesContainer, container.nextSibling);
            container.dataset.modified = "true";
          });
        })
        .catch(error => {
          console.error("Erro ao buscar últimos comentários:", error);
        });
    }
  });
</script>

but it takes an average of 2 seconds for the content to appear. The same thing happens with the right sidebar blocks with “recent replies”. Is this normal?

1 Like

Why are you adding any code?

This works out of the box without anything special; is it broken for you?

2 Likes

That’s his issue.

I would say it’s expected because the code makes several API requests.
It retrieves the latest posts and then makes one request per topic ID (15 here) to retrieve the category name.

At the moment, I don’t know if there is another way besides using a plugin and making a custom SQL query, for example.

3 Likes

Thats exact.
But the Right Sidebar Blocks make requests too? It gives me the same results for recent replies. Its takes 2 seconds to appear.

I will see how to make a plugin. Thank you

Yes, it does the same in retrieving the latest posts, but that’s it. It doesn’t try to get the category name, that’s the difference.

1 Like

I understand. Thank you for the help.

I will try to find another solution

1 Like

finally, i’m using this for now:

<script type="text/discourse-plugin" version="0.11.3">
  api.onPageChange(() => {
    if (window.location.pathname === "/") {
      const container = document.querySelector(".latest-topic-list");
      if (!container) return;
      
      // Evita múltiplas inicializações
      if (window.latestRepliesInitialized) return;
      window.latestRepliesInitialized = true;
      
      // Configurações
      const POLLING_INTERVAL = 2000; // 2 segundos
      const COMMENTS_TO_SHOW = 15;
      const CACHE_DURATION = 30 * 60 * 1000; // 30 minutos
      
      // Chaves de cache
      const CACHE_KEY = "discourse_latest_replies_data";
      const CACHE_TIMESTAMP_KEY = "discourse_latest_replies_timestamp";
      const CACHE_LAST_ID_KEY = "discourse_latest_replies_last_id";
      
      // Armazena o último ID de post visto para comparação
      let lastSeenPostId = parseInt(localStorage.getItem(CACHE_LAST_ID_KEY) || "0");
      let pollingIntervalId = null;
      
      console.log(`Inicializando plugin de últimos comentários (último ID em cache: ${lastSeenPostId})`);
      
      // Função para carregar os comentários
      function loadLatestReplies(silent = false, forceRefresh = false) {
        // Verifica o cache primeiro, se não for uma atualização forçada
        if (!forceRefresh) {
          const cachedData = localStorage.getItem(CACHE_KEY);
          const cacheTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
          const now = Date.now();
          
          // Se temos dados em cache válidos
          if (cachedData && cacheTimestamp && now - parseInt(cacheTimestamp) < CACHE_DURATION) {
            try {
              const results = JSON.parse(cachedData);
              console.log(`Usando dados em cache (${results.length} comentários, cache de ${Math.round((now - parseInt(cacheTimestamp)) / 1000 / 60)} minutos atrás)`);
              renderLatestReplies(results, false);
              
              // Se não for silencioso, não precisamos fazer mais nada
              if (!silent) {
                return;
              }
              
              // Se for silencioso, continuamos para verificar atualizações
            } catch (e) {
              console.error("Erro ao processar cache:", e);
              // Se houver erro no cache, continuamos para buscar dados frescos
            }
          } else if (cachedData) {
            console.log("Cache expirado, buscando dados frescos");
          } else {
            console.log("Nenhum cache encontrado, buscando dados frescos");
          }
        } else {
          console.log("Forçando atualização, ignorando cache");
        }
        
        // Se não for silencioso, mostra o indicador de carregamento
        if (!silent) {
          // Se já existe um contêiner de comentários, não mostra o indicador
          const existingContainer = document.querySelector(".latest-replies-container");
          if (!existingContainer) {
            let loadingIndicator = document.getElementById("latest-replies-loading");
            if (!loadingIndicator) {
              loadingIndicator = document.createElement("div");
              loadingIndicator.id = "latest-replies-loading";
              loadingIndicator.innerHTML = `
                <div style="text-align: center; padding: 20px;">
                  <span class="spinner small"></span>
                  <span style="margin-left: 10px;">Carregando comentários recentes...</span>
                </div>
              `;
              container.parentNode.insertBefore(loadingIndicator, container.nextSibling);
            }
          }
        }
        
        // Busca os dados mais recentes
        fetch("/posts.json?order=created")
          .then(res => res.json())
          .then(data => {
            // Log para diagnóstico
            if (!silent) {
              console.log("Dados recebidos da API:", data.latest_posts.length);
            }
            
            const replies = data.latest_posts
              .filter(p => p.post_number > 1 && !p.topic_slug.includes("private-message"))
              .slice(0, COMMENTS_TO_SHOW);
            
            // Verifica se há novos posts
            const maxId = replies.length > 0 ? Math.max(...replies.map(post => post.id)) : 0;
            const hasNewPosts = maxId > lastSeenPostId;
            
            // Log para diagnóstico
            if (hasNewPosts && !silent) {
              console.log(`Novos posts detectados. Último ID: ${lastSeenPostId}, Novo máximo ID: ${maxId}`);
            }
            
            // Atualiza o último ID visto
            if (maxId > lastSeenPostId) {
              lastSeenPostId = maxId;
              localStorage.setItem(CACHE_LAST_ID_KEY, lastSeenPostId.toString());
            }
            
            // Se não houver novos posts e for uma verificação silenciosa, não faz nada
            if (!hasNewPosts && silent && !forceRefresh) {
              return;
            }
            
            // Busca os detalhes dos tópicos
            const topicPromises = replies.map(post => {
              // Verifica se temos o tópico em cache
              const topicCacheKey = `discourse_topic_${post.topic_id}`;
              const cachedTopic = localStorage.getItem(topicCacheKey);
              
              if (cachedTopic && !forceRefresh) {
                try {
                  return Promise.resolve(JSON.parse(cachedTopic));
                } catch (e) {
                  console.error(`Erro ao processar cache do tópico ${post.topic_id}:`, e);
                  // Se houver erro no cache, buscamos do servidor
                }
              }
              
              return fetch(`/t/${post.topic_id}.json`)
                .then(res => res.json())
                .then(topic => {
                  // Armazena o tópico em cache
                  localStorage.setItem(topicCacheKey, JSON.stringify(topic));
                  return topic;
                })
                .catch(error => {
                  console.error(`Erro ao buscar tópico ${post.topic_id}:`, error);
                  return { category_id: null, category_name: null, tags: [] };
                });
            });
            
            Promise.all(topicPromises)
              .then(topics => {
                const results = replies.map((post, index) => {
                  const topic = topics[index];
                  return {
                    post,
                    category: topic.category_id ? topic.category_name : null,
                    tags: topic.tags || []
                  };
                });
                
                // Armazena os resultados em cache
                localStorage.setItem(CACHE_KEY, JSON.stringify(results));
                localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
                
                // Renderiza os resultados
                renderLatestReplies(results, hasNewPosts || forceRefresh);
              })
              .catch(error => {
                console.error("Erro ao processar tópicos:", error);
              });
          })
          .catch(error => {
            console.error("Erro ao buscar últimos comentários:", error);
            // Remove o indicador de carregamento em caso de erro
            if (!silent) {
              const loadingElement = document.getElementById("latest-replies-loading");
              if (loadingElement) loadingElement.remove();
            }
          });
      }
      
      // Função para renderizar os resultados
      function renderLatestReplies(results, animate = false) {
        // Remove o indicador de carregamento
        const loadingElement = document.getElementById("latest-replies-loading");
        if (loadingElement) loadingElement.remove();
        
        // Se não houver resultados, não faz nada
        if (!results || results.length === 0) {
          console.log("Nenhum comentário encontrado para exibir");
          return;
        }
        
        const rows = results.map(({ post, category, tags }) => {
          const url = `/t/${post.topic_slug}/${post.topic_id}/${post.post_number}`;
          const avatarUrl = post.avatar_template.replace("{size}", "45");
          const excerpt = post.excerpt?.replace(/<\/?[^>]+(>|$)/g, "")?.slice(0, 120) + (post.excerpt?.length > 120 ? '...' : '') || '';

          const categoryHtml = category
            ? `<span style="font-size: 0.85em; color: #666;">Categoria: <strong>${category}</strong></span><br>`
            : '';

          const tagsHtml = tags.length
            ? `<span style="font-size: 0.85em; color: #666;">Tags: ${tags.map(tag => `<span style="background:#eee; padding:2px 6px; border-radius:3px; margin-right:4px;">${tag}</span>`).join("")}</span>`
            : '';

          const animationClass = animate ? 'new-comment' : '';

          return `
            <tr class="topic-list-item ${animationClass}" data-post-id="${post.id}">
              <td class="main-link clearfix">
                <div style="display: flex; align-items: center; gap: 16px; padding: 8px 0;">
                  <div style="flex-shrink: 0;">
                    <a class="avatar-link" href="/u/${post.username}">
                      <img loading="lazy" width="45" height="45" src="${avatarUrl}" class="avatar" alt="${post.username}">
                    </a>
                  </div>
                  <div style="display: flex; flex-direction: column; justify-content: center; padding-top: 8px; padding-bottom: 8px;">
                    <span class="link-top-line" style="margin-bottom: 6px;">
                      <a href="${url}" class="title raw-link">${excerpt}</a>
                    </span>
                    <div class="link-bottom-line">
                      ${categoryHtml}
                      ${tagsHtml}
                    </div>
                  </div>
                </div>
              </td>
            </tr>
          `;
        }).join("");

        // Adiciona o estilo de animação se ainda não existir
        if (!document.getElementById('latest-replies-style')) {
          const style = document.createElement('style');
          style.id = 'latest-replies-style';
          style.textContent = `
            @keyframes highlightNew {
              0% { background-color: rgba(255, 255, 0, 0.3); }
              100% { background-color: transparent; }
            }
            .new-comment {
              animation: highlightNew 2s ease-out;
            }
          `;
          document.head.appendChild(style);
        }

        // Remove o contêiner existente, se houver
        const existingContainer = document.querySelector(".latest-replies-container");
        if (existingContainer) {
          existingContainer.remove();
        }

        // Cria o contêiner da seção de últimos comentários
        const latestRepliesContainer = document.createElement("div");
        latestRepliesContainer.className = "latest-replies-container";
        latestRepliesContainer.style.marginTop = "2em";

        // Adiciona botão de atualização manual e indicador de status
        const cacheTime = new Date(parseInt(localStorage.getItem(CACHE_TIMESTAMP_KEY) || Date.now()));
        const formattedTime = cacheTime.toLocaleTimeString();
        
        latestRepliesContainer.innerHTML = `
          <table class="topic-list latest-topic-list">
            <thead>
              <tr>
                <th class="default">
                  Últimos Comentários
                  <span id="comments-status" style="font-size: 0.8em; font-weight: normal; margin-left: 10px;">
                  </span>
                  <button id="refresh-comments" class="btn btn-flat no-text btn-icon" style="float: right;" title="Atualizar comentários">
                    <svg class="fa d-icon d-icon-sync svg-icon svg-string" width="16" height="16" aria-hidden="true"><use xlink:href="#sync"></use></svg>
                  </button>
                </th>
              </tr>
            </thead>
            <tbody id="latest-replies-tbody">
              ${rows}
            </tbody>
          </table>
        `;

        container.parentNode.insertBefore(latestRepliesContainer, container.nextSibling);
        container.dataset.modified = "true";
        
        // Adiciona evento de clique ao botão de atualização
        document.getElementById("refresh-comments").addEventListener("click", function() {
          // Atualiza o texto de status
          const statusElement = document.getElementById("comments-status");
          if (statusElement) {
            statusElement.textContent = "(atualizando...)";
          }
          
          // Força a atualização
          loadLatestReplies(false, true);
        });
        
        console.log(`Renderizados ${results.length} comentários`);
      }
      
      // Inicia o carregamento inicial (usando cache)
      loadLatestReplies(false, false);
      
      // Configura o polling de alta frequência
      pollingIntervalId = setInterval(() => {
        // Só atualiza se o usuário estiver na página inicial
        if (window.location.pathname === "/") {
          loadLatestReplies(true, false); // Silencioso, usa cache se disponível
        }
      }, POLLING_INTERVAL);
      
      console.log(`Polling configurado a cada ${POLLING_INTERVAL}ms`);
      
      // Limpa o intervalo quando o usuário sai da página
      api.onPageChange((url) => {
        if (url !== "/") {
          console.log("Saindo da página inicial, limpando recursos");
          
          if (pollingIntervalId) {
            clearInterval(pollingIntervalId);
            pollingIntervalId = null;
          }
          
          window.latestRepliesInitialized = false;
        }
      });
    }
  });
</script>

it’s working as expected. It’s automatically add the new comments, then add to the cache. I know thats not the ideal. I will try a plugin soon

the goal here is: instead of a category with latest topics page style, i would love a category with latest replies.