From 9d77ea5a9712b978c98a0c3285e7e94ab4254fcc Mon Sep 17 00:00:00 2001 From: Adrian Victor Date: Mon, 13 Jul 2026 11:24:19 -0300 Subject: [PATCH 01/37] Change layout to look more like WhatsApp for Windows Phone and less like AI slop. --- {public => docs}/app.js | 4 +- {public => docs}/config.js | 0 {public => docs}/index.html | 27 ++--- {public => docs}/style.css | 184 +++++++++++++++++++--------------- {public => docs}/ui.js | 22 ++-- {public => docs}/utils.js | 0 {public => docs}/waha.js | 4 + {public => docs}/websocket.js | 0 index.js | 4 +- 9 files changed, 132 insertions(+), 113 deletions(-) rename {public => docs}/app.js (98%) rename {public => docs}/config.js (100%) rename {public => docs}/index.html (82%) rename {public => docs}/style.css (84%) rename {public => docs}/ui.js (90%) rename {public => docs}/utils.js (100%) rename {public => docs}/waha.js (96%) rename {public => docs}/websocket.js (100%) diff --git a/public/app.js b/docs/app.js similarity index 98% rename from public/app.js rename to docs/app.js index 8e2ebfb..019cb0b 100644 --- a/public/app.js +++ b/docs/app.js @@ -115,7 +115,7 @@ function handleIncomingMessage(msg) { async function fetchChats() { const userInfo = await waha.getMyInfo(); elements.loggedUserName.textContent = userInfo.pushName; - elements.userIcon.innerText = userInfo.pushName[0]; + // elements.userIcon.innerText = userInfo.pushName[0]; elements.chatsLoader.classList.remove('hidden'); try { chatsState = await waha.getChats(); @@ -143,7 +143,7 @@ async function selectChat(chat) { ui.renderChatList(chatsState, activeChatState, selectChat); ui.toggleChatState(true); - elements.activeChatName.textContent = chat.name; + elements.activeChatName.textContent = chat.name.toUpperCase(); elements.activeChatAvatar.textContent = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?'; elements.messagesContainer.innerHTML = '
Loading messages...
'; diff --git a/public/config.js b/docs/config.js similarity index 100% rename from public/config.js rename to docs/config.js diff --git a/public/index.html b/docs/index.html similarity index 82% rename from public/index.html rename to docs/index.html index eb79442..f8fc47e 100644 --- a/public/index.html +++ b/docs/index.html @@ -8,39 +8,34 @@ - +
- -
- -
-
-
-
-

Select a contact from the sidebar to view the conversation or start a new chat.

-
-
- - - -
+ + +
+
+ +
+
+ + +
+
+ +
+ + +
+
+ +
-
diff --git a/js/app.js b/js/app.js index 244dc67..3834d72 100644 --- a/js/app.js +++ b/js/app.js @@ -120,14 +120,6 @@ function setupEventListeners() { } }); - elements.refreshChatsBtn.addEventListener('click', () => { - elements.refreshChatsBtn.classList.add('spinning'); - loadChats() - setTimeout(() => { - elements.refreshChatsBtn.classList.remove('spinning'); - }, 600); - }); - elements.chatSearch.addEventListener('input', (e) => { const query = e.target.value.toLowerCase(); const filtered = getChats().filter(chat => @@ -164,9 +156,13 @@ function setupEventListeners() { elements.backToSidebarBtn.addEventListener('click', () => { closeActiveChat(false); }); - - elements.settingsIconBtn.addEventListener('click', openSettings); - elements.cancelSettingsBtn.addEventListener('click', () => ui.toggleModal(false)); + + elements.desktopSidebarButtons.forEach(sidebarBtn => { + sidebarBtn.addEventListener('click', () => { + ui.showExtraPage(sidebarBtn.dataset.page); + }) + }) + elements.saveSettingsBtn.addEventListener('click', saveSettings); } @@ -271,20 +267,15 @@ async function closeActiveChat(isPopState = false) { if (window.innerWidth <= 768) { scrollToList(); - setTimeout(() => { - if (!activeChatState) { - elements.appContainer.classList.add('no-active-chat'); - // ui.toggleChatState(false); - } - }, 350); } else { ui.toggleChatState(false); } + if (!isPopState) { - // if (window.location.hash.startsWith('#chat-')) { - // history.back(); - // } + if (window.location.hash.startsWith('#chat-')) { + history.back(); + } } } @@ -383,18 +374,13 @@ async function sendFileMessage(file) { } } -function openSettings() { - - ui.toggleModal(true); -} - function saveSettings() { config.save( elements.inputWahaUrl.value, elements.inputSession.value, elements.inputApiKey.value ); - ui.toggleModal(false); + location.reload(); loadChats(); checkWahaStatus(); initWebSocket(); diff --git a/js/ui.js b/js/ui.js index f6593e8..f5a1bc5 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1,13 +1,10 @@ import { formatTime, normalizeId } from "./utils.js"; -import { waha } from "./waha.js"; -import { config } from "./config.js"; import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage.js"; export const elements = { chatList: document.getElementById('chat-list'), chatsLoader: document.getElementById('chats-loader'), chatSearch: document.getElementById('chat-search'), - refreshChatsBtn: document.getElementById('refresh-chats-btn'), backendStatusText: document.getElementById('backend-status-text'), apiStatusIndicator: document.querySelector('.pulse-dot'), noChatState: document.getElementById('no-chat-state'), @@ -21,8 +18,7 @@ export const elements = { sidebar: document.querySelector('.sidebar'), appContainer: document.querySelector('.app-container'), settingsModal: document.getElementById('settings-page'), - settingsIconBtn: document.querySelector('.header-actions button[title="Settings"]'), - cancelSettingsBtn: document.getElementById('cancel-settings'), + settingsIconBtn: document.getElementById('settings-sidebar-btn'), saveSettingsBtn: document.getElementById('save-settings'), inputWahaUrl: document.getElementById('settings-waha-url'), inputSession: document.getElementById('settings-session'), @@ -33,20 +29,21 @@ export const elements = { 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 = { - /** - * Show or hide Settings connection modal - */ - toggleModal(show) { - if (show) { - elements.settingsModal.scrollTo(); - } else { - elements.sidebar.scrollTo(); - } + 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 */ diff --git a/loading.css b/loading.css index 9f152d2..ae2f8c9 100644 --- a/loading.css +++ b/loading.css @@ -1,7 +1,3 @@ -body { - background-color: #4e8ba6; -} - .dots { position: relative; width: 100%; diff --git a/style.css b/style.css index 2eed9ee..4b54b0a 100644 --- a/style.css +++ b/style.css @@ -364,6 +364,16 @@ input:focus { to { transform: rotate(360deg); } } +/* Desktop aside */ +#desktop-aside { + height: 100%; + background-color: black; + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + padding: .2em; +} + /* Sidebar Footer */ .sidebar-footer { padding: 14px 20px; @@ -383,14 +393,14 @@ input:focus { height: 8px; background-color: var(--online-color); border-radius: 50%; - box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); animation: pulse 1.8s infinite; } @keyframes pulse { 0% { transform: scale(0.95); - box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); } 70% { transform: scale(1); @@ -740,6 +750,7 @@ input:focus { /* Extra Pages */ .extra-page { display: none; + width: 100%; } .extra-page-content { @@ -747,6 +758,10 @@ input:focus { color: var(--text-secondary); } +#chat-page { + display: flex; +} + @media (max-width: 768px) { .app-container { width: 100%; @@ -806,11 +821,6 @@ input:focus { overflow-y: auto; } - /* Disable scrolling to active chat if none is selected */ - /* .app-container.no-active-chat .chat-area { - display: none !important; - } */ - .back-btn { display: flex; margin-right: 12px; @@ -823,6 +833,10 @@ input:focus { .message-image-attachement { max-height: 20em; } + + #desktop-aside { + display: none; + } } /* Modal Styling */ @@ -847,6 +861,7 @@ input:focus { backdrop-filter: var(--glass-blur); display: flex; flex-direction: column; + height: 100%; } .modal-header { From cad283e06be7ae43314a885b63d49cb3464974a2 Mon Sep 17 00:00:00 2001 From: Adrian Victor Date: Fri, 17 Jul 2026 15:22:26 -0300 Subject: [PATCH 30/37] Lazy-load chat images; scroll to bottom on incoming message only when user has not scrolled up. --- js/app.js | 27 ++++++++---------- js/ui.js | 84 ++++++++++++++++++++++++++++++++++++------------------- style.css | 72 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 123 insertions(+), 60 deletions(-) diff --git a/js/app.js b/js/app.js index 3834d72..a274c37 100644 --- a/js/app.js +++ b/js/app.js @@ -26,9 +26,9 @@ document.addEventListener('DOMContentLoaded', async () => { }); function loadChats() { + elements.chatsLoader.classList.remove('hidden'); try { - elements.chatsLoader.classList.remove('hidden'); - fetchChats().then(() => { + fetchChats().then(async () => { ui.renderChatList(getChats(), activeChatState, selectChat); const hash = window.location.hash; @@ -181,8 +181,6 @@ function initWebSocket() { async function handleIncomingMessage(msg) { if (!msg) return; - // console.log('[WS] handleIncomingMessage payload:', msg); - ui.updateChatInChatList(msg); const rawChatId = msg.chatId || msg.from || (msg.chat && msg.chat.id); @@ -201,21 +199,13 @@ async function handleIncomingMessage(msg) { const msgId = normalizeId(msg.id) || msg.id; const exists = document.getElementById(msgId); if (!exists) { + const scrolled = elements.messagesContainer.scrollTop == elements.messagesContainer.scrollTopMax; ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, (await getAppUser()).id); - ui.scrollToBottom(); + if (scrolled) { + ui.scrollToBottom(); + } } } - - const chatIndex = getChats().findIndex(c => c.id === msgChatId); - if (chatIndex !== -1) { - const chat = getChats()[chatIndex]; - chat.lastMessage = msg.body || msg.text || 'Media message'; - chat.timestamp = msg.timestamp ? (msg.timestamp * 1000) : Date.now(); - - if (!msg.fromMe && (!activeChatState || activeChatState.id !== msgChatId)) { - chat.unreadCount = (chat.unreadCount || 0) + 1; - } - } } async function selectChat(chat, isPopState = false, smoothScroll = true) { @@ -249,6 +239,7 @@ async function selectChat(chat, isPopState = false, smoothScroll = true) { } if (!isPopState && window.location.hash !== `#chat-${chat.id}`) { + window.location.hash = ``; window.location.hash = `chat-${chat.id}`; } @@ -394,3 +385,7 @@ async function checkWahaStatus() { ui.updateConnectionStatus(false, 'WAHA Server Offline'); } } + +export function getCurrentChat() { + return activeChatState; +} \ No newline at end of file diff --git a/js/ui.js b/js/ui.js index f5a1bc5..0ee54e2 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1,5 +1,6 @@ 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'), @@ -43,7 +44,7 @@ export const ui = { } }) }, - + /** * Switch view state when a contact chat is opened or closed */ @@ -80,7 +81,6 @@ export const ui = { async renderChatList(chats, activeChat, onChatSelect) { elements.chatList.innerHTML = ''; - chats.sort((a, b) => b.timestamp - a.timestamp); if (chats.length === 0) { @@ -93,34 +93,61 @@ export const ui = { li.className = `chat-item ${activeChat && activeChat.id === chat.id ? 'active' : ''}`; li.dataset.id = chat.id; - const picture = await getChatPicture(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}` : ''} -
-
- `; +
+ ${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 span = document.querySelector(`.chat-item-msg[data-chatid="${msg.fromMe ? msg.to : msg.from}"]`); - if (span) { - span.innerText = msg.body; + 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; + } } }, @@ -134,7 +161,7 @@ export const ui = { elements.messagesContainer.innerHTML = '
No messages. Say hello!
'; return; } - + const loadMore = document.createElement("button"); loadMore.classList.add("load-more-btn"); loadMore.innerText = "Load more"; @@ -149,7 +176,7 @@ export const ui = { this.scrollToBottom(); }, - + async loadMoreMessages(chatId, userId) { const oldest = document.querySelector('.message-group:first-of-type'); const oldestTimestamp = oldest.dataset.timestamp; @@ -157,17 +184,17 @@ export const ui = { 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); }, @@ -229,7 +256,7 @@ export const ui = { const bubble = document.createElement('div'); bubble.className = 'message-bubble'; - + let prevUid; if (msg.participant) { @@ -237,7 +264,7 @@ export const ui = { } else { prevUid = msg.from; } - + if (!isOutgoing && (!prevMsgEl || prevUid !== prevMsgEl.dataset.from)) { const senderEl = document.createElement('span'); senderEl.className = 'message-sender'; @@ -310,17 +337,18 @@ export const ui = { 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); @@ -352,7 +380,7 @@ export const ui = { toggleChatBottomBar() { elements.chatBottomBar.classList.toggle("collapsed"); }, - + removeChatMessage(msgId) { const message = document.getElementById(msgId); if (message) message.remove(); diff --git a/style.css b/style.css index 4b54b0a..831eae1 100644 --- a/style.css +++ b/style.css @@ -45,8 +45,8 @@ body { background-color: var(--bg-main); color: var(--text-primary); background-image: - radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.12) 0px, transparent 50%), - radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.12) 0px, transparent 50%); + radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.12) 0px, transparent 50%), + radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.12) 0px, transparent 50%); } a { @@ -222,6 +222,7 @@ input:focus { border: none; font-size: 0.9rem; transition: all 0.25s ease; + background-color: var(--bg-secondary); } #chat-search:focus { @@ -281,6 +282,9 @@ input:focus { .chat-item:hover { background: rgba(255, 255, 255, 0.04); + transform: scale(0.98) skewX(10deg); + filter: blur(.1px); + opacity: .6; } .chat-item-info { @@ -470,14 +474,46 @@ input:focus { height: 100%; position: relative; overflow: hidden; + z-index: 0; +} + +.active-chat-container::before { + content: ""; + position: absolute; + inset: 0; + background-image: var(--background-image); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + opacity: 0.4; + pointer-events: none; + z-index: 0; +} + +.active-chat-container > * { + position: relative; + z-index: 1; } .chat-header { - padding: 16px 24px; + padding: .6em 1em; display: flex; - justify-content: space-between; - align-items: center; - background: var(--bg-main); + position: relative; +} + +.chat-header::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + z-index: -1; + + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.75), + rgba(0, 0, 0, 0.35), + transparent + ); } .active-contact-info { @@ -516,14 +552,14 @@ input:focus { .messages-container { flex: 1; overflow-y: auto; - padding: 24px; + padding: 1rem; + padding-bottom: .6rem; display: flex; flex-direction: column; - gap: 16px; user-select: text; background-image: - radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.03) 0%, transparent 40%), - radial-gradient(circle at 90% 80%, rgba(168, 85, 247, 0.03) 0%, transparent 40%); + radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.03) 0%, transparent 40%), + radial-gradient(circle at 90% 80%, rgba(168, 85, 247, 0.03) 0%, transparent 40%); } .load-more-btn { @@ -539,6 +575,7 @@ input:focus { } .message-group { + margin-top: .8em; display: flex; flex-direction: column; max-width: 65%; @@ -552,11 +589,14 @@ input:focus { align-self: flex-end; } +.message-group.same-sender { + margin-top: .4em; +} + .message-sender { font-size: 0.75rem; font-weight: 600; color: var(--text-primary); - margin-bottom: 2px; width: 100%; display: flex; } @@ -570,8 +610,7 @@ input:focus { .message-bubble { display: flex; flex-direction: column; - gap: .4em; - padding: 12px 16px; + padding: 8px 10px; font-size: 0.92rem; line-height: 1.5; position: relative; @@ -630,6 +669,7 @@ input:focus { display: flex; align-items: center; gap: 16px; + background-color: var(--bg-main); } .alternate-panel { @@ -825,15 +865,15 @@ input:focus { display: flex; margin-right: 12px; } - + .message-group { max-width: 90%; } - + .message-image-attachement { max-height: 20em; } - + #desktop-aside { display: none; } From 36f8448bf984f8746d1eb2a9ef070c160e298064 Mon Sep 17 00:00:00 2001 From: Adrian Victor Date: Fri, 17 Jul 2026 16:06:29 -0300 Subject: [PATCH 31/37] Add config option to configure chat background --- index.html | 12 +++++++++++- js/app.js | 8 +++++++- js/config.js | 8 +++++++- js/ui.js | 2 ++ style.css | 13 ++++++++++--- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index 09f089b..e24f39a 100644 --- a/index.html +++ b/index.html @@ -130,6 +130,7 @@

SETTINGS

- +
diff --git a/js/app.js b/js/app.js index a274c37..10b4934 100644 --- a/js/app.js +++ b/js/app.js @@ -13,6 +13,10 @@ document.addEventListener('DOMContentLoaded', async () => { elements.inputApiKey.value = config.apiKey; elements.inputWahaUrl.value = config.wahaUrl; elements.inputSession.value = config.session; + elements.inputBackgroundImage.value = config.bgImg; + elements.inputBackgroundOpacity.value = config.bgOpacity; + elements.activeChatContainer.style.setProperty('--background-image', `URL("${config.bgImg}")`); + elements.activeChatContainer.style.setProperty('--background-opacity', `${config.bgOpacity}`); await updateOnlineStatus(); setupEventListeners(); try { @@ -369,7 +373,9 @@ function saveSettings() { config.save( elements.inputWahaUrl.value, elements.inputSession.value, - elements.inputApiKey.value + elements.inputApiKey.value, + elements.inputBackgroundImage.value, + elements.inputBackgroundOpacity.value ); location.reload(); loadChats(); diff --git a/js/config.js b/js/config.js index a571dbb..edb15f6 100644 --- a/js/config.js +++ b/js/config.js @@ -4,14 +4,20 @@ export const config = { wahaUrl: localStorage.getItem('waha_url') || 'http://inspiran.beetal-castor.ts.net:3100', session: localStorage.getItem('waha_session') || 'session_01kxc62bk5fs8vh4v127k88a7j', apiKey: localStorage.getItem('waha_api_key') || '', + bgImg: localStorage.getItem('background_image') || '', + bgOpacity: localStorage.getItem('background_opacity') || '0.4', - save(url, session, apiKey) { + save(url, session, apiKey, bgImg, bgOpacity) { this.wahaUrl = url.trim().replace(/\/$/, ""); this.session = session.trim(); this.apiKey = apiKey.trim(); + this.bgImg = bgImg.trim(); + this.bgOpacity = bgOpacity.trim(); localStorage.setItem('waha_url', this.wahaUrl); localStorage.setItem('waha_session', this.session); localStorage.setItem('waha_api_key', this.apiKey); + localStorage.setItem('background_image', this.bgImg); + localStorage.setItem('background_opacity', this.bgOpacity); } }; diff --git a/js/ui.js b/js/ui.js index 0ee54e2..698cd6a 100644 --- a/js/ui.js +++ b/js/ui.js @@ -24,6 +24,8 @@ export const elements = { 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'), diff --git a/style.css b/style.css index 831eae1..f5f85e0 100644 --- a/style.css +++ b/style.css @@ -19,6 +19,7 @@ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1); --glass-blur: blur(20px); + --high-box-shadow: 2px 7px 5px rgba(0, 0, 0, 0.3), 0px -4px 10px rgba(0, 0, 0, 0.3); } * { @@ -65,7 +66,7 @@ button:hover { } input { - background: var(--bg-main); + background: var(--bg-secondary); color: var(--accent); border: none; padding: 1em; @@ -485,7 +486,7 @@ input:focus { background-size: cover; background-position: center; background-repeat: no-repeat; - opacity: 0.4; + opacity: var(--background-opacity); pointer-events: none; z-index: 0; } @@ -564,9 +565,10 @@ input:focus { .load-more-btn { margin: auto; - background-color: var(--bg-secondary); + background-color: rgba(255, 255, 255, 0.1); padding: .4em; cursor: pointer; + box-shadow: var(--high-box-shadow); } .load-more-btn:hover { @@ -904,6 +906,11 @@ input:focus { height: 100%; } +.modal-body h3 { + font-size: xx-large; + font-weight: 100; +} + .modal-header { display: flex; justify-content: space-between; From 1116228151fa4b404a41098e015e0cfafc07c931 Mon Sep 17 00:00:00 2001 From: Adrian Victor Date: Fri, 17 Jul 2026 16:08:01 -0300 Subject: [PATCH 32/37] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c0a2bd..34209f9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It's heavily inspired on Windows Phone 8 MetroUI. - [X] Local message storing - [ ] Message encryption - [ ] Light theme -- [ ] Custom wallpaper +- [X] Custom wallpaper 1. Does not show contact name in any engine other than WEBJS 2. Loads the last 40 messages From 73e0496260cdc2b337dd8d21dc3f04876443e6de Mon Sep 17 00:00:00 2001 From: Adrian Victor Date: Fri, 17 Jul 2026 21:07:08 -0300 Subject: [PATCH 33/37] Change loading animation to Sven Scharfenberg's that does not rely on JS. --- index.html | 23 +++---- js/app.js | 17 +++-- loading.css | 185 +++++++++++++++++++++++++++++----------------------- 3 files changed, 121 insertions(+), 104 deletions(-) diff --git a/index.html b/index.html index e24f39a..dfe1f76 100644 --- a/index.html +++ b/index.html @@ -12,15 +12,6 @@ -
@@ -119,6 +113,17 @@
+
+

Pandora User

+
+ +
+

Pandora User

+ +
+
+
+
diff --git a/js/app.js b/js/app.js index 43ad99d..8f5a749 100644 --- a/js/app.js +++ b/js/app.js @@ -2,8 +2,8 @@ import { config } from "./config.js"; import { waha } from "./waha.js"; import { ui, elements } from "./ui.js"; import { websocket } from "./websocket.js"; -import { compensateMessageOrdering, formatTime, normalizeId } from "./utils.js"; -import { fetchChats, getAppUser, getChatMessages, getChats, updateOnlineStatus } from "./storage.js"; +import { compensateMessageOrdering, debounce, formatTime, normalizeId } from "./utils.js"; +import { fetchChats, getAppUser, getChatMessages, getChatPicture, getChats, getUser, getUserAbout, sendStatus, updateOnlineStatus } from "./storage.js"; import { upsertMessages } from "./db.js"; let activeChatState = null; @@ -20,7 +20,7 @@ document.addEventListener('DOMContentLoaded', async () => { await updateOnlineStatus(); setupEventListeners(); try { - elements.loggedUserName.textContent = (await getAppUser()).pushName; + setupElementsData(); loadChats(); checkWahaStatus(); initWebSocket(); @@ -29,6 +29,25 @@ document.addEventListener('DOMContentLoaded', async () => { } }); +async function setupElementsData() { + 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; + }) + elements.contentUserNumber.forEach(async e => { + e.innerHTML = usrInfo.number; + }) + elements.resourceUserPic.forEach(async e => { + e.src = usrPic; + }) + elements.valueUserStatus.forEach(async e => { + e.value = usrAbout.trim(); + }) +} + function loadChats() { elements.chatsLoader.classList.remove('hidden'); try { @@ -168,6 +187,17 @@ function setupEventListeners() { }) elements.saveSettingsBtn.addEventListener('click', saveSettings); + + elements.inputUserStatus.addEventListener('input', debounce(async function() { + const result = await sendStatus(elements.inputUserStatus.value); + console.log(result); + }, 2000)) + + // elements.selectable.forEach(e => { + // e.addEventListener('pointerdown', () => { + + // }) + // }) } function initWebSocket() { diff --git a/js/storage.js b/js/storage.js index f83d66c..238b4b9 100644 --- a/js/storage.js +++ b/js/storage.js @@ -43,8 +43,20 @@ export function getGroups() { return chats.filter(c => c.id.endsWith("@g.us")); } -export function getUser(number) { - return users.find(user => user.id === number); +export async function getUser(number) { + if (online) { + return await waha.getUser(number); + } else { + return; + } +} + +export async function getUserAbout(userId) { + if (online) { + return await waha.getUserAbout(userId); + } else { + return; + } } export function getChats() { @@ -125,4 +137,14 @@ export async function getChatPicture(chatId) { export function isOnline() { return online; +} + +export async function sendStatus(text) { + if (online) { + return await waha.setStatus(text); + } else { + return { + success: false + } + } } \ No newline at end of file diff --git a/js/ui.js b/js/ui.js index 698cd6a..d2e1261 100644 --- a/js/ui.js +++ b/js/ui.js @@ -33,7 +33,13 @@ export const elements = { attachmentInput: document.getElementById('attachment-input'), attachmentBtn: document.getElementById('attachment-btn'), extraPages: document.querySelectorAll('.extra-page'), - desktopSidebarButtons: document.querySelectorAll("#desktop-aside button") + desktopSidebarButtons: document.querySelectorAll("#desktop-aside button"), + contentUserName: document.querySelectorAll('[data-content="app-user"]'), + contentUserNumber: document.querySelectorAll('[data-content="app-user-number"]'), + resourceUserPic: document.querySelectorAll('[data-resource="app-user-image"]'), + valueUserStatus: document.querySelectorAll('[data-value="app-user-status"]'), + inputUserStatus: document.getElementById('profile-page-status-input'), + selectable: document.querySelectorAll('.selectable'), }; export const ui = { @@ -92,7 +98,7 @@ export const ui = { for (const chat of chats) { const li = document.createElement('li'); - li.className = `chat-item ${activeChat && activeChat.id === chat.id ? 'active' : ''}`; + li.className = `chat-item selectable ${activeChat && activeChat.id === chat.id ? 'active' : ''}`; li.dataset.id = chat.id; const initials = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?'; @@ -235,7 +241,7 @@ export const ui = { const prevMsgEl = getPrevMessageElem(); const groupDiv = document.createElement('div'); - groupDiv.className = `message-group ${isOutgoing ? 'outgoing' : 'incoming'}`; + groupDiv.className = `message-group selectable ${isOutgoing ? 'outgoing' : 'incoming'}`; groupDiv.id = normalizeId(msg._serialized ? msg : msg.id); groupDiv.dataset.timestamp = msg.timestamp; groupDiv.dataset.from = msg.participant || msg.from; diff --git a/js/utils.js b/js/utils.js index 9e8418b..81f05f0 100644 --- a/js/utils.js +++ b/js/utils.js @@ -112,4 +112,12 @@ export function normalizeId(raw) { return raw._serialized || raw.user || JSON.stringify(raw); } return raw; +} + +export function debounce(func, delay) { + let timeoutId; + return function() { + clearTimeout(timeoutId); + timeoutId = setTimeout(func, delay); + }; } \ No newline at end of file diff --git a/js/waha.js b/js/waha.js index 66683b1..2c5c077 100644 --- a/js/waha.js +++ b/js/waha.js @@ -102,6 +102,14 @@ export const waha = { return request(`/api/${config.session}/chats/${chatId}/picture`); }, + async getUser(chatId) { + return request(`/api/${config.session}/contacts/${chatId}`); + }, + + async getUserAbout(chatId) { + return request(`/api/contacts/about?contactId=${chatId}&session=${config.session}`); + }, + async readChat(chatId) { return request('/api/sendSeen', { method: 'POST', @@ -143,6 +151,15 @@ export const waha = { }); }, + async setStatus(text) { + return request(`/api/${config.session}/profile/status`, { + method: 'PUT', + body: JSON.stringify({ + status: text + }) + }); + }, + async sendFileMessage(chatId, file) { const fileBase64 = await getBase64(file); const body = { diff --git a/style.css b/style.css index 1c17b27..c1ae0e3 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,33 @@ +@font-face { + font-family: Selawik; + src: url(./fonts/selawk.ttf); + font-weight: normal; +} + +@font-face { + font-family: Selawik; + src: url(./fonts/selawkl.ttf); + font-weight: 200; +} + +@font-face { + font-family: Selawik; + src: url(./fonts/selawksl.ttf); + font-weight: 300; +} + +@font-face { + font-family: Selawik; + src: url(./fonts/selawkb.ttf); + font-weight: bold; +} + +@font-face { + font-family: Selawik; + src: url(./fonts/selawkb.ttf); + font-weight: 800; +} + :root { --bg-main: black; --bg-secondary: #242424; @@ -44,7 +74,7 @@ body.light { margin: 0; padding: 0; box-sizing: border-box; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-family: Selawik, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; user-select: none; } @@ -106,7 +136,7 @@ input:focus { } .app-name { - font-weight: bolder; + font-weight: bold; } /* Sidebar Styling */ @@ -299,15 +329,28 @@ input:focus { border: 1px solid transparent; } +.selected { + background: rgba(255, 255, 255, 0.04); + transform: scale(0.98) skewX(10deg); + filter: blur(.1px); + opacity: .6; +} + @media (hover: hover) { - .chat-item:hover { - background: rgba(255, 255, 255, 0.04); transform: scale(0.98) skewX(10deg); filter: blur(.1px); opacity: .6; + .selectable:hover { + background: rgba(255, 255, 255, 0.04); + transform: scale(0.98) skewX(10deg); + filter: blur(.1px); + opacity: .6; } } @media (hover: none) { - .chat-item:active { - background: rgba(255, 255, 255, 0.04); transform: scale(0.98) skewX(10deg); filter: blur(.1px); opacity: .6; + .selectable:active { + background: rgba(255, 255, 255, 0.04); + transform: scale(0.98) skewX(10deg); + filter: blur(.1px); + opacity: .6; } } @@ -351,6 +394,7 @@ input:focus { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + font-weight: 300; flex: 1; } @@ -403,11 +447,6 @@ input:focus { } /* Sidebar Footer */ -.sidebar-footer { - padding: 14px 20px; - background: var(--bg-secondary); -} - .api-status-badge { display: flex; align-items: center; @@ -555,7 +594,7 @@ input:focus { .active-contact-info h3 { font-size: 1rem; - font-weight: bolder; + font-weight: bold; } .contact-status { @@ -604,6 +643,7 @@ input:focus { display: flex; flex-direction: column; max-width: 65%; + transition: .1s; } .message-group.incoming { @@ -826,6 +866,49 @@ input:focus { display: flex; } +#profile-page { + flex-direction: column; + background-color: var(--bg-main); +} + +#profile-page .content { + padding: 1.4rem; + display: flex; + gap: 1em; +} + +#profile-page-user-info { + flex: 1; +} + +#profile-page h2 { + padding: 1.4rem; + font-size: 5rem; + text-wrap-mode: nowrap; + overflow: hidden; + font-weight: 200; +} + +#profile-page-picture { + width: 100%; + max-width: 10%; + height: auto; + aspect-ratio: 1/1; +} + +#profile-page-status-input { + width: 100%; + background-color: var(--bg-main); + font-size: xx-large; + border-bottom: medium solid transparent; + padding: 0; +} + +#profile-page-status-input:focus { + color: var(--text-primary); + border-bottom-color: var(--text-primary); +} + @media (max-width: 768px) { .app-container { width: 100%; @@ -901,6 +984,19 @@ input:focus { #desktop-aside { display: none; } + + #profile-page .content { + flex-direction: column; + } + + #profile-page-picture { + max-width: 50%; + margin: auto; + } + + #profile-page-user-number { + text-align: center; + } } /* Modal Styling */ From 5ba95800a3907bed587a20bab61a9edfc8f1d04d Mon Sep 17 00:00:00 2001 From: Adrian Victor Date: Sat, 18 Jul 2026 14:42:18 -0300 Subject: [PATCH 37/37] Only load when not already loading one. Don't animate .selectable on hover. --- js/app.js | 35 ++++++++++++++++++++++++++++++----- style.css | 20 +++++--------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/js/app.js b/js/app.js index 8f5a749..1611b35 100644 --- a/js/app.js +++ b/js/app.js @@ -8,6 +8,8 @@ import { upsertMessages } from "./db.js"; let activeChatState = null; const messageTone = new Audio("./message.ogg"); +const longPressEvent = new CustomEvent("longpress"); +export let isLoadingChat = false; document.addEventListener('DOMContentLoaded', async () => { elements.inputApiKey.value = config.apiKey; @@ -178,7 +180,7 @@ function setupEventListeners() { elements.backToSidebarBtn.addEventListener('click', () => { closeActiveChat(false); - }); + }); elements.desktopSidebarButtons.forEach(sidebarBtn => { sidebarBtn.addEventListener('click', () => { @@ -193,11 +195,29 @@ function setupEventListeners() { console.log(result); }, 2000)) - // elements.selectable.forEach(e => { - // e.addEventListener('pointerdown', () => { + elements.selectable.forEach(e => { + let timerId, longPressed; - // }) - // }) + e.addEventListener('mousedown', () => { + longPressed = false; + + timerId = setTimeout(() => { + longPressed = true; + e.dispatchEvent(longPressEvent); + }) + }) + + e.addEventListener('click', () => { + if (longPressed) { + e.preventDefault(); + clearTimeout(timerId); + } + }) + + e.addEventListener('mouseleave', () => { + clearTimeout(timerId); + }) + }) } function initWebSocket() { @@ -243,6 +263,9 @@ async function handleIncomingMessage(msg) { } async function selectChat(chat, isPopState = false, smoothScroll = true) { + if (isLoadingChat) return; + + isLoadingChat = true; activeChatState = chat; chat.unreadCount = 0; @@ -284,6 +307,8 @@ async function selectChat(chat, isPopState = false, smoothScroll = true) { console.error('Failed to load messages:', error); elements.messagesContainer.innerHTML = '
Error loading messages
'; } + + isLoadingChat = false; } async function closeActiveChat(isPopState = false) { diff --git a/style.css b/style.css index c1ae0e3..ddc4e0b 100644 --- a/style.css +++ b/style.css @@ -336,22 +336,12 @@ input:focus { opacity: .6; } -@media (hover: hover) { - .selectable:hover { - background: rgba(255, 255, 255, 0.04); - transform: scale(0.98) skewX(10deg); - filter: blur(.1px); - opacity: .6; - } -} -@media (hover: none) { - .selectable:active { - background: rgba(255, 255, 255, 0.04); - transform: scale(0.98) skewX(10deg); - filter: blur(.1px); - opacity: .6; - } +.selectable:active { + background: rgba(255, 255, 255, 0.04); + transform: scale(0.98) skewX(10deg); + filter: blur(.1px); + opacity: .6; } .chat-item-info {