diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index c2d2ff8..0000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: Build Vite
-
-on:
- push:
- branches:
- - master
- workflow_dispatch:
-
-permissions:
- contents: write
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- strategy:
- matrix:
- node-version: [24.x]
-
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node-version }}
-
- - name: Install dependencies & build
- run: |
- npm ci
- npm run build
-
- - name: Deploy
- uses: peaceiris/actions-gh-pages@v4
- with:
- publish_dir: ./dist
- github_token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ffe0fcc..819f592 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,29 @@
-dist/
+# 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/
-.idea
\ No newline at end of file
+
+# Generated by Cordova
+/plugins/
+/platforms/
+
+pandora.apks
+/android/app/build/
+test-upload-key.jks
diff --git a/README.md b/README.md
index b7b3008..f310aab 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
Pandora is an open-source web client for the WAHA API.
@@ -13,7 +13,7 @@ Pandora is an open-source web client for the WAHA API.
- [ ] Sending audio messages
- [x] Sending attachments (sends image/video as such)4
- [x] Notification sound5
-- [x] Browser notification
+- [ ] Browser notification
- [x] Download attachments
- [x] Automatically download images6
- [X] Local message storing
diff --git a/web/fonts/selawk.ttf b/fonts/selawk.ttf
similarity index 100%
rename from web/fonts/selawk.ttf
rename to fonts/selawk.ttf
diff --git a/web/fonts/selawkb.ttf b/fonts/selawkb.ttf
similarity index 100%
rename from web/fonts/selawkb.ttf
rename to fonts/selawkb.ttf
diff --git a/web/fonts/selawkl.ttf b/fonts/selawkl.ttf
similarity index 100%
rename from web/fonts/selawkl.ttf
rename to fonts/selawkl.ttf
diff --git a/web/fonts/selawksb.ttf b/fonts/selawksb.ttf
similarity index 100%
rename from web/fonts/selawksb.ttf
rename to fonts/selawksb.ttf
diff --git a/web/fonts/selawksl.ttf b/fonts/selawksl.ttf
similarity index 100%
rename from web/fonts/selawksl.ttf
rename to fonts/selawksl.ttf
diff --git a/web/public/icon-192.png b/icon-192.png
similarity index 100%
rename from web/public/icon-192.png
rename to icon-192.png
diff --git a/web/public/icon-512.png b/icon-512.png
similarity index 100%
rename from web/public/icon-512.png
rename to icon-512.png
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..1f455f7
--- /dev/null
+++ b/index.html
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+ Pandora
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/app.ts b/js/app.js
similarity index 63%
rename from web/src/app.ts
rename to js/app.js
index cdf0242..1611b35 100644
--- a/web/src/app.ts
+++ b/js/app.js
@@ -1,38 +1,28 @@
-import { config } from "./config";
-import { waha } from "./waha";
-import { ui, elements, views } from "./ui";
-import { websocket } from "./websocket";
-import { compensateMessageOrdering, debounce, formatTime, normalizeId } from "./utils";
-import { fetchChats, getAppUser, getChatMessages, getChatPicture, getChats, getUser, getUserAbout, markRead, sendStatus, updateOnlineStatus } from "./storage";
-import { deleteDatabase, upsertMessages } from "./db";
-import { showNotification } from "./notification";
-import type { Chat, Message, WebSocketEvent } from "./types";
-import { activeChatState, setActiveChatState } from "./states";
+import { config } from "./config.js";
+import { waha } from "./waha.js";
+import { ui, elements } from "./ui.js";
+import { websocket } from "./websocket.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;
const messageTone = new Audio("./message.ogg");
const longPressEvent = new CustomEvent("longpress");
export let isLoadingChat = false;
-export let notificationAuthorization: NotificationPermission = "default";
-const mainViewEl = document.getElementById("main-view");
-if (!mainViewEl) throw console.error();
-const mainView = views.get(mainViewEl);
document.addEventListener('DOMContentLoaded', async () => {
- updateSidebarPosition();
- askForNotificationPermission();
- if (elements.inputApiKey) elements.inputApiKey.value = config.apiKey;
- if (elements.inputWahaUrl) elements.inputWahaUrl.value = config.wahaUrl;
- if (elements.inputSession) elements.inputSession.value = config.session;
- if (elements.inputBackgroundImage) elements.inputBackgroundImage.value = config.bgImg;
- if (elements.inputBackgroundOpacity) elements.inputBackgroundOpacity.value = config.bgOpacity;
- if (elements.activeChatContainer) {
- elements.activeChatContainer.style.setProperty('--background-image', `URL("${config.bgImg}")`);
- elements.activeChatContainer.style.setProperty('--background-opacity', `${config.bgOpacity}`);
- }
+ 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 {
- setupElementsData();
+ setupElementsData();
loadChats();
checkWahaStatus();
initWebSocket();
@@ -41,46 +31,29 @@ document.addEventListener('DOMContentLoaded', async () => {
}
});
-async function askForNotificationPermission() {
- notificationAuthorization = await Notification.requestPermission();
-}
-
async function setupElementsData() {
- try {
- const usr = await getAppUser();
- const usrPic = (await getChatPicture(usr.id))?.url;
- const usrInfo = await getUser(usr.id);
- const usrAbout = (await getUserAbout(usr.id))?.about;
- elements.contentUserName.forEach(e => {
- e.innerHTML = usr.pushName || usr.name || '';
- })
- elements.contentUserNumber.forEach(async e => {
- if (usrInfo) e.innerHTML = usrInfo.number;
- })
- elements.resourceUserPic.forEach(async e => {
- if (usrPic) e.src = usrPic;
- })
- elements.valueUserStatus.forEach(async e => {
- if (usrAbout) e.value = usrAbout.trim();
- })
- } catch (error: any) {
- console.error(error.message);
- }
-}
-
-function purgeDatabase(ask: boolean = true) {
- if (ask) {
- if (!(confirm("Are you sure you want to delete all cached messages?") && confirm("This cannot be undone. Proceed?"))) return;
- }
- localStorage.clear();
- deleteDatabase();
- location.reload();
+ 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 {
- fetchChats().then(async () => {
+ fetchChats().then(async () => {
ui.renderChatList(getChats(), activeChatState, selectChat);
const hash = window.location.hash;
@@ -92,7 +65,7 @@ function loadChats() {
}
}
});
- } catch (error: any) {
+ } catch (error) {
console.error('Failed to load chats:', error);
elements.chatList.innerHTML = `
@@ -109,7 +82,7 @@ function loadChats() {
}
let isScrollingProgrammatically = false;
-let scrollTimeout: ReturnType | null = null;
+let scrollTimeout = null;
function scrollToChat(smooth = true) {
isScrollingProgrammatically = true;
@@ -130,8 +103,6 @@ function scrollToList(smooth = true) {
}
function setupEventListeners() {
- window.addEventListener('resize', updateSidebarPosition);
-
if (!window.location.hash) {
window.location.hash = '';
}
@@ -148,12 +119,12 @@ function setupEventListeners() {
closeActiveChat(true);
}
});
-
+
elements.appContainer.addEventListener('scroll', () => {
if (window.innerWidth > 768) return;
if (isScrollingProgrammatically) return;
- if (scrollTimeout) clearTimeout(scrollTimeout);
+ clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const scrollLeft = elements.appContainer.scrollLeft;
const width = elements.appContainer.clientWidth;
@@ -166,15 +137,7 @@ function setupEventListeners() {
}
}, 100);
});
-
- elements.appContainer.addEventListener('resize', () => {
- if (window.innerWidth > 768) {
- elements.desktopAside.after(elements.sidebar);
- } else {
- mainViewEl?.insertBefore(elements.sidebar, mainViewEl.firstChild);
- }
- })
-
+
document.addEventListener('keydown', (e) => {
if (e.code == "Escape") {
e.preventDefault();
@@ -182,9 +145,9 @@ function setupEventListeners() {
}
});
- elements.chatSearch.addEventListener('input', (e: Event) => {
- const query = (e.target as HTMLInputElement).value.toLowerCase();
- const filtered = getChats().filter(chat =>
+ elements.chatSearch.addEventListener('input', (e) => {
+ const query = e.target.value.toLowerCase();
+ const filtered = getChats().filter(chat =>
chat.name.toLowerCase().includes(query)
);
ui.renderChatList(filtered, activeChatState, selectChat);
@@ -207,89 +170,59 @@ function setupEventListeners() {
if (e.target == e.currentTarget) ui.toggleChatBottomBar();
});
- elements.markreadBtn.addEventListener('click', async () => {
- if (activeChatState) {
- const result = await markRead(activeChatState.id);
- if (result) ui.updateChatInChatList2(result);
- }
- })
elements.attachmentBtn.addEventListener('click', () => {
elements.attachmentInput.click();
})
- elements.attachmentInput.addEventListener('change', function (this: HTMLInputElement) {
- const firstFile = this.files?.[0];
- if (firstFile) sendFileMessage(firstFile);
+ elements.attachmentInput.addEventListener('change', function () {
+ const firstFile = this.files[0];
+ sendFileMessage(firstFile);
})
elements.backToSidebarBtn.addEventListener('click', () => {
closeActiveChat(false);
- });
-
+ });
+
elements.desktopSidebarButtons.forEach(sidebarBtn => {
sidebarBtn.addEventListener('click', () => {
- const page = sidebarBtn.dataset.page;
- if (!page) return;
- const pageEl = document.getElementById(page);
- if (!pageEl) return;
- if (page) mainView?.scrollTo(pageEl);
+ ui.showExtraPage(sidebarBtn.dataset.page);
})
})
-
+
elements.saveSettingsBtn.addEventListener('click', saveSettings);
-
- elements.purgeDatabaseButton.addEventListener('click', () => purgeDatabase(true));
-
+
elements.inputUserStatus.addEventListener('input', debounce(async function() {
const result = await sendStatus(elements.inputUserStatus.value);
- if (result?.success) {
- showNotification("Status updated successfully!", "", 2000);
- } else {
- showNotification("Failed to update status...", "", 2000);
- }
console.log(result);
}, 2000))
-
+
elements.selectable.forEach(e => {
- let timerId: ReturnType, longPressed: boolean;
-
+ let timerId, longPressed;
+
e.addEventListener('mousedown', () => {
longPressed = false;
-
+
timerId = setTimeout(() => {
longPressed = true;
e.dispatchEvent(longPressEvent);
- }, 500); // 500ms for long press
+ })
})
-
- e.addEventListener('click', (event) => {
+
+ e.addEventListener('click', () => {
if (longPressed) {
- event.preventDefault();
+ e.preventDefault();
clearTimeout(timerId);
}
})
-
+
e.addEventListener('mouseleave', () => {
clearTimeout(timerId);
})
})
}
-function updateSidebarPosition() {
- if (!mainViewEl || !elements.sidebar || !elements.desktopAside) return;
-
- if (window.innerWidth > 768) {
- if (elements.sidebar.parentElement !== elements.appContainer) {
- elements.desktopAside.after(elements.sidebar);
- }
- } else {
- if (elements.sidebar.parentElement !== mainViewEl) {
- mainViewEl.insertBefore(elements.sidebar, mainViewEl.firstChild);
- }
- }
-}
-
function initWebSocket() {
- websocket.connect((data: WebSocketEvent) => {
+ websocket.connect((data) => {
+ // console.log('[WS] Received event:', data.event, data);
const ev = data.event;
if (ev === 'message' || ev === 'message.any' || ev === 'message.ack') {
handleIncomingMessage(data.payload);
@@ -299,47 +232,46 @@ function initWebSocket() {
}
-async function handleIncomingMessage(msg: Message) {
+async function handleIncomingMessage(msg) {
if (!msg) return;
ui.updateChatInChatList(msg);
- const rawChatId = msg.chatId || (typeof msg.from === 'string' ? msg.from : (msg.from as any)?._serialized) || (msg.chat && msg.chat.id);
+ const rawChatId = msg.chatId || msg.from || (msg.chat && msg.chat.id);
const msgChatId = normalizeId(rawChatId);
if (!msgChatId) {
console.warn('[WS] Could not resolve chatId from payload:', msg);
return;
}
+ // console.log('[WS] Resolved msgChatId:', msgChatId, '| activeChatState:', activeChatState?.id);
if (!msg.fromMe) {
messageTone.play();
}
- if (notificationAuthorization === "granted") {
- new Notification("New message", { body: msg.body || msg.text });
- }
if (activeChatState && activeChatState.id === msgChatId) {
- const msgId = normalizeId(msg.id as any) || (msg.id as string);
+ const msgId = normalizeId(msg.id) || msg.id;
const exists = document.getElementById(msgId);
if (!exists) {
- const container = elements.messagesContainer;
- const scrolled = container.scrollTop === (container.scrollHeight - container.clientHeight);
+ const scrolled = elements.messagesContainer.scrollTop == elements.messagesContainer.scrollTopMax;
ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, (await getAppUser()).id);
if (scrolled) {
ui.scrollToBottom();
- }
+ }
}
}
}
-async function selectChat(chat: Chat, isPopState = false, smoothScroll = true) {
+async function selectChat(chat, isPopState = false, smoothScroll = true) {
if (isLoadingChat) return;
isLoadingChat = true;
- setActiveChatState(chat);
+ activeChatState = chat;
chat.unreadCount = 0;
+ // ui.renderChatList(chatsState, activeChatState, selectChat);
+
ui.toggleChatState(true);
elements.activeChatName.textContent = chat.name.toUpperCase();
elements.activeChatAvatar.textContent = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?';
@@ -375,19 +307,19 @@ async function selectChat(chat: 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) {
- setActiveChatState(null);
+ activeChatState = null;
if (window.innerWidth <= 768) {
scrollToList();
} else {
ui.toggleChatState(false);
}
-
+
if (!isPopState) {
if (window.location.hash.startsWith('#chat-')) {
@@ -409,7 +341,7 @@ async function sendMessage() {
sender: 'me',
timestamp: new Date().toISOString(),
status: 'sending'
- } as any;
+ };
ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id);
ui.scrollToBottom();
@@ -433,7 +365,7 @@ async function sendMessage() {
if (!activeChatState.id.endsWith('@lid')) {
await waha.readChat(activeChatState.id);
}
- } catch (e: any) {
+ } catch (e) {
console.warn('readChat failed (non-fatal):', e.message);
}
@@ -442,26 +374,25 @@ async function sendMessage() {
const tempBubble = document.getElementById(tempMsg.id);
if (tempBubble) {
if (responseData && responseData.id) {
- tempBubble.id = normalizeId(responseData.id as any) || tempBubble.id;
+ tempBubble.id = normalizeId(responseData.id);
}
const meta = tempBubble.querySelector('.message-meta');
- if (meta) meta.innerHTML = `${formatTime(new Date())} `;
+ meta.innerHTML = `${formatTime(new Date())} `;
}
activeChatState.lastMessage = text;
- activeChatState.timestamp = new Date().toISOString();
+ activeChatState.timestamp = new Date();
} catch (error) {
console.error('Failed to send message:', error);
const tempBubble = document.getElementById(tempMsg.id);
if (tempBubble) {
const meta = tempBubble.querySelector('.message-meta');
- if (meta) meta.innerHTML = `Failed to send `;
+ meta.innerHTML = `Failed to send `;
}
}
}
-async function sendFileMessage(file: File) {
- if (!activeChatState) return;
+async function sendFileMessage(file) {
try {
const tempId = 'temp-' + Date.now();
const tempMsg = {
@@ -479,15 +410,15 @@ async function sendFileMessage(file: File) {
url: URL.createObjectURL(file),
filename: file.name
}
- } as any;
+ };
- ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id, true);
+ ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id, activeChatState.id, true);
ui.scrollToBottom();
const result = await waha.sendFileMessage(activeChatState.id, file);
ui.removeChatMessage(tempId);
- ui.appendSingleMessage(result, activeChatState.name, (await getAppUser()).id);
- } catch (error: any) {
+ ui.appendSingleMessage(result.id, activeChatState.name, (await getAppUser()).id, activeChatState.id);
+ } catch (error) {
console.error(error.message);
}
}
@@ -517,4 +448,4 @@ async function checkWahaStatus() {
export function getCurrentChat() {
return activeChatState;
-}
+}
\ No newline at end of file
diff --git a/web/src/config.ts b/js/config.js
similarity index 60%
rename from web/src/config.ts
rename to js/config.js
index cb53254..edb15f6 100644
--- a/web/src/config.ts
+++ b/js/config.js
@@ -1,20 +1,13 @@
-export interface Config {
- wahaUrl: string;
- session: string;
- apiKey: string;
- bgImg: string;
- bgOpacity: string;
- save(url: string, session: string, apiKey: string, bgImg: string, bgOpacity: string): void;
-}
+// Client Configuration State & Storage Manager
-export const config: Config = {
- wahaUrl: localStorage.getItem('waha_url') || '',
- session: localStorage.getItem('waha_session') || '',
+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: string, session: string, apiKey: string, bgImg: string, bgOpacity: string): void {
+ save(url, session, apiKey, bgImg, bgOpacity) {
this.wahaUrl = url.trim().replace(/\/$/, "");
this.session = session.trim();
this.apiKey = apiKey.trim();
diff --git a/web/src/db.ts b/js/db.js
similarity index 68%
rename from web/src/db.ts
rename to js/db.js
index 1c1f9fb..f1eb2a2 100644
--- a/web/src/db.ts
+++ b/js/db.js
@@ -1,26 +1,25 @@
-import { normalizeId } from "./utils";
-import type { Chat, Message, StoredMedia } from "./types";
+import { normalizeId } from "./utils.js";
const DB_NAME = "pandora";
const DB_VERSION = 6;
-let dbPromise: Promise | null = null;
+let dbPromise = null;
-function openDb(): Promise {
+function openDb() {
if (dbPromise) return dbPromise;
dbPromise = new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
- req.onupgradeneeded = () => {
+ req.onupgradeneeded = (e) => {
const db = req.result;
- const tx = req.transaction!;
+ const tx = req.transaction;
if (!db.objectStoreNames.contains("chats")) {
const store = db.createObjectStore("chats", { keyPath: "id" });
store.createIndex("timestamp", "timestamp", { unique: false });
}
- let msgStore: IDBObjectStore;
+ let msgStore;
if (!db.objectStoreNames.contains("messages")) {
msgStore = db.createObjectStore("messages", { keyPath: "id" });
msgStore.createIndex("from", "from", { unique: false });
@@ -34,21 +33,22 @@ function openDb(): Promise {
msgStore.createIndex("chatId_timestamp", ["chatId", "timestamp"], { unique: false });
}
- // Migration logic
- msgStore.openCursor().onsuccess = (event) => {
- const cursor = (event.target as IDBRequest).result;
- if (cursor) {
- const m = cursor.value as Message;
- const from = normalizeId(m.from);
- const to = normalizeId(m.to);
- const chatId = normalizeId(m.chatId) || (m.fromMe ? to : from);
- if (!m.chatId && chatId) {
- m.chatId = chatId;
- cursor.update(m);
+ if (db.objectStoreNames.contains("messages")) {
+ msgStore.openCursor().onsuccess = (event) => {
+ const cursor = event.target.result;
+ if (cursor) {
+ const m = cursor.value;
+ const from = normalizeId(m.from);
+ const to = normalizeId(m.to);
+ const chatId = normalizeId(m.chatId) || (m.fromMe ? to : from);
+ if (!m.chatId && chatId) {
+ m.chatId = chatId;
+ cursor.update(m);
+ }
+ cursor.continue();
}
- cursor.continue();
- }
- };
+ };
+ }
if (!db.objectStoreNames.contains("media")) {
db.createObjectStore("media", { keyPath: "reqId" });
@@ -62,7 +62,7 @@ function openDb(): Promise {
return dbPromise;
}
-export async function upsertChats(chats: Chat[]): Promise {
+export async function upsertChats(chats) {
const db = await openDb();
return new Promise((resolve, reject) => {
@@ -84,7 +84,7 @@ export async function upsertChats(chats: Chat[]): Promise {
});
}
-export async function loadChatsSorted(): Promise {
+export async function loadChatsSorted() {
const db = await openDb();
return new Promise((resolve, reject) => {
@@ -92,9 +92,9 @@ export async function loadChatsSorted(): Promise {
const store = tx.objectStore("chats");
const idx = store.index("timestamp");
- const result: Chat[] = [];
+ const result = [];
idx.openCursor(null, "prev").onsuccess = (e) => {
- const cursor = (e.target as IDBRequest).result;
+ const cursor = e.target.result;
if (cursor) {
result.push(cursor.value);
cursor.continue();
@@ -107,20 +107,7 @@ export async function loadChatsSorted(): Promise {
});
}
-export async function loadChat(chatId: string): Promise {
- const db = await openDb();
-
- return new Promise((resolve, reject) => {
- const tx = db.transaction("chats", "readonly");
- const store = tx.objectStore("chats");
- const req = store.get(chatId);
-
- req.onsuccess = () => resolve(req.result);
- req.onerror = () => reject(req.error);
- });
-}
-
-function mapMessage(m: Message): any {
+function mapMessage(m) {
const from = normalizeId(m.from);
const to = normalizeId(m.to);
const chatId = normalizeId(m.chatId) || (m.fromMe ? to : from);
@@ -139,7 +126,7 @@ function mapMessage(m: Message): any {
}
}
-export async function upsertMessages(messages: Message[]): Promise {
+export async function upsertMessages(messages) {
const db = await openDb();
return new Promise((resolve, reject) => {
@@ -155,7 +142,7 @@ export async function upsertMessages(messages: Message[]): Promise {
});
}
-export async function loadLatestMessages(chatId: string, limit: number = 50): Promise {
+export async function loadLatestMessages(chatId, limit = 50) {
const db = await openDb();
return new Promise((resolve, reject) => {
@@ -163,12 +150,12 @@ export async function loadLatestMessages(chatId: string, limit: number = 50): Pr
const store = tx.objectStore("messages");
const idx = store.index("chatId_timestamp");
- const out: Message[] = [];
+ const out = [];
const range = IDBKeyRange.bound([chatId, -Infinity], [chatId, Infinity]);
idx.openCursor(range, "prev").onsuccess = (e) => {
- const cursor = (e.target as IDBRequest).result;
+ const cursor = e.target.result;
if (!cursor) return resolve(out);
out.push(cursor.value);
@@ -180,7 +167,7 @@ export async function loadLatestMessages(chatId: string, limit: number = 50): Pr
});
}
-export async function loadOlderMessages(chatId: string, oldestTimestamp: any, oldestId: string, limit: number = 50): Promise {
+export async function loadOlderMessages(chatId, oldestTimestamp, oldestId, limit = 50) {
const db = await openDb();
return new Promise((resolve, reject) => {
@@ -188,7 +175,7 @@ export async function loadOlderMessages(chatId: string, oldestTimestamp: any, ol
const store = tx.objectStore("messages");
const idx = store.index("cidTimestampId");
- const out: Message[] = [];
+ const out = [];
const parsedTimestamp = isNaN(oldestTimestamp) ? oldestTimestamp : Number(oldestTimestamp);
@@ -200,7 +187,7 @@ export async function loadOlderMessages(chatId: string, oldestTimestamp: any, ol
);
idx.openCursor(range, "prev").onsuccess = (e) => {
- const cursor = (e.target as IDBRequest).result;
+ const cursor = e.target.result;
if (!cursor) return resolve(out);
out.push(cursor.value);
@@ -213,7 +200,7 @@ export async function loadOlderMessages(chatId: string, oldestTimestamp: any, ol
});
}
-export async function upsertMedia(reqId: string, blob: Blob, filename: string): Promise {
+export async function upsertMedia(reqId, blob, filename) {
const db = await openDb();
return new Promise((resolve, reject) => {
@@ -231,7 +218,7 @@ export async function upsertMedia(reqId: string, blob: Blob, filename: string):
});
}
-export async function loadMedia(reqId: string): Promise {
+export async function loadMedia(reqId) {
const db = await openDb();
return new Promise((resolve, reject) => {
@@ -242,8 +229,4 @@ export async function loadMedia(reqId: string): Promise
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
-}
-
-export function deleteDatabase() {
- indexedDB.deleteDatabase(DB_NAME);
}
\ No newline at end of file
diff --git a/web/src/storage.ts b/js/storage.js
similarity index 51%
rename from web/src/storage.ts
rename to js/storage.js
index 19977b1..238b4b9 100644
--- a/web/src/storage.ts
+++ b/js/storage.js
@@ -1,11 +1,10 @@
-import { loadChat, loadChatsSorted, loadLatestMessages, loadMedia, loadOlderMessages, upsertChats, upsertMedia, upsertMessages } from "./db";
-import { waha } from "./waha";
-import type { Chat, Message, AppUser, ContactInfo, UserAboutResponse, ChatPictureResponse, StatusResponse, DownloadedMedia } from "./types";
+import { loadChatsSorted, loadLatestMessages, loadMedia, loadOlderMessages, upsertChats, upsertMedia, upsertMessages } from "./db.js";
+import { waha } from "./waha.js";
let online = false;
-let chats: Chat[] = [];
+let chats = [];
-export async function updateOnlineStatus(): Promise {
+export async function updateOnlineStatus() {
try {
await waha.getVersion();
online = true;
@@ -15,17 +14,17 @@ export async function updateOnlineStatus(): Promise {
}
}
-export async function fetchChats(): Promise {
+export async function fetchChats() {
if (online) {
await getRemoteChats()
}
chats = await loadChatsSorted();
}
-export async function getRemoteChats(): Promise {
+export async function getRemoteChats() {
const u = await waha.getChats();
- const mapped: Chat[] = u.map(chat => ({
+ const mapped = u.map(chat => ({
id: chat.id,
name: chat.name,
lastMessage: chat.lastMessage,
@@ -36,15 +35,15 @@ export async function getRemoteChats(): Promise {
await upsertChats(mapped);
}
-export function getUsers(): Chat[] {
+export function getUsers() {
return chats.filter(c => c.id.endsWith("@c.us"));
}
-export function getGroups(): Chat[] {
+export function getGroups() {
return chats.filter(c => c.id.endsWith("@g.us"));
}
-export async function getUser(number: string): Promise {
+export async function getUser(number) {
if (online) {
return await waha.getUser(number);
} else {
@@ -52,7 +51,7 @@ export async function getUser(number: string): Promise
}
}
-export async function getUserAbout(userId: string): Promise {
+export async function getUserAbout(userId) {
if (online) {
return await waha.getUserAbout(userId);
} else {
@@ -60,26 +59,25 @@ export async function getUserAbout(userId: string): Promise {
+export async function getAppUser() {
if (online) {
const info = await waha.getMyInfo();
- localStorage.setItem('pandora-last-username', info.pushName || info.name || '');
+ localStorage.setItem('pandora-last-username', info.name);
localStorage.setItem('pandora-last-userid', info.id);
return info;
} else {
return {
pushName: localStorage.getItem('pandora-last-username') || 'Unknown',
- name: localStorage.getItem('pandora-last-username') || 'Unknown',
id: localStorage.getItem('pandora-last-userid') || 'Unknown'
- } as AppUser;
+ }
}
}
-export async function getMessage(chatId: string, msgId: string, downloadMedia: boolean): Promise {
+export async function getMessage(chatId, msgId, downloadMedia) {
if (online) {
const newMessage = await waha.getSingleChatMessage(chatId, msgId, downloadMedia);
upsertMessages([newMessage]);
@@ -90,14 +88,14 @@ export async function getMessage(chatId: string, msgId: string, downloadMedia: b
body: "You're offline",
from: "system",
timestamp: new Date().toISOString()
- } as Message;
+ };
}
}
-export async function getMedia(reqId: string): Promise {
+export async function getMedia(reqId) {
const cached = await loadMedia(reqId);
if (cached) {
- return { blob: cached.blob, filename: cached.filename };
+ return cached;
}
try {
@@ -111,7 +109,7 @@ export async function getMedia(reqId: string): Promise {
+export async function getChatMessages(chatId) {
if (online) {
const newMessages = await waha.getChatMessages(chatId);
upsertMessages(newMessages);
@@ -121,7 +119,7 @@ export async function getChatMessages(chatId: string): Promise {
}
}
-export async function getMoreChatMessages(chatId: string, oldestTimestamp: any, oldestId: string): Promise {
+export async function getMoreChatMessages(chatId, oldestTimestamp, oldestId, limit = 50) {
if (online) {
return waha.getChatMessages(chatId, oldestTimestamp);
} else {
@@ -129,7 +127,7 @@ export async function getMoreChatMessages(chatId: string, oldestTimestamp: any,
}
}
-export async function getChatPicture(chatId: string): Promise {
+export async function getChatPicture(chatId) {
if (online) {
return await waha.getChatPicture(chatId);
} else {
@@ -137,11 +135,11 @@ export async function getChatPicture(chatId: string): Promise {
+export async function sendStatus(text) {
if (online) {
return await waha.setStatus(text);
} else {
@@ -149,19 +147,4 @@ export async function sendStatus(text: string): Promise {
success: false
}
}
-}
-
-export async function markRead(chatId: string): Promise {
- if (online) {
- await waha.readChat(chatId);
- }
-
- const chat = await loadChat(chatId);
- if (chat) {
- chat.unreadCount = 0;
- // Note: upsertMessages was called with [chat] in JS, but chat is a Chat object, not Message.
- // Keeping JS behavior but chat is Chat type here.
- await upsertChats([chat]);
- }
- return chat;
}
\ No newline at end of file
diff --git a/js/ui.js b/js/ui.js
new file mode 100644
index 0000000..d2e1261
--- /dev/null
+++ b/js/ui.js
@@ -0,0 +1,396 @@
+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'),
+ chatsLoader: document.getElementById('chats-loader'),
+ chatSearch: document.getElementById('chat-search'),
+ backendStatusText: document.getElementById('backend-status-text'),
+ apiStatusIndicator: document.querySelector('.pulse-dot'),
+ noChatState: document.getElementById('no-chat-state'),
+ activeChatContainer: document.getElementById('active-chat-container'),
+ activeChatName: document.getElementById('active-chat-name'),
+ activeChatAvatar: document.getElementById('active-chat-avatar'),
+ messagesContainer: document.getElementById('messages-container'),
+ messageForm: document.getElementById('message-form'),
+ messageInput: document.getElementById('message-input'),
+ backToSidebarBtn: document.querySelector('.chat-header'),
+ sidebar: document.querySelector('.sidebar'),
+ appContainer: document.querySelector('.app-container'),
+ settingsModal: document.getElementById('settings-page'),
+ settingsIconBtn: document.getElementById('settings-sidebar-btn'),
+ saveSettingsBtn: document.getElementById('save-settings'),
+ 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'),
+ 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"),
+ 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 = {
+ 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
+ */
+ toggleChatState(hasActive) {
+ if (hasActive) {
+ elements.noChatState.classList.add('hidden');
+ elements.activeChatContainer.classList.remove('hidden');
+ } else {
+ elements.noChatState.classList.remove('hidden');
+ elements.activeChatContainer.classList.add('hidden');
+ }
+ },
+
+ /**
+ * Scroll message list automatically to bottom
+ */
+ scrollToBottom() {
+ elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight;
+ },
+
+ /**
+ * Update connection status badge in sidebar footer
+ */
+ updateConnectionStatus(isConnected, text) {
+ elements.backendStatusText.textContent = text;
+ if (isConnected) {
+ elements.apiStatusIndicator.style.backgroundColor = 'var(--online-color)';
+ elements.apiStatusIndicator.style.animation = 'pulse 1.8s infinite';
+ } else {
+ elements.apiStatusIndicator.style.backgroundColor = '#ef4444';
+ elements.apiStatusIndicator.style.animation = 'none';
+ }
+ },
+
+ async renderChatList(chats, activeChat, onChatSelect) {
+ elements.chatList.innerHTML = '';
+ chats.sort((a, b) => b.timestamp - a.timestamp);
+
+ if (chats.length === 0) {
+ elements.chatList.innerHTML = `No chats found `;
+ return;
+ }
+
+ for (const chat of chats) {
+ const li = document.createElement('li');
+ 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() : '?';
+ const hasUnread = chat.unreadCount && chat.unreadCount > 0;
+ const timeStr = formatTime(chat.timestamp || new Date());
+
+ li.innerHTML = `
+
+
+
+
+
+ ${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 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;
+ }
+ }
+ },
+
+ /**
+ * Render chat message log inside chat view container
+ */
+ async renderMessages(messages, activeChatName, userID, chatId) {
+ elements.messagesContainer.innerHTML = '';
+
+ if (messages.length === 0) {
+ elements.messagesContainer.innerHTML = 'No messages. Say hello!
';
+ return;
+ }
+
+ const loadMore = document.createElement("button");
+ loadMore.classList.add("load-more-btn");
+ loadMore.innerText = "Load more";
+ loadMore.addEventListener('click', () => {
+ this.loadMoreMessages(chatId, userID);
+ });
+ elements.messagesContainer.appendChild(loadMore);
+
+ for (const msg of messages) {
+ this.appendSingleMessage(msg, userID, chatId);
+ }
+
+ this.scrollToBottom();
+ },
+
+ async loadMoreMessages(chatId, userId) {
+ const oldest = document.querySelector('.message-group:first-of-type');
+ const oldestTimestamp = oldest.dataset.timestamp;
+ const oldestId = oldest.id;
+ 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);
+ },
+
+ /**
+ * Append a single message (used for optimistic updates immediately upon sending)
+ */
+ appendSingleMessage(msg, userID, chatId, isLocal = false) {
+ elements.messagesContainer.appendChild(this.generateMessage(msg, userID, chatId, isLocal))
+ },
+
+ generateTempMessageLink(msg) {
+ const a = document.createElement('a');
+ a.target = "_blank";
+ a.href = msg.media.url;
+
+ if (msg._data?.mimetype?.startsWith('image/')) {
+ const img = document.createElement('img');
+ img.classList.add('message-image-attachement');
+ img.src = msg.media.url;
+ a.appendChild(img);
+ } else {
+ a.textContent = msg.media.filename || "Download file";
+ a.download = msg.media.filename;
+ }
+
+ return a;
+ },
+
+ generateMessage(msg, userID, chatId, isLocal = false) {
+ const isOutgoing = msg.fromMe || msg.sender === 'me';
+
+ function getPrevMessageElem() {
+ return elements.messagesContainer.lastElementChild;
+ }
+
+ const prevMsgEl = getPrevMessageElem();
+
+ const groupDiv = document.createElement('div');
+ 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;
+
+ const senderName = isOutgoing ? userID : (msg._data.notifyName || msg.from);
+ const timeStr = formatTime(msg.timestamp || new Date());
+
+ let statusCheck = '';
+ if (isOutgoing) {
+ if (msg.status === 'read') {
+ statusCheck = ' ';
+ } else if (msg.status === 'delivered') {
+ statusCheck = ' ';
+ } else if (msg.status === 'sending') {
+ statusCheck = ' ';
+ } else {
+ statusCheck = ' ';
+ }
+ }
+
+ const bubble = document.createElement('div');
+ bubble.className = 'message-bubble';
+
+ let prevUid;
+
+ if (msg.participant) {
+ prevUid = msg.participant;
+ } else {
+ prevUid = msg.from;
+ }
+
+ if (!isOutgoing && (!prevMsgEl || prevUid !== prevMsgEl.dataset.from)) {
+ 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.hasMedia) {
+ let a;
+
+ if (isLocal) {
+ a = this.generateTempMessageLink(msg);
+ } else {
+ a = document.createElement('a');
+ a.innerText = `[Request media]`;
+ a.target = "_blank";
+
+ const clickListener = async (e) => {
+ a.removeEventListener('click', clickListener);
+ a.innerText = `[Downloading]`;
+ const mediaMsg = msg.media ? msg : await getMessage(chatId, normalizeId(msg._serialized ? msg : msg.id), true);
+ if (!mediaMsg || !mediaMsg?.media.url) {
+ a.addEventListener('click', clickListener);
+ a.innerText = `[Error, click to try again]`
+ return;
+ }
+ const url = new URL(mediaMsg.media.url);
+ const reqID = url.pathname.split('/').filter(Boolean).pop();
+
+ const { blob, filename } = await getMedia(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;
+ bubble.after(img);
+ bubble.querySelector('.message-content').remove();
+ } else {
+ e.target.textContent = filename || `Download ${mediaMsg.media.filename}`;
+ }
+ }
+
+ a.addEventListener('click', clickListener);
+ }
+
+ contentEl.appendChild(a);
+
+ if (!isLocal && msg._data?.mimetype?.startsWith('image/')) {
+ a.click();
+ }
+ }
+
+
+ const meta = document.createElement('div');
+ meta.className = 'message-meta';
+ meta.innerHTML = `${timeStr} ${statusCheck}`;
+
+ bubble.appendChild(meta);
+
+ 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);
+ return groupDiv;
+ },
+
+ updateMessage(originalMsgId, generatedMsg) {
+ const originalMsg = document.querySelector(`#${originalMsgId}`);
+ if (originalMsg) {
+ originalMsg.replaceWith(generatedMsg)
+ }
+ },
+
+ updateMessageTick(id, status) {
+ let statusCheck;
+ if (status === 'read') {
+ statusCheck = ' ';
+ } else if (status === 'delivered') {
+ statusCheck = ' ';
+ } else if (status === 'sending') {
+ statusCheck = ' ';
+ } else {
+ statusCheck = ' ';
+ }
+
+ document.getElementById(id).querySelector('.message-meta').outerHTML = statusCheck;
+ },
+
+ toggleChatBottomBar() {
+ elements.chatBottomBar.classList.toggle("collapsed");
+ },
+
+ removeChatMessage(msgId) {
+ const message = document.getElementById(msgId);
+ if (message) message.remove();
+ }
+};
diff --git a/web/src/utils.ts b/js/utils.js
similarity index 69%
rename from web/src/utils.ts
rename to js/utils.js
index 261f1bf..81f05f0 100644
--- a/web/src/utils.ts
+++ b/js/utils.js
@@ -1,13 +1,13 @@
-import type { Message, MessageWithTime } from "./types";
+// Helper utilities and algorithms
/**
* Format timestamps (supports Unix epoch seconds/ms, strings and ISO dates)
* @param {string|number|Date} dateVal
* @returns {string} Formatted HH:MM AM/PM string
*/
-export function formatTime(dateVal: string | number | Date): string {
+export function formatTime(dateVal) {
if (!dateVal) return '';
- let date: Date;
+ let date;
if (typeof dateVal === 'number') {
date = new Date(dateVal < 10000000000 ? dateVal * 1000 : dateVal);
} else if (typeof dateVal === 'string' && /^\d+$/.test(dateVal)) {
@@ -18,12 +18,12 @@ export function formatTime(dateVal: string | number | Date): string {
}
let hours = date.getHours();
- const minutes = date.getMinutes();
+ let minutes = date.getMinutes();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12;
- const minutesStr = minutes < 10 ? '0' + minutes : String(minutes);
- return `${hours}:${minutesStr} ${ampm}`;
+ minutes = minutes < 10 ? '0' + minutes : minutes;
+ return `${hours}:${minutes} ${ampm}`;
}
/**
@@ -32,22 +32,24 @@ export function formatTime(dateVal: string | number | Date): string {
* @param {Array} messages List of raw messages from WAHA
* @returns {Array} Compensated chronological message array
*/
-export function compensateMessageOrdering(messages: Message[]): Message[] {
+export function compensateMessageOrdering(messages) {
if (!Array.isArray(messages)) return [];
- // convert to ms
- const msgs: MessageWithTime[] = messages.map(m => {
- let t: number;
- if (typeof m.timestamp === 'number') {
- t = m.timestamp < 10000000000 ? m.timestamp * 1000 : m.timestamp;
+ // Convert timestamps to numeric milliseconds for stable comparison
+ const msgs = messages.map(m => {
+ let t = m.timestamp;
+ if (typeof t === 'number') {
+ if (t < 10000000000) t = t * 1000;
} else {
- t = new Date(m.timestamp).getTime();
+ t = new Date(t).getTime();
}
return { ...m, _time: t };
});
+ // Initial chronological sort
msgs.sort((a, b) => a._time - b._time);
+ // Apply drift bubble adjustments
let changed = true;
while (changed) {
changed = false;
@@ -56,12 +58,12 @@ export function compensateMessageOrdering(messages: Message[]): Message[] {
const next = msgs[i + 1];
const timeDiff = next._time - current._time;
- // swap if outgoing message is sorted before incoming message within 30-sec window
+ // Swap if an outgoing message is sorted before an incoming message within a 30-sec window
if (current.fromMe && !next.fromMe && timeDiff >= 0 && timeDiff <= 30000) {
msgs[i] = next;
msgs[i + 1] = current;
- // advance the outgoing message timestamp to exactly 1 sec after the incoming
+ // Advance the outgoing message's timestamp to be exactly 1 second after the incoming one
current._time = next._time + 1000;
if (typeof current.timestamp === 'number') {
current.timestamp = Math.floor(current._time / 1000);
@@ -74,16 +76,16 @@ export function compensateMessageOrdering(messages: Message[]): Message[] {
}
}
- // clean temp property
+ // Clean up temporary property
return msgs.map(({ _time, ...m }) => m);
}
-export function getBase64(file: File): Promise {
+export function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
- resolve((reader.result as string).split(",")[1]);
+ resolve(reader.result.split(",")[1]);
};
reader.onerror = (e) => {
@@ -104,7 +106,7 @@ export function getBase64(file: File): Promise {
* @param {string|object} raw
* @returns {string|null}
*/
-export function normalizeId(raw: string | { _serialized?: string; user?: string } | null | undefined): string | null {
+export function normalizeId(raw) {
if (!raw) return null;
if (typeof raw === 'object') {
return raw._serialized || raw.user || JSON.stringify(raw);
@@ -112,10 +114,10 @@ export function normalizeId(raw: string | { _serialized?: string; user?: string
return raw;
}
-export function debounce(func: () => void, delay: number): () => void {
- let timeoutId: ReturnType;
+export function debounce(func, delay) {
+ let timeoutId;
return function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(func, delay);
};
-}
+}
\ No newline at end of file
diff --git a/web/src/waha.ts b/js/waha.js
similarity index 53%
rename from web/src/waha.ts
rename to js/waha.js
index 353c973..2c5c077 100644
--- a/web/src/waha.ts
+++ b/js/waha.js
@@ -1,19 +1,19 @@
-import { config } from "./config";
-import { showNotification } from "./notification";
-import { getBase64 } from "./utils";
-import type { Message, VersionResponse, AppUser, ContactInfo, UserAboutResponse, ChatPictureResponse, StatusResponse } from "./types";
+import { config } from "./config.js";
+import { getBase64 } from "./utils.js";
-async function request(path: string, options: RequestInit = {}): Promise {
+async function request(path, options = {}) {
const url = `${config.wahaUrl}${path}`;
- const headers: Record = {
+ const headers = {
'Content-Type': 'application/json',
'accept': '*/*',
- ...(options.headers as Record)
+ ...options.headers
};
if (config.apiKey) {
headers['X-Api-Key'] = config.apiKey;
}
+ // console.log(`[WAHA] ${options.method || 'GET'} ${url}`, options.body ? JSON.parse(options.body) : '');
+
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
let errorDetail = '';
@@ -24,24 +24,24 @@ async function request(path: string, options: RequestInit = {}): Promise {
} catch (_) {
errorDetail = await response.text().catch(() => '');
}
-
- showNotification("API Error", `WAHA API returned ${response.status}: ${response.statusText} — ${errorDetail}`, 4000);
throw new Error(`WAHA API returned ${response.status}: ${response.statusText} — ${errorDetail}`);
}
return response.json();
}
-async function downloadFile(path: string, options: RequestInit = {}): Promise<{ blob: Blob, filename: string }> {
+async function downloadFile(path, options = {}) {
const url = `${config.wahaUrl}${path}`;
- const headers: Record = {
- 'Content-Type': (options.headers as Record | undefined)?.['Content-Type'] ?? 'application/json',
+ const headers = {
+ 'Content-Type': options.headers?.['Content-Type'] ?? 'application/json',
'accept': '*/*',
- ...(options.headers as Record)
+ ...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) {
@@ -66,80 +66,82 @@ async function downloadFile(path: string, options: RequestInit = {}): Promise<{
return { blob, filename };
}
+
export const waha = {
- async getVersion(): Promise {
- return await request('/api/version');
+ async getVersion() {
+ return await request('/api/version');
},
- async getChats(): Promise {
- const data = await request(`/api/${config.session}/chats`);
+ async getChats() {
+ const data = await request(`/api/${config.session}/chats`);
return 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 || null,
- name: chat.name || null,
+ id: chatId || chat.chatId || chat.name,
+ name: chat.name || "Unknown Contact",
unreadCount: chat.unreadCount || 0,
- lastMessage: chat.lastMessage?.body || chat.lastMessageText || null,
- timestamp: chat.lastMessage?.timestamp || null
+ lastMessage: chat.lastMessage?.body || chat.lastMessageText || "Click to open chat",
+ timestamp: chat.lastMessage?.timestamp || new Date()
};
});
},
- async getChatMessages(chatId: string, beforeTimestamp?: any): Promise {
- return request(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40${beforeTimestamp ? `&filter.timestamp.lte=${beforeTimestamp}` : "" }`);
+ async getChatMessages(chatId, beforeTimestamp) {
+ console.log(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40&sortBy=timestamp${beforeTimestamp ? `&filter.timestamp.gte=${beforeTimestamp}` : "" }`);
+ return request(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40${beforeTimestamp ? `&filter.timestamp.lte=${beforeTimestamp}` : "" }`);
},
- async getSingleChatMessage(chatId: string, messageId: string, downloadMedia: boolean): Promise {
- return request(`/api/${config.session}/chats/${chatId}/messages/${messageId}?downloadMedia=${downloadMedia}`);
+ async getSingleChatMessage(chatId, messageId, downladMedia) {
+ return request(`/api/${config.session}/chats/${chatId}/messages/${messageId}?downloadMedia=${downladMedia}`);
},
- async getChatPicture(chatId: string): Promise {
- return request(`/api/${config.session}/chats/${chatId}/picture`);
+ async getChatPicture(chatId) {
+ return request(`/api/${config.session}/chats/${chatId}/picture`);
},
- async getUser(chatId: string): Promise {
- return request(`/api/${config.session}/contacts/${chatId}`);
+ async getUser(chatId) {
+ return request(`/api/${config.session}/contacts/${chatId}`);
},
- async getUserAbout(chatId: string): Promise {
- return request(`/api/contacts/about?contactId=${chatId}&session=${config.session}`);
+ async getUserAbout(chatId) {
+ return request(`/api/contacts/about?contactId=${chatId}&session=${config.session}`);
},
- async readChat(chatId: string): Promise {
+ async readChat(chatId) {
return request('/api/sendSeen', {
method: 'POST',
body: JSON.stringify({ chatId, session: config.session })
});
},
- async downloadMedia(file: string): Promise<{ blob: Blob, filename: string }> {
+ async downloadMedia(file) {
const { blob, filename } = await downloadFile(`/api/files/${config.session}/${file}`);
return { blob, filename };
},
- async getMyInfo(): Promise {
- return request(`/api/sessions/${config.session}/me`);
+ async getMyInfo() {
+ return request(`/api/sessions/${config.session}/me`);
},
- async startTyping(chatId: string): Promise {
+ async startTyping(chatId) {
return request('/api/startTyping', {
method: 'POST',
body: JSON.stringify({ chatId, session: config.session })
});
},
- async stopTyping(chatId: string): Promise {
+ async stopTyping(chatId) {
return request('/api/stopTyping', {
method: 'POST',
body: JSON.stringify({ chatId, session: config.session })
});
},
- async sendTextMessage(chatId: string, text: string): Promise {
- return request('/api/sendText', {
+ async sendTextMessage(chatId, text) {
+ return request('/api/sendText', {
method: 'POST',
body: JSON.stringify({
chatId,
@@ -149,8 +151,8 @@ export const waha = {
});
},
- async setStatus(text: string): Promise {
- return request(`/api/${config.session}/profile/status`, {
+ async setStatus(text) {
+ return request(`/api/${config.session}/profile/status`, {
method: 'PUT',
body: JSON.stringify({
status: text
@@ -158,9 +160,9 @@ export const waha = {
});
},
- async sendFileMessage(chatId: string, file: File): Promise {
+ async sendFileMessage(chatId, file) {
const fileBase64 = await getBase64(file);
- const body: RequestInit = {
+ const body = {
method: 'POST',
body: JSON.stringify({
chatId,
@@ -178,7 +180,7 @@ export const waha = {
if (file.type.startsWith('image/')) endpoint = '/api/sendImage';
if (file.type.startsWith('video/')) endpoint = '/api/sendVideo';
- const result = await request(endpoint, body);
+ const result = await request(endpoint, body);
return result;
}
};
diff --git a/web/src/websocket.ts b/js/websocket.js
similarity index 78%
rename from web/src/websocket.ts
rename to js/websocket.js
index dd6e86f..c1c0aa2 100644
--- a/web/src/websocket.ts
+++ b/js/websocket.js
@@ -1,14 +1,13 @@
-import { config } from "./config";
-import { isOnline, updateOnlineStatus } from "./storage";
-import type { WebSocketEvent } from "./types";
+import { config } from "./config.js";
+import { isOnline, updateOnlineStatus } from "./storage.js";
-let socket: WebSocket | null = null;
-let reconnectTimer: ReturnType | null = null;
-let currentOnMessageCallback: ((data: WebSocketEvent) => void) | null = null;
+let socket = null;
+let reconnectTimer = null;
+let currentOnMessageCallback = null;
export const websocket = {
- connect(onMessageCallback: (data: WebSocketEvent) => void) {
- if (!isOnline()) return;
+ connect(onMessageCallback) {
+ if (!isOnline) return;
currentOnMessageCallback = onMessageCallback;
this.disconnect(false);
@@ -49,7 +48,7 @@ export const websocket = {
socket.onmessage = (event) => {
try {
- const data = JSON.parse(event.data) as WebSocketEvent;
+ const data = JSON.parse(event.data);
if (currentOnMessageCallback) {
currentOnMessageCallback(data);
}
@@ -65,13 +64,10 @@ export const websocket = {
socket.onclose = (event) => {
console.log(`[WS] Connection closed (code: ${event.code}). Reconnecting in 5 seconds...`);
socket = null;
-
+
reconnectTimer = setTimeout(() => {
- updateOnlineStatus().then(() => {
- if (currentOnMessageCallback) {
- this.connect(currentOnMessageCallback);
- }
- });
+ updateOnlineStatus();
+ this.connect(currentOnMessageCallback);
}, 5000);
};
} catch (e) {
diff --git a/web/loading.css b/loading.css
similarity index 100%
rename from web/loading.css
rename to loading.css
diff --git a/web/public/manifest.json b/manifest.json
similarity index 100%
rename from web/public/manifest.json
rename to manifest.json
diff --git a/web/public/message-long.ogg b/message-long.ogg
similarity index 100%
rename from web/public/message-long.ogg
rename to message-long.ogg
diff --git a/web/public/message.ogg b/message.ogg
similarity index 100%
rename from web/public/message.ogg
rename to message.ogg
diff --git a/web/metroicons.css b/metroicons.css
similarity index 100%
rename from web/metroicons.css
rename to metroicons.css
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 7ebca84..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,1249 +0,0 @@
-{
- "name": "pandora",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "pandora",
- "version": "1.0.0",
- "license": "ISC",
- "devDependencies": {
- "typescript": "^7.0.2",
- "vite": "^8.1.5"
- }
- },
- "node_modules/@emnapi/core": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
- "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.2",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
- "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
- "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
- "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@tybys/wasm-util": "^0.10.3"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "peerDependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1"
- }
- },
- "node_modules/@oxc-project/types": {
- "version": "0.139.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
- "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
- "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
- "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
- "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
- "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
- "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
- "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
- "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
- "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
- "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
- "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
- "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
- "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
- "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "1.11.1",
- "@emnapi/runtime": "1.11.1",
- "@napi-rs/wasm-runtime": "^1.1.6"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
- "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
- "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
- "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tybys/wasm-util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
- "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@typescript/typescript-aix-ppc64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
- "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-darwin-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
- "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-darwin-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
- "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-freebsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
- "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-freebsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
- "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-arm": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
- "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
- "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-loong64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
- "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-mips64el": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
- "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-ppc64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
- "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-riscv64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
- "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-s390x": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
- "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-linux-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
- "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-netbsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
- "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-netbsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
- "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-openbsd-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
- "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-openbsd-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
- "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-sunos-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
- "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-win32-arm64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
- "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/@typescript/typescript-win32-x64": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
- "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/lightningcss": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
- "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.32.0",
- "lightningcss-darwin-arm64": "1.32.0",
- "lightningcss-darwin-x64": "1.32.0",
- "lightningcss-freebsd-x64": "1.32.0",
- "lightningcss-linux-arm-gnueabihf": "1.32.0",
- "lightningcss-linux-arm64-gnu": "1.32.0",
- "lightningcss-linux-arm64-musl": "1.32.0",
- "lightningcss-linux-x64-gnu": "1.32.0",
- "lightningcss-linux-x64-musl": "1.32.0",
- "lightningcss-win32-arm64-msvc": "1.32.0",
- "lightningcss-win32-x64-msvc": "1.32.0"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
- "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
- "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
- "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
- "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
- "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
- "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
- "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
- "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
- "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
- "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.32.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
- "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.16",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
- "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.20",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
- "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.16",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/rolldown": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
- "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "=0.139.0",
- "@rolldown/pluginutils": "^1.0.0"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.1.5",
- "@rolldown/binding-darwin-arm64": "1.1.5",
- "@rolldown/binding-darwin-x64": "1.1.5",
- "@rolldown/binding-freebsd-x64": "1.1.5",
- "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
- "@rolldown/binding-linux-arm64-gnu": "1.1.5",
- "@rolldown/binding-linux-arm64-musl": "1.1.5",
- "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
- "@rolldown/binding-linux-s390x-gnu": "1.1.5",
- "@rolldown/binding-linux-x64-gnu": "1.1.5",
- "@rolldown/binding-linux-x64-musl": "1.1.5",
- "@rolldown/binding-openharmony-arm64": "1.1.5",
- "@rolldown/binding-wasm32-wasi": "1.1.5",
- "@rolldown/binding-win32-arm64-msvc": "1.1.5",
- "@rolldown/binding-win32-x64-msvc": "1.1.5"
- }
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.17",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
- "license": "0BSD",
- "optional": true
- },
- "node_modules/typescript": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
- "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc"
- },
- "engines": {
- "node": ">=16.20.0"
- },
- "optionalDependencies": {
- "@typescript/typescript-aix-ppc64": "7.0.2",
- "@typescript/typescript-darwin-arm64": "7.0.2",
- "@typescript/typescript-darwin-x64": "7.0.2",
- "@typescript/typescript-freebsd-arm64": "7.0.2",
- "@typescript/typescript-freebsd-x64": "7.0.2",
- "@typescript/typescript-linux-arm": "7.0.2",
- "@typescript/typescript-linux-arm64": "7.0.2",
- "@typescript/typescript-linux-loong64": "7.0.2",
- "@typescript/typescript-linux-mips64el": "7.0.2",
- "@typescript/typescript-linux-ppc64": "7.0.2",
- "@typescript/typescript-linux-riscv64": "7.0.2",
- "@typescript/typescript-linux-s390x": "7.0.2",
- "@typescript/typescript-linux-x64": "7.0.2",
- "@typescript/typescript-netbsd-arm64": "7.0.2",
- "@typescript/typescript-netbsd-x64": "7.0.2",
- "@typescript/typescript-openbsd-arm64": "7.0.2",
- "@typescript/typescript-openbsd-x64": "7.0.2",
- "@typescript/typescript-sunos-x64": "7.0.2",
- "@typescript/typescript-win32-arm64": "7.0.2",
- "@typescript/typescript-win32-x64": "7.0.2"
- }
- },
- "node_modules/vite": {
- "version": "8.1.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
- "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "lightningcss": "^1.32.0",
- "picomatch": "^4.0.5",
- "postcss": "^8.5.17",
- "rolldown": "~1.1.5",
- "tinyglobby": "^0.2.17"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.3.0",
- "esbuild": "^0.27.0 || ^0.28.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "@vitejs/devtools": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- }
- }
-}
diff --git a/package.json b/package.json
deleted file mode 100644
index f8b9b76..0000000
--- a/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "pandora",
- "version": "1.0.0",
- "description": " ",
- "main": "index.js",
- "scripts": {
- "dev": "vite",
- "build": "tsc && vite build",
- "preview": "tsc && vite build && vite preview"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "devDependencies": {
- "typescript": "^7.0.2",
- "vite": "^8.1.5"
- }
-}
diff --git a/web/style.css b/style.css
similarity index 92%
rename from web/style.css
rename to style.css
index 387b52a..ddc4e0b 100644
--- a/web/style.css
+++ b/style.css
@@ -93,6 +93,9 @@ html, body {
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%);
}
a {
@@ -126,44 +129,16 @@ input:focus {
.app-container {
display: flex;
- flex-direction: row;
- width: 100%;
- height: 100dvh;
- overflow-x: auto;
- overflow-y: hidden;
- scroll-snap-type: x mandatory;
- scrollbar-width: none;
- -ms-overflow-style: none;
-}
-
-.main-holder {
- display: flex;
-}
-
-._scrollableView, ._scrollableContainer {
- display: flex;
- flex-direction: row;
width: 100%;
height: 100%;
- overflow-x: auto;
- overflow-y: hidden;
- scroll-snap-type: x mandatory;
- scrollbar-width: none;
- -ms-overflow-style: none;
-}
-
-.app-container::-webkit-scrollbar, ._scrollableContainer::-webkit-scrollbar, ._scrollableView::-webkit-scrollbar {
- display: none;
+ overflow: hidden;
+ position: relative;
}
.app-name {
font-weight: bold;
}
-.dim {
- opacity: .6;
-}
-
/* Sidebar Styling */
.sidebar {
width: 380px;
@@ -465,7 +440,6 @@ input:focus {
.api-status-badge {
display: flex;
align-items: center;
- justify-content: center;
gap: 8px;
font-size: 0.75rem;
color: var(--text-secondary);
@@ -502,7 +476,6 @@ input:focus {
display: flex;
flex-direction: column;
position: relative;
- height: 100%;
}
/* Empty State */
@@ -870,14 +843,8 @@ input:focus {
/* Extra Pages */
.extra-page {
- display: flex;
- flex-direction: column;
+ display: none;
width: 100%;
- height: 100%;
- scroll-snap-align: start;
- scroll-snap-stop: always;
- flex: 0 0 100%;
-
}
.extra-page-content {
@@ -904,7 +871,7 @@ input:focus {
flex: 1;
}
-.big-title {
+#profile-page h2 {
padding: 1.4rem;
font-size: 5rem;
text-wrap-mode: nowrap;
@@ -932,27 +899,6 @@ input:focus {
border-bottom-color: var(--text-primary);
}
-
-.notification-box {
- transition: transform 1s cubic-bezier(0.19, 1, 0.22, 1);
- width: 100%;
- position: fixed;
- top: 0;
- left: 0;
- background-color: var(--bg-secondary);
- transform: translateY(-100%);
- padding: .4em;
- text-transform: uppercase;
-}
-
-.notification-box.shown {
- transform: none;
-}
-
-.notification-box h1 {
- font-size: 1rem;
-}
-
@media (max-width: 768px) {
.app-container {
width: 100%;
@@ -964,12 +910,13 @@ input:focus {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
- scrollbar-width: none;
- -ms-overflow-style: none;
+ /* Hide scrollbars */
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE and Edge */
}
.app-container::-webkit-scrollbar {
- display: none;
+ display: none; /* Chrome, Safari, Opera */
}
.sidebar {
@@ -997,7 +944,7 @@ input:focus {
}
.extra-page {
- display: flex;
+ display: block;
width: 100%;
flex: 0 0 100%;
position: relative;
@@ -1046,6 +993,7 @@ input:focus {
.modal-content {
width: 100%;
background: var(--bg-main);
+ padding: 24px;
box-shadow: var(--shadow-lg);
backdrop-filter: var(--glass-blur);
display: flex;
@@ -1062,7 +1010,7 @@ input:focus {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 1em 2em 0em 2em;
+ margin-bottom: 20px;
}
.modal-header h2 {
@@ -1074,11 +1022,6 @@ input:focus {
display: flex;
flex-direction: column;
gap: 16px;
- min-height: 0;
- overflow-y: auto;
- flex: 1;
- overflow-x: hidden;
- padding: 0 2rem 0 2rem;
}
.form-group {
@@ -1103,7 +1046,7 @@ input:focus {
display: flex;
justify-content: flex-end;
gap: 12px;
- padding: 1rem 2rem;
+ margin-top: auto;
}
.btn {
@@ -1116,15 +1059,8 @@ input:focus {
}
.primary-btn {
- background: transparent;
- color: var(--text-primary);
- border: medium solid var(--bg-secondary);
-}
-
-.primary-btn:hover {
- background: var(--text-primary);
- color: var(--bg-main);
- border-color: var(--text-primary);
+ background: var(--accent-gradient);
+ color: white;
}
.secondary-btn {
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 3af771c..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2022",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "isolatedModules": true,
- "skipLibCheck": true,
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
- "outDir": "./dist",
- "rootDir": "./web/src",
- "sourceMap": true
- },
- "include": ["web/src"]
-}
diff --git a/vite.config.ts b/vite.config.ts
deleted file mode 100644
index bdf4746..0000000
--- a/vite.config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { defineConfig } from 'vite';
-
-export default defineConfig({
- root: 'web',
- base: '/pandora/',
- build: {
- outDir: '../dist',
- emptyOutDir: true,
- },
-});
diff --git a/web/app.html b/web/app.html
deleted file mode 100644
index 2f7be0d..0000000
--- a/web/app.html
+++ /dev/null
@@ -1,175 +0,0 @@
-
-
-
-
-
-
- Pandora
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/index.html b/web/index.html
deleted file mode 100644
index ab8d213..0000000
--- a/web/index.html
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
-
- Pandora
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/src/notification.ts b/web/src/notification.ts
deleted file mode 100644
index 3b25164..0000000
--- a/web/src/notification.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export async function showNotification(title: string, subtitle: string, time: number = 5000, _hint?: string): Promise {
- const notificationBox = document.createElement('div');
- notificationBox.classList.add('notification-box');
-
- const notificationTitle = document.createElement('h1');
- notificationTitle.innerHTML = title;
-
- const notificationSubtitle = document.createElement('p');
- notificationSubtitle.innerHTML = subtitle;
-
- notificationBox.appendChild(notificationTitle);
- if (subtitle) {
- notificationBox.appendChild(notificationSubtitle);
- }
- document.querySelector('body')!.appendChild(notificationBox);
-
- let clicked = false;
-
- notificationBox.addEventListener('click', () => {
- clicked = true;
- hideNotification(notificationBox);
- })
-
- await new Promise(requestAnimationFrame);
- notificationBox.classList.add("shown");
-
- await new Promise(r => setTimeout(r, time));
- if (!clicked) {
- hideNotification(notificationBox);
- }
-}
-
-async function hideNotification(notificationBox: HTMLDivElement): Promise {
- notificationBox.classList.remove('shown');
- await new Promise(r => setTimeout(r, 1000));
- notificationBox.remove();
-}
diff --git a/web/src/setup.ts b/web/src/setup.ts
deleted file mode 100644
index 5be0007..0000000
--- a/web/src/setup.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { config } from "./config";
-import { elements, ScrollableView, ui, views } from "./ui";
-import { waha } from "./waha";
-
-const page = views.get(elements.appContainer);
-page?.scrollToIndex(0);
-const statusConnection = document.getElementById("connecting-status");
-const connectingPage = document.getElementById("connecting-page");
-const loading = document.getElementById("connecting-animation");
-const connectingNextBtn = document.getElementById("connecting-next-btn");
-const settingsApiKey = document.getElementById("settings-api-key") as HTMLInputElement;
-const settingsSession = document.getElementById("settings-session") as HTMLInputElement;
-const settingsWahaURL = document.getElementById("settings-waha-url") as HTMLInputElement;
-
-if ('scrollRestoration' in history) {
- history.scrollRestoration = 'manual';
-}
-
-window.addEventListener('load', () => {
- window.scrollTo(0, 0);
- if (localStorage.getItem('setupComplete') == 'true') {
- window.location.href = 'app.html';
- }
-});
-
-if (connectingPage) {
- connectingPage.addEventListener('intoView', () => {
- testConnection();
- })
-}
-
-connectingNextBtn?.addEventListener('click', () => {
- localStorage.setItem('setupComplete', 'true');
- window.location.href = "app.html";
-})
-
-async function testConnection() {
- console.log(settingsWahaURL?.value)
- config.save(settingsWahaURL?.value, settingsSession?.value, settingsApiKey?.value, "", "");
- if (!statusConnection || !connectingNextBtn || !loading) return;
- statusConnection.textContent = "Asking for server version...";
-
- try {
- const resp = await waha.getMyInfo();
-
- if (resp.pushName != null) {
- statusConnection.textContent = `Logged in as ${resp.pushName}`
- connectingNextBtn.style.display = 'flex';
- loading.style.display = 'none';
- } else {
- page?.previows();
- }
- } catch (e) {
- console.log(e);
- page?.previows();
- }
-}
\ No newline at end of file
diff --git a/web/src/states.ts b/web/src/states.ts
deleted file mode 100644
index f707e88..0000000
--- a/web/src/states.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Chat } from "./types";
-
-export let activeChatState: Chat | null = null;
-
-export function setActiveChatState(value: Chat | null) {
- activeChatState = value;
-}
\ No newline at end of file
diff --git a/web/src/types.ts b/web/src/types.ts
deleted file mode 100644
index d65614c..0000000
--- a/web/src/types.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-/** Represents a chat entry in the sidebar */
-export interface Chat {
- id: string;
- name: string;
- lastMessage: string;
- timestamp: number | string | Date;
- unreadCount: number;
-}
-
-/** Raw chat object returned from the WAHA API */
-export interface WahaChat {
- id: string | { _serialized?: string; user?: string };
- chatId?: string;
- name?: string;
- unreadCount?: number;
- lastMessage?: {
- body?: string;
- timestamp?: number | string;
- };
- lastMessageText?: string;
-}
-
-/** Message data sub-object (_data field) */
-export interface MessageData {
- notifyName?: string;
- mimetype?: string;
- [key: string]: unknown;
-}
-
-/** Media attachment info */
-export interface MediaInfo {
- url: string;
- filename?: string;
-}
-
-/** A WhatsApp message */
-export interface Message {
- _data?: MessageData;
- _serialized?: string | { _serialized?: string; user?: string };
- id: string | { _serialized?: string; user?: string };
- body?: string;
- text?: string;
- from?: string;
- to?: string;
- fromMe?: boolean;
- sender?: string;
- timestamp: number | string;
- status?: string;
- ack?: number;
- hasMedia?: boolean;
- media?: MediaInfo;
- chatId?: string;
- chat?: { id: string };
- participant?: string;
-}
-
-/** Temporary message used for optimistic UI updates */
-export interface TempMessage extends Message {
- status: string;
-}
-
-/** File-sending temporary message */
-export interface TempFileMessage extends TempMessage {
- _data: MessageData;
- hasMedia: true;
- media: MediaInfo;
-}
-
-/** WAHA version response */
-export interface VersionResponse {
- version?: string;
-}
-
-/** WAHA user info (getMyInfo) */
-export interface AppUser {
- id: string;
- pushName?: string;
- name?: string;
-}
-
-/** WAHA contact info */
-export interface ContactInfo {
- number: string;
- [key: string]: unknown;
-}
-
-/** WAHA user about response */
-export interface UserAboutResponse {
- about?: string;
-}
-
-/** Chat picture response */
-export interface ChatPictureResponse {
- url?: string;
-}
-
-/** Status update response */
-export interface StatusResponse {
- success?: boolean;
-}
-
-/** WebSocket event payload envelope */
-export interface WebSocketEvent {
- event: string;
- payload: Message;
-}
-
-/** IndexedDB stored media record */
-export interface StoredMedia {
- reqId: string;
- blob: Blob;
- filename: string;
-}
-
-/** Downloaded media result */
-export interface DownloadedMedia {
- blob: Blob;
- filename: string;
-}
-
-/** Message with _time field used during ordering compensation */
-export interface MessageWithTime extends Message {
- _time: number;
-}
diff --git a/web/src/ui.ts b/web/src/ui.ts
deleted file mode 100644
index 13f14bf..0000000
--- a/web/src/ui.ts
+++ /dev/null
@@ -1,553 +0,0 @@
-import { formatTime, normalizeId } from "./utils";
-import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage";
-import type { Chat, Message } from "./types";
-import { activeChatState } from "./states";
-
-export const elements = {
- chatList: document.getElementById('chat-list') as HTMLUListElement,
- chatsLoader: document.getElementById('chats-loader') as HTMLDivElement,
- chatSearch: document.getElementById('chat-search') as HTMLInputElement,
- backendStatusText: document.getElementById('backend-status-text') as HTMLSpanElement,
- apiStatusIndicator: document.querySelector('.pulse-dot') as HTMLSpanElement,
- noChatState: document.getElementById('no-chat-state') as HTMLDivElement,
- activeChatContainer: document.getElementById('active-chat-container') as HTMLDivElement,
- activeChatName: document.getElementById('active-chat-name') as HTMLHeadingElement,
- activeChatAvatar: document.getElementById('active-chat-avatar') as HTMLDivElement,
- messagesContainer: document.getElementById('messages-container') as HTMLDivElement,
- messageForm: document.getElementById('message-form') as HTMLFormElement,
- messageInput: document.getElementById('message-input') as HTMLInputElement,
- backToSidebarBtn: document.querySelector('.chat-header') as HTMLElement,
- sidebar: document.querySelector('.sidebar') as HTMLElement,
- appContainer: document.querySelector('.app-container') as HTMLElement,
- settingsModal: document.getElementById('settings-page') as HTMLElement,
- settingsIconBtn: document.getElementById('settings-sidebar-btn') as HTMLButtonElement,
- saveSettingsBtn: document.getElementById('save-settings') as HTMLButtonElement,
- purgeDatabaseButton: document.getElementById('purge-database') as HTMLButtonElement,
- inputWahaUrl: document.getElementById('settings-waha-url') as HTMLInputElement,
- inputSession: document.getElementById('settings-session') as HTMLInputElement,
- inputApiKey: document.getElementById('settings-api-key') as HTMLInputElement,
- inputBackgroundImage: document.getElementById('settings-background-image') as HTMLInputElement,
- inputBackgroundOpacity: document.getElementById('settings-background-opacity') as HTMLInputElement,
- loggedUserName: document.getElementById('pandora-username') as HTMLHeadingElement,
- chatBottomBar: document.getElementById('chat-bottom-bar') as HTMLElement,
- chatBottomBarBtn: document.getElementById('chat-bottom-bar-btn') as HTMLButtonElement,
- chatInputPanel: document.getElementById('chat-input-panel') as HTMLElement,
- attachmentInput: document.getElementById('attachment-input') as HTMLInputElement,
- attachmentBtn: document.getElementById('attachment-btn') as HTMLButtonElement,
- markreadBtn: document.getElementById('markread-btn') as HTMLButtonElement,
- extraPages: document.querySelectorAll('.extra-page') as NodeListOf,
- desktopSidebarButtons: document.querySelectorAll("#desktop-aside button") as NodeListOf,
- desktopAside: document.getElementById('desktop-aside') as HTMLElement,
- contentUserName: document.querySelectorAll('[data-content="app-user"]') as NodeListOf,
- contentUserNumber: document.querySelectorAll('[data-content="app-user-number"]') as NodeListOf,
- resourceUserPic: document.querySelectorAll('[data-resource="app-user-image"]') as NodeListOf,
- valueUserStatus: document.querySelectorAll('[data-value="app-user-status"]') as NodeListOf,
- inputUserStatus: document.getElementById('profile-page-status-input') as HTMLInputElement,
- selectable: document.querySelectorAll('.selectable') as NodeListOf,
- nextPageButtons: document.querySelectorAll('.next-page-btn') as NodeListOf,
- scrollableViews: document.querySelectorAll('._scrollableView') as NodeListOf
-};
-
-export const views = new Map;
-
-export const ui = {
- /**
- * Switch view state when a contact chat is opened or closed
- */
- toggleChatState(hasActive: boolean) {
- if (hasActive) {
- elements.noChatState.classList.add('hidden');
- elements.activeChatContainer.classList.remove('hidden');
- } else {
- elements.noChatState.classList.remove('hidden');
- elements.activeChatContainer.classList.add('hidden');
- }
- },
-
- /**
- * Scroll message list automatically to bottom
- */
- scrollToBottom() {
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight;
- });
- });
- },
-
- ensureScroll(container: HTMLElement, after: Function) {
- const scrolled = container.scrollTop === (container.scrollHeight - container.clientHeight);
- after();
- if (scrolled) {
- container.scrollTop = container.scrollHeight;
- }
- },
-
- /**
- * Update connection status badge in sidebar footer
- */
- updateConnectionStatus(isConnected: boolean, text: string) {
- elements.backendStatusText.textContent = text;
- if (isConnected) {
- elements.apiStatusIndicator.style.backgroundColor = 'var(--online-color)';
- elements.apiStatusIndicator.style.animation = 'pulse 1.8s infinite';
- } else {
- elements.apiStatusIndicator.style.backgroundColor = '#ef4444';
- elements.apiStatusIndicator.style.animation = 'none';
- }
- },
-
- async renderChatList(chats: Chat[], activeChat: Chat | null, onChatSelect: (chat: Chat) => void) {
- elements.chatList.innerHTML = '';
- chats.sort((a, b) => {
- const timeA = new Date(a.timestamp).getTime();
- const timeB = new Date(b.timestamp).getTime();
- return timeB - timeA;
- });
-
- if (chats.length === 0) {
- elements.chatList.innerHTML = `No chats found `;
- return;
- }
-
- for (const chat of chats) {
- if (chat.timestamp == null) return;
- const li = document.createElement('li');
- 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() : '?';
- const hasUnread = chat.unreadCount && chat.unreadCount > 0;
- const timeStr = formatTime(chat.timestamp || new Date());
-
- li.innerHTML = `
-
-
-
-
-
- ${chat.name}
- ${timeStr}
-
-
-
- ${chat.lastMessage || '...'}
-
- ${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}"]`) as HTMLImageElement;
- if (img) img.src = picture.url ? picture.url : '';
- } catch (e) {
- }
- })();
- }
- },
-
- async updateChatInChatList(msg: Message) {
- const chatNode = document.querySelector(`.chat-item[data-id="${msg.fromMe ? msg.to : msg.from}"]`) as HTMLElement;
-
- if (chatNode) {
- const messageItem = chatNode.querySelector('.chat-item-msg') as HTMLElement;
- const time = chatNode.querySelector('.chat-item-time') as HTMLElement;
- messageItem.innerText = msg.body || msg.text || 'Media message';
- time.innerText = msg.timestamp ? formatTime(msg.timestamp) : formatTime(Date.now());
-
- if (!msg.fromMe && (!activeChatState || activeChatState.id !== (msg.fromMe ? msg.to : msg.from))) {
- const unreadBadge = chatNode.querySelector('.unread-badge') as HTMLElement;
- if (unreadBadge) {
- unreadBadge.innerText = (Number(unreadBadge.innerHTML) || 0) + 1 + "";
- } else {
- const preview = chatNode.querySelector('.chat-item-preview') as HTMLElement;
- const badge = document.createElement('span');
- badge.className = 'unread-badge';
- badge.innerText = '1';
- preview.appendChild(badge);
- }
- }
- }
- },
-
- async updateChatInChatList2(chat: Chat) {
- const chatNode = document.querySelector(`.chat-item[data-id="${chat.id}"]`) as HTMLElement;
-
- if (chatNode) {
- const messageItem = chatNode.querySelector('.chat-item-msg') as HTMLElement;
- const time = chatNode.querySelector('.chat-item-time') as HTMLElement;
- messageItem.innerText = chat.lastMessage || 'Media message';
- time.innerText = chat.timestamp ? formatTime(chat.timestamp) : formatTime(Date.now());
- const unreadBadge = chatNode.querySelector('.unread-badge') as HTMLElement;
- const newCount = (Number(unreadBadge?.innerHTML) || 0);
- if (unreadBadge && newCount != 0) {
- unreadBadge.innerText = newCount + "";
- } else {
- if (unreadBadge) unreadBadge.remove();
- }
- }
- },
-
- /**
- * Render chat message log inside chat view container
- */
- async renderMessages(messages: Message[], _activeChatName: string, userID: string, chatId: string) {
- elements.messagesContainer.innerHTML = '';
-
- if (messages.length === 0) {
- elements.messagesContainer.innerHTML = 'No messages. Say hello!
';
- return;
- }
-
- const loadMore = document.createElement("button");
- loadMore.classList.add("load-more-btn");
- loadMore.innerText = "Load more";
- loadMore.onclick = () => {
- this.loadMoreMessages(chatId, userID);
- };
- elements.messagesContainer.appendChild(loadMore);
-
- for (const msg of messages) {
- this.appendSingleMessage(msg, userID, chatId);
- }
-
- this.scrollToBottom();
- },
-
- async loadMoreMessages(chatId: string, userId: string) {
- const oldest = document.querySelector('.message-group:first-of-type') as HTMLElement;
- if (!oldest) return;
- const oldestTimestamp = oldest.dataset.timestamp;
- const oldestId = oldest.id;
-
- const loadMoreButton = document.querySelector('.load-more-btn') as HTMLButtonElement;
-
- const msgs = await getMoreChatMessages(chatId, oldestTimestamp, oldestId);
- // JS version had msgs.shift(), probably to avoid duplication of the oldest message
- msgs.shift();
-
- msgs.forEach(async msg => {
- loadMoreButton.after(this.generateMessage(msg, userId, chatId));
- });
- },
-
- /**
- * Append a single message (used for optimistic updates immediately upon sending)
- */
- appendSingleMessage(msg: Message, userID: string, chatId: string, isLocal: boolean = false) {
- elements.messagesContainer.appendChild(this.generateMessage(msg, userID, chatId, isLocal))
- },
-
- generateTempMessageLink(msg: Message) {
- const a = document.createElement('a');
- a.target = "_blank";
- if (msg.media) {
- a.href = msg.media.url;
-
- if (msg._data?.mimetype?.startsWith('image/')) {
- const img = document.createElement('img');
- img.classList.add('message-image-attachement');
- img.src = msg.media.url;
- a.appendChild(img);
- } else {
- a.textContent = msg.media.filename || "Download file";
- a.download = msg.media.filename || "file";
- }
- }
-
- return a;
- },
-
- generateMessage(msg: Message, userID: string, chatId: string, isLocal: boolean = false) {
- const isOutgoing = msg.fromMe || msg.sender === 'me';
-
- function getPrevMessageElem() {
- return elements.messagesContainer.lastElementChild as HTMLElement | null;
- }
-
- const prevMsgEl = getPrevMessageElem();
-
- const groupDiv = document.createElement('div');
- groupDiv.className = `message-group selectable ${isOutgoing ? 'outgoing' : 'incoming'}`;
- groupDiv.id = normalizeId(msg._serialized ? (msg._serialized as any) : msg.id) || "msg-id";
- groupDiv.dataset.timestamp = msg.timestamp?.toString();
- groupDiv.dataset.from = msg.participant || (msg.from as string);
-
- const senderName = isOutgoing ? userID : (msg._data?.notifyName || (msg.from as string));
- const timeStr = formatTime(msg.timestamp || new Date());
-
- let statusCheck = '';
- if (isOutgoing) {
- if (msg.status === 'read') {
- statusCheck = ' ';
- } else if (msg.status === 'delivered') {
- statusCheck = ' ';
- } else if (msg.status === 'sending') {
- statusCheck = ' ';
- } else {
- statusCheck = ' ';
- }
- }
-
- const bubble = document.createElement('div');
- bubble.className = 'message-bubble';
-
- let prevUid: string | undefined;
-
- if (msg.participant) {
- prevUid = msg.participant;
- } else {
- prevUid = msg.from as string;
- }
-
- if (!isOutgoing && (!prevMsgEl || prevUid !== prevMsgEl.dataset.from)) {
- 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.hasMedia) {
- let a: HTMLAnchorElement;
-
- if (isLocal) {
- a = this.generateTempMessageLink(msg);
- } else {
- a = document.createElement('a');
- a.innerText = `[Request media]`;
- a.target = "_blank";
-
- const clickListener = async (e: MouseEvent) => {
- a.removeEventListener('click', clickListener);
- a.innerText = `[Downloading]`;
- const mediaMsg = msg.media ? msg : await getMessage(chatId, normalizeId(msg._serialized ? (msg._serialized as any) : msg.id) || "", true);
- if (!mediaMsg || !mediaMsg?.media?.url) {
- a.addEventListener('click', clickListener);
- a.innerText = `[Error, click to try again]`
- return;
- }
- const url = new URL(mediaMsg.media.url);
- const reqID = url.pathname.split('/').filter(Boolean).pop();
-
- if (!reqID) return;
- const media = await getMedia(reqID);
- if (!media) return;
-
- const objectUrl = URL.createObjectURL(media.blob);
- (e.target as HTMLAnchorElement).href = objectUrl;
-
- if (media.blob.type.startsWith('image/')) {
- a.textContent = "";
- const img = document.createElement('img');
- img.classList.add('message-image-attachement');
- img.src = objectUrl;
- this.ensureScroll(elements.messagesContainer, () => {
- bubble.before(img);
- })
- // const content = bubble.querySelector('.message-content');
- // if (content) content.remove(); // TODO make images attachment with text look cooler
- } else {
- (e.target as HTMLAnchorElement).textContent = media.filename || `Download ${mediaMsg.media.filename}`;
- }
- }
-
- a.addEventListener('click', clickListener);
- }
-
- this.ensureScroll(elements.messagesContainer, () => {
- contentEl.appendChild(a);
- });
-
- if (!isLocal && msg._data?.mimetype?.startsWith('image/')) {
- a.click();
- }
- }
-
-
- const meta = document.createElement('div');
- meta.className = 'message-meta';
- meta.innerHTML = `${timeStr} ${statusCheck}`;
-
- bubble.appendChild(meta);
-
- if (isOutgoing) {
- if (prevMsgEl && prevMsgEl.classList.contains('outgoing')) {
- groupDiv.classList.add('same-sender');
- } else {
- const indicator = document.createElement('div');
- indicator.className = 'message-indicator';
- groupDiv.appendChild(indicator);
- }
- } else if (msg.participant) {
- if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) {
- const indicator = document.createElement('div');
- indicator.className = 'message-indicator';
- groupDiv.appendChild(indicator);
- } else groupDiv.classList.add('same-sender');
- } else {
- if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) {
- const indicator = document.createElement('div');
- indicator.className = 'message-indicator';
- groupDiv.appendChild(indicator);
- } else groupDiv.classList.add('same-sender');
- }
-
- groupDiv.appendChild(bubble);
- return groupDiv;
- },
-
- updateMessage(originalMsgId: string, generatedMsg: HTMLElement) {
- const originalMsg = document.querySelector(`#${originalMsgId}`);
- if (originalMsg) {
- originalMsg.replaceWith(generatedMsg)
- }
- },
-
- updateMessageTick(id: string, status: string) {
- let statusCheck;
- if (status === 'read') {
- statusCheck = ' ';
- } else if (status === 'delivered') {
- statusCheck = ' ';
- } else if (status === 'sending') {
- statusCheck = ' ';
- } else {
- statusCheck = ' ';
- }
-
- const msgNode = document.getElementById(id);
- if (msgNode) {
- const meta = msgNode.querySelector('.message-meta');
- if (meta) meta.outerHTML = statusCheck;
- }
- },
-
- toggleChatBottomBar() {
- elements.chatBottomBar.classList.toggle("collapsed");
- },
-
- removeChatMessage(msgId: string) {
- const message = document.getElementById(msgId);
- if (message) message.remove();
- },
-};
-
-export class ScrollableView {
- private observer: IntersectionObserver | null = null;
- private activePage: HTMLElement | null = null;
- container: HTMLElement;
- isScrollingProgrammatically = false;
- scrollTimeout: ReturnType | null = null;
-
- constructor(container: HTMLElement) {
- this.container = container;
- this.setupIntersectionObserver();
-
- window.addEventListener('scroll', (e) => {
- if (this.isScrollingProgrammatically) e.preventDefault();
- });
-
- container.querySelectorAll('.next-page-btn').forEach(nextBtn => {
- nextBtn.addEventListener('click', () => {
- this.next();
- })
- })
- }
-
- private setupIntersectionObserver() {
- this.observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting && entry.target !== this.activePage) {
- this.activePage = entry.target as HTMLElement;
-
- entry.target.dispatchEvent(new CustomEvent('intoView', {
- bubbles: true,
- detail: {
- page: entry.target,
- index: this.getCurrentIndex()
- }
- }));
- }
- });
- }, {
- root: this.container,
- threshold: 0.6
- });
-
- this.observePages();
- }
-
- observePages() {
- if (!this.observer) return;
- Array.from(this.container.children).forEach(child => {
- this.observer?.observe(child);
- });
- }
-
- get pages(): HTMLElement[] {
- return Array.from(this.container.children) as HTMLElement[];
- }
-
- getCurrentIndex(): number {
- const width = this.container.clientWidth;
- if (width === 0) return 0;
- return Math.round(this.container.scrollLeft / width);
- }
-
- getCurrentScreen(): HTMLElement | null {
- const pages = this.pages;
- return pages[this.getCurrentIndex()] || null;
- }
-
- scrollToIndex(index: number, smooth = true) {
- const pages = this.pages;
- if (index >= 0 && index < pages.length) {
- this.scrollTo(pages[index], smooth);
- }
- }
-
- next(smooth = true) {
- this.scrollToIndex(this.getCurrentIndex() + 1, smooth);
- }
-
- previows(smooth = true) {
- this.scrollToIndex(this.getCurrentIndex() - 1, smooth);
- }
-
- scrollTo(element: HTMLElement, smooth = true) {
- this.isScrollingProgrammatically = true;
- element.scrollIntoView({
- behavior: smooth ? 'smooth' : 'auto',
- inline: 'start',
- block: 'nearest'
- });
- setTimeout(() => { this.isScrollingProgrammatically = false; }, smooth ? 400 : 50);
- }
-
- getCurrentExtraPage(): number {
- const pages = Array.from(elements.extraPages);
- const activeIndex = pages.findIndex(page => page.classList.contains("shown"));
- return activeIndex !== -1 ? activeIndex : 0;
- }
-}
-
-elements.scrollableViews.forEach(view => {
- views.set(view, new ScrollableView(view));
-})
\ No newline at end of file