import { Component, Prop, State, Event, EventEmitter, h, Element, Watch, Fragment } from '@stencil/core'; import { ApiService } from '../../services/api.service'; import { ChatStorageService } from '../../services/chat-storage.service'; import { BackendMessage, ChatListItem, PaginatedMessagesResponse } from '../../types/api'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import 'dayjs/locale/pl'; import 'dayjs/locale/en'; dayjs.extend(relativeTime); @Component({ tag: 'bcx-chat-list', styleUrl: 'bcx-chat-list.scss', shadow: true, }) export class BcxChatList { @Element() el: HTMLElement; @Prop() apiService: ApiService; @Prop() language: 'pl' | 'en' = 'en'; @Prop() theme: 'light' | 'dark' = 'light'; @State() chats: ChatListItem[] = []; @State() selectedChatId: string | null = null; @State() messages: BackendMessage[] = []; @State() isLoading: boolean = false; @State() isLoadingMore: boolean = false; @State() hasMore: boolean = false; @State() currentPage: number = 1; @State() error: string | null = null; @Event() chatSelected: EventEmitter; @Event() close: EventEmitter; private messagesContainerRef: HTMLDivElement; private observer: IntersectionObserver | null = null; private loadMoreTriggerRef: HTMLDivElement; private pageSize: number = 20; @Watch('language') async onLanguageChange() { // Set dayjs locale based on language prop dayjs.locale(this.language === 'pl' ? 'pl' : 'en'); } componentWillLoad() { // Set initial dayjs locale dayjs.locale(this.language === 'pl' ? 'pl' : 'en'); this.loadChats(); } componentDidLoad() { // Setup infinite scroll after messages are loaded if (this.selectedChatId) { // OPTYMALIZACJA: Użycie requestAnimationFrame zamiast setTimeout requestAnimationFrame(() => { requestAnimationFrame(() => { this.setupInfiniteScroll(); }); }); } } componentDidUpdate() { // Re-setup infinite scroll when selected chat changes if (this.selectedChatId && this.messages.length > 0) { // OPTYMALIZACJA: Użycie requestAnimationFrame zamiast setTimeout requestAnimationFrame(() => { requestAnimationFrame(() => { this.setupInfiniteScroll(); }); }); } } disconnectedCallback() { if (this.observer) { this.observer.disconnect(); } } private loadChats() { this.chats = ChatStorageService.getChats(); } private async selectChat(chatId: string) { if (this.selectedChatId === chatId) { return; } // Cleanup previous observer if (this.observer) { this.observer.disconnect(); this.observer = null; } this.selectedChatId = chatId; this.messages = []; this.currentPage = 1; this.hasMore = false; this.error = null; await this.loadMessages(chatId, 1); // OPTYMALIZACJA: Użycie requestAnimationFrame zamiast setTimeout requestAnimationFrame(() => { requestAnimationFrame(() => { this.setupInfiniteScroll(); }); }); } private async loadMessages(chatId: string, page: number) { if (page === 1) { this.isLoading = true; } else { this.isLoadingMore = true; } try { const response: PaginatedMessagesResponse = await this.apiService.getChatMessages(chatId, page, this.pageSize); if (page === 1) { this.messages = response.results; } else { // Append older messages at the top (since we're loading backwards) this.messages = [...response.results, ...this.messages]; } this.hasMore = response.has_next; this.currentPage = page; } catch (error) { this.error = error.message || 'Failed to load messages'; console.error('[ChatList] Error loading messages:', error); } finally { this.isLoading = false; this.isLoadingMore = false; } } private async loadMoreMessages() { if (!this.selectedChatId || this.isLoadingMore || !this.hasMore) { return; } await this.loadMessages(this.selectedChatId, this.currentPage + 1); } private setupInfiniteScroll() { if (!this.loadMoreTriggerRef) { return; } this.observer = new IntersectionObserver( entries => { if (entries[0].isIntersecting && this.hasMore && !this.isLoadingMore) { this.loadMoreMessages(); } }, { root: this.messagesContainerRef, rootMargin: '100px', threshold: 0.1, }, ); this.observer.observe(this.loadMoreTriggerRef); } private formatMessageTime(timestamp: string): string { // Determine locale based on current language const locale = this.language === 'pl' ? 'pl' : 'en'; // Use dayjs with locale directly on the instance // dayjs automatically handles locale loading if imported return dayjs(timestamp).locale(locale).fromNow(); } private formatChatTime(timestamp: string): string { // Determine locale based on current language const locale = this.language === 'pl' ? 'pl' : 'en'; // Use dayjs with locale directly on the instance // dayjs automatically handles locale loading if imported return dayjs(timestamp).locale(locale).fromNow(); } private truncateMessage(message: string, maxLength: number = 60): string { if (message.length <= maxLength) { return message; } return message.substring(0, maxLength) + '...'; } private handleBackClick = () => { this.selectedChatId = null; this.messages = []; this.currentPage = 1; this.hasMore = false; }; private handleChatClick = (chatId: string) => { this.selectChat(chatId); this.chatSelected.emit(chatId); }; private handleCloseClick = () => { this.close.emit(); }; render() { if (this.selectedChatId) { return (
{/* Header */}

{this.language === 'pl' ? 'Wiadomości' : 'Messages'}

{/* Messages */}
(this.messagesContainerRef = el)} data-adblock-bypass="true"> {this.isLoading && this.messages.length === 0 ? (
{this.language === 'pl' ? 'Ładowanie...' : 'Loading...'}
) : this.error ? (

{this.error}

) : ( {/* Load more trigger */} {this.hasMore && (
(this.loadMoreTriggerRef = el)} class="bcx-chat-list__load-more-trigger"> {this.isLoadingMore && (
)}
)} {this.messages.map(message => (
{message.content}
{this.formatMessageTime(message.created_at)}
))}
)}
); } return (
{/* Header */}

{this.language === 'pl' ? 'Wszystkie konwersacje' : 'All conversations'}

{/* Chat List */}
{this.chats.length === 0 ? (

{this.language === 'pl' ? 'Brak konwersacji' : 'No conversations yet'}

) : ( this.chats.map(chat => ( )) )}
); } }