Lazy-load chat images; scroll to bottom on incoming message only when user has not scrolled up.

This commit is contained in:
天クマ 2026-07-17 15:22:26 -03:00
commit cad283e06b
3 changed files with 123 additions and 60 deletions

View file

@ -26,9 +26,9 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
function loadChats() { function loadChats() {
elements.chatsLoader.classList.remove('hidden');
try { try {
elements.chatsLoader.classList.remove('hidden'); fetchChats().then(async () => {
fetchChats().then(() => {
ui.renderChatList(getChats(), activeChatState, selectChat); ui.renderChatList(getChats(), activeChatState, selectChat);
const hash = window.location.hash; const hash = window.location.hash;
@ -181,8 +181,6 @@ function initWebSocket() {
async function handleIncomingMessage(msg) { async function handleIncomingMessage(msg) {
if (!msg) return; if (!msg) return;
// console.log('[WS] handleIncomingMessage payload:', msg);
ui.updateChatInChatList(msg); ui.updateChatInChatList(msg);
const rawChatId = msg.chatId || msg.from || (msg.chat && msg.chat.id); const rawChatId = msg.chatId || msg.from || (msg.chat && msg.chat.id);
@ -201,21 +199,13 @@ async function handleIncomingMessage(msg) {
const msgId = normalizeId(msg.id) || msg.id; const msgId = normalizeId(msg.id) || msg.id;
const exists = document.getElementById(msgId); const exists = document.getElementById(msgId);
if (!exists) { if (!exists) {
const scrolled = elements.messagesContainer.scrollTop == elements.messagesContainer.scrollTopMax;
ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, (await getAppUser()).id); ui.appendSingleMessage({ ...msg, chatId: msgChatId }, activeChatState.name, (await getAppUser()).id);
ui.scrollToBottom(); if (scrolled) {
ui.scrollToBottom();
}
} }
} }
const chatIndex = getChats().findIndex(c => c.id === msgChatId);
if (chatIndex !== -1) {
const chat = getChats()[chatIndex];
chat.lastMessage = msg.body || msg.text || 'Media message';
chat.timestamp = msg.timestamp ? (msg.timestamp * 1000) : Date.now();
if (!msg.fromMe && (!activeChatState || activeChatState.id !== msgChatId)) {
chat.unreadCount = (chat.unreadCount || 0) + 1;
}
}
} }
async function selectChat(chat, isPopState = false, smoothScroll = true) { async function selectChat(chat, isPopState = false, smoothScroll = true) {
@ -249,6 +239,7 @@ async function selectChat(chat, isPopState = false, smoothScroll = true) {
} }
if (!isPopState && window.location.hash !== `#chat-${chat.id}`) { if (!isPopState && window.location.hash !== `#chat-${chat.id}`) {
window.location.hash = ``;
window.location.hash = `chat-${chat.id}`; window.location.hash = `chat-${chat.id}`;
} }
@ -394,3 +385,7 @@ async function checkWahaStatus() {
ui.updateConnectionStatus(false, 'WAHA Server Offline'); ui.updateConnectionStatus(false, 'WAHA Server Offline');
} }
} }
export function getCurrentChat() {
return activeChatState;
}

View file

@ -1,5 +1,6 @@
import { formatTime, normalizeId } from "./utils.js"; import { formatTime, normalizeId } from "./utils.js";
import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage.js"; import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage.js";
import { getCurrentChat } from "./app.js";
export const elements = { export const elements = {
chatList: document.getElementById('chat-list'), chatList: document.getElementById('chat-list'),
@ -43,7 +44,7 @@ export const ui = {
} }
}) })
}, },
/** /**
* Switch view state when a contact chat is opened or closed * Switch view state when a contact chat is opened or closed
*/ */
@ -80,7 +81,6 @@ export const ui = {
async renderChatList(chats, activeChat, onChatSelect) { async renderChatList(chats, activeChat, onChatSelect) {
elements.chatList.innerHTML = ''; elements.chatList.innerHTML = '';
chats.sort((a, b) => b.timestamp - a.timestamp); chats.sort((a, b) => b.timestamp - a.timestamp);
if (chats.length === 0) { if (chats.length === 0) {
@ -93,34 +93,61 @@ export const ui = {
li.className = `chat-item ${activeChat && activeChat.id === chat.id ? 'active' : ''}`; li.className = `chat-item ${activeChat && activeChat.id === chat.id ? 'active' : ''}`;
li.dataset.id = chat.id; li.dataset.id = chat.id;
const picture = await getChatPicture(chat.id);
const initials = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?'; const initials = chat.name ? chat.name.substring(0, 1).toUpperCase() : '?';
const hasUnread = chat.unreadCount && chat.unreadCount > 0; const hasUnread = chat.unreadCount && chat.unreadCount > 0;
const timeStr = formatTime(chat.timestamp || new Date()); const timeStr = formatTime(chat.timestamp || new Date());
li.innerHTML = ` li.innerHTML = `
<div class="avatar"><img src="${picture.url ? picture.url : ""}" alt="${initials}"></div> <div class="avatar">
<div class="chat-item-info"> <img
<div class="chat-item-meta"> src=""
<span class="chat-item-name">${chat.name}</span> alt="${initials}"
<span class="chat-item-time">${timeStr}</span> data-chat-avatar="${chat.id}"
</div> />
<div class="chat-item-preview"> </div>
<span class="chat-item-msg" data-chatid="${chat.id}">${chat.lastMessage || 'No messages yet'}</span> <div class="chat-item-info">
${hasUnread ? `<span class="unread-badge">${chat.unreadCount}</span>` : ''} <div class="chat-item-meta">
</div> <span class="chat-item-name">${chat.name}</span>
</div> <span class="chat-item-time">${timeStr}</span>
`; </div>
<div class="chat-item-preview">
<span class="chat-item-msg" data-chatid="${chat.id}">
${chat.lastMessage || 'No messages yet'}
</span>
${hasUnread ? `<span class="unread-badge">${chat.unreadCount}</span>` : ''}
</div>
</div>
`;
li.addEventListener('click', () => onChatSelect(chat)); li.addEventListener('click', () => onChatSelect(chat));
elements.chatList.appendChild(li); 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) { async updateChatInChatList(msg) {
const span = document.querySelector(`.chat-item-msg[data-chatid="${msg.fromMe ? msg.to : msg.from}"]`); const chat = document.querySelector(`.chat-item[data-id="${msg.fromMe ? msg.to : msg.from}"]`);
if (span) {
span.innerText = msg.body; 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;
}
} }
}, },
@ -134,7 +161,7 @@ export const ui = {
elements.messagesContainer.innerHTML = '<div class="loading-chats">No messages. Say hello!</div>'; elements.messagesContainer.innerHTML = '<div class="loading-chats">No messages. Say hello!</div>';
return; return;
} }
const loadMore = document.createElement("button"); const loadMore = document.createElement("button");
loadMore.classList.add("load-more-btn"); loadMore.classList.add("load-more-btn");
loadMore.innerText = "Load more"; loadMore.innerText = "Load more";
@ -149,7 +176,7 @@ export const ui = {
this.scrollToBottom(); this.scrollToBottom();
}, },
async loadMoreMessages(chatId, userId) { async loadMoreMessages(chatId, userId) {
const oldest = document.querySelector('.message-group:first-of-type'); const oldest = document.querySelector('.message-group:first-of-type');
const oldestTimestamp = oldest.dataset.timestamp; const oldestTimestamp = oldest.dataset.timestamp;
@ -157,17 +184,17 @@ export const ui = {
console.log(oldest) console.log(oldest)
console.log(oldestId) console.log(oldestId)
console.log(oldestTimestamp) console.log(oldestTimestamp)
const loadMoreButton = document.querySelector('.load-more-btn'); const loadMoreButton = document.querySelector('.load-more-btn');
loadMoreButton.removeEventListener('click', this.loadMoreMessages); loadMoreButton.removeEventListener('click', this.loadMoreMessages);
const msgs = await getMoreChatMessages(chatId, oldestTimestamp, oldestId); const msgs = await getMoreChatMessages(chatId, oldestTimestamp, oldestId);
msgs.shift(); msgs.shift();
msgs.forEach(async msg => { msgs.forEach(async msg => {
loadMoreButton.after(this.generateMessage(msg, userId, chatId)); loadMoreButton.after(this.generateMessage(msg, userId, chatId));
}); });
loadMoreButton.addEventListener('click', this.loadMoreMessages); loadMoreButton.addEventListener('click', this.loadMoreMessages);
}, },
@ -229,7 +256,7 @@ export const ui = {
const bubble = document.createElement('div'); const bubble = document.createElement('div');
bubble.className = 'message-bubble'; bubble.className = 'message-bubble';
let prevUid; let prevUid;
if (msg.participant) { if (msg.participant) {
@ -237,7 +264,7 @@ export const ui = {
} else { } else {
prevUid = msg.from; prevUid = msg.from;
} }
if (!isOutgoing && (!prevMsgEl || prevUid !== prevMsgEl.dataset.from)) { if (!isOutgoing && (!prevMsgEl || prevUid !== prevMsgEl.dataset.from)) {
const senderEl = document.createElement('span'); const senderEl = document.createElement('span');
senderEl.className = 'message-sender'; senderEl.className = 'message-sender';
@ -310,17 +337,18 @@ export const ui = {
if (isOutgoing) { if (isOutgoing) {
if (prevMsgEl && prevMsgEl.classList.contains('outgoing')) { if (prevMsgEl && prevMsgEl.classList.contains('outgoing')) {
groupDiv.classList.add('same-sender');
} else { } else {
groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; groupDiv.appendChild(document.createElement('div')).className = 'message-indicator';
} }
} else if (msg.participant) { } else if (msg.participant) {
if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) { if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) {
groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; groupDiv.appendChild(document.createElement('div')).className = 'message-indicator';
} } else groupDiv.classList.add('same-sender');
} else { } else {
if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) { if (!prevMsgEl || prevUid !== prevMsgEl.dataset.from) {
groupDiv.appendChild(document.createElement('div')).className = 'message-indicator'; groupDiv.appendChild(document.createElement('div')).className = 'message-indicator';
} } else groupDiv.classList.add('same-sender');
} }
groupDiv.appendChild(bubble); groupDiv.appendChild(bubble);
@ -352,7 +380,7 @@ export const ui = {
toggleChatBottomBar() { toggleChatBottomBar() {
elements.chatBottomBar.classList.toggle("collapsed"); elements.chatBottomBar.classList.toggle("collapsed");
}, },
removeChatMessage(msgId) { removeChatMessage(msgId) {
const message = document.getElementById(msgId); const message = document.getElementById(msgId);
if (message) message.remove(); if (message) message.remove();

View file

@ -45,8 +45,8 @@ body {
background-color: var(--bg-main); background-color: var(--bg-main);
color: var(--text-primary); color: var(--text-primary);
background-image: background-image:
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.12) 0px, transparent 50%), 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%); radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.12) 0px, transparent 50%);
} }
a { a {
@ -222,6 +222,7 @@ input:focus {
border: none; border: none;
font-size: 0.9rem; font-size: 0.9rem;
transition: all 0.25s ease; transition: all 0.25s ease;
background-color: var(--bg-secondary);
} }
#chat-search:focus { #chat-search:focus {
@ -281,6 +282,9 @@ input:focus {
.chat-item:hover { .chat-item:hover {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
transform: scale(0.98) skewX(10deg);
filter: blur(.1px);
opacity: .6;
} }
.chat-item-info { .chat-item-info {
@ -470,14 +474,46 @@ input:focus {
height: 100%; height: 100%;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
z-index: 0;
}
.active-chat-container::before {
content: "";
position: absolute;
inset: 0;
background-image: var(--background-image);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0.4;
pointer-events: none;
z-index: 0;
}
.active-chat-container > * {
position: relative;
z-index: 1;
} }
.chat-header { .chat-header {
padding: 16px 24px; padding: .6em 1em;
display: flex; display: flex;
justify-content: space-between; position: relative;
align-items: center; }
background: var(--bg-main);
.chat-header::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
z-index: -1;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.75),
rgba(0, 0, 0, 0.35),
transparent
);
} }
.active-contact-info { .active-contact-info {
@ -516,14 +552,14 @@ input:focus {
.messages-container { .messages-container {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 24px; padding: 1rem;
padding-bottom: .6rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px;
user-select: text; user-select: text;
background-image: background-image:
radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.03) 0%, transparent 40%), radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.03) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(168, 85, 247, 0.03) 0%, transparent 40%); radial-gradient(circle at 90% 80%, rgba(168, 85, 247, 0.03) 0%, transparent 40%);
} }
.load-more-btn { .load-more-btn {
@ -539,6 +575,7 @@ input:focus {
} }
.message-group { .message-group {
margin-top: .8em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 65%; max-width: 65%;
@ -552,11 +589,14 @@ input:focus {
align-self: flex-end; align-self: flex-end;
} }
.message-group.same-sender {
margin-top: .4em;
}
.message-sender { .message-sender {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
margin-bottom: 2px;
width: 100%; width: 100%;
display: flex; display: flex;
} }
@ -570,8 +610,7 @@ input:focus {
.message-bubble { .message-bubble {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: .4em; padding: 8px 10px;
padding: 12px 16px;
font-size: 0.92rem; font-size: 0.92rem;
line-height: 1.5; line-height: 1.5;
position: relative; position: relative;
@ -630,6 +669,7 @@ input:focus {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
background-color: var(--bg-main);
} }
.alternate-panel { .alternate-panel {
@ -825,15 +865,15 @@ input:focus {
display: flex; display: flex;
margin-right: 12px; margin-right: 12px;
} }
.message-group { .message-group {
max-width: 90%; max-width: 90%;
} }
.message-image-attachement { .message-image-attachement {
max-height: 20em; max-height: 20em;
} }
#desktop-aside { #desktop-aside {
display: none; display: none;
} }