import { formatTime, normalizeId } from "./utils.js"; import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage.js"; import { getCurrentChat } from "./app.js"; export const elements = { chatList: document.getElementById('chat-list'), chatsLoader: document.getElementById('chats-loader'), chatSearch: document.getElementById('chat-search'), backendStatusText: document.getElementById('backend-status-text'), apiStatusIndicator: document.querySelector('.pulse-dot'), noChatState: document.getElementById('no-chat-state'), activeChatContainer: document.getElementById('active-chat-container'), activeChatName: document.getElementById('active-chat-name'), activeChatAvatar: document.getElementById('active-chat-avatar'), messagesContainer: document.getElementById('messages-container'), messageForm: document.getElementById('message-form'), messageInput: document.getElementById('message-input'), backToSidebarBtn: document.querySelector('.chat-header'), sidebar: document.querySelector('.sidebar'), appContainer: document.querySelector('.app-container'), settingsModal: document.getElementById('settings-page'), settingsIconBtn: document.getElementById('settings-sidebar-btn'), saveSettingsBtn: document.getElementById('save-settings'), inputWahaUrl: document.getElementById('settings-waha-url'), inputSession: document.getElementById('settings-session'), inputApiKey: document.getElementById('settings-api-key'), inputBackgroundImage: document.getElementById('settings-background-image'), inputBackgroundOpacity: document.getElementById('settings-background-opacity'), loggedUserName: document.getElementById('pandora-username'), chatBottomBar: document.getElementById('chat-bottom-bar'), chatBottomBarBtn: document.getElementById('chat-bottom-bar-btn'), chatInputPanel: document.getElementById('chat-input-panel'), attachmentInput: document.getElementById('attachment-input'), attachmentBtn: document.getElementById('attachment-btn'), extraPages: document.querySelectorAll('.extra-page'), desktopSidebarButtons: document.querySelectorAll("#desktop-aside button") }; export const ui = { showExtraPage(pageId) { elements.extraPages.forEach(page => { if (page.id != pageId) { page.style.display = "none"; } else { page.style.display = "flex"; } }) }, /** * Switch view state when a contact chat is opened or closed */ toggleChatState(hasActive) { if (hasActive) { elements.noChatState.classList.add('hidden'); elements.activeChatContainer.classList.remove('hidden'); } else { elements.noChatState.classList.remove('hidden'); elements.activeChatContainer.classList.add('hidden'); } }, /** * Scroll message list automatically to bottom */ scrollToBottom() { elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight; }, /** * Update connection status badge in sidebar footer */ updateConnectionStatus(isConnected, text) { elements.backendStatusText.textContent = text; if (isConnected) { elements.apiStatusIndicator.style.backgroundColor = 'var(--online-color)'; elements.apiStatusIndicator.style.animation = 'pulse 1.8s infinite'; } else { elements.apiStatusIndicator.style.backgroundColor = '#ef4444'; elements.apiStatusIndicator.style.animation = 'none'; } }, async renderChatList(chats, activeChat, onChatSelect) { elements.chatList.innerHTML = ''; chats.sort((a, b) => b.timestamp - a.timestamp); if (chats.length === 0) { elements.chatList.innerHTML = `
  • No chats found
  • `; return; } for (const chat of chats) { const li = document.createElement('li'); li.className = `chat-item ${activeChat && activeChat.id === chat.id ? 'active' : ''}`; li.dataset.id = chat.id; const initials = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?'; const hasUnread = chat.unreadCount && chat.unreadCount > 0; const timeStr = formatTime(chat.timestamp || new Date()); li.innerHTML = `
    ${initials}
    ${chat.name} ${timeStr}
    ${chat.lastMessage || 'No messages yet'} ${hasUnread ? `${chat.unreadCount}` : ''}
    `; li.addEventListener('click', () => onChatSelect(chat)); elements.chatList.appendChild(li); (async () => { try { const picture = await getChatPicture(chat.id); const img = li.querySelector(`img[data-chat-avatar="${chat.id}"]`); if (img) img.src = picture.url ? picture.url : ''; } catch (e) { } })(); } }, async updateChatInChatList(msg) { const chat = document.querySelector(`.chat-item[data-id="${msg.fromMe ? msg.to : msg.from}"]`); if (chat) { const messageItem = chat.querySelector('.chat-item-msg'); const time = chat.querySelector('.chat-item-time') messageItem.innerText = msg.body || msg.text || 'Media message'; time.innerText = msg.timestamp ? formatTime(msg.timestamp) : Date.now(); const activeChatState = getCurrentChat(); if (!msg.fromMe && (!activeChatState || activeChatState.id !== (msg.fromMe ? msg.to : msg.from))) { const unreadBadge = chat.querySelector('.unread-badge'); unreadBadge.innerText = (Number(unreadBadge.innerHTML) || 0) + 1; } } }, /** * Render chat message log inside chat view container */ async renderMessages(messages, activeChatName, userID, chatId) { elements.messagesContainer.innerHTML = ''; if (messages.length === 0) { elements.messagesContainer.innerHTML = '
    No messages. Say hello!
    '; return; } const loadMore = document.createElement("button"); loadMore.classList.add("load-more-btn"); loadMore.innerText = "Load more"; loadMore.addEventListener('click', () => { this.loadMoreMessages(chatId, userID); }); elements.messagesContainer.appendChild(loadMore); for (const msg of messages) { this.appendSingleMessage(msg, userID, chatId); } this.scrollToBottom(); }, async loadMoreMessages(chatId, userId) { const oldest = document.querySelector('.message-group:first-of-type'); const oldestTimestamp = oldest.dataset.timestamp; const oldestId = oldest.id; console.log(oldest) console.log(oldestId) console.log(oldestTimestamp) const loadMoreButton = document.querySelector('.load-more-btn'); loadMoreButton.removeEventListener('click', this.loadMoreMessages); const msgs = await getMoreChatMessages(chatId, oldestTimestamp, oldestId); msgs.shift(); msgs.forEach(async msg => { loadMoreButton.after(this.generateMessage(msg, userId, chatId)); }); loadMoreButton.addEventListener('click', this.loadMoreMessages); }, /** * Append a single message (used for optimistic updates immediately upon sending) */ appendSingleMessage(msg, userID, chatId, isLocal = false) { elements.messagesContainer.appendChild(this.generateMessage(msg, userID, chatId, isLocal)) }, generateTempMessageLink(msg) { const a = document.createElement('a'); a.target = "_blank"; a.href = msg.media.url; if (msg._data?.mimetype?.startsWith('image/')) { const img = document.createElement('img'); img.classList.add('message-image-attachement'); img.src = msg.media.url; a.appendChild(img); } else { a.textContent = msg.media.filename || "Download file"; a.download = msg.media.filename; } return a; }, generateMessage(msg, userID, chatId, isLocal = false) { const isOutgoing = msg.fromMe || msg.sender === 'me'; function getPrevMessageElem() { return elements.messagesContainer.lastElementChild; } const prevMsgEl = getPrevMessageElem(); const groupDiv = document.createElement('div'); groupDiv.className = `message-group ${isOutgoing ? 'outgoing' : 'incoming'}`; groupDiv.id = normalizeId(msg._serialized ? msg : msg.id); groupDiv.dataset.timestamp = msg.timestamp; groupDiv.dataset.from = msg.participant || msg.from; const senderName = isOutgoing ? userID : (msg._data.notifyName || msg.from); const timeStr = formatTime(msg.timestamp || new Date()); let statusCheck = ''; if (isOutgoing) { if (msg.status === 'read') { statusCheck = ''; } else if (msg.status === 'delivered') { statusCheck = ''; } else if (msg.status === 'sending') { statusCheck = ''; } else { statusCheck = ''; } } const bubble = document.createElement('div'); bubble.className = 'message-bubble'; let prevUid; if (msg.participant) { prevUid = msg.participant; } else { prevUid = msg.from; } if (!isOutgoing && (!prevMsgEl || prevUid !== prevMsgEl.dataset.from)) { const senderEl = document.createElement('span'); senderEl.className = 'message-sender'; senderEl.textContent = senderName; bubble.appendChild(senderEl); } const contentEl = document.createElement('div'); contentEl.classList.add('message-content'); const textEl = document.createElement('div'); textEl.innerHTML = msg.body || msg.text || ""; contentEl.appendChild(textEl); bubble.appendChild(contentEl); if (msg.hasMedia) { let a; if (isLocal) { a = this.generateTempMessageLink(msg); } else { a = document.createElement('a'); a.innerText = `[Request media]`; a.target = "_blank"; const clickListener = async (e) => { a.removeEventListener('click', clickListener); a.innerText = `[Downloading]`; const mediaMsg = msg.media ? msg : await getMessage(chatId, normalizeId(msg._serialized ? msg : msg.id), true); if (!mediaMsg || !mediaMsg?.media.url) { a.addEventListener('click', clickListener); a.innerText = `[Error, click to try again]` return; } const url = new URL(mediaMsg.media.url); const reqID = url.pathname.split('/').filter(Boolean).pop(); const { blob, filename } = await getMedia(reqID); const objectUrl = URL.createObjectURL(blob); e.target.href = objectUrl; if (blob.type.startsWith('image/')) { a.textContent = ""; const img = document.createElement('img'); img.classList.add('message-image-attachement'); img.src = objectUrl; bubble.after(img); bubble.querySelector('.message-content').remove(); } else { e.target.textContent = filename || `Download ${mediaMsg.media.filename}`; } } a.addEventListener('click', clickListener); } contentEl.appendChild(a); if (!isLocal && msg._data?.mimetype?.startsWith('image/')) { a.click(); } } const meta = document.createElement('div'); meta.className = 'message-meta'; meta.innerHTML = `${timeStr}${statusCheck}`; bubble.appendChild(meta); if (isOutgoing) { if (prevMsgEl && prevMsgEl.classList.contains('outgoing')) { groupDiv.classList.add('same-sender'); } else { groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; } } else if (msg.participant) { if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) { groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; } else groupDiv.classList.add('same-sender'); } else { if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) { groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; } else groupDiv.classList.add('same-sender'); } groupDiv.appendChild(bubble); return groupDiv; }, updateMessage(originalMsgId, generatedMsg) { const originalMsg = document.querySelector(`#${originalMsgId}`); if (originalMsg) { originalMsg.replaceWith(generatedMsg) } }, updateMessageTick(id, status) { let statusCheck; if (status === 'read') { statusCheck = ''; } else if (status === 'delivered') { statusCheck = ''; } else if (status === 'sending') { statusCheck = ''; } else { statusCheck = ''; } document.getElementById(id).querySelector('.message-meta').outerHTML = statusCheck; }, toggleChatBottomBar() { elements.chatBottomBar.classList.toggle("collapsed"); }, removeChatMessage(msgId) { const message = document.getElementById(msgId); if (message) message.remove(); } };