import { config } from "./config"; import { waha } from "./waha"; import { ui, elements, views } from "./ui"; import { websocket } from "./websocket"; import { compensateMessageOrdering, debounce, formatTime, normalizeId } from "./utils"; import { fetchChats, getAppUser, getChatMessages, getChatPicture, getChats, getGroupUsers, getUser, getUserAbout, getUsersFromGroup, markRead, sendStatus, updateOnlineStatus } from "./storage"; import { deleteDatabase, upsertMessages } from "./db"; import { showNotification } from "./notification"; import type { Chat, GroupUser, Message, WebSocketEvent } from "./types"; import { activeChatState, clearMentionCache, clearMentionedContacts, getMentionedIDs, mentionCacheID, mentionCacheText, mentionedContact, mentionedContacts, removeMentionedContact, setActiveChatState } from "./states"; if (localStorage.getItem('setupComplete') !== "true") window.location.href = "index.html"; const messageTone = new Audio("./message.ogg"); const longPressEvent = new CustomEvent("longpress"); export let isLoadingChat = false; export let notificationAuthorization: NotificationPermission = "default"; const mainViewEl = document.getElementById("main-view"); if (!mainViewEl) throw console.error(); const mainView = views.get(mainViewEl); document.addEventListener('DOMContentLoaded', async () => { ui.load(async () => { ui.loadingMessage("Drawing sidebar..."); updateSidebarPosition(); ui.loadingMessage("Asking for notification permission..."); askForNotificationPermission(); ui.loadingMessage("Loading configuration..."); if (elements.inputApiKey) elements.inputApiKey.value = config.apiKey; if (elements.inputWahaUrl) elements.inputWahaUrl.value = config.wahaUrl; if (elements.inputSession) elements.inputSession.value = config.session; if (elements.inputBackgroundImage) elements.inputBackgroundImage.value = config.bgImg; if (elements.inputBackgroundOpacity) elements.inputBackgroundOpacity.value = config.bgOpacity; if (elements.activeChatContainer) { elements.activeChatContainer.style.setProperty('--background-image', `URL("${config.bgImg}")`); elements.activeChatContainer.style.setProperty('--background-opacity', `${config.bgOpacity}`); } ui.loadingMessage("Getting server version..."); await updateOnlineStatus(); ui.loadingMessage("Setting up event listeners..."); setupEventListeners(); try { ui.loadingMessage("Replacing placeholders..."); await setupElementsData(); ui.loadingMessage("Loading chats..."); await loadChats(); ui.loadingMessage("Checking server status..."); await checkWahaStatus(); ui.loadingMessage("Telling server to send new messages..."); await initWebSocket(); } finally { elements.chatsLoader.classList.add('hidden'); } }) }); async function askForNotificationPermission() { notificationAuthorization = await Notification.requestPermission(); } async function setupElementsData() { try { const usr = await getAppUser(); const usrPic = (await getChatPicture(usr.id))?.url; const usrInfo = await getUser(usr.id); const usrAbout = (await getUserAbout(usr.id))?.about; elements.contentUserName.forEach(e => { e.innerHTML = usr.pushName || usr.name || ''; }) elements.contentUserNumber.forEach(async e => { if (usrInfo) e.innerHTML = usrInfo.number; }) elements.resourceUserPic.forEach(async e => { if (usrPic) e.src = usrPic; }) elements.valueUserStatus.forEach(async e => { if (usrAbout) e.value = usrAbout.trim(); }) } catch (error: any) { console.error(error.message); } } function purgeDatabase(ask: boolean = true) { if (ask) { if (!(confirm("Are you sure you want to delete all cached messages?") && confirm("This cannot be undone. Proceed?"))) return; } localStorage.clear(); deleteDatabase(); location.reload(); } function loadChats() { elements.chatsLoader.classList.remove('hidden'); try { fetchChats().then(async () => { ui.renderChatList(getChats(), activeChatState, selectChat); const hash = window.location.hash; if (hash && hash.startsWith('#chat-')) { const chatId = hash.replace('#chat-', ''); const chat = getChats().find(c => c.id === chatId); if (chat) { selectChat(chat, true, false); } } }); } catch (error: any) { console.error('Failed to load chats:', error); elements.chatList.innerHTML = `
  • Connection to WAHA failed.

    Ensure WAHA server is running and CORS is enabled, or click Settings to configure.

    ${error.message}

  • `; } finally { elements.chatsLoader.classList.add('hidden'); } } let isScrollingProgrammatically = false; let scrollTimeout: ReturnType | null = null; function scrollToChat(smooth = true) { isScrollingProgrammatically = true; elements.appContainer.scrollTo({ left: elements.appContainer.clientWidth, behavior: smooth ? 'smooth' : 'auto' }); setTimeout(() => { isScrollingProgrammatically = false; }, smooth ? 400 : 50); } function scrollToList(smooth = true) { isScrollingProgrammatically = true; mainViewEl?.scrollTo({ left: 0, behavior: smooth ? 'smooth' : 'auto' }); setTimeout(() => { isScrollingProgrammatically = false; }, smooth ? 400 : 50); } function setupEventListeners() { window.addEventListener('resize', updateSidebarPosition); if (!window.location.hash) { window.location.hash = ''; } window.addEventListener('hashchange', () => { const hash = window.location.hash; if (hash && hash.startsWith('#chat-')) { const chatId = hash.replace('#chat-', ''); const chat = getChats().find(c => c.id === chatId); if (chat) { selectChat(chat, true); } } else { closeActiveChat(true); } }); elements.appContainer.addEventListener('scroll', () => { if (window.innerWidth > 768) return; if (isScrollingProgrammatically) return; if (scrollTimeout) clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { const scrollLeft = elements.appContainer.scrollLeft; const width = elements.appContainer.clientWidth; if (scrollLeft < width * 0.2) { if (activeChatState) { closeActiveChat(false); } } }, 100); }); elements.appContainer.addEventListener('resize', () => { if (window.innerWidth > 768) { elements.desktopAside.after(elements.sidebar); } else { mainViewEl?.insertBefore(elements.sidebar, mainViewEl.firstChild); } }) document.addEventListener('keydown', (e) => { if (e.code == "Escape") { e.preventDefault(); closeActiveChat(false); } }); elements.chatSearch.addEventListener('input', (e: Event) => { const query = (e.target as HTMLInputElement).value.toLowerCase(); const filtered = getChats().filter(chat => chat.name.toLowerCase().includes(query) ); ui.renderChatList(filtered, activeChatState, selectChat); }); ui.autoResizeTextArea(elements.messageInput); elements.messageForm.addEventListener('submit', (e) => { e.preventDefault(); sendMessage(); }); elements.messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); elements.messageInput.addEventListener('input', (e) => { const inputEvent = e as InputEvent; mentionedContacts.forEach(c => { if (!elements.messageInput.value.includes(`@${c.number}`)) { removeMentionedContact(c); } }) if (inputEvent.data === "@") { suggestMention(); } else { elements.mentioningSuggestion.classList.add('collapsed'); } }); elements.chatBottomBar.style.height = `${elements.chatInputPanel.offsetHeight}px`; const observer = new ResizeObserver(() => { elements.chatBottomBar.style.height = `${elements.chatInputPanel.offsetHeight}px`; }); elements.mentioningIndicator.addEventListener('click', clearMentionCache); observer.observe(elements.chatInputPanel); elements.chatBottomBarBtn.addEventListener('click', ui.toggleChatBottomBar); elements.chatBottomBar.addEventListener('click', (e) => { if (e.target == e.currentTarget) ui.toggleChatBottomBar(); }); elements.markreadBtn.addEventListener('click', async () => { if (activeChatState) { const result = await markRead(activeChatState.id); if (result) ui.updateChatInChatList2(result); } }); elements.attachmentBtn.addEventListener('click', () => { elements.attachmentInput.click(); }); elements.attachmentInput.addEventListener('change', function (this: HTMLInputElement) { const firstFile = this.files?.[0]; if (firstFile) sendFileMessage(firstFile); }); elements.mentionBtn.addEventListener('click', async () => { suggestMention(); ui.toggleChatBottomBar(); }); elements.backToSidebarBtn.addEventListener('click', () => { closeActiveChat(false); }); elements.desktopSidebarButtons.forEach(sidebarBtn => { sidebarBtn.addEventListener('click', () => { const page = sidebarBtn.dataset.page; if (!page) return; const pageEl = document.getElementById(page); if (!pageEl) return; if (page) mainView?.scrollTo(pageEl); }) }) elements.saveSettingsBtn.addEventListener('click', saveSettings); elements.purgeDatabaseButton.addEventListener('click', () => purgeDatabase(true)); elements.inputUserStatus.addEventListener('input', debounce(async function() { const result = await sendStatus(elements.inputUserStatus.value); if (result?.success) { showNotification("Status updated successfully!", "", 2000); } else { showNotification("Failed to update status...", "", 2000); } }, 2000)) elements.selectable.forEach(e => { let timerId: ReturnType, longPressed: boolean; e.addEventListener('mousedown', () => { longPressed = false; timerId = setTimeout(() => { longPressed = true; e.dispatchEvent(longPressEvent); }, 500); // 500ms for long press }) e.addEventListener('click', (event) => { if (longPressed) { event.preventDefault(); clearTimeout(timerId); } }) e.addEventListener('mouseleave', () => { clearTimeout(timerId); }) }) } function updateSidebarPosition() { if (!mainViewEl || !elements.sidebar || !elements.desktopAside) return; if (window.innerWidth > 768) { if (elements.sidebar.parentElement !== elements.appContainer) { elements.desktopAside.after(elements.sidebar); } } else { if (elements.sidebar.parentElement !== mainViewEl) { mainViewEl.insertBefore(elements.sidebar, mainViewEl.firstChild); } } } function initWebSocket() { websocket.connect((data: WebSocketEvent) => { const ev = data.event; if (ev === 'message' || ev === 'message.any' || ev === 'message.ack') { handleIncomingMessage(data.payload); upsertMessages([data.payload]); } }); } async function handleIncomingMessage(msg: Message) { if (!msg) return; ui.updateChatInChatList(msg); const rawChatId = msg.chatId || (typeof msg.from === 'string' ? msg.from : (msg.from as any)?._serialized) || (msg.chat && msg.chat.id); const msgChatId = normalizeId(rawChatId); if (!msgChatId) { console.warn('[WS] Could not resolve chatId from payload:', msg); return; } if (!msg.fromMe) { messageTone.play(); if (notificationAuthorization === "granted") { new Notification("New message", { body: msg.body || msg.text }); } } if (activeChatState && activeChatState.id === msgChatId) { const msgId = normalizeId(msg.id as any) || (msg.id as string); const exists = document.getElementById(msgId); if (!exists) { const container = elements.messagesContainer; const scrolled = container.scrollTop === (container.scrollHeight - container.clientHeight); ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, (await getAppUser()).id); if (scrolled && window.innerWidth > 768) { ui.scrollToBottom(); } } } } async function selectChat(chat: Chat, isPopState = false, smoothScroll = true) { if (isLoadingChat) return; clearMentionCache(); clearMentionedContacts(); const pageEl = document.getElementById("chat-page"); if (!pageEl) return; mainView?.scrollTo(pageEl); isLoadingChat = true; setActiveChatState(chat); chat.unreadCount = 0; ui.toggleChatState(true); elements.activeChatName.textContent = chat.name.toUpperCase(); elements.activeChatAvatar.textContent = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?'; elements.messagesContainer.innerHTML = `

    `; elements.appContainer.classList.remove('no-active-chat'); if (window.innerWidth <= 768) { scrollToChat(smoothScroll); } if (!isPopState && window.location.hash !== `#chat-${chat.id}`) { window.location.hash = ``; window.location.hash = `chat-${chat.id}`; } try { const rawMessages = await getChatMessages(chat.id); const processedMessages = compensateMessageOrdering(rawMessages); ui.renderMessages(processedMessages, chat.name, (await getAppUser()).id, chat.id); } catch (error) { console.error('Failed to load messages:', error); elements.messagesContainer.innerHTML = '
    Error loading messages
    '; } isLoadingChat = false; } async function closeActiveChat(isPopState = false) { setActiveChatState(null); if (window.innerWidth <= 768) { scrollToList(); } else { ui.toggleChatState(false); } if (!isPopState) { if (window.location.hash.startsWith('#chat-')) { history.back(); } } } async function sendMessage() { const text = elements.messageInput.value.trim(); if (!text || !activeChatState) return; const _mentionCacheID = mentionCacheID; const _mentionCacheText = mentionCacheText; clearMentionCache(); elements.messageInput.value = ''; elements.messageInput.dispatchEvent(new Event("input", { bubbles: true })); const tempMsg = { id: 'temp-' + Date.now(), body: text, fromMe: true, sender: 'me', timestamp: new Date().toISOString(), status: 'sending', replyTo: _mentionCacheID ? { body: _mentionCacheText || "Mention (no text)" } : null } as any; ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id); ui.scrollToBottom(); try { try { if (!activeChatState.id.endsWith('@lid')) { await waha.readChat(activeChatState.id); } } catch (e: any) { console.warn('readChat failed (non-fatal):', e.message); } try { await waha.startTyping(activeChatState.id); const delay = Math.min(4000, Math.max(1000, text.length * 50)); await new Promise(resolve => setTimeout(resolve, delay)); } catch (e) { console.warn('Presence start failed:', e); } try { await waha.stopTyping(activeChatState.id); } catch (e) { console.warn('Presence stop failed:', e); } const responseData = await waha.sendTextMessage(activeChatState.id, text, getMentionedIDs(), _mentionCacheID); const tempBubble = document.getElementById(tempMsg.id); if (tempBubble) { if (responseData && responseData.id) { tempBubble.id = normalizeId(responseData.id as any) || tempBubble.id; } const meta = tempBubble.querySelector('.message-meta'); if (meta) meta.innerHTML = `${formatTime(new Date())}`; } activeChatState.lastMessage = text; activeChatState.timestamp = new Date().toISOString(); } catch (error) { console.error('Failed to send message:', error); const tempBubble = document.getElementById(tempMsg.id); if (tempBubble) { const meta = tempBubble.querySelector('.message-meta'); if (meta) meta.innerHTML = `Failed to send`; } } } async function sendFileMessage(file: File) { if (!activeChatState) return; try { const tempId = 'temp-' + Date.now(); const tempMsg = { _data: { mimetype: file.type }, id: tempId, body: "", fromMe: true, sender: 'me', timestamp: new Date().toISOString(), status: 'sending', hasMedia: true, media: { url: URL.createObjectURL(file), filename: file.name } } as any; ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id, true); ui.scrollToBottom(); const result = await waha.sendFileMessage(activeChatState.id, file); ui.removeChatMessage(tempId); ui.appendSingleMessage(result, activeChatState.name, (await getAppUser()).id); } catch (error: any) { console.error(error.message); } } function saveSettings() { config.save( elements.inputWahaUrl.value, elements.inputSession.value, elements.inputApiKey.value, elements.inputBackgroundImage.value, elements.inputBackgroundOpacity.value ); location.reload(); loadChats(); checkWahaStatus(); initWebSocket(); } async function suggestMention() { elements.mentionSuggestions.innerHTML = ""; if (activeChatState) { const gpUsrs: GroupUser[] | undefined = await getGroupUsers(activeChatState.id); if (!gpUsrs) return; const usrs = await getUsersFromGroup(gpUsrs); if (!usrs) return; elements.mentioningSuggestion.classList.remove('collapsed'); usrs.forEach(u => { if (!u) return; const contact = document.createElement('div'); contact.classList = "mention-suggestion"; const name = u.name ?? u.pushname; if (!name) return; contact.innerText = name; contact.addEventListener('click', () => { mentionedContact(u); elements.messageInput.value = elements.messageInput.value.substring(0, elements.messageInput.value.length - 1); elements.messageInput.value += `@${u.number} `; elements.messageInput.dispatchEvent(new Event("input", { bubbles: true })); elements.mentioningSuggestion.classList.add("collapsed"); elements.messageInput?.focus(); }); elements.mentionSuggestions.appendChild(contact); }) } } async function checkWahaStatus() { try { const data = await waha.getVersion(); ui.updateConnectionStatus(true, `WAHA Connected: v${data.version || 'OK'}`); } catch (e) { ui.updateConnectionStatus(false, 'WAHA Server Offline'); } } export function getCurrentChat() { return activeChatState; }