.
This commit is contained in:
parent
0289db28e5
commit
1c586b6bcb
7 changed files with 357 additions and 130 deletions
39
docs/app.js
39
docs/app.js
|
|
@ -6,9 +6,11 @@ import { compensateMessageOrdering, formatTime } from "./utils.js";
|
|||
|
||||
let chatsState = [];
|
||||
let activeChatState = null;
|
||||
let userInfo;
|
||||
|
||||
// Initialize app when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
userInfo = await waha.getMyInfo();
|
||||
setupEventListeners();
|
||||
fetchChats();
|
||||
checkWahaStatus();
|
||||
|
|
@ -16,6 +18,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.code == "Escape") {
|
||||
ui.toggleChatState();
|
||||
}
|
||||
});
|
||||
|
||||
elements.refreshChatsBtn.addEventListener('click', () => {
|
||||
elements.refreshChatsBtn.classList.add('spinning');
|
||||
fetchChats().finally(() => {
|
||||
|
|
@ -44,7 +54,6 @@ function setupEventListeners() {
|
|||
});
|
||||
|
||||
elements.settingsIconBtn.addEventListener('click', openSettings);
|
||||
elements.closeSettingsBtn.addEventListener('click', () => ui.toggleModal(false));
|
||||
elements.cancelSettingsBtn.addEventListener('click', () => ui.toggleModal(false));
|
||||
elements.saveSettingsBtn.addEventListener('click', saveSettings);
|
||||
}
|
||||
|
|
@ -85,7 +94,7 @@ function handleIncomingMessage(msg) {
|
|||
const msgId = normalizeChatId(msg.id) || msg.id;
|
||||
const exists = document.getElementById(msgId);
|
||||
if (!exists) {
|
||||
ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name);
|
||||
ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, userInfo.id);
|
||||
ui.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
|
@ -106,28 +115,28 @@ function handleIncomingMessage(msg) {
|
|||
return tB - tA;
|
||||
});
|
||||
|
||||
ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||
// ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||
} else {
|
||||
fetchChats();
|
||||
// fetchChats();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchChats() {
|
||||
const userInfo = await waha.getMyInfo();
|
||||
elements.loggedUserName.textContent = userInfo.pushName;
|
||||
// elements.userIcon.innerText = userInfo.pushName[0];
|
||||
elements.chatsLoader.classList.remove('hidden');
|
||||
try {
|
||||
elements.loggedUserName.textContent = userInfo.pushName;
|
||||
// elements.userIcon.innerText = userInfo.pushName[0];
|
||||
elements.chatsLoader.classList.remove('hidden');
|
||||
chatsState = await waha.getChats();
|
||||
ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||
} catch (error) {
|
||||
console.error('Failed to load chats:', error);
|
||||
elements.chatList.innerHTML = `
|
||||
<li class="loading-chats" style="color: #f87171; text-align: center; padding: 20px;">
|
||||
<li class="loading-chats" style="color: var(--text-primary); text-align: center; padding: 20px;">
|
||||
<p>Connection to WAHA failed.</p>
|
||||
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 8px;">
|
||||
<p style="font-size: 0.75rem; color: var(--text-primary); margin-top: 8px;">
|
||||
Ensure WAHA server is running and CORS is enabled, or click Settings to configure.
|
||||
</p>
|
||||
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 8px;">${error.message}</p>
|
||||
</li>
|
||||
`;
|
||||
} finally {
|
||||
|
|
@ -140,7 +149,7 @@ async function selectChat(chat) {
|
|||
|
||||
chat.unreadCount = 0;
|
||||
|
||||
ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||
// ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||
|
||||
ui.toggleChatState(true);
|
||||
elements.activeChatName.textContent = chat.name.toUpperCase();
|
||||
|
|
@ -155,7 +164,7 @@ async function selectChat(chat) {
|
|||
try {
|
||||
const rawMessages = await waha.getChatMessages(chat.id);
|
||||
const processedMessages = compensateMessageOrdering(rawMessages);
|
||||
ui.renderMessages(processedMessages, chat.name);
|
||||
ui.renderMessages(processedMessages, chat.name, userInfo.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to load messages:', error);
|
||||
elements.messagesContainer.innerHTML = '<div class="loading-chats">Error loading messages</div>';
|
||||
|
|
@ -177,7 +186,7 @@ async function sendMessage() {
|
|||
status: 'sending'
|
||||
};
|
||||
|
||||
ui.appendSingleMessage(tempMsg, activeChatState.name);
|
||||
ui.appendSingleMessage(tempMsg, activeChatState.name, userInfo.id);
|
||||
ui.scrollToBottom();
|
||||
|
||||
try {
|
||||
|
|
@ -223,7 +232,7 @@ async function sendMessage() {
|
|||
const tB = new Date(b.timestamp).getTime();
|
||||
return tB - tA;
|
||||
});
|
||||
ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||
// ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
const tempBubble = document.getElementById(tempMsg.id);
|
||||
|
|
|
|||
|
|
@ -8,9 +8,20 @@
|
|||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="loading.css">
|
||||
<link rel="stylesheet" href="metroicons.css">
|
||||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script defer>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll(".dots").forEach(el => {
|
||||
el.classList.add("animate");
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
<div class="app-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
|
|
@ -23,7 +34,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="text-btn" title="New Chat">new chat</button>
|
||||
<!-- <button class="text-btn" title="New Chat">new chat</button> -->
|
||||
<button class="text-btn" id="refresh-chats-btn" title="Refresh Chats">refresh</button>
|
||||
<button class="text-btn" title="Settings">settings</button>
|
||||
</div>
|
||||
|
|
@ -37,8 +48,13 @@
|
|||
|
||||
<div class="chat-list-container">
|
||||
<div class="loading-chats" id="chats-loader">
|
||||
<div class="spinner"></div>
|
||||
<span>Loading chats...</span>
|
||||
<div class='dots'>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="chat-list" id="chat-list">
|
||||
<!-- Chats will be dynamically injected here -->
|
||||
|
|
@ -58,11 +74,9 @@
|
|||
<!-- No Chat Selected State -->
|
||||
<div class="no-chat-state" id="no-chat-state">
|
||||
<div class="empty-state-content">
|
||||
<div class="empty-state-icon">
|
||||
<i data-lucide="message-square"></i>
|
||||
<div class="empty-state-icon mif-qa mif-3x">
|
||||
</div>
|
||||
<h2>Welcome to Pandora Chat</h2>
|
||||
<p>Select a contact from the sidebar to view the conversation or start a new chat. Your chats are synced directly with the WAHA API backend.</p>
|
||||
<p>Select a contact from the sidebar to view the conversation or start a new chat.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -74,7 +88,7 @@
|
|||
<div class="avatar active-avatar" id="active-chat-avatar">C</div>
|
||||
<div>
|
||||
<h3 id="active-chat-name">Contact Name</h3>
|
||||
<span class="contact-status" id="active-chat-status">Active now</span>
|
||||
<span class="contact-status" id="active-chat-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-actions">
|
||||
|
|
@ -93,13 +107,12 @@
|
|||
<!-- Input Panel -->
|
||||
<footer class="chat-input-panel">
|
||||
<div class="input-actions-left">
|
||||
<button class="icon-btn" title="Attach file"><i data-lucide="paperclip"></i></button>
|
||||
<button class="icon-btn send-btn mif-attachment mif-3x" title="Attach file"></button>
|
||||
<!-- <button class="icon-btn" title="Add emoji"><i data-lucide="smile"></i></button> -->
|
||||
</div>
|
||||
<form class="input-form" id="message-form">
|
||||
<input type="text" id="message-input" placeholder="Type a message..." autocomplete="off">
|
||||
<button type="submit" class="send-btn" id="send-button">
|
||||
<i data-lucide="send"></i>
|
||||
<button type="submit" class="send-btn mif-paper-plane mif-3x" id="send-button">
|
||||
</button>
|
||||
</form>
|
||||
</footer>
|
||||
|
|
@ -112,7 +125,6 @@
|
|||
<div class="modal-content">
|
||||
<header class="modal-header">
|
||||
<h2>SETTINGS</h2>
|
||||
<button class="icon-btn close-modal-btn" id="close-settings"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
|
|
@ -127,7 +139,7 @@
|
|||
<label for="settings-api-key">API Key (X-API-KEY)</label>
|
||||
<input type="password" id="settings-api-key" placeholder="Enter API Key">
|
||||
</div>
|
||||
<p class="settings-warning">⚠️ Note: Requests are sent directly from your browser to the WAHA server. Make sure CORS is enabled on your WAHA instance.</p>
|
||||
<p class="settings-warning">Note: Requests are sent directly from your browser to the WAHA server. Make sure CORS is enabled on your WAHA instance.</p>
|
||||
</div>
|
||||
<footer class="modal-footer">
|
||||
<button class="btn secondary-btn" id="cancel-settings">Cancel</button>
|
||||
|
|
|
|||
95
docs/loading.css
Normal file
95
docs/loading.css
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
body {
|
||||
background-color: #4e8ba6;
|
||||
}
|
||||
|
||||
.dots {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dots span:nth-child(1) {
|
||||
animation-delay: 0.05s;
|
||||
}
|
||||
|
||||
.dots span:nth-child(1):after {
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
.dots span:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.dots span:nth-child(2):after {
|
||||
left: -40px;
|
||||
}
|
||||
|
||||
.dots span:nth-child(3) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.dots span:nth-child(3):after {
|
||||
left: -60px;
|
||||
}
|
||||
|
||||
.dots span:nth-child(4) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.dots span:nth-child(4):after {
|
||||
left: -80px;
|
||||
}
|
||||
|
||||
.dots span:nth-child(5) {
|
||||
animation-delay: 0.25s;
|
||||
}
|
||||
|
||||
.dots span:nth-child(5):after {
|
||||
left: -100px;
|
||||
}
|
||||
|
||||
.dots span {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
animation-duration: 4s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.dots span:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
background-color: #f0f0f0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dots.animate span {
|
||||
animation-name: dots;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%,20% {
|
||||
left: 0;
|
||||
animation-timing-function: ease-out;
|
||||
opacity: 0;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
35% {
|
||||
left: 45%;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
65% {
|
||||
left: 55%;
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
75% {
|
||||
opacity: 1;
|
||||
}
|
||||
80%,100% {
|
||||
left: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
1
docs/metroicons.css
Normal file
1
docs/metroicons.css
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,7 @@
|
|||
--border-hover: rgba(255, 255, 255, 0.15);
|
||||
--text-primary: #f8fafc;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--text-muted: rgb(134, 134, 134);
|
||||
--accent: white;
|
||||
--accent-gradient: black;
|
||||
--accent-hover: #4f46e5;
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
|
|
@ -38,6 +39,10 @@ body {
|
|||
radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.12) 0px, transparent 50%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--bg-main);
|
||||
border: none;
|
||||
|
|
@ -159,7 +164,7 @@ input:focus {
|
|||
height: 38px;
|
||||
border: none;
|
||||
background: var(--bg-main);
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -171,7 +176,6 @@ input:focus {
|
|||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.icon-btn:active {
|
||||
|
|
@ -415,15 +419,15 @@ input:focus {
|
|||
.empty-state-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
/* border: 1px solid var(--border-color); */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 24px auto;
|
||||
color: var(--accent);
|
||||
font-size: 2rem;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.empty-state-content h2 {
|
||||
|
|
@ -494,6 +498,7 @@ input:focus {
|
|||
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%);
|
||||
|
|
@ -559,6 +564,16 @@ input:focus {
|
|||
border-bottom: 10px solid var(--bubble-incoming);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .4em;
|
||||
}
|
||||
|
||||
.message-content * {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -568,6 +583,11 @@ input:focus {
|
|||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.message-image-attachement {
|
||||
max-width: 100%;
|
||||
max-height: 10em;
|
||||
}
|
||||
|
||||
/* Chat Input Panel */
|
||||
.chat-input-panel {
|
||||
padding: 20px 24px;
|
||||
|
|
@ -590,6 +610,7 @@ input:focus {
|
|||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#message-input {
|
||||
|
|
@ -600,6 +621,7 @@ input:focus {
|
|||
color: var(--bg-main);
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#message-input:focus {
|
||||
|
|
@ -624,11 +646,6 @@ input:focus {
|
|||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
transform: scale(1.05) translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.send-btn:active {
|
||||
transform: scale(0.98) translateY(0);
|
||||
}
|
||||
|
|
@ -679,7 +696,7 @@ input:focus {
|
|||
|
||||
.sidebar.hidden {
|
||||
transform: translateX(-100%);
|
||||
display: flex !important; /* overrides standard .hidden */
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
|
|
@ -690,6 +707,14 @@ input:focus {
|
|||
display: flex;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.message-group {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-image-attachement {
|
||||
max-height: 20em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Styling */
|
||||
|
|
@ -707,10 +732,13 @@ input:focus {
|
|||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
background: var(--bg-main);
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
|
|
@ -770,11 +798,6 @@ input:focus {
|
|||
color: white;
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
|
|
|
|||
182
docs/ui.js
182
docs/ui.js
|
|
@ -1,5 +1,6 @@
|
|||
import { formatTime } from "./utils.js";
|
||||
import { waha } from "./waha.js";
|
||||
import { config } from "./config.js";
|
||||
|
||||
// DOM Selector Cache
|
||||
export const elements = {
|
||||
|
|
@ -17,11 +18,10 @@ export const elements = {
|
|||
messageForm: document.getElementById('message-form'),
|
||||
messageInput: document.getElementById('message-input'),
|
||||
// backToSidebarBtn: document.getElementById('back-to-sidebar'),
|
||||
backToSidebarBtn: document.getElementById('active-chat-container'),
|
||||
backToSidebarBtn: document.querySelector('.chat-header'),
|
||||
sidebar: document.querySelector('.sidebar'),
|
||||
settingsModal: document.getElementById('settings-modal'),
|
||||
settingsIconBtn: document.querySelector('.header-actions button[title="Settings"]'),
|
||||
closeSettingsBtn: document.getElementById('close-settings'),
|
||||
cancelSettingsBtn: document.getElementById('cancel-settings'),
|
||||
saveSettingsBtn: document.getElementById('save-settings'),
|
||||
inputWahaUrl: document.getElementById('settings-waha-url'),
|
||||
|
|
@ -33,8 +33,8 @@ export const elements = {
|
|||
|
||||
export const ui = {
|
||||
/**
|
||||
* Show or hide Settings connection modal
|
||||
*/
|
||||
* Show or hide Settings connection modal
|
||||
*/
|
||||
toggleModal(show) {
|
||||
if (show) {
|
||||
elements.settingsModal.classList.remove('hidden');
|
||||
|
|
@ -44,8 +44,8 @@ export const ui = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Switch view state when a contact chat is opened or closed
|
||||
*/
|
||||
* Switch view state when a contact chat is opened or closed
|
||||
*/
|
||||
toggleChatState(hasActive) {
|
||||
if (hasActive) {
|
||||
elements.noChatState.classList.add('hidden');
|
||||
|
|
@ -57,15 +57,15 @@ export const ui = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Scroll message list automatically to bottom
|
||||
*/
|
||||
* Scroll message list automatically to bottom
|
||||
*/
|
||||
scrollToBottom() {
|
||||
elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update connection status badge in sidebar footer
|
||||
*/
|
||||
* Update connection status badge in sidebar footer
|
||||
*/
|
||||
updateConnectionStatus(isConnected, text) {
|
||||
elements.backendStatusText.textContent = text;
|
||||
if (isConnected) {
|
||||
|
|
@ -78,8 +78,8 @@ export const ui = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Render sidebar contact chats list
|
||||
*/
|
||||
* Render sidebar contact chats list
|
||||
*/
|
||||
renderChatList(chats, activeChat, onChatSelect) {
|
||||
elements.chatList.innerHTML = '';
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ export const ui = {
|
|||
return;
|
||||
}
|
||||
|
||||
chats.forEach(async (chat) => {
|
||||
chats.forEach(async (chat) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = `chat-item ${activeChat && activeChat.id === chat.id ? 'active' : ''}`;
|
||||
li.dataset.id = chat.id;
|
||||
|
|
@ -118,9 +118,9 @@ export const ui = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Render chat message log inside chat view container
|
||||
*/
|
||||
renderMessages(messages, activeChatName) {
|
||||
* Render chat message log inside chat view container
|
||||
*/
|
||||
async renderMessages(messages, activeChatName, userID) {
|
||||
elements.messagesContainer.innerHTML = '';
|
||||
|
||||
if (messages.length === 0) {
|
||||
|
|
@ -128,73 +128,117 @@ export const ui = {
|
|||
return;
|
||||
}
|
||||
|
||||
messages.forEach(msg => {
|
||||
const isOutgoing = msg.fromMe || msg.sender === 'me';
|
||||
const groupDiv = document.createElement('div');
|
||||
groupDiv.className = `message-group ${isOutgoing ? 'outgoing' : 'incoming'}`;
|
||||
groupDiv.id = msg.id;
|
||||
|
||||
const senderName = isOutgoing ? 'Me' : (msg.from || activeChatName);
|
||||
const timeStr = formatTime(msg.timestamp || new Date());
|
||||
|
||||
let statusCheck = '';
|
||||
if (isOutgoing) {
|
||||
if (msg.status === 'read') {
|
||||
statusCheck = '<i data-lucide="check-check" style="color: var(--online-color); width:14px; height:14px;"></i>';
|
||||
} else if (msg.status === 'delivered') {
|
||||
statusCheck = '<i data-lucide="check-check" style="width:14px; height:14px;"></i>';
|
||||
} else {
|
||||
statusCheck = '<i data-lucide="check" style="width:14px; height:14px;"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
groupDiv.innerHTML = `
|
||||
<div class="message-indicator"></div>
|
||||
<div class="message-bubble">
|
||||
${!isOutgoing ? `<span class="message-sender">${senderName}</span>` : ''}
|
||||
${msg.body || msg.text || "<i>Empty message, maybe something shows up on your phone...</i>"}
|
||||
<div class="message-meta">
|
||||
<span>${timeStr}</span>
|
||||
${statusCheck}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
elements.messagesContainer.appendChild(groupDiv);
|
||||
});
|
||||
for (const msg of messages) {
|
||||
this.appendSingleMessage(msg, activeChatName, userID);
|
||||
}
|
||||
|
||||
lucide.createIcons();
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
/**
|
||||
* Append a single message (used for optimistic updates immediately upon sending)
|
||||
*/
|
||||
appendSingleMessage(msg, activeChatName) {
|
||||
if (elements.messagesContainer.querySelector('.loading-chats')) {
|
||||
elements.messagesContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
* Append a single message (used for optimistic updates immediately upon sending)
|
||||
*/
|
||||
appendSingleMessage(msg, activeChatName, userID) {
|
||||
console.log(msg);
|
||||
const isOutgoing = msg.fromMe || msg.sender === 'me';
|
||||
|
||||
const groupDiv = document.createElement('div');
|
||||
groupDiv.className = `message-group ${isOutgoing ? 'outgoing' : 'incoming'}`;
|
||||
groupDiv.id = msg.id;
|
||||
groupDiv.dataset.from = msg.participant || msg.from;
|
||||
|
||||
const senderName = isOutgoing ? 'Me' : activeChatName;
|
||||
const senderName = isOutgoing ? userID : (msg._data.notifyName || activeChatName);
|
||||
const timeStr = formatTime(msg.timestamp || new Date());
|
||||
|
||||
groupDiv.innerHTML = `
|
||||
${!isOutgoing ? `<span class="message-sender">${senderName}</span>` : ''}
|
||||
<div class="message-bubble">
|
||||
${msg.body || msg.text}
|
||||
<div class="message-meta">
|
||||
<span>${timeStr}</span>
|
||||
<i data-lucide="clock" style="width:12px; height:12px; opacity:0.6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
let statusCheck = '';
|
||||
if (isOutgoing) {
|
||||
if (msg.status === 'read') {
|
||||
statusCheck = '<i data-lucide="check-check" style="color: var(--online-color); width:14px; height:14px;"></i>';
|
||||
} else if (msg.status === 'delivered') {
|
||||
statusCheck = '<i data-lucide="check-check" style="width:14px; height:14px;"></i>';
|
||||
} else {
|
||||
statusCheck = '<i data-lucide="check" style="width:14px; height:14px;"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'message-bubble';
|
||||
|
||||
if (!isOutgoing) {
|
||||
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.media?.url) {
|
||||
const a = document.createElement('a');
|
||||
a.innerText = `Request media (${msg.media.filename})`;
|
||||
|
||||
a.addEventListener('click', async (e) => {
|
||||
const url = new URL(msg.media.url);
|
||||
const reqID = url.pathname.split('/').filter(Boolean).pop();
|
||||
|
||||
const { blob, filename } = await waha.downloadMedia(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;
|
||||
a.appendChild(img);
|
||||
} else {
|
||||
e.target.textContent = filename || `Download ${msg.media.filename}`;
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
contentEl.appendChild(a);
|
||||
}
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'message-meta';
|
||||
meta.innerHTML = `<span>${timeStr}</span>${statusCheck}`;
|
||||
|
||||
bubble.appendChild(meta);
|
||||
let uid = "";
|
||||
|
||||
function getPrevMessageElem() {
|
||||
return elements.messagesContainer.lastElementChild; // <‑‑ key change
|
||||
}
|
||||
|
||||
const prevMsgEl = getPrevMessageElem();
|
||||
|
||||
if (isOutgoing) {
|
||||
if (prevMsgEl && prevMsgEl.classList.contains('outgoing')) {
|
||||
} else {
|
||||
groupDiv.appendChild(document.createElement('div')).className = 'message-indicator';
|
||||
}
|
||||
} else if (msg.participant) {
|
||||
uid = msg.participant;
|
||||
if (!prevMsgEl || uid !== prevMsgEl.dataset.from) {
|
||||
groupDiv.appendChild(document.createElement('div')).className = 'message-indicator';
|
||||
}
|
||||
} else {
|
||||
uid = msg.from;
|
||||
if (!prevMsgEl || uid !== prevMsgEl.dataset.from) {
|
||||
groupDiv.appendChild(document.createElement('div')).className = 'message-indicator';
|
||||
}
|
||||
}
|
||||
|
||||
groupDiv.appendChild(bubble);
|
||||
|
||||
elements.messagesContainer.appendChild(groupDiv);
|
||||
lucide.createIcons();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
47
docs/waha.js
47
docs/waha.js
|
|
@ -28,13 +28,51 @@ async function request(path, options = {}) {
|
|||
return response.json();
|
||||
}
|
||||
|
||||
async function downloadFile(path, options = {}) {
|
||||
const url = `${config.wahaUrl}${path}`;
|
||||
|
||||
const headers = {
|
||||
'Content-Type': options.headers?.['Content-Type'] ?? 'application/json',
|
||||
'accept': '*/*',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
if (config.apiKey) headers['X-Api-Key'] = config.apiKey;
|
||||
|
||||
console.log(`[WAHA] ${options.method || 'GET'} ${url}`);
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (!response.ok) {
|
||||
let errorDetail = '';
|
||||
try {
|
||||
errorDetail = JSON.stringify(await response.json());
|
||||
} catch (_) {
|
||||
errorDetail = await response.text().catch(() => '');
|
||||
}
|
||||
throw new Error(`WAHA API returned ${response.status}: ${response.statusText} — ${errorDetail}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
let filename = 'download';
|
||||
const cd = response.headers.get('content-disposition');
|
||||
if (cd) {
|
||||
const m = cd.match(/filename\*=UTF-8''([^;]+)|filename="?([^"]+)"?/i);
|
||||
filename = decodeURIComponent(m?.[1] || m?.[2] || filename);
|
||||
}
|
||||
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
|
||||
export const waha = {
|
||||
async getVersion() {
|
||||
return request('/api/version');
|
||||
},
|
||||
|
||||
async getChats() {
|
||||
const data = await request(`/api/${config.session}/chats`);
|
||||
const data = await request(`/api/${config.session}/chats?sortBy=conversationTimestamp`);
|
||||
return data.map(chat => {
|
||||
let chatId = chat.id;
|
||||
if (chatId && typeof chatId === "object") {
|
||||
|
|
@ -51,7 +89,7 @@ export const waha = {
|
|||
},
|
||||
|
||||
async getChatMessages(chatId) {
|
||||
return request(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40`);
|
||||
return request(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=true&limit=40`);
|
||||
},
|
||||
|
||||
async getChatPicture(chatId) {
|
||||
|
|
@ -65,6 +103,11 @@ export const waha = {
|
|||
});
|
||||
},
|
||||
|
||||
async downloadMedia(file) {
|
||||
const { blob, filename } = await downloadFile(`/api/files/${config.session}/${file}`);
|
||||
return { blob, filename };
|
||||
},
|
||||
|
||||
async getMyInfo() {
|
||||
return request(`/api/sessions/${config.session}/me`);
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue