Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28361f178d | |||
| f0066d6015 | |||
| 82c99e128e | |||
| 9b51159e42 |
9 changed files with 390 additions and 50 deletions
|
|
@ -106,7 +106,11 @@
|
|||
</div>
|
||||
|
||||
<!-- Input Panel -->
|
||||
<div id="mentioning-suggestion" class="collapsed">
|
||||
<div id="mention-suggestions"></div>
|
||||
</div>
|
||||
<div id="mentioning-indicator" class="collapsed">
|
||||
<span></span>
|
||||
</div>
|
||||
<footer id="chat-input-panel" class="chat-input-panel">
|
||||
<div class="input-actions-left">
|
||||
|
|
@ -123,6 +127,7 @@
|
|||
<button id="markread-btn" class="icon-btn send-btn mif-done_all mif-2x" title="Mark read"></button>
|
||||
<input id="attachment-input" style="display: none;" type="file">
|
||||
<button id="attachment-btn" class="icon-btn send-btn mif-attachment mif-2x" title="Attach file"></button>
|
||||
<button id="mention-btn" class="icon-btn send-btn mif-face mif-2x" title="Mention user"></button>
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
125
web/skills-lock.json
Normal file
125
web/skills-lock.json
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"cavecrew": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/cavecrew/SKILL.md",
|
||||
"computedHash": "7bccdb8d49f41bcb480481cd4f8381b054f69847bed67fb003d83249dd6f0030"
|
||||
},
|
||||
"caveman": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman/SKILL.md",
|
||||
"computedHash": "48646eeec87b27067502d8bd450484faf08af588df783706b1571ee72f2d918c"
|
||||
},
|
||||
"caveman-commit": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-commit/SKILL.md",
|
||||
"computedHash": "790a4eeace0be35c6691faf923518ba5bd50f1f1305d1101d09dd4971be94e00"
|
||||
},
|
||||
"caveman-compress": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-compress/SKILL.md",
|
||||
"computedHash": "a248d98b6ab6b7d5b2975f8978c6064e3c3310e15eb155ba3958395c7f44a207"
|
||||
},
|
||||
"caveman-discover": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-discover/SKILL.md",
|
||||
"computedHash": "8e8a2f12e7609891721c31ee79a2d89669658c38c9fe23d28ad02c3be4902b22"
|
||||
},
|
||||
"caveman-evidence-review": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-evidence-review/SKILL.md",
|
||||
"computedHash": "40bbecf3b951f6cdecab7204a696be7267084041d876d73c8ca9eecd25bdd1c6"
|
||||
},
|
||||
"caveman-explore": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-explore/SKILL.md",
|
||||
"computedHash": "52bb43d1be0667b6322c5ae0c816e21c3c4d0475e3dcf4b69d3561c461b35485"
|
||||
},
|
||||
"caveman-help": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-help/SKILL.md",
|
||||
"computedHash": "dd85267e76baad76995157e7b9f762dfa557cd58951ee92af0c283f48aa26537"
|
||||
},
|
||||
"caveman-learn": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-learn/SKILL.md",
|
||||
"computedHash": "371abc1b81840fe5605eee1ecc8c859528a7ce84241fa1eba7b1e0d2683e0572"
|
||||
},
|
||||
"caveman-manage": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-manage/SKILL.md",
|
||||
"computedHash": "fa77c71e35182d034291386a0cdc30c2ba858cf4ff7c0f920d95ec649dd73d33"
|
||||
},
|
||||
"caveman-optimize": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-optimize/SKILL.md",
|
||||
"computedHash": "2d4c31992efca5f51d8714ac6e204e28a3637a0514d5d107e49cd17623a0c377"
|
||||
},
|
||||
"caveman-review": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-review/SKILL.md",
|
||||
"computedHash": "fb7214a1c5793bae6ba8b1be4329e2e6f40dbec6dd911dfb335ad29f09c316a1"
|
||||
},
|
||||
"caveman-setup": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-setup/SKILL.md",
|
||||
"computedHash": "66da8df44278508e519cc54ce79f8fc6bc614c8f255b47210ccd3c46f84e9676"
|
||||
},
|
||||
"caveman-stats": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/caveman-stats/SKILL.md",
|
||||
"computedHash": "57c7db449641379e2afd1389fdb17cec8f33d9c8e25b1d28093937b4431895e8"
|
||||
},
|
||||
"investigate-first": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/investigate-first/SKILL.md",
|
||||
"computedHash": "59b581b5abfc0e19b7dd5a7c3bb2b16621ab5e94e085065c7b724a9c86cc3d4d"
|
||||
},
|
||||
"lean-build": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/lean-build/SKILL.md",
|
||||
"computedHash": "6074721f1653e6e9d6381595b094284807b8d5d001d7d3977ec534679b16a63c"
|
||||
},
|
||||
"migration": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/migration/SKILL.md",
|
||||
"computedHash": "7348b2d21ca05bdecc1e4ec2c8b63515ccb1700c8a261fdbd458b04b7383945b"
|
||||
},
|
||||
"safe-refactor": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/safe-refactor/SKILL.md",
|
||||
"computedHash": "9df34a21a7a2c18ce13dbbe4a3f66b4e91df9e4bc33c1c9da8a80261583367ec"
|
||||
},
|
||||
"surgical-patch": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/surgical-patch/SKILL.md",
|
||||
"computedHash": "028b9c3574142d19639d9f8eb11fbb01c58479f8f441e957d598a7215e79f95d"
|
||||
},
|
||||
"verify-and-stop": {
|
||||
"source": "JuliusBrussee/caveman",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/verify-and-stop/SKILL.md",
|
||||
"computedHash": "3d67de16a2b3c48b643bc511ebc4cb86e6c7105215d8428b6140cbcb73aba4e6"
|
||||
}
|
||||
}
|
||||
}
|
||||
108
web/src/app.ts
108
web/src/app.ts
|
|
@ -3,11 +3,11 @@ 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 { fetchChats, getAppUser, getChatMessages, getChatPicture, getChats, getGroupUsers, getUser, getUserAbout, getUsersFromGroup, markRead, sendStatus, updateOnlineStatus } from "./storage";
|
||||
import { deleteDatabase, upsertMessages } from "./db";
|
||||
import { showNotification } from "./notification";
|
||||
import type { Chat, Message, WebSocketEvent } from "./types";
|
||||
import { activeChatState, clearMentionCache, mentionCacheID, mentionCacheText, setActiveChatState } from "./states";
|
||||
import type { Chat, GroupUser, Message, WebSocketEvent } from "./types";
|
||||
import { activeChatState, clearMentionCache, clearMentionedContacts, getMentionedIDs, mentionCacheID, mentionCacheText, mentionedContact, mentionedContacts, removeMentionedContact, setActiveChatState } from "./states";
|
||||
if (localStorage.getItem('setupComplete') !== "true") window.location.href = "index.html";
|
||||
|
||||
const messageTone = new Audio("./message.ogg");
|
||||
|
|
@ -135,7 +135,7 @@ function scrollToChat(smooth = true) {
|
|||
|
||||
function scrollToList(smooth = true) {
|
||||
isScrollingProgrammatically = true;
|
||||
elements.appContainer.scrollTo({
|
||||
mainViewEl?.scrollTo({
|
||||
left: 0,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
});
|
||||
|
|
@ -172,7 +172,6 @@ function setupEventListeners() {
|
|||
const width = elements.appContainer.clientWidth;
|
||||
|
||||
if (scrollLeft < width * 0.2) {
|
||||
// User swiped back to the list view
|
||||
if (activeChatState) {
|
||||
closeActiveChat(false);
|
||||
}
|
||||
|
|
@ -217,12 +216,28 @@ function setupEventListeners() {
|
|||
}
|
||||
});
|
||||
|
||||
elements.messageInput.addEventListener('input', (e) => {
|
||||
const inputEvent = e as InputEvent;
|
||||
|
||||
mentionedContacts.forEach(c => {
|
||||
if (!elements.messageInput.value.includes(`@${c.number}`)) {
|
||||
removeMentionedContact(c);
|
||||
}
|
||||
})
|
||||
|
||||
if (inputEvent.data === "@") {
|
||||
suggestMention();
|
||||
} else {
|
||||
elements.mentioningSuggestion.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
|
||||
elements.chatBottomBar.style.height = `${elements.chatInputPanel.offsetHeight}px`;
|
||||
const observer = new ResizeObserver(() => {
|
||||
elements.chatBottomBar.style.height =
|
||||
`${elements.chatInputPanel.offsetHeight}px`;
|
||||
});
|
||||
|
||||
|
||||
elements.mentioningIndicator.addEventListener('click', clearMentionCache);
|
||||
|
||||
observer.observe(elements.chatInputPanel);
|
||||
|
|
@ -236,14 +251,20 @@ function setupEventListeners() {
|
|||
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.mentionBtn.addEventListener('click', async () => {
|
||||
suggestMention();
|
||||
ui.toggleChatBottomBar();
|
||||
});
|
||||
|
||||
elements.backToSidebarBtn.addEventListener('click', () => {
|
||||
closeActiveChat(false);
|
||||
|
|
@ -270,7 +291,6 @@ function setupEventListeners() {
|
|||
} else {
|
||||
showNotification("Failed to update status...", "", 2000);
|
||||
}
|
||||
console.log(result);
|
||||
}, 2000))
|
||||
|
||||
elements.selectable.forEach(e => {
|
||||
|
|
@ -349,7 +369,7 @@ async function handleIncomingMessage(msg: Message) {
|
|||
const container = elements.messagesContainer;
|
||||
const scrolled = container.scrollTop === (container.scrollHeight - container.clientHeight);
|
||||
ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, (await getAppUser()).id);
|
||||
if (scrolled) {
|
||||
if (scrolled && window.innerWidth > 768) {
|
||||
ui.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
|
@ -358,8 +378,9 @@ async function handleIncomingMessage(msg: Message) {
|
|||
|
||||
async function selectChat(chat: Chat, isPopState = false, smoothScroll = true) {
|
||||
if (isLoadingChat) return;
|
||||
|
||||
|
||||
clearMentionCache();
|
||||
clearMentionedContacts();
|
||||
|
||||
const pageEl = document.getElementById("chat-page");
|
||||
if (!pageEl) return;
|
||||
|
|
@ -429,8 +450,12 @@ async function closeActiveChat(isPopState = false) {
|
|||
async function sendMessage() {
|
||||
const text = elements.messageInput.value.trim();
|
||||
if (!text || !activeChatState) return;
|
||||
const _mentionCacheID = mentionCacheID;
|
||||
const _mentionCacheText = mentionCacheText;
|
||||
clearMentionCache();
|
||||
|
||||
elements.messageInput.value = '';
|
||||
elements.messageInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
|
||||
const tempMsg = {
|
||||
id: 'temp-' + Date.now(),
|
||||
|
|
@ -439,8 +464,8 @@ async function sendMessage() {
|
|||
sender: 'me',
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'sending',
|
||||
replyTo: mentionCacheID ? {
|
||||
body: mentionCacheText || "Mention (no text)"
|
||||
replyTo: _mentionCacheID ? {
|
||||
body: _mentionCacheText || "Mention (no text)"
|
||||
} : null
|
||||
} as any;
|
||||
|
||||
|
|
@ -448,6 +473,14 @@ async function sendMessage() {
|
|||
ui.scrollToBottom();
|
||||
|
||||
try {
|
||||
try {
|
||||
if (!activeChatState.id.endsWith('@lid')) {
|
||||
await waha.readChat(activeChatState.id);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('readChat failed (non-fatal):', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await waha.startTyping(activeChatState.id);
|
||||
const delay = Math.min(4000, Math.max(1000, text.length * 50));
|
||||
|
|
@ -462,15 +495,7 @@ async function sendMessage() {
|
|||
console.warn('Presence stop failed:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!activeChatState.id.endsWith('@lid')) {
|
||||
await waha.readChat(activeChatState.id);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('readChat failed (non-fatal):', e.message);
|
||||
}
|
||||
|
||||
const responseData = await waha.sendTextMessage(activeChatState.id, text, mentionCacheID);
|
||||
const responseData = await waha.sendTextMessage(activeChatState.id, text, getMentionedIDs(), _mentionCacheID);
|
||||
|
||||
const tempBubble = document.getElementById(tempMsg.id);
|
||||
if (tempBubble) {
|
||||
|
|
@ -491,8 +516,6 @@ async function sendMessage() {
|
|||
if (meta) meta.innerHTML = `<span style="color: #ef4444;">Failed to send</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
clearMentionCache();
|
||||
}
|
||||
|
||||
async function sendFileMessage(file: File) {
|
||||
|
|
@ -541,6 +564,39 @@ function saveSettings() {
|
|||
initWebSocket();
|
||||
}
|
||||
|
||||
async function suggestMention() {
|
||||
elements.mentionSuggestions.innerHTML = "";
|
||||
if (activeChatState) {
|
||||
const gpUsrs: GroupUser[] | undefined = await getGroupUsers(activeChatState.id);
|
||||
if (!gpUsrs) return;
|
||||
const usrs = await getUsersFromGroup(gpUsrs);
|
||||
if (!usrs) return;
|
||||
|
||||
elements.mentioningSuggestion.classList.remove('collapsed');
|
||||
|
||||
usrs.forEach(u => {
|
||||
if (!u) return;
|
||||
const contact = document.createElement('div');
|
||||
contact.classList = "mention-suggestion";
|
||||
|
||||
const name = u.name ?? u.pushname;
|
||||
if (!name) return;
|
||||
|
||||
contact.innerText = name;
|
||||
contact.addEventListener('click', () => {
|
||||
mentionedContact(u);
|
||||
elements.messageInput.value = elements.messageInput.value.substring(0, elements.messageInput.value.length - 1);
|
||||
elements.messageInput.value += `@${u.number} `;
|
||||
elements.messageInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
elements.mentioningSuggestion.classList.add("collapsed");
|
||||
elements.messageInput?.focus();
|
||||
});
|
||||
|
||||
elements.mentionSuggestions.appendChild(contact);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function checkWahaStatus() {
|
||||
try {
|
||||
const data = await waha.getVersion();
|
||||
|
|
@ -552,4 +608,4 @@ async function checkWahaStatus() {
|
|||
|
||||
export function getCurrentChat() {
|
||||
return activeChatState;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { Chat } from "./types";
|
||||
import { Chat, Contact } from "./types";
|
||||
import { elements } from "./ui";
|
||||
|
||||
export let activeChatState: Chat | null = null;
|
||||
export let mentionCacheID: string | null = null;
|
||||
export let mentionCacheText: string | null = null;
|
||||
export let mentionedContacts: Contact[] = [];
|
||||
|
||||
export function setActiveChatState(value: Chat | null) {
|
||||
activeChatState = value;
|
||||
|
|
@ -12,13 +13,37 @@ export function setActiveChatState(value: Chat | null) {
|
|||
export function prepareMention(id: string, text: string) {
|
||||
mentionCacheID = id;
|
||||
mentionCacheText = text;
|
||||
elements.mentioningIndicator.innerText = `Mentioning "${text}".`;
|
||||
const span = elements.mentioningIndicator.querySelector('span');
|
||||
if (span) span.innerText = `Mentioning "${text}".`;
|
||||
elements.mentioningIndicator.classList.remove('collapsed');
|
||||
}
|
||||
|
||||
export function clearMentionCache() {
|
||||
mentionCacheID = null;
|
||||
mentionCacheText = null;
|
||||
elements.mentioningIndicator.innerText = ``;
|
||||
const span = elements.mentioningIndicator.querySelector('span');
|
||||
if (span) span.innerText = '';
|
||||
elements.mentioningIndicator.classList.add('collapsed');
|
||||
}
|
||||
|
||||
export function mentionedContact(contact: Contact) {
|
||||
mentionedContacts.push(contact);
|
||||
}
|
||||
|
||||
export function clearMentionedContacts() {
|
||||
mentionedContacts = [];
|
||||
}
|
||||
|
||||
export function removeMentionedContact(contact: Contact) {
|
||||
const index = mentionedContacts.indexOf(contact);
|
||||
if (index === -1) return;
|
||||
mentionedContacts.splice(index, 1);
|
||||
}
|
||||
|
||||
export function getMentionedIDs(): string[] {
|
||||
const m = mentionedContacts.map(x => x.id);
|
||||
const r: string[] = [];
|
||||
m.forEach(_m => { if (_m) r.push(_m) })
|
||||
|
||||
return r;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
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 type { Chat, Message, AppUser, ContactInfo, UserAboutResponse, ChatPictureResponse, StatusResponse, DownloadedMedia, GroupUser, Contact } from "./types";
|
||||
|
||||
let online = false;
|
||||
let chats: Chat[] = [];
|
||||
|
|
@ -60,6 +60,14 @@ export async function getUserAbout(userId: string): Promise<UserAboutResponse |
|
|||
}
|
||||
}
|
||||
|
||||
export async function getContact(id: string): Promise<Contact | undefined> {
|
||||
if (online) {
|
||||
return await waha.getContact(id);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function getChats(): Chat[] {
|
||||
return chats;
|
||||
}
|
||||
|
|
@ -164,4 +172,28 @@ export async function markRead(chatId: string): Promise<Chat | undefined> {
|
|||
await upsertChats([chat]);
|
||||
}
|
||||
return chat;
|
||||
}
|
||||
|
||||
export async function getGroupUsers(groupId: string): Promise<GroupUser[] | undefined> {
|
||||
if (online) {
|
||||
return await waha.getGroupUsers(groupId);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getUsersFromGroup(users: (GroupUser | undefined)[]): Promise<(Contact | undefined)[]> {
|
||||
if (!online) {
|
||||
return users.map(() => undefined);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
users.map(async u => {
|
||||
if (u?.id._serialized) {
|
||||
return await getContact(u.id._serialized);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -123,3 +123,31 @@ export interface DownloadedMedia {
|
|||
export interface MessageWithTime extends Message {
|
||||
_time: number;
|
||||
}
|
||||
|
||||
export interface GroupUser {
|
||||
id: {
|
||||
server: string | null,
|
||||
user: string | null,
|
||||
_serialized: string | null
|
||||
},
|
||||
isAdmin: boolean | null,
|
||||
isSuperAdmin: boolean | null
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
id: string | null,
|
||||
number: string | null,
|
||||
isBusiness: boolean | null,
|
||||
isEnterprise: boolean | null,
|
||||
name: string | null,
|
||||
pushname: string | null,
|
||||
shortName: string | null,
|
||||
statusMute: boolean | null,
|
||||
type: string | null,
|
||||
isMe: boolean | null,
|
||||
isUser: boolean | null,
|
||||
isGroup: boolean | null,
|
||||
isWAContact: boolean | null,
|
||||
isMyContact: boolean | null,
|
||||
isBlocked: boolean | null
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ export const elements = {
|
|||
chatInputPanel: document.getElementById('chat-input-panel') as HTMLElement,
|
||||
attachmentInput: document.getElementById('attachment-input') as HTMLInputElement,
|
||||
attachmentBtn: document.getElementById('attachment-btn') as HTMLButtonElement,
|
||||
mentionBtn: document.getElementById('mention-btn') as HTMLButtonElement,
|
||||
markreadBtn: document.getElementById('markread-btn') as HTMLButtonElement,
|
||||
extraPages: document.querySelectorAll('.extra-page') as NodeListOf<HTMLElement>,
|
||||
desktopSidebarButtons: document.querySelectorAll("#desktop-aside button") as NodeListOf<HTMLButtonElement>,
|
||||
|
|
@ -49,7 +50,9 @@ export const elements = {
|
|||
scrollableViews: document.querySelectorAll('._scrollableView') as NodeListOf<HTMLElement>,
|
||||
loadingScreen: document.querySelector('#loading-screen') as HTMLElement,
|
||||
loadingScreenStatus: document.querySelector('#loading-screen-status') as HTMLElement,
|
||||
mentioningIndicator: document.querySelector('#mentioning-indicator') as HTMLElement
|
||||
mentioningIndicator: document.querySelector('#mentioning-indicator') as HTMLElement,
|
||||
mentioningSuggestion: document.querySelector('#mentioning-suggestion') as HTMLElement,
|
||||
mentionSuggestions: document.querySelector('#mention-suggestions') as HTMLElement
|
||||
};
|
||||
|
||||
export const views = new Map<HTMLElement, ScrollableView>;
|
||||
|
|
@ -348,7 +351,7 @@ export const ui = {
|
|||
replyIndicatorEl.addEventListener('click', () => {
|
||||
const _msg = document.querySelector(`[id*="${replyTo.id}"]`) as HTMLElement;
|
||||
if (_msg) {
|
||||
_msg.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
_msg.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
this.tempClass(_msg, "mentioned-highlight", 1000);
|
||||
}
|
||||
});
|
||||
|
|
@ -457,6 +460,7 @@ export const ui = {
|
|||
|
||||
bubble.addEventListener('dblclick', () => {
|
||||
prepareMention(msg.id.toString(), parsed);
|
||||
elements.messageInput?.focus();
|
||||
});
|
||||
|
||||
groupDiv.appendChild(bubble);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { config } from "./config";
|
||||
import { showNotification } from "./notification";
|
||||
import { getBase64 } from "./utils";
|
||||
import type { Message, VersionResponse, AppUser, ContactInfo, UserAboutResponse, ChatPictureResponse, StatusResponse } from "./types";
|
||||
import type { Message, VersionResponse, AppUser, ContactInfo, UserAboutResponse, ChatPictureResponse, StatusResponse, GroupUser, Contact } from "./types";
|
||||
|
||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = `${config.wahaUrl}${path}`;
|
||||
|
|
@ -88,6 +88,10 @@ export const waha = {
|
|||
});
|
||||
},
|
||||
|
||||
async getContact(id: string) : Promise<Contact | undefined> {
|
||||
return request<Contact>(`/api/${config.session}/contacts/${id}`);
|
||||
},
|
||||
|
||||
async getChatMessages(chatId: string, beforeTimestamp?: any): Promise<Message[]> {
|
||||
return request<Message[]>(`/api/${config.session}/chats/${chatId}/messages?downloadMedia=false&limit=40${beforeTimestamp ? `&filter.timestamp.lte=${beforeTimestamp}` : "" }`);
|
||||
},
|
||||
|
|
@ -138,14 +142,15 @@ export const waha = {
|
|||
});
|
||||
},
|
||||
|
||||
async sendTextMessage(chatId: string, text: string, replyTo: string | null = null): Promise<Message> {
|
||||
async sendTextMessage(chatId: string, text: string, mentions: string[] = [], replyTo: string | null = null): Promise<Message> {
|
||||
return request<Message>('/api/sendText', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
chatId,
|
||||
text,
|
||||
session: config.session,
|
||||
replyTo: replyTo
|
||||
replyTo: replyTo,
|
||||
mentions: mentions
|
||||
})
|
||||
});
|
||||
},
|
||||
|
|
@ -181,5 +186,9 @@ export const waha = {
|
|||
|
||||
const result = await request<Message>(endpoint, body);
|
||||
return result;
|
||||
},
|
||||
|
||||
async getGroupUsers(groupId: string): Promise<GroupUser[]> {
|
||||
return request<GroupUser[]>(`/api/${config.session}/groups/${groupId}/participants`);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -66,10 +66,14 @@ body.light {
|
|||
--text-primary: black;
|
||||
--text-secondary: black;
|
||||
--text-muted: rgb(134, 134, 134);
|
||||
--accent: black; --accent-gradient: white; --accent-hover: #4f46e5; --online-color: rgba(0, 0, 000, 0.6);
|
||||
--accent: black;
|
||||
--accent-gradient: white;
|
||||
--accent-hover: #4f46e5;
|
||||
--online-color: rgba(0, 0, 000, 0.6);
|
||||
--bubble-incoming: #01abaa;
|
||||
--bubble-outgoing: #016f6e;
|
||||
--input-bg: white; --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--input-bg: white;
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1); --glass-blur: blur(20px);
|
||||
--high-box-shadow: 2px 7px 5px rgba(0, 0, 0, 0.3), 0px -4px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
|
@ -884,21 +888,21 @@ li {
|
|||
}
|
||||
|
||||
/* Scrollbars styling */
|
||||
::-webkit-scrollbar {
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
|
|
@ -1029,35 +1033,73 @@ li {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
outline: thick solid var(--bg-secondary);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#loading-screen.collapsed {
|
||||
transform: translateY(100%);
|
||||
transform-origin: top center;
|
||||
animation: fall 0.8s cubic-bezier(.4, 0, .8, .2) forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#mentioning-indicator {
|
||||
transition: all .4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background-color: var(--bg-secondary);
|
||||
padding: 0.4rem 0.8rem;
|
||||
max-height: 3em;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0.4em 0.8em;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
margin-top: .8rem;
|
||||
}
|
||||
|
||||
#mentioning-indicator.collapsed {
|
||||
#mentioning-indicator > span {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#mentioning-indicator.collapsed, #mentioning-suggestion.collapsed {
|
||||
max-height: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
opacity: 0;
|
||||
/* opacity: 0; */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#mentioning-suggestion {
|
||||
transition: all .4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
height: 4rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
#mention-suggestions {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mention-suggestion::before {
|
||||
content: "[ ";
|
||||
}
|
||||
|
||||
.mention-suggestion::after {
|
||||
content: " ]";
|
||||
}
|
||||
|
||||
#mention-suggestions > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-container {
|
||||
width: 100%;
|
||||
|
|
@ -1252,3 +1294,17 @@ h2.modal-title {
|
|||
color: var(--bg-main);
|
||||
}
|
||||
|
||||
@keyframes fall {
|
||||
0% {
|
||||
transform: perspective(600px) rotateX(0deg);
|
||||
}
|
||||
70% {
|
||||
transform: perspective(600px) rotateX(82deg);
|
||||
}
|
||||
85% {
|
||||
transform: perspective(600px) rotateX(94deg);
|
||||
}
|
||||
100% {
|
||||
transform: perspective(600px) rotateX(90deg);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue