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); })