Add support for mentioning users; change loading screen loading animation to falling; restore chat close functionality on mobile; make message input resize on message send.
This commit is contained in:
parent
82c99e128e
commit
f0066d6015
8 changed files with 264 additions and 49 deletions
|
|
@ -106,7 +106,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input Panel -->
|
<!-- Input Panel -->
|
||||||
|
<div id="mentioning-suggestion" class="collapsed">
|
||||||
|
<div id="mention-suggestions"></div>
|
||||||
|
</div>
|
||||||
<div id="mentioning-indicator" class="collapsed">
|
<div id="mentioning-indicator" class="collapsed">
|
||||||
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
<footer id="chat-input-panel" class="chat-input-panel">
|
<footer id="chat-input-panel" class="chat-input-panel">
|
||||||
<div class="input-actions-left">
|
<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>
|
<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">
|
<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="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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
106
web/src/app.ts
106
web/src/app.ts
|
|
@ -3,11 +3,11 @@ import { waha } from "./waha";
|
||||||
import { ui, elements, views } from "./ui";
|
import { ui, elements, views } from "./ui";
|
||||||
import { websocket } from "./websocket";
|
import { websocket } from "./websocket";
|
||||||
import { compensateMessageOrdering, debounce, formatTime, normalizeId } from "./utils";
|
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 { deleteDatabase, upsertMessages } from "./db";
|
||||||
import { showNotification } from "./notification";
|
import { showNotification } from "./notification";
|
||||||
import type { Chat, Message, WebSocketEvent } from "./types";
|
import type { Chat, GroupUser, Message, WebSocketEvent } from "./types";
|
||||||
import { activeChatState, clearMentionCache, mentionCacheID, mentionCacheText, setActiveChatState } from "./states";
|
import { activeChatState, clearMentionCache, clearMentionedContacts, getMentionedIDs, mentionCacheID, mentionCacheText, mentionedContact, mentionedContacts, removeMentionedContact, setActiveChatState } from "./states";
|
||||||
if (localStorage.getItem('setupComplete') !== "true") window.location.href = "index.html";
|
if (localStorage.getItem('setupComplete') !== "true") window.location.href = "index.html";
|
||||||
|
|
||||||
const messageTone = new Audio("./message.ogg");
|
const messageTone = new Audio("./message.ogg");
|
||||||
|
|
@ -135,7 +135,7 @@ function scrollToChat(smooth = true) {
|
||||||
|
|
||||||
function scrollToList(smooth = true) {
|
function scrollToList(smooth = true) {
|
||||||
isScrollingProgrammatically = true;
|
isScrollingProgrammatically = true;
|
||||||
elements.appContainer.scrollTo({
|
mainViewEl?.scrollTo({
|
||||||
left: 0,
|
left: 0,
|
||||||
behavior: smooth ? 'smooth' : 'auto'
|
behavior: smooth ? 'smooth' : 'auto'
|
||||||
});
|
});
|
||||||
|
|
@ -172,7 +172,6 @@ function setupEventListeners() {
|
||||||
const width = elements.appContainer.clientWidth;
|
const width = elements.appContainer.clientWidth;
|
||||||
|
|
||||||
if (scrollLeft < width * 0.2) {
|
if (scrollLeft < width * 0.2) {
|
||||||
// User swiped back to the list view
|
|
||||||
if (activeChatState) {
|
if (activeChatState) {
|
||||||
closeActiveChat(false);
|
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`;
|
elements.chatBottomBar.style.height = `${elements.chatInputPanel.offsetHeight}px`;
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
elements.chatBottomBar.style.height =
|
elements.chatBottomBar.style.height =
|
||||||
`${elements.chatInputPanel.offsetHeight}px`;
|
`${elements.chatInputPanel.offsetHeight}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.mentioningIndicator.addEventListener('click', clearMentionCache);
|
elements.mentioningIndicator.addEventListener('click', clearMentionCache);
|
||||||
|
|
||||||
observer.observe(elements.chatInputPanel);
|
observer.observe(elements.chatInputPanel);
|
||||||
|
|
@ -236,14 +251,20 @@ function setupEventListeners() {
|
||||||
const result = await markRead(activeChatState.id);
|
const result = await markRead(activeChatState.id);
|
||||||
if (result) ui.updateChatInChatList2(result);
|
if (result) ui.updateChatInChatList2(result);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
elements.attachmentBtn.addEventListener('click', () => {
|
elements.attachmentBtn.addEventListener('click', () => {
|
||||||
elements.attachmentInput.click();
|
elements.attachmentInput.click();
|
||||||
})
|
});
|
||||||
elements.attachmentInput.addEventListener('change', function (this: HTMLInputElement) {
|
elements.attachmentInput.addEventListener('change', function (this: HTMLInputElement) {
|
||||||
const firstFile = this.files?.[0];
|
const firstFile = this.files?.[0];
|
||||||
if (firstFile) sendFileMessage(firstFile);
|
if (firstFile) sendFileMessage(firstFile);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
elements.mentionBtn.addEventListener('click', async () => {
|
||||||
|
suggestMention();
|
||||||
|
ui.toggleChatBottomBar();
|
||||||
|
});
|
||||||
|
|
||||||
elements.backToSidebarBtn.addEventListener('click', () => {
|
elements.backToSidebarBtn.addEventListener('click', () => {
|
||||||
closeActiveChat(false);
|
closeActiveChat(false);
|
||||||
|
|
@ -270,7 +291,6 @@ function setupEventListeners() {
|
||||||
} else {
|
} else {
|
||||||
showNotification("Failed to update status...", "", 2000);
|
showNotification("Failed to update status...", "", 2000);
|
||||||
}
|
}
|
||||||
console.log(result);
|
|
||||||
}, 2000))
|
}, 2000))
|
||||||
|
|
||||||
elements.selectable.forEach(e => {
|
elements.selectable.forEach(e => {
|
||||||
|
|
@ -358,8 +378,9 @@ async function handleIncomingMessage(msg: Message) {
|
||||||
|
|
||||||
async function selectChat(chat: Chat, isPopState = false, smoothScroll = true) {
|
async function selectChat(chat: Chat, isPopState = false, smoothScroll = true) {
|
||||||
if (isLoadingChat) return;
|
if (isLoadingChat) return;
|
||||||
|
|
||||||
clearMentionCache();
|
clearMentionCache();
|
||||||
|
clearMentionedContacts();
|
||||||
|
|
||||||
const pageEl = document.getElementById("chat-page");
|
const pageEl = document.getElementById("chat-page");
|
||||||
if (!pageEl) return;
|
if (!pageEl) return;
|
||||||
|
|
@ -429,8 +450,12 @@ async function closeActiveChat(isPopState = false) {
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const text = elements.messageInput.value.trim();
|
const text = elements.messageInput.value.trim();
|
||||||
if (!text || !activeChatState) return;
|
if (!text || !activeChatState) return;
|
||||||
|
const _mentionCacheID = mentionCacheID;
|
||||||
|
const _mentionCacheText = mentionCacheText;
|
||||||
|
clearMentionCache();
|
||||||
|
|
||||||
elements.messageInput.value = '';
|
elements.messageInput.value = '';
|
||||||
|
elements.messageInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
|
||||||
const tempMsg = {
|
const tempMsg = {
|
||||||
id: 'temp-' + Date.now(),
|
id: 'temp-' + Date.now(),
|
||||||
|
|
@ -439,8 +464,8 @@ async function sendMessage() {
|
||||||
sender: 'me',
|
sender: 'me',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
status: 'sending',
|
status: 'sending',
|
||||||
replyTo: mentionCacheID ? {
|
replyTo: _mentionCacheID ? {
|
||||||
body: mentionCacheText || "Mention (no text)"
|
body: _mentionCacheText || "Mention (no text)"
|
||||||
} : null
|
} : null
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
|
|
@ -448,6 +473,14 @@ async function sendMessage() {
|
||||||
ui.scrollToBottom();
|
ui.scrollToBottom();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
try {
|
||||||
|
if (!activeChatState.id.endsWith('@lid')) {
|
||||||
|
await waha.readChat(activeChatState.id);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn('readChat failed (non-fatal):', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await waha.startTyping(activeChatState.id);
|
await waha.startTyping(activeChatState.id);
|
||||||
const delay = Math.min(4000, Math.max(1000, text.length * 50));
|
const delay = Math.min(4000, Math.max(1000, text.length * 50));
|
||||||
|
|
@ -462,15 +495,7 @@ async function sendMessage() {
|
||||||
console.warn('Presence stop failed:', e);
|
console.warn('Presence stop failed:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const responseData = await waha.sendTextMessage(activeChatState.id, text, getMentionedIDs(), _mentionCacheID);
|
||||||
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 tempBubble = document.getElementById(tempMsg.id);
|
const tempBubble = document.getElementById(tempMsg.id);
|
||||||
if (tempBubble) {
|
if (tempBubble) {
|
||||||
|
|
@ -491,8 +516,6 @@ async function sendMessage() {
|
||||||
if (meta) meta.innerHTML = `<span style="color: #ef4444;">Failed to send</span>`;
|
if (meta) meta.innerHTML = `<span style="color: #ef4444;">Failed to send</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMentionCache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendFileMessage(file: File) {
|
async function sendFileMessage(file: File) {
|
||||||
|
|
@ -541,6 +564,39 @@ function saveSettings() {
|
||||||
initWebSocket();
|
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() {
|
async function checkWahaStatus() {
|
||||||
try {
|
try {
|
||||||
const data = await waha.getVersion();
|
const data = await waha.getVersion();
|
||||||
|
|
@ -552,4 +608,4 @@ async function checkWahaStatus() {
|
||||||
|
|
||||||
export function getCurrentChat() {
|
export function getCurrentChat() {
|
||||||
return activeChatState;
|
return activeChatState;
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Chat } from "./types";
|
import { Chat, Contact } from "./types";
|
||||||
import { elements } from "./ui";
|
import { elements } from "./ui";
|
||||||
|
|
||||||
export let activeChatState: Chat | null = null;
|
export let activeChatState: Chat | null = null;
|
||||||
export let mentionCacheID: string | null = null;
|
export let mentionCacheID: string | null = null;
|
||||||
export let mentionCacheText: string | null = null;
|
export let mentionCacheText: string | null = null;
|
||||||
|
export let mentionedContacts: Contact[] = [];
|
||||||
|
|
||||||
export function setActiveChatState(value: Chat | null) {
|
export function setActiveChatState(value: Chat | null) {
|
||||||
activeChatState = value;
|
activeChatState = value;
|
||||||
|
|
@ -12,13 +13,37 @@ export function setActiveChatState(value: Chat | null) {
|
||||||
export function prepareMention(id: string, text: string) {
|
export function prepareMention(id: string, text: string) {
|
||||||
mentionCacheID = id;
|
mentionCacheID = id;
|
||||||
mentionCacheText = text;
|
mentionCacheText = text;
|
||||||
elements.mentioningIndicator.innerText = `Mentioning "${text}".`;
|
const span = elements.mentioningIndicator.querySelector('span');
|
||||||
|
if (span) span.innerText = `Mentioning "${text}".`;
|
||||||
elements.mentioningIndicator.classList.remove('collapsed');
|
elements.mentioningIndicator.classList.remove('collapsed');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearMentionCache() {
|
export function clearMentionCache() {
|
||||||
mentionCacheID = null;
|
mentionCacheID = null;
|
||||||
mentionCacheText = null;
|
mentionCacheText = null;
|
||||||
elements.mentioningIndicator.innerText = ``;
|
const span = elements.mentioningIndicator.querySelector('span');
|
||||||
|
if (span) span.innerText = '';
|
||||||
elements.mentioningIndicator.classList.add('collapsed');
|
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 { loadChat, loadChatsSorted, loadLatestMessages, loadMedia, loadOlderMessages, upsertChats, upsertMedia, upsertMessages } from "./db";
|
||||||
import { waha } from "./waha";
|
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 online = false;
|
||||||
let chats: Chat[] = [];
|
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[] {
|
export function getChats(): Chat[] {
|
||||||
return chats;
|
return chats;
|
||||||
}
|
}
|
||||||
|
|
@ -164,4 +172,28 @@ export async function markRead(chatId: string): Promise<Chat | undefined> {
|
||||||
await upsertChats([chat]);
|
await upsertChats([chat]);
|
||||||
}
|
}
|
||||||
return 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 {
|
export interface MessageWithTime extends Message {
|
||||||
_time: number;
|
_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,
|
chatInputPanel: document.getElementById('chat-input-panel') as HTMLElement,
|
||||||
attachmentInput: document.getElementById('attachment-input') as HTMLInputElement,
|
attachmentInput: document.getElementById('attachment-input') as HTMLInputElement,
|
||||||
attachmentBtn: document.getElementById('attachment-btn') as HTMLButtonElement,
|
attachmentBtn: document.getElementById('attachment-btn') as HTMLButtonElement,
|
||||||
|
mentionBtn: document.getElementById('mention-btn') as HTMLButtonElement,
|
||||||
markreadBtn: document.getElementById('markread-btn') as HTMLButtonElement,
|
markreadBtn: document.getElementById('markread-btn') as HTMLButtonElement,
|
||||||
extraPages: document.querySelectorAll('.extra-page') as NodeListOf<HTMLElement>,
|
extraPages: document.querySelectorAll('.extra-page') as NodeListOf<HTMLElement>,
|
||||||
desktopSidebarButtons: document.querySelectorAll("#desktop-aside button") as NodeListOf<HTMLButtonElement>,
|
desktopSidebarButtons: document.querySelectorAll("#desktop-aside button") as NodeListOf<HTMLButtonElement>,
|
||||||
|
|
@ -49,7 +50,9 @@ export const elements = {
|
||||||
scrollableViews: document.querySelectorAll('._scrollableView') as NodeListOf<HTMLElement>,
|
scrollableViews: document.querySelectorAll('._scrollableView') as NodeListOf<HTMLElement>,
|
||||||
loadingScreen: document.querySelector('#loading-screen') as HTMLElement,
|
loadingScreen: document.querySelector('#loading-screen') as HTMLElement,
|
||||||
loadingScreenStatus: document.querySelector('#loading-screen-status') 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>;
|
export const views = new Map<HTMLElement, ScrollableView>;
|
||||||
|
|
@ -348,7 +351,7 @@ export const ui = {
|
||||||
replyIndicatorEl.addEventListener('click', () => {
|
replyIndicatorEl.addEventListener('click', () => {
|
||||||
const _msg = document.querySelector(`[id*="${replyTo.id}"]`) as HTMLElement;
|
const _msg = document.querySelector(`[id*="${replyTo.id}"]`) as HTMLElement;
|
||||||
if (_msg) {
|
if (_msg) {
|
||||||
_msg.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
_msg.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
this.tempClass(_msg, "mentioned-highlight", 1000);
|
this.tempClass(_msg, "mentioned-highlight", 1000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -457,6 +460,7 @@ export const ui = {
|
||||||
|
|
||||||
bubble.addEventListener('dblclick', () => {
|
bubble.addEventListener('dblclick', () => {
|
||||||
prepareMention(msg.id.toString(), parsed);
|
prepareMention(msg.id.toString(), parsed);
|
||||||
|
elements.messageInput?.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
groupDiv.appendChild(bubble);
|
groupDiv.appendChild(bubble);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
import { showNotification } from "./notification";
|
import { showNotification } from "./notification";
|
||||||
import { getBase64 } from "./utils";
|
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, Chat, Contact } from "./types";
|
||||||
|
|
||||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
const url = `${config.wahaUrl}${path}`;
|
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[]> {
|
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}` : "" }`);
|
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', {
|
return request<Message>('/api/sendText', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chatId,
|
chatId,
|
||||||
text,
|
text,
|
||||||
session: config.session,
|
session: config.session,
|
||||||
replyTo: replyTo
|
replyTo: replyTo,
|
||||||
|
mentions: mentions
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -181,5 +186,9 @@ export const waha = {
|
||||||
|
|
||||||
const result = await request<Message>(endpoint, body);
|
const result = await request<Message>(endpoint, body);
|
||||||
return result;
|
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-primary: black;
|
||||||
--text-secondary: black;
|
--text-secondary: black;
|
||||||
--text-muted: rgb(134, 134, 134);
|
--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-incoming: #01abaa;
|
||||||
--bubble-outgoing: #016f6e;
|
--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);
|
--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);
|
--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 */
|
/* Scrollbars styling */
|
||||||
::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(255, 255, 255, 0.16);
|
background: rgba(255, 255, 255, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1029,35 +1033,73 @@ li {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
outline: thick solid var(--bg-secondary);
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loading-screen.collapsed {
|
#loading-screen.collapsed {
|
||||||
transform: translateY(100%);
|
transform-origin: top center;
|
||||||
|
animation: fall 0.8s cubic-bezier(.4, 0, .8, .2) forwards;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mentioning-indicator {
|
#mentioning-indicator {
|
||||||
transition: all .4s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all .4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.4em 0.8em;
|
||||||
max-height: 3em;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
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;
|
max-height: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
opacity: 0;
|
/* opacity: 0; */
|
||||||
pointer-events: none;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.app-container {
|
.app-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -1252,3 +1294,17 @@ h2.modal-title {
|
||||||
color: var(--bg-main);
|
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