Ported to Cordova, it sucks though, I'm trying Capacitor tomorrow.

This commit is contained in:
天クマ 2026-07-13 22:29:54 -03:00
commit 85038b45a3
81 changed files with 1696 additions and 497 deletions

27
.gitignore vendored
View file

@ -1,2 +1,25 @@
.env
node_modules
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
.DS_Store
# Generated by package manager
node_modules/
# Generated by Cordova
/plugins/
/platforms/

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

45
config.xml Normal file
View file

@ -0,0 +1,45 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="org.adrianvictor.hellocordova" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Pandora</name>
<description>
A messaging client for WAHA API.
</description>
<author email="adrianvictor@disroot.org" href="https://adrianvic.github.io">
Adrian Victor (tenkuma)
</author>
<content src="index.html" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
<platform name="android">
<adaptive-icon background="#000000" foreground="resources/android/icon.png" />
<icon density="mdpi" src="resources/android/icon/drawable-mdpi-icon.png" />
<icon density="hdpi" src="resources/android/icon/drawable-hdpi-icon.png" />
<icon density="xhdpi" src="resources/android/icon/drawable-xhdpi-icon.png" />
<icon density="xxhdpi" src="resources/android/icon/drawable-xxhdpi-icon.png" />
<icon density="xxxhdpi" src="resources/android/icon/drawable-xxxhdpi-icon.png" />
<icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" />
<splash density="land-ldpi" src="resources/android/splash/drawable-land-ldpi-screen.png" />
<splash density="land-mdpi" src="resources/android/splash/drawable-land-mdpi-screen.png" />
<splash density="land-hdpi" src="resources/android/splash/drawable-land-hdpi-screen.png" />
<splash density="land-xhdpi" src="resources/android/splash/drawable-land-xhdpi-screen.png" />
<splash density="land-xxhdpi" src="resources/android/splash/drawable-land-xxhdpi-screen.png" />
<splash density="land-xxxhdpi" src="resources/android/splash/drawable-land-xxxhdpi-screen.png" />
<splash density="port-ldpi" src="resources/android/splash/drawable-port-ldpi-screen.png" />
<splash density="port-mdpi" src="resources/android/splash/drawable-port-mdpi-screen.png" />
<splash density="port-hdpi" src="resources/android/splash/drawable-port-hdpi-screen.png" />
<splash density="port-xhdpi" src="resources/android/splash/drawable-port-xhdpi-screen.png" />
<splash density="port-xxhdpi" src="resources/android/splash/drawable-port-xxhdpi-screen.png" />
<splash density="port-xxxhdpi" src="resources/android/splash/drawable-port-xxxhdpi-screen.png" />
<icon background="resources/android/icon/ldpi-background.png" density="ldpi" foreground="resources/android/icon/ldpi-foreground.png" />
<icon background="resources/android/icon/mdpi-background.png" density="mdpi" foreground="resources/android/icon/mdpi-foreground.png" />
<icon background="resources/android/icon/hdpi-background.png" density="hdpi" foreground="resources/android/icon/hdpi-foreground.png" />
<icon background="resources/android/icon/xhdpi-background.png" density="xhdpi" foreground="resources/android/icon/xhdpi-foreground.png" />
<icon background="resources/android/icon/xxhdpi-background.png" density="xxhdpi" foreground="resources/android/icon/xxhdpi-foreground.png" />
<icon background="resources/android/icon/xxxhdpi-background.png" density="xxxhdpi" foreground="resources/android/icon/xxxhdpi-foreground.png" />
<preference name="AndroidWindowSplashScreenAnimatedIcon" value="resources/icon.png" />
<preference name="AndroidWindowSplashScreenBackground" value="#000000" />
<preference name="AndroidWindowSplashScreenAnimationDuration" value="200" />
<preference name="AndroidWindowSplashScreenAnimatedIcon" value="resources/android/splash/icon.png" />
<preference name="SplashScreenBackgroundColor" value="#000000" />
</platform>
</widget>

View file

@ -1,21 +0,0 @@
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
const app = express();
const PORT = process.env.PORT || 3200;
// Resolve __dirname in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Serve static assets from the public folder
app.use(express.static(path.join(__dirname, "docs")));
// Start static file server
app.listen(PORT, () => {
console.log(`==================================================`);
console.log(` Pandora Chat Static Server is running!`);
console.log(` URL: http://localhost:${PORT}`);
console.log(`==================================================`);
});

1962
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,26 @@
{
"name": "pandora",
"name": "org.adrianvictor.pandorabox",
"displayName": "Pandora",
"version": "1.0.0",
"type": "module",
"description": "A messaging client for WAHA API.",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
"runAndroid": "cordova run android --device",
"runBrowser": "cordova run browser"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.18.1",
"dotenv": "^17.4.2",
"express": "^5.2.1"
"keywords": [
"ecosystem:cordova"
],
"author": "Apache Cordova Team",
"license": "Apache-2.0",
"devDependencies": {
"cordova-android": "github:apache/cordova-android#14.0.1",
"cordova-browser": "^7.0.0"
},
"cordova": {
"platforms": [
"browser",
"android"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
resources/android/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View file

@ -1,77 +0,0 @@
import axios from "axios";
const session = "session_01kxc62bk5fs8vh4v127k88a7j";
const wahaBaseUrl = "http://inspiran.beetal-castor.ts.net:3100";
const apiKey = process.env.WAHA_API_KEY;
const client = axios.create({
baseURL: wahaBaseUrl,
headers: {
"X-API-KEY": apiKey,
"Content-Type": "application/json"
}
});
export const wahaService = {
getConfigStatus() {
return {
wahaBaseUrl,
session,
hasApiKey: !!apiKey
};
},
async getChats() {
const response = await client.get(`/api/${session}/chats`, { timeout: 5000 });
if (!Array.isArray(response.data)) {
throw new Error("Invalid chats response format from WAHA API");
}
return response.data.map(chat => {
let chatId = chat.id;
if (chatId && typeof chatId === "object") {
chatId = chatId._serialized || chatId.user || JSON.stringify(chatId);
}
return {
id: chatId || chat.chatId || chat.name,
name: chat.name || "Unknown Contact",
unreadCount: chat.unreadCount || 0,
lastMessage: chat.lastMessage?.body || chat.lastMessageText || "Click to open chat",
timestamp: chat.lastMessage?.timestamp || new Date()
};
});
},
async getChatMessages(chatId) {
const response = await client.get(`/api/${session}/chats/${chatId}/messages?downloadMedia=false&merge=true&limit=10`);
return response.data;
},
async getMyInfo(chatId) {
const response = await client.get(`/api/${session}/me`);
return response.data;
},
async startTyping(chatId) {
await client.post("/api/startTyping", {
chatId,
session
}, { timeout: 3000 });
},
async stopTyping(chatId) {
await client.post("/api/stopTyping", {
chatId,
session
}, { timeout: 2000 });
},
async sendTextMessage(chatId, text) {
const response = await client.post("/api/sendText", {
chatId,
text
});
return response.data;
}
};

View file

@ -10,11 +10,26 @@ let userInfo;
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', async () => {
userInfo = await waha.getMyInfo();
setupEventListeners();
fetchChats();
checkWahaStatus();
initWebSocket();
try {
userInfo = await waha.getMyInfo();
fetchChats();
checkWahaStatus();
initWebSocket();
} catch (error) {
console.error('Failed to load chats:', error);
elements.chatList.innerHTML = `
<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-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 {
elements.chatsLoader.classList.add('hidden');
}
});
function setupEventListeners() {
@ -155,7 +170,18 @@ async function selectChat(chat) {
elements.activeChatName.textContent = chat.name.toUpperCase();
elements.activeChatAvatar.textContent = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?';
elements.messagesContainer.innerHTML = '<div class="loading-chats"><div class="spinner"></div><span>Loading messages...</span></div>';
elements.messagesContainer.innerHTML = `
<div class="loading-chats">
<div class='dots'>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<span>Loading messages...
</span>
</div>`;
if (window.innerWidth <= 768) {
elements.sidebar.classList.add('hidden');
@ -164,7 +190,7 @@ async function selectChat(chat) {
try {
const rawMessages = await waha.getChatMessages(chat.id);
const processedMessages = compensateMessageOrdering(rawMessages);
ui.renderMessages(processedMessages, chat.name, userInfo.id);
ui.renderMessages(processedMessages, chat.name, userInfo.id, chat.id);
} catch (error) {
console.error('Failed to load messages:', error);
elements.messagesContainer.innerHTML = '<div class="loading-chats">Error loading messages</div>';

View file

@ -19,7 +19,7 @@
document.querySelectorAll(".dots").forEach(el => {
el.classList.add("animate");
});
}, 1000);
}, 100);
});
</script>
<div class="app-container">

View file

@ -77,50 +77,50 @@ export const ui = {
}
},
/**
* Render sidebar contact chats list
*/
renderChatList(chats, activeChat, onChatSelect) {
async renderChatList(chats, activeChat, onChatSelect) {
elements.chatList.innerHTML = '';
chats.sort((a, b) => b.timestamp - a.timestamp);
if (chats.length === 0) {
elements.chatList.innerHTML = `<li class="loading-chats">No chats found</li>`;
return;
}
chats.forEach(async (chat) => {
for (const chat of chats) {
console.log(chat);
const li = document.createElement('li');
li.className = `chat-item ${activeChat && activeChat.id === chat.id ? 'active' : ''}`;
li.dataset.id = chat.id;
const picture = await waha.getChatPicture(chat.id);
const picture = await waha.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 = `
<div class="avatar"><img src="${picture.url}"></div>
<div class="chat-item-info">
<div class="chat-item-meta">
<span class="chat-item-name">${chat.name}</span>
<span class="chat-item-time">${timeStr}</span>
</div>
<div class="chat-item-preview">
<span class="chat-item-msg">${chat.lastMessage || 'No messages yet'}</span>
${hasUnread ? `<span class="unread-badge">${chat.unreadCount}</span>` : ''}
</div>
<div class="avatar"><img src="${picture.url}" alt="${initials}"></div>
<div class="chat-item-info">
<div class="chat-item-meta">
<span class="chat-item-name">${chat.name}</span>
<span class="chat-item-time">${timeStr}</span>
</div>
`;
<div class="chat-item-preview">
<span class="chat-item-msg">${chat.lastMessage || 'No messages yet'}</span>
${hasUnread ? `<span class="unread-badge">${chat.unreadCount}</span>` : ''}
</div>
</div>
`;
li.addEventListener('click', () => onChatSelect(chat));
elements.chatList.appendChild(li);
});
}
},
/**
* Render chat message log inside chat view container
*/
async renderMessages(messages, activeChatName, userID) {
async renderMessages(messages, activeChatName, userID, chatId) {
elements.messagesContainer.innerHTML = '';
if (messages.length === 0) {
@ -129,7 +129,7 @@ export const ui = {
}
for (const msg of messages) {
this.appendSingleMessage(msg, activeChatName, userID);
this.appendSingleMessage(msg, activeChatName, userID, chatId);
}
lucide.createIcons();
@ -139,7 +139,7 @@ export const ui = {
/**
* Append a single message (used for optimistic updates immediately upon sending)
*/
appendSingleMessage(msg, activeChatName, userID) {
appendSingleMessage(msg, activeChatName, userID, chatId) {
console.log(msg);
const isOutgoing = msg.fromMe || msg.sender === 'me';
@ -179,12 +179,14 @@ export const ui = {
contentEl.appendChild(textEl);
bubble.appendChild(contentEl);
if (msg.media?.url) {
if (msg.hasMedia) {
const a = document.createElement('a');
a.innerText = `Request media (${msg.media.filename})`;
a.innerText = `[Request media]`;
a.addEventListener('click', async (e) => {
const url = new URL(msg.media.url);
const mediaMsg = await waha.getSingleChatMessage(chatId, msg.id, true);
const url = new URL(mediaMsg.media.url);
const reqID = url.pathname.split('/').filter(Boolean).pop();
const { blob, filename } = await waha.downloadMedia(reqID);
@ -199,7 +201,7 @@ export const ui = {
img.src = objectUrl;
a.appendChild(img);
} else {
e.target.textContent = filename || `Download ${msg.media.filename}`;
e.target.textContent = filename || `Download ${mediaMsg.media.filename}`;
}
})

View file

@ -89,7 +89,11 @@ export const waha = {
},
async getChatMessages(chatId) {
return request(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=true&limit=40`);
return request(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40`);
},
async getSingleChatMessage(chatId, messageId, downladMedia) {
return request(`/api/${config.session}/chats/${chatId}/messages/${messageId}?downloadMedia=${downladMedia}`);
},
async getChatPicture(chatId) {