Недавние ответы отображаются за 2 секунды

Есть ли способ отображать последние ответы мгновенно? Сейчас я использую этот код:

  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;">Категория: <strong>${category}</strong></span><br>`
                : '';

              const tagsHtml = tags.length
                ? `<span style="font-size: 0.85em; color: #666;">Теги: ${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("");

            // Создаём контейнер секции последних комментариев
            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">Последние комментарии</th>
                  </tr>
                </thead>
                <tbody>
                  ${rows}
                </tbody>
              </table>
            `;

            container.parentNode.insertBefore(latestRepliesContainer, container.nextSibling);
            container.dataset.modified = "true";
          });
        })
        .catch(error => {
          console.error("Ошибка при загрузке последних комментариев:", error);
        });
    }
  });
</script>

однако отображение контента занимает в среднем 2 секунды. То же самое происходит с блоками «последние ответы» в правой боковой панели. Это нормально?

Зачем вы добавляете какой-либо код?

Это работает из коробки без каких-либо специальных настроек; у вас что-то сломано?

В этом и заключается его проблема.

Я бы сказал, что это ожидаемо, поскольку код выполняет несколько запросов к API.
Он извлекает последние сообщения, а затем делает один запрос на каждый ID темы (в данном случае 15), чтобы получить название категории.

На данный момент я не знаю, есть ли другой способ, кроме использования плагина и выполнения пользовательского SQL-запроса, например.

Это точно.
Но блоки правой боковой панели тоже выполняют запросы? У меня отображаются те же результаты для последних ответов. Это занимает 2 секунды, чтобы появиться.

Я посмотрю, как создать плагин. Спасибо.

Да, при получении последних постов оно работает так же, но на этом всё. Оно не пытается получить название категории — в этом и разница.

Понимаю. Спасибо за помощь.

Я попробую найти другое решение.

Наконец-то, я пока использую это:

<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;
      
      // Предотвращаем множественные инициализации
      if (window.latestRepliesInitialized) return;
      window.latestRepliesInitialized = true;
      
      // Настройки
      const POLLING_INTERVAL = 2000; // 2 секунды
      const COMMENTS_TO_SHOW = 15;
      const CACHE_DURATION = 30 * 60 * 1000; // 30 минут
      
      // Ключи кэша
      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";
      
      // Храним последний просмотренный ID поста для сравнения
      let lastSeenPostId = parseInt(localStorage.getItem(CACHE_LAST_ID_KEY) || "0");
      let pollingIntervalId = null;
      
      console.log(`Инициализация плагина последних комментариев (последний ID в кэше: ${lastSeenPostId})`);
      
      // Функция для загрузки комментариев
      function loadLatestReplies(silent = false, forceRefresh = false) {
        // Сначала проверяем кэш, если это не принудительное обновление
        if (!forceRefresh) {
          const cachedData = localStorage.getItem(CACHE_KEY);
          const cacheTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
          const now = Date.now();
          
          // Если у нас есть валидные данные в кэше
          if (cachedData && cacheTimestamp && now - parseInt(cacheTimestamp) < CACHE_DURATION) {
            try {
              const results = JSON.parse(cachedData);
              console.log(`Используем данные из кэша (${results.length} комментариев, кэш ${Math.round((now - parseInt(cacheTimestamp)) / 1000 / 60)} минут назад)`);
              renderLatestReplies(results, false);
              
              // Если не в тихом режиме, больше ничего делать не нужно
              if (!silent) {
                return;
              }
              
              // Если в тихом режиме, продолжаем проверку обновлений
            } catch (e) {
              console.error("Ошибка при обработке кэша:", e);
              // Если ошибка в кэше, продолжаем получение свежих данных
            }
          } else if (cachedData) {
            console.log("Кэш устарел, запрашиваем свежие данные");
          } else {
            console.log("Кэш не найден, запрашиваем свежие данные");
          }
        } else {
          console.log("Принудительное обновление, игнорируем кэш");
        }
        
        // Если не в тихом режиме, показываем индикатор загрузки
        if (!silent) {
          // Если контейнер комментариев уже существует, не показываем индикатор
          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;">Загрузка последних комментариев...</span>
                </div>
              `;
              container.parentNode.insertBefore(loadingIndicator, container.nextSibling);
            }
          }
        }
        
        // Запрашиваем самые свежие данные
        fetch("/posts.json?order=created")
          .then(res => res.json())
          .then(data => {
            // Лог для диагностики
            if (!silent) {
              console.log("Данные получены от 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);
            
            // Проверяем наличие новых постов
            const maxId = replies.length > 0 ? Math.max(...replies.map(post => post.id)) : 0;
            const hasNewPosts = maxId > lastSeenPostId;
            
            // Лог для диагностики
            if (hasNewPosts && !silent) {
              console.log(`Обнаружены новые посты. Последний ID: ${lastSeenPostId}, Новый максимальный ID: ${maxId}`);
            }
            
            // Обновляем последний просмотренный ID
            if (maxId > lastSeenPostId) {
              lastSeenPostId = maxId;
              localStorage.setItem(CACHE_LAST_ID_KEY, lastSeenPostId.toString());
            }
            
            // Если новых постов нет и это тихая проверка без принудительного обновления, ничего не делаем
            if (!hasNewPosts && silent && !forceRefresh) {
              return;
            }
            
            // Запрашиваем детали тем
            const topicPromises = replies.map(post => {
              // Проверяем, есть ли тема в кэше
              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(`Ошибка при обработке кэша темы ${post.topic_id}:`, e);
                  // Если ошибка в кэше, запрашиваем с сервера
                }
              }
              
              return fetch(`/t/${post.topic_id}.json`)
                .then(res => res.json())
                .then(topic => {
                  // Сохраняем тему в кэш
                  localStorage.setItem(topicCacheKey, JSON.stringify(topic));
                  return topic;
                })
                .catch(error => {
                  console.error(`Ошибка при запросе темы ${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 || []
                  };
                });
                
                // Сохраняем результаты в кэш
                localStorage.setItem(CACHE_KEY, JSON.stringify(results));
                localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
                
                // Рендерим результаты
                renderLatestReplies(results, hasNewPosts || forceRefresh);
              })
              .catch(error => {
                console.error("Ошибка при обработке тем:", error);
              });
          })
          .catch(error => {
            console.error("Ошибка при запросе последних комментариев:", error);
            // Удаляем индикатор загрузки в случае ошибки
            if (!silent) {
              const loadingElement = document.getElementById("latest-replies-loading");
              if (loadingElement) loadingElement.remove();
            }
          });
      }
      
      // Функция для рендеринга результатов
      function renderLatestReplies(results, animate = false) {
        // Удаляем индикатор загрузки
        const loadingElement = document.getElementById("latest-replies-loading");
        if (loadingElement) loadingElement.remove();
        
        // Если нет результатов, ничего не делаем
        if (!results || results.length === 0) {
          console.log("Комментарии для отображения не найдены");
          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;">Категория: <strong>${category}</strong></span><br>`
            : '';

          const tagsHtml = tags.length
            ? `<span style="font-size: 0.85em; color: #666;">Теги: ${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("");

        // Добавляем стиль анимации, если его еще нет
        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);
        }

        // Удаляем существующий контейнер, если есть
        const existingContainer = document.querySelector(".latest-replies-container");
        if (existingContainer) {
          existingContainer.remove();
        }

        // Создаем контейнер секции последних комментариев
        const latestRepliesContainer = document.createElement("div");
        latestRepliesContainer.className = "latest-replies-container";
        latestRepliesContainer.style.marginTop = "2em";

        // Добавляем кнопку ручного обновления и индикатор статуса
        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">
                  Последние комментарии
                  <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="Обновить комментарии">
                    <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";
        
        // Добавляем обработчик клика для кнопки обновления
        document.getElementById("refresh-comments").addEventListener("click", function() {
          // Обновляем текст статуса
          const statusElement = document.getElementById("comments-status");
          if (statusElement) {
            statusElement.textContent = "(обновление...)";
          }
          
          // Принудительное обновление
          loadLatestReplies(false, true);
        });
        
        console.log(`Отрендерено ${results.length} комментариев`);
      }
      
      // Запускаем начальную загрузку (используя кэш)
      loadLatestReplies(false, false);
      
      // Настраиваем высокочастотный опрос
      pollingIntervalId = setInterval(() => {
        // Обновляем только если пользователь на главной странице
        if (window.location.pathname === "/") {
          loadLatestReplies(true, false); // Тихий режим, используем кэш, если доступен
        }
      }, POLLING_INTERVAL);
      
      console.log(`Опрос настроен каждые ${POLLING_INTERVAL}мс`);
      
      // Очищаем интервал, когда пользователь уходит со страницы
      api.onPageChange((url) => {
        if (url !== "/") {
          console.log("Уход с главной страницы, очистка ресурсов");
          
          if (pollingIntervalId) {
            clearInterval(pollingIntervalId);
            pollingIntervalId = null;
          }
          
          window.latestRepliesInitialized = false;
        }
      });
    }
  });
</script>

Это работает как ожидалось. Новые комментарии автоматически добавляются и сохраняются в кэш. Я знаю, что это не идеальный вариант. Скоро попробую сделать полноценный плагин.

Моя цель здесь: вместо категории со страницей в стиле «последние темы», я бы хотел категорию с последними ответами.