Cleaned up message rendering a bit
This commit is contained in:
parent
9a0ba22fe8
commit
29b9fce07d
@ -5,10 +5,9 @@ from snek.system.template import whitelist_attributes
|
||||
class ChannelMessageService(BaseService):
|
||||
mapper_name = "channel_message"
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._configured_indexes = False
|
||||
self._configured_indexes = False
|
||||
|
||||
async def maintenance(self):
|
||||
args = {}
|
||||
@ -18,7 +17,7 @@ class ChannelMessageService(BaseService):
|
||||
html = message["html"]
|
||||
await self.save(message)
|
||||
|
||||
self.mapper.db['channel_message'].upsert(
|
||||
self.mapper.db["channel_message"].upsert(
|
||||
{
|
||||
"uid": message["uid"],
|
||||
"updated_at": updated_at,
|
||||
@ -27,7 +26,7 @@ class ChannelMessageService(BaseService):
|
||||
)
|
||||
if html != message["html"]:
|
||||
print("Reredefined message", message["uid"])
|
||||
|
||||
|
||||
while True:
|
||||
changed = 0
|
||||
async for message in self.find(is_final=False):
|
||||
@ -41,7 +40,6 @@ class ChannelMessageService(BaseService):
|
||||
if not changed:
|
||||
break
|
||||
|
||||
|
||||
async def create(self, channel_uid, user_uid, message, is_final=True):
|
||||
model = await self.new()
|
||||
|
||||
@ -72,13 +70,19 @@ class ChannelMessageService(BaseService):
|
||||
|
||||
if await super().save(model):
|
||||
if not self._configured_indexes:
|
||||
if not self.mapper.db["channel_message"].has_index(['is_final','user_uid','channel_uid']):
|
||||
self.mapper.db["channel_message"].create_index(['is_final','user_uid','channel_uid'], 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
|
||||
if not self.mapper.db["channel_message"].has_index(
|
||||
["is_final", "user_uid", "channel_uid"]
|
||||
):
|
||||
self.mapper.db["channel_message"].create_index(
|
||||
["is_final", "user_uid", "channel_uid"], 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
|
||||
return model
|
||||
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"])
|
||||
if not user:
|
||||
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 {
|
||||
"uid": message["uid"],
|
||||
"color": user["color"],
|
||||
@ -115,14 +124,13 @@ class ChannelMessageService(BaseService):
|
||||
model["html"] = whitelist_attributes(model["html"])
|
||||
return await super().save(model)
|
||||
|
||||
|
||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||
channel = await self.services.channel.get(uid=channel_uid)
|
||||
if not channel:
|
||||
return []
|
||||
history_start_filter = ""
|
||||
if channel["history_start"]:
|
||||
history_start_filter = f" AND created_at > '{channel['history_start']}'"
|
||||
history_start_filter = f" AND created_at > '{channel['history_start']}'"
|
||||
results = []
|
||||
offset = page * page_size
|
||||
try:
|
||||
|
@ -144,7 +144,7 @@ footer {
|
||||
|
||||
.chat-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
|
@ -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.
|
||||
|
||||
// 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 {
|
||||
constructor() {
|
||||
super();
|
||||
app.ws.addEventListener("update_message_text", (data) => {
|
||||
this.updateMessageText(data.uid, data);
|
||||
this.upsertMessage(data);
|
||||
});
|
||||
app.ws.addEventListener("set_typing", (data) => {
|
||||
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() {
|
||||
const messagesContainer = this
|
||||
messagesContainer.addEventListener('click', (e) => {
|
||||
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', (e) => {
|
||||
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');
|
||||
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;'
|
||||
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;';
|
||||
|
||||
const urlObj = new URL(img.currentSrc || img.src)
|
||||
urlObj.searchParams.delete("width");
|
||||
urlObj.searchParams.delete("height");
|
||||
const urlObj = new URL(img.currentSrc || img.src, window.location.origin);
|
||||
urlObj.searchParams.delete('width');
|
||||
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();
|
||||
fullImg.alt = img.alt;
|
||||
fullImg.style.maxWidth = '90%';
|
||||
fullImg.style.maxHeight = '90%';
|
||||
|
||||
overlay.appendChild(fullImg);
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', () => document.body.removeChild(overlay));
|
||||
overlay.appendChild(fullImg);
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', () => {
|
||||
if (overlay.parentNode) {
|
||||
overlay.parentNode.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) {
|
||||
if (!element) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
}
|
||||
}
|
||||
isScrolledToBottom() {
|
||||
return this.isElementVisible(this.querySelector(".message-list-bottom"));
|
||||
return this.isElementVisible(this.firstElementChild);
|
||||
}
|
||||
scrollToBottom(force) {
|
||||
//this.scrollTop = this.scrollHeight;
|
||||
|
||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||
scrollToBottom(force = false, behavior= 'smooth') {
|
||||
if (force || this.isScrolledToBottom()) {
|
||||
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
|
||||
setTimeout(() => {
|
||||
|
||||
// this.scrollTop = this.scrollHeight;
|
||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||
},200)
|
||||
}
|
||||
updateMessageText(uid, message) {
|
||||
const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
|
||||
|
||||
if (!messageDiv) {
|
||||
return;
|
||||
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
|
||||
}, 200);
|
||||
}
|
||||
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)
|
||||
let lastElement = null;
|
||||
this.querySelectorAll(".avatar").forEach((el) => {
|
||||
const div = el.closest("a");
|
||||
if (el.href.indexOf(uid) != -1) {
|
||||
|
||||
triggerGlow(uid, color) {
|
||||
if (!uid || !color) return;
|
||||
app.starField.glowColor(color);
|
||||
let lastElement = null;
|
||||
this.querySelectorAll('.avatar').forEach((el) => {
|
||||
const anchor = el.closest('a');
|
||||
if (anchor && typeof anchor.href === 'string' && anchor.href.includes(uid)) {
|
||||
lastElement = el;
|
||||
}
|
||||
});
|
||||
if (lastElement) {
|
||||
lastElement.classList.add("glow");
|
||||
lastElement.classList.add('glow');
|
||||
setTimeout(() => {
|
||||
lastElement.classList.remove("glow");
|
||||
lastElement.classList.remove('glow');
|
||||
}, 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);
|
||||
|
@ -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>
|
||||
{% endif %}
|
||||
|
||||
{% for message in messages %}
|
||||
{% for message in messages|reverse %}
|
||||
{% autoescape false %}
|
||||
{{ message.html }}
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
<div class="message-list-bottom"></div>
|
||||
</message-list>
|
||||
<chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
|
||||
</section>
|
||||
@ -73,7 +72,7 @@ function throttle(fn, wait) {
|
||||
// --- Scroll: load extra messages, throttled ---
|
||||
let isLoadingExtra = false;
|
||||
async function loadExtra() {
|
||||
const firstMessage = messagesContainer.querySelector(".message:first-child");
|
||||
const firstMessage = messagesContainer.children[messagesContainer.children.length - 1];
|
||||
if (isLoadingExtra || !isScrolledPastHalf() || !firstMessage) return;
|
||||
isLoadingExtra = true;
|
||||
const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);
|
||||
@ -84,7 +83,7 @@ async function loadExtra() {
|
||||
temp.innerHTML = msg.html;
|
||||
frag.appendChild(temp.firstChild);
|
||||
});
|
||||
firstMessage.parentNode.insertBefore(frag, firstMessage);
|
||||
messagesContainer.appendChild(frag);
|
||||
updateLayout(false);
|
||||
}
|
||||
isLoadingExtra = false;
|
||||
@ -93,32 +92,7 @@ messagesContainer.addEventListener("scroll", throttle(loadExtra, 200));
|
||||
|
||||
// --- Only update visible times ---
|
||||
function updateTimes() {
|
||||
const containers = messagesContainer.querySelectorAll(".time");
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
messagesContainer.updateTimes();
|
||||
}
|
||||
setInterval(() => requestIdleCallback(updateTimes), 30000);
|
||||
|
||||
@ -164,7 +138,7 @@ chatInputField.textarea.focus();
|
||||
|
||||
// --- Reply helper ---
|
||||
function replyMessage(message) {
|
||||
chatInputField.value = "```markdown\n> " + (message || '') + "\n```\n";
|
||||
chatInputField.value = "```markdown\n> " + (message || '').split("\n").join("\n> ") + "\n```\n";
|
||||
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}"]`);
|
||||
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);
|
||||
messagesContainer.upsertMessage(data)
|
||||
app.rpc.markAsRead(channelUid);
|
||||
});
|
||||
|
||||
@ -284,27 +244,6 @@ document.addEventListener('keydown', function(event) {
|
||||
// --- Layout update ---
|
||||
function updateLayout(doScrollDown) {
|
||||
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?.();
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user