From 5ac49522d9368577b3ef649ba76c99737902aa9e Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 18 Jul 2025 23:57:41 +0200 Subject: [PATCH 1/6] Fixed scrolling behavior, reply, cross channel messages --- src/snek/static/message-list.js | 9 +++++---- src/snek/templates/web.html | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/snek/static/message-list.js b/src/snek/static/message-list.js index f8352f0..386a435 100644 --- a/src/snek/static/message-list.js +++ b/src/snek/static/message-list.js @@ -10,7 +10,7 @@ import {app} from "./app.js"; const LONG_TIME = 1000 * 60 * 20 class MessageElement extends HTMLElement { - static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid']; + // static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid']; isVisible() { if (!this) return false; @@ -99,7 +99,9 @@ class MessageList extends HTMLElement { constructor() { super(); app.ws.addEventListener("update_message_text", (data) => { - this.upsertMessage(data); + if (this.messageMap.has(data.uid)) { + this.upsertMessage(data); + } }); app.ws.addEventListener("set_typing", (data) => { this.triggerGlow(data.user_uid,data.color); @@ -119,7 +121,6 @@ class MessageList extends HTMLElement { this.visibleSet.delete(entry.target); } }); - console.log(this.visibleSet); }, { root: this, threshold: 0.1 @@ -189,7 +190,7 @@ class MessageList extends HTMLElement { isScrolledToBottom() { return this.isElementVisible(this.firstElementChild); } - scrollToBottom(force = false, behavior= 'smooth') { + scrollToBottom(force = false, behavior= 'instant') { if (force || this.isScrolledToBottom()) { this.firstElementChild.scrollIntoView({ behavior, block: 'start' }); setTimeout(() => { diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html index 46a0593..0719b36 100644 --- a/src/snek/templates/web.html +++ b/src/snek/templates/web.html @@ -72,12 +72,13 @@ function throttle(fn, wait) { // --- Scroll: load extra messages, throttled --- let isLoadingExtra = false; async function loadExtra() { - const firstMessage = messagesContainer.children[messagesContainer.children.length - 1]; + const firstMessage = messagesContainer.lastElementChild; if (isLoadingExtra || !isScrolledPastHalf() || !firstMessage) return; isLoadingExtra = true; const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at); if (messages.length) { const frag = document.createDocumentFragment(); + messages.reverse(); messages.forEach(msg => { const temp = document.createElement("div"); temp.innerHTML = msg.html; @@ -142,6 +143,17 @@ function replyMessage(message) { chatInputField.focus(); } +messagesContainer.addEventListener("click", (e) => { + if (e.target.tagName === "A" && e.target.getAttribute("href") === "#reply") { + e.preventDefault(); + const messageElement = e.target.closest("chat-message"); + if (messageElement) { + const messageText = messageElement.querySelector(".text").textContent.trim(); + replyMessage(messageText); + } + } +}) + // --- Mention helpers --- function extractMentions(message) { return [...new Set(message.match(/@\w+/g) || [])]; @@ -254,7 +266,7 @@ function updateLayout(doScrollDown) { function isScrolledPastHalf() { let scrollTop = messagesContainer.scrollTop; let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight; - return scrollTop < scrollableHeight / 2; + return Math.abs(scrollTop) > scrollableHeight / 2; } // --- Initial layout update --- From 11e19f48e86542cceb52c4c4f624b11ad11cf2e1 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 19 Jul 2025 00:00:29 +0200 Subject: [PATCH 2/6] Fix g scroll --- src/snek/templates/web.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html index 0719b36..b19c820 100644 --- a/src/snek/templates/web.html +++ b/src/snek/templates/web.html @@ -227,7 +227,7 @@ document.addEventListener('keydown', function(event) { keyTimeout = setTimeout(() => { gPressCount = 0; }, 300); if (gPressCount === 2) { gPressCount = 0; - messagesContainer.querySelector(".message:first-child")?.scrollIntoView({ block: "end", inline: "nearest" }); + messagesContainer.lastElementChild?.scrollIntoView({ block: "end", inline: "nearest" }); loadExtra(); } } From ac47d201d8c4025f4418143e167630019170293f Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 19 Jul 2025 00:13:06 +0200 Subject: [PATCH 3/6] Fixed scrolled to bottom check --- src/snek/static/message-list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snek/static/message-list.js b/src/snek/static/message-list.js index 386a435..68fc098 100644 --- a/src/snek/static/message-list.js +++ b/src/snek/static/message-list.js @@ -191,7 +191,7 @@ class MessageList extends HTMLElement { return this.isElementVisible(this.firstElementChild); } scrollToBottom(force = false, behavior= 'instant') { - if (force || this.isScrolledToBottom()) { + if (force || !this.isScrolledToBottom()) { this.firstElementChild.scrollIntoView({ behavior, block: 'start' }); setTimeout(() => { this.firstElementChild.scrollIntoView({ behavior, block: 'start' }); From 70eebefac7730c87ece64668adbfe237098242e4 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 19 Jul 2025 23:31:23 +0200 Subject: [PATCH 4/6] Fixed upsert error when typing --- src/snek/static/message-list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snek/static/message-list.js b/src/snek/static/message-list.js index 68fc098..9f11453 100644 --- a/src/snek/static/message-list.js +++ b/src/snek/static/message-list.js @@ -230,7 +230,7 @@ class MessageList extends HTMLElement { let message = this.messageMap.get(data.uid); const newMessage = !!message; if (message) { - message.parentElement.removeChild(message); + message.parentElement?.removeChild(message); } if (!data.message) return @@ -240,7 +240,7 @@ class MessageList extends HTMLElement { wrapper.innerHTML = data.html; if (message) { - message.updateMessage(...wrapper.firstElementChild._originalChildren); + message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children)); } else { message = wrapper.firstElementChild; this.messageMap.set(data.uid, message); From 3e2dd7ea04ccd8b01d32fb9af1f5b51c1282052a Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 20 Jul 2025 01:11:05 +0200 Subject: [PATCH 5/6] Moved replay to custom event --- src/snek/static/message-list.js | 13 +++++++++++++ src/snek/templates/web.html | 12 +++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/snek/static/message-list.js b/src/snek/static/message-list.js index 9f11453..519ee9e 100644 --- a/src/snek/static/message-list.js +++ b/src/snek/static/message-list.js @@ -9,6 +9,13 @@ import {app} from "./app.js"; const LONG_TIME = 1000 * 60 * 20 +export class ReplyEvent extends Event { + constructor(messageTextTarget) { + super('reply', { bubbles: true, composed: true }); + this.messageTextTarget = messageTextTarget; + } +} + class MessageElement extends HTMLElement { // static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid']; @@ -51,6 +58,12 @@ class MessageElement extends HTMLElement { } this.timeDiv = this.querySelector('.time span'); + this.replyDiv = this.querySelector('.time a'); + + this.replyDiv.addEventListener('click', (e) => { + e.preventDefault(); + this.dispatchEvent(new ReplyEvent(this.messageDiv)); + }) } if (!this.siblingGenerated && this.nextElementSibling) { diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html index b19c820..f2ff699 100644 --- a/src/snek/templates/web.html +++ b/src/snek/templates/web.html @@ -143,15 +143,9 @@ function replyMessage(message) { chatInputField.focus(); } -messagesContainer.addEventListener("click", (e) => { - if (e.target.tagName === "A" && e.target.getAttribute("href") === "#reply") { - e.preventDefault(); - const messageElement = e.target.closest("chat-message"); - if (messageElement) { - const messageText = messageElement.querySelector(".text").textContent.trim(); - replyMessage(messageText); - } - } +messagesContainer.addEventListener("reply", (e) => { + const messageText = e.messageTextTarget.textContent.trim(); + replyMessage(messageText); }) // --- Mention helpers --- From 8c2e20dfe8e26e892c8d29e92c78011775bd6539 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 20 Jul 2025 03:38:18 +0200 Subject: [PATCH 6/6] Fix reply text --- src/snek/static/base.css | 2 +- src/snek/static/message-list.js | 93 ++++++++++++++++++++++++--------- src/snek/templates/web.html | 5 +- 3 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/snek/static/base.css b/src/snek/static/base.css index 423471a..a1c6d30 100644 --- a/src/snek/static/base.css +++ b/src/snek/static/base.css @@ -368,7 +368,7 @@ input[type="text"], .chat-input textarea { } } -.message.switch-user + .message, .message.long-time + .message, .message:first-child { +.message.switch-user + .message, .message.long-time + .message, .message-list-bottom + .message{ .time { display: block; opacity: 1; diff --git a/src/snek/static/message-list.js b/src/snek/static/message-list.js index 519ee9e..db01550 100644 --- a/src/snek/static/message-list.js +++ b/src/snek/static/message-list.js @@ -11,8 +11,46 @@ const LONG_TIME = 1000 * 60 * 20 export class ReplyEvent extends Event { constructor(messageTextTarget) { - super('reply', { bubbles: true, composed: true }); - this.messageTextTarget = messageTextTarget; + super('reply', { bubbles: true, composed: true }); + this.messageTextTarget = messageTextTarget; + + const newMessage = messageTextTarget.cloneNode(true); + newMessage.style.maxHeight = "0" + messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget); + + newMessage.querySelectorAll('.embed-url-link').forEach(link => { + link.remove() + }) + + newMessage.querySelectorAll('picture').forEach(picture => { + const img = picture.querySelector('img'); + if (img) { + picture.replaceWith(img); + } + }) + + newMessage.querySelectorAll('img').forEach(img => { + const src = img.src || img.currentSrc; + img.replaceWith(document.createTextNode(src)); + }) + + newMessage.querySelectorAll('iframe').forEach(iframe => { + const src = iframe.src || iframe.currentSrc; + iframe.replaceWith(document.createTextNode(src)); + }) + + newMessage.querySelectorAll('a').forEach(a => { + const href = a.getAttribute('href'); + const text = a.innerText || a.textContent; + if (text === href || text === '') { + a.replaceWith(document.createTextNode(href)); + } else { + a.replaceWith(document.createTextNode(`[${text}](${href})`)); + } + }) + + this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim(); + newMessage.remove() } } @@ -123,28 +161,33 @@ class MessageList extends HTMLElement { this.messageMap = new Map(); this.visibleSet = new Set(); this._observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - this.visibleSet.add(entry.target); - const messageElement = entry.target; - if (messageElement instanceof MessageElement) { - messageElement.updateUI(); - } - } else { - this.visibleSet.delete(entry.target); + entries.forEach((entry) => { + if (entry.isIntersecting) { + this.visibleSet.add(entry.target); + const messageElement = entry.target; + if (messageElement instanceof MessageElement) { + messageElement.updateUI(); } - }); + } else { + this.visibleSet.delete(entry.target); + } + }); }, { - root: this, - threshold: 0.1 + root: this, + threshold: 0, }) for(const c of this.children) { this._observer.observe(c); - if (c instanceof MessageElement) { - this.messageMap.set(c.dataset.uid, c); - } + if (c instanceof MessageElement) { + this.messageMap.set(c.dataset.uid, c); + } } + + this.endOfMessages = document.createElement('div'); + this.endOfMessages.classList.add('message-list-bottom'); + this.prepend(this.endOfMessages); + this.scrollToBottom(true); } @@ -188,7 +231,6 @@ class MessageList extends HTMLElement { }; document.addEventListener('keydown', escListener); }) - } isElementVisible(element) { if (!element) return false; @@ -201,13 +243,13 @@ class MessageList extends HTMLElement { ); } isScrolledToBottom() { - return this.isElementVisible(this.firstElementChild); + return this.isElementVisible(this.endOfMessages); } scrollToBottom(force = false, behavior= 'instant') { if (force || !this.isScrolledToBottom()) { - this.firstElementChild.scrollIntoView({ behavior, block: 'start' }); + this.firstElementChild.scrollIntoView({ behavior, block: 'end' }); setTimeout(() => { - this.firstElementChild.scrollIntoView({ behavior, block: 'start' }); + this.firstElementChild.scrollIntoView({ behavior, block: 'end' }); }, 200); } } @@ -241,7 +283,6 @@ class MessageList extends HTMLElement { upsertMessage(data) { let message = this.messageMap.get(data.uid); - const newMessage = !!message; if (message) { message.parentElement?.removeChild(message); } @@ -255,14 +296,14 @@ class MessageList extends HTMLElement { if (message) { message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children)); } else { - message = wrapper.firstElementChild; - this.messageMap.set(data.uid, message); - this._observer.observe(message); + message = wrapper.firstElementChild; + this.messageMap.set(data.uid, message); + this._observer.observe(message); } const scrolledToBottom = this.isScrolledToBottom(); this.prepend(message); - if (scrolledToBottom) this.scrollToBottom(true, !newMessage ? 'smooth' : 'auto'); + if (scrolledToBottom) this.scrollToBottom(true); } } diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html index f2ff699..0e60bec 100644 --- a/src/snek/templates/web.html +++ b/src/snek/templates/web.html @@ -139,12 +139,13 @@ chatInputField.textarea.focus(); // --- Reply helper --- function replyMessage(message) { - chatInputField.value = "```markdown\n> " + (message || '').split("\n").join("\n> ") + "\n```\n"; + chatInputField.value = "```markdown\n> " + (message || '').trim().split("\n").join("\n> ") + "\n```\n"; + chatInputField.textarea.dispatchEvent(new Event('change', { bubbles: true })); chatInputField.focus(); } messagesContainer.addEventListener("reply", (e) => { - const messageText = e.messageTextTarget.textContent.trim(); + const messageText = e.replyText || e.messageTextTarget.textContent.trim(); replyMessage(messageText); })