Corp/backend/static/js/components/chatHistoryLoader.js
Игорь Надеин 845c59311c refactor(messenger): Phase 2 - унификация рендеринга сообщений
- Добавлен метод MessageRenderer.createMessageElement() для создания DOM элементов
- Обновлен chatComposer для использования MessageRenderer
- Обновлен chatHistoryLoader для использования MessageRenderer
- Обновлен chatDetail.js для передачи messageRenderer в модули
- Все модули теперь используют единый источник рендеринга
- Сохранена обратная совместимость через fallback на createMessageElement()

Phase 2 of 4: Rendering unification completed
2026-01-12 10:42:11 +03:00

256 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
createMessageElement,
createDayDivider,
formatDay,
toTimestamp
} from './chatMessageTemplates.js';
/**
* Chat History Loader - загрузка ранних сообщений при прокрутке вверх
*
* РЕФАКТОРИНГ: Теперь использует MessageRenderer для создания элементов сообщений
*/
const DEFAULT_CONFIG = {
scrollSelector: '#chatScroll',
loaderSelector: '[data-history-loader]',
triggerThreshold: 140,
pageSize: 30
};
function parseBool(value) {
if (typeof value === 'boolean') return value;
return value === '1' || value === 'true';
}
function getAvatarMapSnapshot() {
if (window?.chatAvatarMap?.getAll) {
return window.chatAvatarMap.getAll();
}
if (window?.__CHAT_AVATARS__) {
return { ...window.__CHAT_AVATARS__ };
}
return {};
}
function updateOldestDataset(scrollEl) {
const firstMsg = scrollEl.querySelector('.msg[data-id]');
if (!firstMsg) {
scrollEl.dataset.oldestId = '';
scrollEl.dataset.oldestTs = '';
return null;
}
scrollEl.dataset.oldestId = String(firstMsg.dataset.id || '');
scrollEl.dataset.oldestTs = String(firstMsg.dataset.ts || '');
return firstMsg;
}
function ensureLoader(scrollEl, selector) {
let loader = scrollEl.querySelector(selector);
if (!loader) {
loader = document.createElement('div');
loader.className = 'history-loader d-none';
loader.setAttribute('data-history-loader', '1');
loader.innerHTML = `
<div class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></div>
<span class="small">Загружаем ранние сообщения…</span>
`;
scrollEl.prepend(loader);
}
return loader;
}
export function initChatHistoryLoader(options = {}) {
const config = { ...DEFAULT_CONFIG, ...options };
const scrollEl = document.querySelector(config.scrollSelector);
if (!scrollEl) return null;
const fetchUrl = scrollEl.dataset.fetchUrl;
if (!fetchUrl) return null;
const loaderEl = ensureLoader(scrollEl, config.loaderSelector);
const meId = Number(scrollEl.dataset.meId || 0);
const profileUrl = scrollEl.dataset.profileUrl || '/employees/profile/';
const detailUrlTemplate = scrollEl.dataset.detailUrlTemplate || '/employees/0/';
const avatarMap = getAvatarMapSnapshot();
const messageRenderer = options.messageRenderer || null; // РЕФАКТОРИНГ: Принимаем MessageRenderer
const state = {
loading: false,
hasMore: parseBool(scrollEl.dataset.hasMore || false),
oldestId: (() => {
const raw = scrollEl.dataset.oldestId;
if (!raw || raw === 'None') return null;
return raw;
})(),
fetchUrl,
limit: Number(scrollEl.dataset.pageSize || config.pageSize) || config.pageSize
};
function showLoader() {
loaderEl.classList.remove('d-none');
}
function hideLoader() {
loaderEl.classList.add('d-none');
}
function dedupeDayDividers(scrollEl) {
const dividerEls = scrollEl.querySelectorAll('.day-divider');
let lastLabel = null;
dividerEls.forEach((divider) => {
const label = divider.textContent.trim();
if (label === lastLabel) {
divider.remove();
} else {
lastLabel = label;
}
});
}
function prependMessages(messages = [], ctx) {
if (!messages.length) {
return;
}
const {
scrollEl,
loaderEl,
meId,
profileUrl,
detailUrlTemplate,
avatarMap,
messageRenderer,
state
} = ctx;
const prevHeight = scrollEl.scrollHeight;
const fragment = document.createDocumentFragment();
let prevDay = null;
messages.forEach((msg) => {
const ts = toTimestamp(msg);
const day = msg.day || formatDay(ts);
if (day !== prevDay) {
fragment.appendChild(createDayDivider(day));
}
prevDay = day;
// РЕФАКТОРИНГ: Используем MessageRenderer если доступен
let element;
if (messageRenderer) {
element = messageRenderer.createMessageElement(msg, false);
} else {
// Fallback для обратной совместимости
element = createMessageElement(msg, {
meId,
profileUrl,
detailUrlTemplate,
avatarMap
});
}
element.classList.remove('message-pending', 'message-pending--resolved');
fragment.appendChild(element);
});
const anchor = loaderEl.nextSibling || scrollEl.firstChild;
scrollEl.insertBefore(fragment, anchor);
const delta = scrollEl.scrollHeight - prevHeight;
scrollEl.scrollTop += delta;
dedupeDayDividers(scrollEl);
const firstMsg = updateOldestDataset(scrollEl);
if (firstMsg) {
state.oldestId = firstMsg.dataset.id;
}
}
async function loadMore() {
if (state.loading || !state.hasMore || !state.oldestId) {
return;
}
state.loading = true;
showLoader();
try {
const url = new URL(state.fetchUrl, window.location.origin);
url.searchParams.set('limit', String(state.limit));
url.searchParams.set('before_id', state.oldestId);
const response = await fetch(url.toString(), {
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
const payload = await response.json();
if (!response.ok || !payload.ok) {
throw new Error(payload.error || 'Не удалось загрузить сообщения');
}
if (Array.isArray(payload.messages) && payload.messages.length) {
prependMessages(payload.messages, {
scrollEl,
loaderEl,
meId,
profileUrl,
detailUrlTemplate,
avatarMap,
messageRenderer,
state
});
state.oldestId = String(payload.messages[0].id);
} else if (payload.next_before_id) {
state.oldestId = String(payload.next_before_id);
} else {
state.oldestId = null;
}
state.hasMore = Boolean(payload.has_more);
scrollEl.dataset.hasMore = state.hasMore ? '1' : '0';
} catch (error) {
console.error('ChatHistoryLoader: history request failed', error);
} finally {
state.loading = false;
hideLoader();
}
}
function handleScroll() {
if (!state.hasMore || state.loading) {
return;
}
if (scrollEl.scrollTop <= config.triggerThreshold) {
loadMore();
}
}
scrollEl.addEventListener('scroll', handleScroll, { passive: true });
if (!state.hasMore) {
hideLoader();
} else if (scrollEl.scrollHeight <= scrollEl.clientHeight + config.triggerThreshold) {
// Если сообщений мало и страница «короткая», сразу догружаем ещё
loadMore();
}
return {
loadMore,
hasMore: () => state.hasMore
};
}
// УДАЛЕНО: Авто-инициализация теперь происходит через chat_detail_scripts.html
// if (document.readyState === 'loading') {
// document.addEventListener('DOMContentLoaded', () => initChatHistoryLoader());
// } else {
// initChatHistoryLoader();
// }