.
This commit is contained in:
parent
0289db28e5
commit
1c586b6bcb
7 changed files with 357 additions and 130 deletions
35
docs/app.js
35
docs/app.js
|
|
@ -6,9 +6,11 @@ import { compensateMessageOrdering, formatTime } from "./utils.js";
|
||||||
|
|
||||||
let chatsState = [];
|
let chatsState = [];
|
||||||
let activeChatState = null;
|
let activeChatState = null;
|
||||||
|
let userInfo;
|
||||||
|
|
||||||
// Initialize app when DOM is ready
|
// Initialize app when DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
userInfo = await waha.getMyInfo();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
fetchChats();
|
fetchChats();
|
||||||
checkWahaStatus();
|
checkWahaStatus();
|
||||||
|
|
@ -16,6 +18,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (e.code == "Escape") {
|
||||||
|
ui.toggleChatState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
elements.refreshChatsBtn.addEventListener('click', () => {
|
elements.refreshChatsBtn.addEventListener('click', () => {
|
||||||
elements.refreshChatsBtn.classList.add('spinning');
|
elements.refreshChatsBtn.classList.add('spinning');
|
||||||
fetchChats().finally(() => {
|
fetchChats().finally(() => {
|
||||||
|
|
@ -44,7 +54,6 @@ function setupEventListeners() {
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.settingsIconBtn.addEventListener('click', openSettings);
|
elements.settingsIconBtn.addEventListener('click', openSettings);
|
||||||
elements.closeSettingsBtn.addEventListener('click', () => ui.toggleModal(false));
|
|
||||||
elements.cancelSettingsBtn.addEventListener('click', () => ui.toggleModal(false));
|
elements.cancelSettingsBtn.addEventListener('click', () => ui.toggleModal(false));
|
||||||
elements.saveSettingsBtn.addEventListener('click', saveSettings);
|
elements.saveSettingsBtn.addEventListener('click', saveSettings);
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +94,7 @@ function handleIncomingMessage(msg) {
|
||||||
const msgId = normalizeChatId(msg.id) || msg.id;
|
const msgId = normalizeChatId(msg.id) || msg.id;
|
||||||
const exists = document.getElementById(msgId);
|
const exists = document.getElementById(msgId);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name);
|
ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, userInfo.id);
|
||||||
ui.scrollToBottom();
|
ui.scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,28 +115,28 @@ function handleIncomingMessage(msg) {
|
||||||
return tB - tA;
|
return tB - tA;
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.renderChatList(chatsState, activeChatState, selectChat);
|
// ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||||
} else {
|
} else {
|
||||||
fetchChats();
|
// fetchChats();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchChats() {
|
async function fetchChats() {
|
||||||
const userInfo = await waha.getMyInfo();
|
try {
|
||||||
elements.loggedUserName.textContent = userInfo.pushName;
|
elements.loggedUserName.textContent = userInfo.pushName;
|
||||||
// elements.userIcon.innerText = userInfo.pushName[0];
|
// elements.userIcon.innerText = userInfo.pushName[0];
|
||||||
elements.chatsLoader.classList.remove('hidden');
|
elements.chatsLoader.classList.remove('hidden');
|
||||||
try {
|
|
||||||
chatsState = await waha.getChats();
|
chatsState = await waha.getChats();
|
||||||
ui.renderChatList(chatsState, activeChatState, selectChat);
|
ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load chats:', error);
|
console.error('Failed to load chats:', error);
|
||||||
elements.chatList.innerHTML = `
|
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>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.
|
Ensure WAHA server is running and CORS is enabled, or click Settings to configure.
|
||||||
</p>
|
</p>
|
||||||
|
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 8px;">${error.message}</p>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -140,7 +149,7 @@ async function selectChat(chat) {
|
||||||
|
|
||||||
chat.unreadCount = 0;
|
chat.unreadCount = 0;
|
||||||
|
|
||||||
ui.renderChatList(chatsState, activeChatState, selectChat);
|
// ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||||
|
|
||||||
ui.toggleChatState(true);
|
ui.toggleChatState(true);
|
||||||
elements.activeChatName.textContent = chat.name.toUpperCase();
|
elements.activeChatName.textContent = chat.name.toUpperCase();
|
||||||
|
|
@ -155,7 +164,7 @@ async function selectChat(chat) {
|
||||||
try {
|
try {
|
||||||
const rawMessages = await waha.getChatMessages(chat.id);
|
const rawMessages = await waha.getChatMessages(chat.id);
|
||||||
const processedMessages = compensateMessageOrdering(rawMessages);
|
const processedMessages = compensateMessageOrdering(rawMessages);
|
||||||
ui.renderMessages(processedMessages, chat.name);
|
ui.renderMessages(processedMessages, chat.name, userInfo.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load messages:', error);
|
console.error('Failed to load messages:', error);
|
||||||
elements.messagesContainer.innerHTML = '<div class="loading-chats">Error loading messages</div>';
|
elements.messagesContainer.innerHTML = '<div class="loading-chats">Error loading messages</div>';
|
||||||
|
|
@ -177,7 +186,7 @@ async function sendMessage() {
|
||||||
status: 'sending'
|
status: 'sending'
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.appendSingleMessage(tempMsg, activeChatState.name);
|
ui.appendSingleMessage(tempMsg, activeChatState.name, userInfo.id);
|
||||||
ui.scrollToBottom();
|
ui.scrollToBottom();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -223,7 +232,7 @@ async function sendMessage() {
|
||||||
const tB = new Date(b.timestamp).getTime();
|
const tB = new Date(b.timestamp).getTime();
|
||||||
return tB - tA;
|
return tB - tA;
|
||||||
});
|
});
|
||||||
ui.renderChatList(chatsState, activeChatState, selectChat);
|
// ui.renderChatList(chatsState, activeChatState, selectChat);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send message:', error);
|
console.error('Failed to send message:', error);
|
||||||
const tempBubble = document.getElementById(tempMsg.id);
|
const tempBubble = document.getElementById(tempMsg.id);
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,20 @@
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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="style.css">
|
||||||
|
<link rel="stylesheet" href="loading.css">
|
||||||
|
<link rel="stylesheet" href="metroicons.css">
|
||||||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
<script src="https://unpkg.com/lucide@latest" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script defer>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelectorAll(".dots").forEach(el => {
|
||||||
|
el.classList.add("animate");
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
|
|
@ -23,7 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<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" id="refresh-chats-btn" title="Refresh Chats">refresh</button>
|
||||||
<button class="text-btn" title="Settings">settings</button>
|
<button class="text-btn" title="Settings">settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,8 +48,13 @@
|
||||||
|
|
||||||
<div class="chat-list-container">
|
<div class="chat-list-container">
|
||||||
<div class="loading-chats" id="chats-loader">
|
<div class="loading-chats" id="chats-loader">
|
||||||
<div class="spinner"></div>
|
<div class='dots'>
|
||||||
<span>Loading chats...</span>
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="chat-list" id="chat-list">
|
<ul class="chat-list" id="chat-list">
|
||||||
<!-- Chats will be dynamically injected here -->
|
<!-- Chats will be dynamically injected here -->
|
||||||
|
|
@ -58,11 +74,9 @@
|
||||||
<!-- No Chat Selected State -->
|
<!-- No Chat Selected State -->
|
||||||
<div class="no-chat-state" id="no-chat-state">
|
<div class="no-chat-state" id="no-chat-state">
|
||||||
<div class="empty-state-content">
|
<div class="empty-state-content">
|
||||||
<div class="empty-state-icon">
|
<div class="empty-state-icon mif-qa mif-3x">
|
||||||
<i data-lucide="message-square"></i>
|
|
||||||
</div>
|
</div>
|
||||||
<h2>Welcome to Pandora Chat</h2>
|
<p>Select a contact from the sidebar to view the conversation or start a new chat.</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -74,7 +88,7 @@
|
||||||
<div class="avatar active-avatar" id="active-chat-avatar">C</div>
|
<div class="avatar active-avatar" id="active-chat-avatar">C</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 id="active-chat-name">Contact Name</h3>
|
<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>
|
</div>
|
||||||
<div class="chat-actions">
|
<div class="chat-actions">
|
||||||
|
|
@ -93,13 +107,12 @@
|
||||||
<!-- Input Panel -->
|
<!-- Input Panel -->
|
||||||
<footer class="chat-input-panel">
|
<footer class="chat-input-panel">
|
||||||
<div class="input-actions-left">
|
<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> -->
|
<!-- <button class="icon-btn" title="Add emoji"><i data-lucide="smile"></i></button> -->
|
||||||
</div>
|
</div>
|
||||||
<form class="input-form" id="message-form">
|
<form class="input-form" id="message-form">
|
||||||
<input type="text" id="message-input" placeholder="Type a message..." autocomplete="off">
|
<input type="text" id="message-input" placeholder="Type a message..." autocomplete="off">
|
||||||
<button type="submit" class="send-btn" id="send-button">
|
<button type="submit" class="send-btn mif-paper-plane mif-3x" id="send-button">
|
||||||
<i data-lucide="send"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
@ -112,7 +125,6 @@
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<header class="modal-header">
|
<header class="modal-header">
|
||||||
<h2>SETTINGS</h2>
|
<h2>SETTINGS</h2>
|
||||||
<button class="icon-btn close-modal-btn" id="close-settings"><i data-lucide="x"></i></button>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -127,7 +139,7 @@
|
||||||
<label for="settings-api-key">API Key (X-API-KEY)</label>
|
<label for="settings-api-key">API Key (X-API-KEY)</label>
|
||||||
<input type="password" id="settings-api-key" placeholder="Enter API Key">
|
<input type="password" id="settings-api-key" placeholder="Enter API Key">
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<footer class="modal-footer">
|
<footer class="modal-footer">
|
||||||
<button class="btn secondary-btn" id="cancel-settings">Cancel</button>
|
<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);
|
--border-hover: rgba(255, 255, 255, 0.15);
|
||||||
--text-primary: #f8fafc;
|
--text-primary: #f8fafc;
|
||||||
--text-secondary: #94a3b8;
|
--text-secondary: #94a3b8;
|
||||||
--text-muted: #64748b;
|
--text-muted: rgb(134, 134, 134);
|
||||||
--accent: white;
|
--accent: white;
|
||||||
--accent-gradient: black;
|
--accent-gradient: black;
|
||||||
--accent-hover: #4f46e5;
|
--accent-hover: #4f46e5;
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -38,6 +39,10 @@ body {
|
||||||
radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.12) 0px, transparent 50%);
|
radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.12) 0px, transparent 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: var(--bg-main);
|
background-color: var(--bg-main);
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -159,7 +164,7 @@ input:focus {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border: none;
|
border: none;
|
||||||
background: var(--bg-main);
|
background: var(--bg-main);
|
||||||
color: var(--text-secondary);
|
color: var(--text-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -171,7 +176,6 @@ input:focus {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-color: var(--border-hover);
|
border-color: var(--border-hover);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:active {
|
.icon-btn:active {
|
||||||
|
|
@ -415,15 +419,15 @@ input:focus {
|
||||||
.empty-state-icon {
|
.empty-state-icon {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
border-radius: 24px;
|
background: var(--bg-secondary);
|
||||||
background: rgba(255, 255, 255, 0.03);
|
/* border: 1px solid var(--border-color); */
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0 auto 24px auto;
|
margin: 0 auto 24px auto;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
align-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state-content h2 {
|
.empty-state-content h2 {
|
||||||
|
|
@ -494,6 +498,7 @@ input:focus {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
user-select: text;
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 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%);
|
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);
|
border-bottom: 10px solid var(--bubble-incoming);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content * {
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
.message-meta {
|
.message-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -568,6 +583,11 @@ input:focus {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-image-attachement {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
/* Chat Input Panel */
|
/* Chat Input Panel */
|
||||||
.chat-input-panel {
|
.chat-input-panel {
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
|
|
@ -590,6 +610,7 @@ input:focus {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-input {
|
#message-input {
|
||||||
|
|
@ -600,6 +621,7 @@ input:focus {
|
||||||
color: var(--bg-main);
|
color: var(--bg-main);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-input:focus {
|
#message-input:focus {
|
||||||
|
|
@ -624,11 +646,6 @@ input:focus {
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
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 {
|
.send-btn:active {
|
||||||
transform: scale(0.98) translateY(0);
|
transform: scale(0.98) translateY(0);
|
||||||
}
|
}
|
||||||
|
|
@ -679,7 +696,7 @@ input:focus {
|
||||||
|
|
||||||
.sidebar.hidden {
|
.sidebar.hidden {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
display: flex !important; /* overrides standard .hidden */
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-area {
|
.chat-area {
|
||||||
|
|
@ -690,6 +707,14 @@ input:focus {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-group {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-image-attachement {
|
||||||
|
max-height: 20em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal Styling */
|
/* Modal Styling */
|
||||||
|
|
@ -707,10 +732,13 @@ input:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
|
width: 100%;
|
||||||
background: var(--bg-main);
|
background: var(--bg-main);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
backdrop-filter: var(--glass-blur);
|
backdrop-filter: var(--glass-blur);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
|
@ -770,11 +798,6 @@ input:focus {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-btn {
|
.secondary-btn {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
|
||||||
136
docs/ui.js
136
docs/ui.js
|
|
@ -1,5 +1,6 @@
|
||||||
import { formatTime } from "./utils.js";
|
import { formatTime } from "./utils.js";
|
||||||
import { waha } from "./waha.js";
|
import { waha } from "./waha.js";
|
||||||
|
import { config } from "./config.js";
|
||||||
|
|
||||||
// DOM Selector Cache
|
// DOM Selector Cache
|
||||||
export const elements = {
|
export const elements = {
|
||||||
|
|
@ -17,11 +18,10 @@ export const elements = {
|
||||||
messageForm: document.getElementById('message-form'),
|
messageForm: document.getElementById('message-form'),
|
||||||
messageInput: document.getElementById('message-input'),
|
messageInput: document.getElementById('message-input'),
|
||||||
// backToSidebarBtn: document.getElementById('back-to-sidebar'),
|
// backToSidebarBtn: document.getElementById('back-to-sidebar'),
|
||||||
backToSidebarBtn: document.getElementById('active-chat-container'),
|
backToSidebarBtn: document.querySelector('.chat-header'),
|
||||||
sidebar: document.querySelector('.sidebar'),
|
sidebar: document.querySelector('.sidebar'),
|
||||||
settingsModal: document.getElementById('settings-modal'),
|
settingsModal: document.getElementById('settings-modal'),
|
||||||
settingsIconBtn: document.querySelector('.header-actions button[title="Settings"]'),
|
settingsIconBtn: document.querySelector('.header-actions button[title="Settings"]'),
|
||||||
closeSettingsBtn: document.getElementById('close-settings'),
|
|
||||||
cancelSettingsBtn: document.getElementById('cancel-settings'),
|
cancelSettingsBtn: document.getElementById('cancel-settings'),
|
||||||
saveSettingsBtn: document.getElementById('save-settings'),
|
saveSettingsBtn: document.getElementById('save-settings'),
|
||||||
inputWahaUrl: document.getElementById('settings-waha-url'),
|
inputWahaUrl: document.getElementById('settings-waha-url'),
|
||||||
|
|
@ -120,7 +120,7 @@ export const ui = {
|
||||||
/**
|
/**
|
||||||
* Render chat message log inside chat view container
|
* Render chat message log inside chat view container
|
||||||
*/
|
*/
|
||||||
renderMessages(messages, activeChatName) {
|
async renderMessages(messages, activeChatName, userID) {
|
||||||
elements.messagesContainer.innerHTML = '';
|
elements.messagesContainer.innerHTML = '';
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
|
|
@ -128,13 +128,27 @@ export const ui = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.forEach(msg => {
|
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, userID) {
|
||||||
|
console.log(msg);
|
||||||
const isOutgoing = msg.fromMe || msg.sender === 'me';
|
const isOutgoing = msg.fromMe || msg.sender === 'me';
|
||||||
|
|
||||||
const groupDiv = document.createElement('div');
|
const groupDiv = document.createElement('div');
|
||||||
groupDiv.className = `message-group ${isOutgoing ? 'outgoing' : 'incoming'}`;
|
groupDiv.className = `message-group ${isOutgoing ? 'outgoing' : 'incoming'}`;
|
||||||
groupDiv.id = msg.id;
|
groupDiv.id = msg.id;
|
||||||
|
groupDiv.dataset.from = msg.participant || msg.from;
|
||||||
|
|
||||||
const senderName = isOutgoing ? 'Me' : (msg.from || activeChatName);
|
const senderName = isOutgoing ? userID : (msg._data.notifyName || activeChatName);
|
||||||
const timeStr = formatTime(msg.timestamp || new Date());
|
const timeStr = formatTime(msg.timestamp || new Date());
|
||||||
|
|
||||||
let statusCheck = '';
|
let statusCheck = '';
|
||||||
|
|
@ -148,53 +162,83 @@ export const ui = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groupDiv.innerHTML = `
|
const bubble = document.createElement('div');
|
||||||
<div class="message-indicator"></div>
|
bubble.className = 'message-bubble';
|
||||||
<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);
|
if (!isOutgoing) {
|
||||||
});
|
const senderEl = document.createElement('span');
|
||||||
|
senderEl.className = 'message-sender';
|
||||||
lucide.createIcons();
|
senderEl.textContent = senderName;
|
||||||
this.scrollToBottom();
|
bubble.appendChild(senderEl);
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append a single message (used for optimistic updates immediately upon sending)
|
|
||||||
*/
|
|
||||||
appendSingleMessage(msg, activeChatName) {
|
|
||||||
if (elements.messagesContainer.querySelector('.loading-chats')) {
|
|
||||||
elements.messagesContainer.innerHTML = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOutgoing = msg.fromMe || msg.sender === 'me';
|
const contentEl = document.createElement('div');
|
||||||
const groupDiv = document.createElement('div');
|
contentEl.classList.add('message-content');
|
||||||
groupDiv.className = `message-group ${isOutgoing ? 'outgoing' : 'incoming'}`;
|
const textEl = document.createElement('div');
|
||||||
groupDiv.id = msg.id;
|
textEl.innerHTML = msg.body || msg.text || "";
|
||||||
|
contentEl.appendChild(textEl);
|
||||||
|
bubble.appendChild(contentEl);
|
||||||
|
|
||||||
const senderName = isOutgoing ? 'Me' : activeChatName;
|
if (msg.media?.url) {
|
||||||
const timeStr = formatTime(msg.timestamp || new Date());
|
const a = document.createElement('a');
|
||||||
|
a.innerText = `Request media (${msg.media.filename})`;
|
||||||
|
|
||||||
groupDiv.innerHTML = `
|
a.addEventListener('click', async (e) => {
|
||||||
${!isOutgoing ? `<span class="message-sender">${senderName}</span>` : ''}
|
const url = new URL(msg.media.url);
|
||||||
<div class="message-bubble">
|
const reqID = url.pathname.split('/').filter(Boolean).pop();
|
||||||
${msg.body || msg.text}
|
|
||||||
<div class="message-meta">
|
const { blob, filename } = await waha.downloadMedia(reqID);
|
||||||
<span>${timeStr}</span>
|
|
||||||
<i data-lucide="clock" style="width:12px; height:12px; opacity:0.6;"></i>
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
</div>
|
e.target.href = objectUrl;
|
||||||
</div>
|
|
||||||
`;
|
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);
|
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();
|
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 = {
|
export const waha = {
|
||||||
async getVersion() {
|
async getVersion() {
|
||||||
return request('/api/version');
|
return request('/api/version');
|
||||||
},
|
},
|
||||||
|
|
||||||
async getChats() {
|
async getChats() {
|
||||||
const data = await request(`/api/${config.session}/chats`);
|
const data = await request(`/api/${config.session}/chats?sortBy=conversationTimestamp`);
|
||||||
return data.map(chat => {
|
return data.map(chat => {
|
||||||
let chatId = chat.id;
|
let chatId = chat.id;
|
||||||
if (chatId && typeof chatId === "object") {
|
if (chatId && typeof chatId === "object") {
|
||||||
|
|
@ -51,7 +89,7 @@ export const waha = {
|
||||||
},
|
},
|
||||||
|
|
||||||
async getChatMessages(chatId) {
|
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) {
|
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() {
|
async getMyInfo() {
|
||||||
return request(`/api/sessions/${config.session}/me`);
|
return request(`/api/sessions/${config.session}/me`);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue