Corp/backend/static/js/config/chatConfig.js
Игорь Надеин 53ec1a7b87 feat: implement Telegram-style image dimension pre-loading to fix auto-scroll
- Backend: added width/height fields to MessageAttachment model with Pillow auto-extraction
- Migration: 0021_add_image_dimensions + populate_image_dimensions management command
- API: serialize width/height in messageAttachmentSerializer and consumers.py
- Frontend: switched from CSS aspect-ratio to explicit pixel calculation (Telegram approach)
  * messageRendererV2.js: calculate exact dimensions (maxWidth=500px, proportional height)
  * Fixes scroll jump issue: images now reserve exact space before loading
  * Debug logging added to validate dimension rendering
- Testing: 18/18 images populated successfully with dimensions

This prevents scroll position shifts when images load after initial positioning,
implementing the same approach as Telegram Web.

Related: scroll positioning, image preloading, chat UX
2026-01-14 17:32:06 +03:00

218 lines
6.5 KiB
JavaScript
Raw Permalink 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.

/**
* @fileoverview Централизованная конфигурация чат-модуля
* @module config/chatConfig
*/
/**
* Конфигурация загрузчика сообщений
*/
export const LOADER_CONFIG = {
/** Количество сообщений для начальной загрузки */
INITIAL_LIMIT: 30,
/** Количество сообщений для загрузки истории */
HISTORY_LIMIT: 20,
/** Максимальное количество попыток при ошибке */
MAX_RETRIES: 3,
/** Базовая задержка между попытками (мс) */
RETRY_DELAY_BASE: 1000,
/** Таймаут запроса (мс) */
REQUEST_TIMEOUT: 15000,
/** Дебаунс для загрузки истории (мс) */
HISTORY_DEBOUNCE: 150
};
/**
* Конфигурация скролла
*/
export const SCROLL_CONFIG = {
/** Порог в px для определения "пользователь внизу" */
BOTTOM_THRESHOLD: 100,
/** Порог в px для триггера загрузки истории */
HISTORY_TRIGGER_THRESHOLD: 140,
/** Отступ для IntersectionObserver (px) */
OBSERVER_ROOT_MARGIN: '100px 0px 0px 0px',
/** Порог видимости для IntersectionObserver (0-1) */
OBSERVER_THRESHOLD: 0.1,
/** Задержка перед восстановлением позиции скролла (мс) */
SCROLL_RESTORE_DELAY: 50
};
/**
* Конфигурация умного автоскролла и индикатора новых сообщений
*/
export const AUTOSCROLL_CONFIG = {
/** ID кнопки "новые сообщения" */
NEW_MESSAGES_BTN_ID: 'new-messages-btn',
/** Класс кнопки "новые сообщения" */
NEW_MESSAGES_BTN_CLASS: 'new-messages-indicator',
/** Длительность анимации показа (мс) */
SHOW_ANIMATION_DURATION: 300,
/** Длительность анимации скрытия (мс) */
HIDE_ANIMATION_DURATION: 200,
/** Использовать плавный скролл для собственных сообщений */
SMOOTH_SCROLL_FOR_OWN: false,
/** Использовать плавный скролл по клику на индикатор */
SMOOTH_SCROLL_ON_CLICK: true,
/** Порог для показа индикатора (px от низа) */
SHOW_INDICATOR_THRESHOLD: 100
};
/**
* Конфигурация рендеринга
*/
export const RENDER_CONFIG = {
/** Использовать visibility:hidden при рендеринге */
USE_VISIBILITY_HIDING: true,
/** Задержка перед скроллом после рендеринга (мс) */
POST_RENDER_DELAY: 50,
/** Максимальное количество сообщений в DOM */
MAX_DOM_MESSAGES: 500,
/** Количество сообщений для виртуализации */
VIRTUALIZATION_BUFFER: 50
};
/**
* Конфигурация WebSocket
*/
export const WS_CONFIG = {
/** Таймаут ожидания подключения (мс) */
CONNECTION_TIMEOUT: 10000,
/** Интервал ping (мс) */
PING_INTERVAL: 30000,
/** Максимальное время ожидания pong (мс) */
PONG_TIMEOUT: 5000
};
/**
* Типы событий Store
* @enum {string}
*/
export const STORE_EVENTS = {
MESSAGE_ADDED: 'message_added',
MESSAGE_UPDATED: 'message_updated',
MESSAGE_REMOVED: 'message_removed',
MESSAGES_LOADED: 'messages_loaded',
CHAT_CLEARED: 'chat_cleared',
OPTIMISTIC_CONFIRMED: 'optimistic_confirmed',
OPTIMISTIC_FAILED: 'optimistic_failed'
};
/**
* Типы событий чата (для window events)
* @enum {string}
*/
export const CHAT_EVENTS = {
INITIALIZED: 'chat:initialized',
MESSAGE_ADDED: 'chat:message-added',
MESSAGE_UPDATED: 'chat:message-updated',
MESSAGE_REMOVED: 'chat:message-removed',
HISTORY_LOADED: 'chat:history-loaded',
SCROLL_TO_BOTTOM: 'chat:scroll-to-bottom',
ERROR: 'chat:error'
};
/**
* Типы WebSocket событий
* @enum {string}
*/
export const WS_EVENTS = {
NEW_MESSAGE: 'ws:new-message',
MESSAGE_EDITED: 'ws:message-edited',
MESSAGE_REMOVED: 'ws:message-removed',
REACTION_ADDED: 'ws:reaction-added',
REACTION_REMOVED: 'ws:reaction-removed'
};
/**
* Статусы сообщения
* @enum {string}
*/
export const MESSAGE_STATUS = {
SENDING: 'sending',
SENT: 'sent',
FAILED: 'failed',
READ: 'read'
};
/**
* API endpoints (шаблоны)
*/
export const API_ENDPOINTS = {
/** Получить сообщения чата */
MESSAGES: (chatId) => `/api/v1/communications/chats/${chatId}/messages/`,
/** Получить сообщения вокруг конкретного ID */
MESSAGES_AROUND: (chatId) => `/api/v1/communications/chats/${chatId}/messages-around/`,
/** Отметить сообщения как прочитанные */
MARK_READ: (chatId) => `/api/v1/communications/chats/${chatId}/mark-read/`,
/** Отправить сообщение */
SEND_MESSAGE: (chatId) => `/api/v1/communications/chats/${chatId}/messages/`,
/** Загрузить файл */
UPLOAD_FILE: (chatId) => `/api/v1/communications/chats/${chatId}/upload/`
};
/**
* Генерирует URL с query параметрами
* @param {string} baseUrl - Базовый URL
* @param {Object} params - Параметры запроса
* @returns {string} URL с параметрами
*/
export function buildUrl(baseUrl, params = {}) {
const url = new URL(baseUrl, window.location.origin);
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
url.searchParams.set(key, String(value));
}
});
return url.toString();
}
/**
* Создает заголовки для AJAX запросов
* @returns {Object} Заголовки
*/
export function getRequestHeaders() {
return {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
};
}
export default {
LOADER_CONFIG,
SCROLL_CONFIG,
AUTOSCROLL_CONFIG,
RENDER_CONFIG,
WS_CONFIG,
STORE_EVENTS,
CHAT_EVENTS,
WS_EVENTS,
MESSAGE_STATUS,
API_ENDPOINTS,
buildUrl,
getRequestHeaders
};