Update.
This commit is contained in:
parent
6337350b60
commit
59b0494328
@ -7,38 +7,40 @@
|
|||||||
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
|
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
|
||||||
import { app } from "./app.js";
|
import { app } from "./app.js";
|
||||||
|
|
||||||
const LONG_TIME = 1000 * 60 * 20
|
const LONG_TIME = 1000 * 60 * 20;
|
||||||
|
|
||||||
export class ReplyEvent extends Event {
|
export class ReplyEvent extends Event {
|
||||||
constructor(messageTextTarget) {
|
constructor(messageTextTarget) {
|
||||||
super('reply', { bubbles: true, composed: true });
|
super('reply', { bubbles: true, composed: true });
|
||||||
this.messageTextTarget = messageTextTarget;
|
this.messageTextTarget = messageTextTarget;
|
||||||
|
|
||||||
|
// Clone and sanitize message node to text-only reply
|
||||||
const newMessage = messageTextTarget.cloneNode(true);
|
const newMessage = messageTextTarget.cloneNode(true);
|
||||||
newMessage.style.maxHeight = "0"
|
newMessage.style.maxHeight = "0";
|
||||||
messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget);
|
messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget);
|
||||||
|
|
||||||
newMessage.querySelectorAll('.embed-url-link').forEach(link => {
|
// Remove all .embed-url-link
|
||||||
link.remove()
|
newMessage.querySelectorAll('.embed-url-link').forEach(link => link.remove());
|
||||||
})
|
|
||||||
|
|
||||||
|
// Replace <picture> with their <img>
|
||||||
newMessage.querySelectorAll('picture').forEach(picture => {
|
newMessage.querySelectorAll('picture').forEach(picture => {
|
||||||
const img = picture.querySelector('img');
|
const img = picture.querySelector('img');
|
||||||
if (img) {
|
if (img) picture.replaceWith(img);
|
||||||
picture.replaceWith(img);
|
});
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// Replace <img> with just their src
|
||||||
newMessage.querySelectorAll('img').forEach(img => {
|
newMessage.querySelectorAll('img').forEach(img => {
|
||||||
const src = img.src || img.currentSrc;
|
const src = img.src || img.currentSrc;
|
||||||
img.replaceWith(document.createTextNode(src));
|
img.replaceWith(document.createTextNode(src));
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// Replace <iframe> with their src
|
||||||
newMessage.querySelectorAll('iframe').forEach(iframe => {
|
newMessage.querySelectorAll('iframe').forEach(iframe => {
|
||||||
const src = iframe.src || iframe.currentSrc;
|
const src = iframe.src || iframe.currentSrc;
|
||||||
iframe.replaceWith(document.createTextNode(src));
|
iframe.replaceWith(document.createTextNode(src));
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// Replace <a> with href or markdown
|
||||||
newMessage.querySelectorAll('a').forEach(a => {
|
newMessage.querySelectorAll('a').forEach(a => {
|
||||||
const href = a.getAttribute('href');
|
const href = a.getAttribute('href');
|
||||||
const text = a.innerText || a.textContent;
|
const text = a.innerText || a.textContent;
|
||||||
@ -47,22 +49,21 @@ export class ReplyEvent extends Event {
|
|||||||
} else {
|
} else {
|
||||||
a.replaceWith(document.createTextNode(`[${text}](${href})`));
|
a.replaceWith(document.createTextNode(`[${text}](${href})`));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim();
|
this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim();
|
||||||
newMessage.remove()
|
newMessage.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageElement extends HTMLElement {
|
class MessageElement extends HTMLElement {
|
||||||
// static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
|
||||||
|
|
||||||
updateUI() {
|
updateUI() {
|
||||||
if (this._originalChildren === undefined) {
|
if (this._originalChildren === undefined) {
|
||||||
const { color, user_nick, created_at, user_uid } = this.dataset;
|
const { color, user_nick, created_at, user_uid } = this.dataset;
|
||||||
this.classList.add('message');
|
this.classList.add('message');
|
||||||
this.style.maxWidth = '100%';
|
this.style.maxWidth = '100%';
|
||||||
this._originalChildren = Array.from(this.children);
|
this._originalChildren = Array.from(this.children);
|
||||||
|
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<a class="avatar" style="background-color: ${color || ''}; color: black;" href="/user/${user_uid || ''}.html">
|
<a class="avatar" style="background-color: ${color || ''}; color: black;" href="/user/${user_uid || ''}.html">
|
||||||
<img class="avatar-img" width="40" height="40" src="/avatar/${user_uid || ''}.svg" alt="${user_nick || ''}" loading="lazy">
|
<img class="avatar-img" width="40" height="40" src="/avatar/${user_uid || ''}.svg" alt="${user_nick || ''}" loading="lazy">
|
||||||
@ -72,12 +73,12 @@ class MessageElement extends HTMLElement {
|
|||||||
<div class="text"></div>
|
<div class="text"></div>
|
||||||
<div class="time no-select" data-created_at="${created_at || ''}">
|
<div class="time no-select" data-created_at="${created_at || ''}">
|
||||||
<span></span>
|
<span></span>
|
||||||
<a href="#reply">reply</a></div>
|
<a href="#reply">reply</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.messageDiv = this.querySelector('.text');
|
this.messageDiv = this.querySelector('.text');
|
||||||
|
|
||||||
if (this._originalChildren && this._originalChildren.length > 0) {
|
if (this._originalChildren && this._originalChildren.length > 0) {
|
||||||
this._originalChildren.forEach(child => {
|
this._originalChildren.forEach(child => {
|
||||||
this.messageDiv.appendChild(child);
|
this.messageDiv.appendChild(child);
|
||||||
@ -90,9 +91,10 @@ class MessageElement extends HTMLElement {
|
|||||||
this.replyDiv.addEventListener('click', (e) => {
|
this.replyDiv.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.dispatchEvent(new ReplyEvent(this.messageDiv));
|
this.dispatchEvent(new ReplyEvent(this.messageDiv));
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sibling logic for user switches and long time gaps
|
||||||
if ((!this.siblingGenerated || this.siblingGenerated !== this.nextElementSibling) && this.nextElementSibling) {
|
if ((!this.siblingGenerated || this.siblingGenerated !== this.nextElementSibling) && this.nextElementSibling) {
|
||||||
this.siblingGenerated = this.nextElementSibling;
|
this.siblingGenerated = this.nextElementSibling;
|
||||||
if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) {
|
if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) {
|
||||||
@ -115,7 +117,7 @@ class MessageElement extends HTMLElement {
|
|||||||
|
|
||||||
updateMessage(...messages) {
|
updateMessage(...messages) {
|
||||||
if (this._originalChildren) {
|
if (this._originalChildren) {
|
||||||
this.messageDiv.replaceChildren(...messages)
|
this.messageDiv.replaceChildren(...messages);
|
||||||
this._originalChildren = messages;
|
this._originalChildren = messages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,20 +126,50 @@ class MessageElement extends HTMLElement {
|
|||||||
this.updateUI();
|
this.updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {}
|
||||||
}
|
connectedMoveCallback() {}
|
||||||
|
|
||||||
connectedMoveCallback() {
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(name, oldValue, newValue) {
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
this.updateUI()
|
this.updateUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageList extends HTMLElement {
|
class MessageList extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.messageMap = new Map();
|
||||||
|
this.visibleSet = new Set();
|
||||||
|
|
||||||
|
this._observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.visibleSet.add(entry.target);
|
||||||
|
if (entry.target instanceof MessageElement) {
|
||||||
|
entry.target.updateUI();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.visibleSet.delete(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
root: this,
|
||||||
|
threshold: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// End-of-messages marker
|
||||||
|
this.endOfMessages = document.createElement('div');
|
||||||
|
this.endOfMessages.classList.add('message-list-bottom');
|
||||||
|
this.prepend(this.endOfMessages);
|
||||||
|
|
||||||
|
// Observe existing children and index by uid
|
||||||
|
for (const c of this.children) {
|
||||||
|
this._observer.observe(c);
|
||||||
|
if (c instanceof MessageElement) {
|
||||||
|
this.messageMap.set(c.dataset.uid, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire up socket events
|
||||||
app.ws.addEventListener("update_message_text", (data) => {
|
app.ws.addEventListener("update_message_text", (data) => {
|
||||||
if (this.messageMap.has(data.uid)) {
|
if (this.messageMap.has(data.uid)) {
|
||||||
this.upsertMessage(data);
|
this.upsertMessage(data);
|
||||||
@ -147,45 +179,17 @@ class MessageList extends HTMLElement {
|
|||||||
this.triggerGlow(data.user_uid, data.color);
|
this.triggerGlow(data.user_uid, data.color);
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
root: this,
|
|
||||||
threshold: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.endOfMessages = document.createElement('div');
|
|
||||||
this.endOfMessages.classList.add('message-list-bottom');
|
|
||||||
this.prepend(this.endOfMessages);
|
|
||||||
|
|
||||||
for(const c of this.children) {
|
|
||||||
this._observer.observe(c);
|
|
||||||
if (c instanceof MessageElement) {
|
|
||||||
this.messageMap.set(c.dataset.uid, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scrollToBottom(true);
|
this.scrollToBottom(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.addEventListener('click', (e) => {
|
this.addEventListener('click', (e) => {
|
||||||
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
|
if (
|
||||||
|
e.target.tagName !== 'IMG' ||
|
||||||
|
e.target.classList.contains('avatar-img')
|
||||||
|
) return;
|
||||||
|
|
||||||
const img = e.target;
|
const img = e.target;
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:pointer;';
|
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:pointer;';
|
||||||
|
|
||||||
@ -206,12 +210,11 @@ class MessageList extends HTMLElement {
|
|||||||
|
|
||||||
overlay.appendChild(fullImg);
|
overlay.appendChild(fullImg);
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
overlay.addEventListener('click', () => {
|
overlay.addEventListener('click', () => {
|
||||||
if (overlay.parentNode) {
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||||
overlay.parentNode.removeChild(overlay);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Optional: ESC key closes overlay
|
// ESC to close
|
||||||
const escListener = (evt) => {
|
const escListener = (evt) => {
|
||||||
if (evt.key === 'Escape') {
|
if (evt.key === 'Escape') {
|
||||||
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||||
@ -219,8 +222,9 @@ class MessageList extends HTMLElement {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', escListener);
|
document.addEventListener('keydown', escListener);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isElementVisible(element) {
|
isElementVisible(element) {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
@ -231,9 +235,11 @@ class MessageList extends HTMLElement {
|
|||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isScrolledToBottom() {
|
isScrolledToBottom() {
|
||||||
return this.visibleSet.has(this.endOfMessages)
|
return this.visibleSet.has(this.endOfMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom(force = false, behavior = 'instant') {
|
scrollToBottom(force = false, behavior = 'instant') {
|
||||||
if (force || !this.isScrolledToBottom()) {
|
if (force || !this.isScrolledToBottom()) {
|
||||||
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
|
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
|
||||||
@ -261,13 +267,12 @@ class MessageList extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateTimes() {
|
updateTimes() {
|
||||||
this.visibleSet.forEach((messageElement) => {
|
this.visibleSet.forEach((messageElement) => {
|
||||||
if (messageElement instanceof MessageElement) {
|
if (messageElement instanceof MessageElement) {
|
||||||
messageElement.updateUI();
|
messageElement.updateUI();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
upsertMessage(data) {
|
upsertMessage(data) {
|
||||||
@ -275,16 +280,16 @@ class MessageList extends HTMLElement {
|
|||||||
if (message) {
|
if (message) {
|
||||||
message.parentElement?.removeChild(message);
|
message.parentElement?.removeChild(message);
|
||||||
}
|
}
|
||||||
|
if (!data.message) return;
|
||||||
if (!data.message) return
|
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
|
|
||||||
wrapper.innerHTML = data.html;
|
wrapper.innerHTML = data.html;
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
|
// If the old element is already custom, only update its message children
|
||||||
message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children));
|
message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children));
|
||||||
} else {
|
} else {
|
||||||
|
// If not, insert the new one and observe
|
||||||
message = wrapper.firstElementChild;
|
message = wrapper.firstElementChild;
|
||||||
this.messageMap.set(data.uid, message);
|
this.messageMap.set(data.uid, message);
|
||||||
this._observer.observe(message);
|
this._observer.observe(message);
|
||||||
@ -298,3 +303,4 @@ class MessageList extends HTMLElement {
|
|||||||
|
|
||||||
customElements.define("chat-message", MessageElement);
|
customElements.define("chat-message", MessageElement);
|
||||||
customElements.define("message-list", MessageList);
|
customElements.define("message-list", MessageList);
|
||||||
|
|
||||||
|
@ -188,6 +188,7 @@ class WebdavApplication(aiohttp.web.Application):
|
|||||||
headers = {
|
headers = {
|
||||||
"DAV": "1, 2",
|
"DAV": "1, 2",
|
||||||
"Allow": "OPTIONS, GET, PUT, DELETE, MKCOL, MOVE, COPY, PROPFIND, PROPPATCH, PROPGET, PROPSET, PROPDEL",
|
"Allow": "OPTIONS, GET, PUT, DELETE, MKCOL, MOVE, COPY, PROPFIND, PROPPATCH, PROPGET, PROPSET, PROPDEL",
|
||||||
|
"MS-Author-Via": "DAV",
|
||||||
}
|
}
|
||||||
return aiohttp.web.Response(status=200, headers=headers)
|
return aiohttp.web.Response(status=200, headers=headers)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user