Merge pull request 'maintenance/clean-up-messages' (#65) from BordedDev/snek:maintenance/clean-up-messages into main
Reviewed-on: #65 Reviewed-by: retoor <retoor@noreply@molodetz.nl>
This commit is contained in:
commit
a2d506cce9
@ -5,7 +5,6 @@ from snek.system.template import whitelist_attributes
|
|||||||
class ChannelMessageService(BaseService):
|
class ChannelMessageService(BaseService):
|
||||||
mapper_name = "channel_message"
|
mapper_name = "channel_message"
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._configured_indexes = False
|
self._configured_indexes = False
|
||||||
@ -18,7 +17,7 @@ class ChannelMessageService(BaseService):
|
|||||||
html = message["html"]
|
html = message["html"]
|
||||||
await self.save(message)
|
await self.save(message)
|
||||||
|
|
||||||
self.mapper.db['channel_message'].upsert(
|
self.mapper.db["channel_message"].upsert(
|
||||||
{
|
{
|
||||||
"uid": message["uid"],
|
"uid": message["uid"],
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
@ -41,7 +40,6 @@ class ChannelMessageService(BaseService):
|
|||||||
if not changed:
|
if not changed:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
async def create(self, channel_uid, user_uid, message, is_final=True):
|
async def create(self, channel_uid, user_uid, message, is_final=True):
|
||||||
model = await self.new()
|
model = await self.new()
|
||||||
|
|
||||||
@ -72,12 +70,18 @@ class ChannelMessageService(BaseService):
|
|||||||
|
|
||||||
if await super().save(model):
|
if await super().save(model):
|
||||||
if not self._configured_indexes:
|
if not self._configured_indexes:
|
||||||
if not self.mapper.db["channel_message"].has_index(['is_final','user_uid','channel_uid']):
|
if not self.mapper.db["channel_message"].has_index(
|
||||||
self.mapper.db["channel_message"].create_index(['is_final','user_uid','channel_uid'], unique=False)
|
["is_final", "user_uid", "channel_uid"]
|
||||||
if not self.mapper.db["channel_message"].has_index(['uid']):
|
):
|
||||||
self.mapper.db["channel_message"].create_index(['uid'], unique=True)
|
self.mapper.db["channel_message"].create_index(
|
||||||
if not self.mapper.db["channel_message"].has_index(['deleted_at']):
|
["is_final", "user_uid", "channel_uid"], unique=False
|
||||||
self.mapper.db["channel_message"].create_index(['deleted_at'], unique=False)
|
)
|
||||||
|
if not self.mapper.db["channel_message"].has_index(["uid"]):
|
||||||
|
self.mapper.db["channel_message"].create_index(["uid"], unique=True)
|
||||||
|
if not self.mapper.db["channel_message"].has_index(["deleted_at"]):
|
||||||
|
self.mapper.db["channel_message"].create_index(
|
||||||
|
["deleted_at"], unique=False
|
||||||
|
)
|
||||||
self._configured_indexes = True
|
self._configured_indexes = True
|
||||||
return model
|
return model
|
||||||
raise Exception(f"Failed to create channel message: {model.errors}.")
|
raise Exception(f"Failed to create channel message: {model.errors}.")
|
||||||
@ -86,6 +90,11 @@ class ChannelMessageService(BaseService):
|
|||||||
user = await self.services.user.get(uid=message["user_uid"])
|
user = await self.services.user.get(uid=message["user_uid"])
|
||||||
if not user:
|
if not user:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
if not message["html"].startswith("<chat-message"):
|
||||||
|
await (await self.get(uid=message["uid"])).save()
|
||||||
|
message["html"] = (await self.get(uid=message["uid"])).html
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uid": message["uid"],
|
"uid": message["uid"],
|
||||||
"color": user["color"],
|
"color": user["color"],
|
||||||
@ -115,7 +124,6 @@ class ChannelMessageService(BaseService):
|
|||||||
model["html"] = whitelist_attributes(model["html"])
|
model["html"] = whitelist_attributes(model["html"])
|
||||||
return await super().save(model)
|
return await super().save(model)
|
||||||
|
|
||||||
|
|
||||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||||
channel = await self.services.channel.get(uid=channel_uid)
|
channel = await self.services.channel.get(uid=channel_uid)
|
||||||
if not channel:
|
if not channel:
|
||||||
|
@ -144,7 +144,7 @@ footer {
|
|||||||
|
|
||||||
.chat-messages {
|
.chat-messages {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -368,7 +368,7 @@ input[type="text"], .chat-input textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message:has(+ .message.switch-user), .message:has(+ .message.long-time), .message:not(:has(+ .message)) {
|
.message.switch-user + .message, .message.long-time + .message, .message:first-child {
|
||||||
.time {
|
.time {
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -298,7 +298,9 @@ textToLeetAdvanced(text) {
|
|||||||
this.appendChild(this.uploadButton);
|
this.appendChild(this.uploadButton);
|
||||||
|
|
||||||
this.textarea.addEventListener("blur", () => {
|
this.textarea.addEventListener("blur", () => {
|
||||||
this.updateFromInput("");
|
this.updateFromInput(this.value, true).then(
|
||||||
|
this.updateFromInput("")
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subscribe("file-uploads-done", (data)=>{
|
this.subscribe("file-uploads-done", (data)=>{
|
||||||
@ -417,7 +419,7 @@ textToLeetAdvanced(text) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
updateFromInput(value) {
|
updateFromInput(value, isFinal = false) {
|
||||||
|
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|
||||||
@ -425,7 +427,7 @@ textToLeetAdvanced(text) {
|
|||||||
|
|
||||||
if (this.liveType && value[0] !== "/") {
|
if (this.liveType && value[0] !== "/") {
|
||||||
const messageText = this.replaceMentionsWithAuthors(value);
|
const messageText = this.replaceMentionsWithAuthors(value);
|
||||||
this.messageUid = this.sendMessage(this.channelUid, messageText, !this.liveType);
|
this.messageUid = this.sendMessage(this.channelUid, messageText, !this.liveType || isFinal);
|
||||||
return this.messageUid;
|
return this.messageUid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,112 +5,252 @@
|
|||||||
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
|
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
|
class MessageElement extends HTMLElement {
|
||||||
|
static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
||||||
|
|
||||||
|
isVisible() {
|
||||||
|
if (!this) return false;
|
||||||
|
const rect = this.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
if (this._originalChildren === undefined) {
|
||||||
|
const { color, user_nick, created_at, user_uid} = this.dataset;
|
||||||
|
this.classList.add('message');
|
||||||
|
this.style.maxWidth = '100%';
|
||||||
|
this._originalChildren = Array.from(this.children);
|
||||||
|
this.innerHTML = `
|
||||||
|
<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">
|
||||||
|
</a>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="author" style="color: ${color || ''};">${user_nick || ''}</div>
|
||||||
|
<div class="text"></div>
|
||||||
|
<div class="time no-select" data-created_at="${created_at || ''}">
|
||||||
|
<span></span>
|
||||||
|
<a href="#reply">reply</a></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.messageDiv = this.querySelector('.text');
|
||||||
|
|
||||||
|
if (this._originalChildren && this._originalChildren.length > 0) {
|
||||||
|
this._originalChildren.forEach(child => {
|
||||||
|
this.messageDiv.appendChild(child);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeDiv = this.querySelector('.time span');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.siblingGenerated && this.nextElementSibling) {
|
||||||
|
this.siblingGenerated = true;
|
||||||
|
if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) {
|
||||||
|
this.classList.add('switch-user');
|
||||||
|
} else {
|
||||||
|
this.classList.remove('switch-user');
|
||||||
|
const siblingTime = new Date(this.nextElementSibling.dataset.created_at);
|
||||||
|
const currentTime = new Date(this.dataset.created_at);
|
||||||
|
|
||||||
|
if (currentTime.getTime() - siblingTime.getTime() > LONG_TIME) {
|
||||||
|
this.classList.add('long-time');
|
||||||
|
} else {
|
||||||
|
this.classList.remove('long-time');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeDiv.innerText = app.timeDescription(this.dataset.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMessage(...messages) {
|
||||||
|
if (this._originalChildren) {
|
||||||
|
this.messageDiv.replaceChildren(...messages)
|
||||||
|
this._originalChildren = messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedMoveCallback() {
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
|
this.updateUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MessageList extends HTMLElement {
|
class MessageList extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
app.ws.addEventListener("update_message_text", (data) => {
|
app.ws.addEventListener("update_message_text", (data) => {
|
||||||
this.updateMessageText(data.uid, data);
|
this.upsertMessage(data);
|
||||||
});
|
});
|
||||||
app.ws.addEventListener("set_typing", (data) => {
|
app.ws.addEventListener("set_typing", (data) => {
|
||||||
this.triggerGlow(data.user_uid,data.color);
|
this.triggerGlow(data.user_uid,data.color);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.items = [];
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(this.visibleSet);
|
||||||
|
}, {
|
||||||
|
root: this,
|
||||||
|
threshold: 0.1
|
||||||
|
})
|
||||||
|
|
||||||
|
for(const c of this.children) {
|
||||||
|
this._observer.observe(c);
|
||||||
|
if (c instanceof MessageElement) {
|
||||||
|
this.messageMap.set(c.dataset.uid, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.scrollToBottom(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const messagesContainer = this
|
this.addEventListener('click', (e) => {
|
||||||
messagesContainer.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;'
|
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;';
|
||||||
|
|
||||||
const urlObj = new URL(img.currentSrc || img.src)
|
const urlObj = new URL(img.currentSrc || img.src, window.location.origin);
|
||||||
urlObj.searchParams.delete("width");
|
urlObj.searchParams.delete('width');
|
||||||
urlObj.searchParams.delete("height");
|
urlObj.searchParams.delete('height');
|
||||||
|
|
||||||
const fullImg = document.createElement('img');
|
const fullImg = document.createElement('img');
|
||||||
|
fullImg.src = urlObj.toString();
|
||||||
|
fullImg.alt = img.alt || '';
|
||||||
|
fullImg.style.maxWidth = '90%';
|
||||||
|
fullImg.style.maxHeight = '90%';
|
||||||
|
fullImg.style.boxShadow = '0 0 32px #000';
|
||||||
|
fullImg.style.borderRadius = '8px';
|
||||||
|
fullImg.style.background = '#222';
|
||||||
|
fullImg.style.objectFit = 'contain';
|
||||||
|
fullImg.loading = 'lazy';
|
||||||
|
|
||||||
fullImg.src = urlObj.toString();
|
overlay.appendChild(fullImg);
|
||||||
fullImg.alt = img.alt;
|
document.body.appendChild(overlay);
|
||||||
fullImg.style.maxWidth = '90%';
|
overlay.addEventListener('click', () => {
|
||||||
fullImg.style.maxHeight = '90%';
|
if (overlay.parentNode) {
|
||||||
|
overlay.parentNode.removeChild(overlay);
|
||||||
overlay.appendChild(fullImg);
|
}
|
||||||
document.body.appendChild(overlay);
|
});
|
||||||
overlay.addEventListener('click', () => document.body.removeChild(overlay));
|
// Optional: ESC key closes overlay
|
||||||
|
const escListener = (evt) => {
|
||||||
|
if (evt.key === 'Escape') {
|
||||||
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||||
|
document.removeEventListener('keydown', escListener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', escListener);
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
isElementVisible(element) {
|
isElementVisible(element) {
|
||||||
|
if (!element) return false;
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
return (
|
return (
|
||||||
rect.top >= 0 &&
|
rect.top >= 0 &&
|
||||||
rect.left >= 0 &&
|
rect.left >= 0 &&
|
||||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
isScrolledToBottom() {
|
isScrolledToBottom() {
|
||||||
return this.isElementVisible(this.querySelector(".message-list-bottom"));
|
return this.isElementVisible(this.firstElementChild);
|
||||||
}
|
}
|
||||||
scrollToBottom(force) {
|
scrollToBottom(force = false, behavior= 'smooth') {
|
||||||
//this.scrollTop = this.scrollHeight;
|
if (force || this.isScrolledToBottom()) {
|
||||||
|
this.firstElementChild.scrollIntoView({ behavior, block: 'start' });
|
||||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
|
||||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
this.firstElementChild.scrollIntoView({ behavior, block: 'start' });
|
||||||
// this.scrollTop = this.scrollHeight;
|
}, 200);
|
||||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
|
||||||
},200)
|
|
||||||
}
|
|
||||||
updateMessageText(uid, message) {
|
|
||||||
const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
|
|
||||||
|
|
||||||
if (!messageDiv) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const scrollToBottom = this.isScrolledToBottom();
|
|
||||||
const receivedHtml = document.createElement("div");
|
|
||||||
receivedHtml.innerHTML = message.html;
|
|
||||||
const html = receivedHtml.querySelector(".text").innerHTML;
|
|
||||||
const textElement = messageDiv.querySelector(".text");
|
|
||||||
textElement.innerHTML = html;
|
|
||||||
textElement.style.display = message.text == "" ? "none" : "block";
|
|
||||||
if(scrollToBottom)
|
|
||||||
this.scrollToBottom(true)
|
|
||||||
}
|
}
|
||||||
triggerGlow(uid,color) {
|
|
||||||
app.starField.glowColor(color)
|
triggerGlow(uid, color) {
|
||||||
let lastElement = null;
|
if (!uid || !color) return;
|
||||||
this.querySelectorAll(".avatar").forEach((el) => {
|
app.starField.glowColor(color);
|
||||||
const div = el.closest("a");
|
let lastElement = null;
|
||||||
if (el.href.indexOf(uid) != -1) {
|
this.querySelectorAll('.avatar').forEach((el) => {
|
||||||
|
const anchor = el.closest('a');
|
||||||
|
if (anchor && typeof anchor.href === 'string' && anchor.href.includes(uid)) {
|
||||||
lastElement = el;
|
lastElement = el;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (lastElement) {
|
if (lastElement) {
|
||||||
lastElement.classList.add("glow");
|
lastElement.classList.add('glow');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
lastElement.classList.remove("glow");
|
lastElement.classList.remove('glow');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set data(items) {
|
|
||||||
this.items = items;
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
this.innerHTML = "";
|
|
||||||
|
|
||||||
//this.insertAdjacentHTML("beforeend", html);
|
updateTimes() {
|
||||||
|
this.visibleSet.forEach((messageElement) => {
|
||||||
|
if (messageElement instanceof MessageElement) {
|
||||||
|
messageElement.updateUI();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertMessage(data) {
|
||||||
|
let message = this.messageMap.get(data.uid);
|
||||||
|
const newMessage = !!message;
|
||||||
|
if (message) {
|
||||||
|
message.parentElement.removeChild(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.message) return
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
|
||||||
|
wrapper.innerHTML = data.html;
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
message.updateMessage(...wrapper.firstElementChild._originalChildren);
|
||||||
|
} else {
|
||||||
|
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("chat-message", MessageElement);
|
||||||
customElements.define("message-list", MessageList);
|
customElements.define("message-list", MessageList);
|
||||||
|
@ -115,7 +115,7 @@ app.starField.renderWord("H4x0r 1337")
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("keydown", async() => {
|
document.addEventListener("keydown", async(event) => {
|
||||||
if(prevKey == "Escape"){
|
if(prevKey == "Escape"){
|
||||||
document.querySelector("chat-input").querySelector("textarea").value = "";
|
document.querySelector("chat-input").querySelector("textarea").value = "";
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
<div style="max-width:100%;" data-uid="{{uid}}" data-color="{{color}}" data-channel_uid="{{channel_uid}}" data-user_nick="{{user_nick}}" data-created_at="{{created_at}}" data-user_uid="{{user_uid}}" class="message"><a class="avatar" style="background-color: {{color}}; color: black;" href="/user/{{user_uid}}.html"><img class="avatar-img" width="40px" height="40px" src="/avatar/{{user_uid}}.svg" /></a><div class="message-content"><div class="author" style="color: {{color}};">{{user_nick}}</div><div class="text">{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}</div><div class="time no-select" data-created_at="{{created_at}}"></div></div></div>
|
<chat-message data-uid="{{uid}}" data-color="{{color}}" data-channel_uid="{{channel_uid}}" data-user_nick="{{user_nick}}" data-created_at="{{created_at}}" data-user_uid="{{user_uid}}">{% autoescape false %}{% emoji %}{% linkify %}{% markdown %}{% autoescape false %}{{ message }}{%raw %} {% endraw%}{%endautoescape%}{% endmarkdown %}{% endlinkify %}{% endemoji %}{% endautoescape %}</chat-message>
|
@ -25,12 +25,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for message in messages %}
|
{% for message in messages|reverse %}
|
||||||
{% autoescape false %}
|
{% autoescape false %}
|
||||||
{{ message.html }}
|
{{ message.html }}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="message-list-bottom"></div>
|
|
||||||
</message-list>
|
</message-list>
|
||||||
<chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
|
<chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
|
||||||
</section>
|
</section>
|
||||||
@ -73,7 +72,7 @@ function throttle(fn, wait) {
|
|||||||
// --- Scroll: load extra messages, throttled ---
|
// --- Scroll: load extra messages, throttled ---
|
||||||
let isLoadingExtra = false;
|
let isLoadingExtra = false;
|
||||||
async function loadExtra() {
|
async function loadExtra() {
|
||||||
const firstMessage = messagesContainer.querySelector(".message:first-child");
|
const firstMessage = messagesContainer.children[messagesContainer.children.length - 1];
|
||||||
if (isLoadingExtra || !isScrolledPastHalf() || !firstMessage) return;
|
if (isLoadingExtra || !isScrolledPastHalf() || !firstMessage) return;
|
||||||
isLoadingExtra = true;
|
isLoadingExtra = true;
|
||||||
const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);
|
const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);
|
||||||
@ -84,7 +83,7 @@ async function loadExtra() {
|
|||||||
temp.innerHTML = msg.html;
|
temp.innerHTML = msg.html;
|
||||||
frag.appendChild(temp.firstChild);
|
frag.appendChild(temp.firstChild);
|
||||||
});
|
});
|
||||||
firstMessage.parentNode.insertBefore(frag, firstMessage);
|
messagesContainer.appendChild(frag);
|
||||||
updateLayout(false);
|
updateLayout(false);
|
||||||
}
|
}
|
||||||
isLoadingExtra = false;
|
isLoadingExtra = false;
|
||||||
@ -93,32 +92,7 @@ messagesContainer.addEventListener("scroll", throttle(loadExtra, 200));
|
|||||||
|
|
||||||
// --- Only update visible times ---
|
// --- Only update visible times ---
|
||||||
function updateTimes() {
|
function updateTimes() {
|
||||||
const containers = messagesContainer.querySelectorAll(".time");
|
messagesContainer.updateTimes();
|
||||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
||||||
containers.forEach(container => {
|
|
||||||
const rect = container.getBoundingClientRect();
|
|
||||||
if (rect.top >= 0 && rect.bottom <= viewportHeight) {
|
|
||||||
const messageDiv = container.closest('.message');
|
|
||||||
let text = messageDiv.querySelector(".text").innerText;
|
|
||||||
const time = document.createElement("span");
|
|
||||||
time.innerText = app.timeDescription(container.dataset.created_at);
|
|
||||||
messageDiv.querySelector(".text").querySelectorAll("img").forEach(img => {
|
|
||||||
text += " " + img.src
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
container.replaceChildren(time);
|
|
||||||
const reply = document.createElement("a");
|
|
||||||
reply.innerText = " reply";
|
|
||||||
reply.href = "#reply";
|
|
||||||
container.appendChild(reply);
|
|
||||||
reply.addEventListener('click', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
replyMessage(text);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setInterval(() => requestIdleCallback(updateTimes), 30000);
|
setInterval(() => requestIdleCallback(updateTimes), 30000);
|
||||||
|
|
||||||
@ -164,7 +138,7 @@ chatInputField.textarea.focus();
|
|||||||
|
|
||||||
// --- Reply helper ---
|
// --- Reply helper ---
|
||||||
function replyMessage(message) {
|
function replyMessage(message) {
|
||||||
chatInputField.value = "```markdown\n> " + (message || '') + "\n```\n";
|
chatInputField.value = "```markdown\n> " + (message || '').split("\n").join("\n> ") + "\n```\n";
|
||||||
chatInputField.focus();
|
chatInputField.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,22 +197,8 @@ app.addEventListener("channel-message", (data) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const lastElement = messagesContainer.querySelector(".message-list-bottom");
|
|
||||||
const doScrollDown = messagesContainer.isScrolledToBottom();
|
|
||||||
|
|
||||||
const oldMessage = messagesContainer.querySelector(`.message[data-uid="${data.uid}"]`);
|
messagesContainer.upsertMessage(data)
|
||||||
if (oldMessage) {
|
|
||||||
oldMessage.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = document.createElement("div");
|
|
||||||
|
|
||||||
|
|
||||||
message.innerHTML = data.html;
|
|
||||||
message.style.display = display;
|
|
||||||
messagesContainer.insertBefore(message.firstChild, lastElement);
|
|
||||||
updateLayout(doScrollDown);
|
|
||||||
setTimeout(() => updateLayout(doScrollDown), 1000);
|
|
||||||
app.rpc.markAsRead(channelUid);
|
app.rpc.markAsRead(channelUid);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -284,27 +244,6 @@ document.addEventListener('keydown', function(event) {
|
|||||||
// --- Layout update ---
|
// --- Layout update ---
|
||||||
function updateLayout(doScrollDown) {
|
function updateLayout(doScrollDown) {
|
||||||
updateTimes();
|
updateTimes();
|
||||||
let previousUser = null, previousDate = null;
|
|
||||||
messagesContainer.querySelectorAll(".message").forEach((message) => {
|
|
||||||
if (previousUser !== message.dataset.user_uid) {
|
|
||||||
message.classList.add("switch-user");
|
|
||||||
previousUser = message.dataset.user_uid;
|
|
||||||
previousDate = new Date(message.dataset.created_at);
|
|
||||||
} else {
|
|
||||||
message.classList.remove("switch-user");
|
|
||||||
if (!previousDate) {
|
|
||||||
previousDate = new Date(message.dataset.created_at);
|
|
||||||
} else {
|
|
||||||
const currentDate = new Date(message.dataset.created_at);
|
|
||||||
if (currentDate.getTime() - previousDate.getTime() > 1000 * 60 * 20) {
|
|
||||||
message.classList.add("long-time");
|
|
||||||
} else {
|
|
||||||
message.classList.remove("long-time");
|
|
||||||
}
|
|
||||||
previousDate = currentDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (doScrollDown) messagesContainer.scrollToBottom?.();
|
if (doScrollDown) messagesContainer.scrollToBottom?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user