Update performance.
This commit is contained in:
parent
0bf714061c
commit
6a74263606
@ -19,340 +19,275 @@
|
||||
{% include "dialog_online.html" %}
|
||||
<script type="module">
|
||||
import { app } from "/app.js";
|
||||
import { Schedule } from "/schedule.js";
|
||||
const channelUid = "{{ channel.uid.value }}";
|
||||
const chatInputField = document.querySelector("chat-input");
|
||||
chatInputField.autoCompletions = {
|
||||
"/online": () =>{
|
||||
showOnline();
|
||||
},
|
||||
"/clear": () => {
|
||||
document.querySelector(".chat-messages").innerHTML = '';
|
||||
},
|
||||
"/live": () =>{
|
||||
|
||||
chatInputField.liveType = !chatInputField.liveType
|
||||
},
|
||||
"/help": () => {
|
||||
showHelp();
|
||||
import { Schedule } from "/schedule.js";
|
||||
|
||||
// --- Cache selectors ---
|
||||
const chatInputField = document.querySelector("chat-input");
|
||||
const messagesContainer = document.querySelector(".chat-messages");
|
||||
const chatArea = document.querySelector(".chat-area");
|
||||
const channelUid = "{{ channel.uid.value }}";
|
||||
const username = "{{ user.username.value }}";
|
||||
|
||||
// --- Command completions ---
|
||||
chatInputField.autoCompletions = {
|
||||
"/online": showOnline,
|
||||
"/clear": () => { messagesContainer.innerHTML = ''; },
|
||||
"/live": () => { chatInputField.liveType = !chatInputField.liveType; },
|
||||
"/help": showHelp
|
||||
};
|
||||
|
||||
// --- Throttle utility ---
|
||||
function throttle(fn, wait) {
|
||||
let last = 0;
|
||||
return function(...args) {
|
||||
const now = Date.now();
|
||||
if (now - last >= wait) {
|
||||
last = now;
|
||||
fn.apply(this, args);
|
||||
}
|
||||
}
|
||||
app.ws.addEventListener("refresh", (data) => {
|
||||
app.starField.showNotify(data.message);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
},4000)
|
||||
})
|
||||
app.ws.addEventListener("deployed", (data) => {
|
||||
app.starField.renderWord("Deployed",{"rainbow":true,"resolution":8});
|
||||
setTimeout(() => {
|
||||
app.starField.shuffleAll(5000);
|
||||
},10000)
|
||||
})
|
||||
app.ws.addEventListener("starfield.render_word", (data) => {
|
||||
app.starField.renderWord(data.word,data);
|
||||
})
|
||||
const textBox = document.querySelector("chat-input").textarea
|
||||
textBox.addEventListener("paste", async (e) => {
|
||||
try {
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
};
|
||||
}
|
||||
|
||||
// DataTransfer needs to be used to modify the files list of the input
|
||||
const dt = new DataTransfer();
|
||||
|
||||
for (const clipboardItem of clipboardItems) {
|
||||
const fileTypes = clipboardItem.types.filter(type => !type.startsWith('text/'))
|
||||
for (const fileType of fileTypes) {
|
||||
|
||||
const blob = await clipboardItem.getType(fileType);
|
||||
// Do something with the image blob.
|
||||
dt.items.add(new File([blob], "image.png", { type: fileType }));
|
||||
}
|
||||
}
|
||||
|
||||
if (dt.items.length > 0) {
|
||||
const uploadButton = chatInputField.uploadButton
|
||||
const input = uploadButton.shadowRoot.querySelector('.file-input')
|
||||
input.files = dt.files;
|
||||
|
||||
await uploadButton.uploadFiles();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to read clipboard contents: ", error);
|
||||
}
|
||||
// --- Scroll: load extra messages, throttled ---
|
||||
let isLoadingExtra = false;
|
||||
async function loadExtra() {
|
||||
const firstMessage = messagesContainer.querySelector(".message:first-child");
|
||||
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.forEach(msg => {
|
||||
const temp = document.createElement("div");
|
||||
temp.innerHTML = msg.html;
|
||||
frag.appendChild(temp.firstChild);
|
||||
});
|
||||
|
||||
|
||||
|
||||
const chatInput = document.querySelector(".chat-area")
|
||||
chatInput.addEventListener("drop", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const dt = e.dataTransfer;
|
||||
if (dt.items.length > 0) {
|
||||
const uploadButton = chatInputField.uploadButton
|
||||
const input = uploadButton.shadowRoot.querySelector('.file-input')
|
||||
input.files = dt.files;
|
||||
|
||||
await uploadButton.uploadFiles();
|
||||
}
|
||||
})
|
||||
chatInput.addEventListener("dragover", async (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "link";
|
||||
|
||||
|
||||
})
|
||||
|
||||
chatInputField.textarea.focus();
|
||||
|
||||
|
||||
|
||||
function replyMessage(message) {
|
||||
const field = chatInputField
|
||||
field.value = "```markdown\n> " + (message || '') + "\n```\n";
|
||||
field.focus();
|
||||
firstMessage.parentNode.insertBefore(frag, firstMessage);
|
||||
updateLayout(false);
|
||||
}
|
||||
isLoadingExtra = false;
|
||||
}
|
||||
messagesContainer.addEventListener("scroll", throttle(loadExtra, 200));
|
||||
|
||||
function updateTimes() {
|
||||
document.querySelectorAll(".time").forEach((container) => {
|
||||
// --- 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');
|
||||
const userNick = messageDiv.dataset.user_nick;
|
||||
const text = messageDiv.querySelector(".text").innerText;
|
||||
const time = document.createElement("span");
|
||||
time.innerText = app.timeDescription(container.dataset.created_at);
|
||||
|
||||
container.replaceChildren(time);
|
||||
const reply = document.createElement("a");
|
||||
reply.innerText = " reply";
|
||||
reply.href = "#reply";
|
||||
container.appendChild(reply);
|
||||
reply.addEventListener('click', (e) => {
|
||||
reply.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
replyMessage(text);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function isElementVisible(element) {
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
const messagesContainer = document.querySelector(".chat-messages");
|
||||
|
||||
function isScrolledPastHalf() {
|
||||
let scrollTop = messagesContainer.scrollTop;
|
||||
let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight;
|
||||
|
||||
if (scrollTop < scrollableHeight / 2) {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let isLoadingExtra = false;
|
||||
|
||||
async function loadExtra() {
|
||||
const firstMessage = messagesContainer.querySelector(".message:first-child");
|
||||
if (isLoadingExtra) {
|
||||
return;
|
||||
}
|
||||
if (!isScrolledPastHalf()) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingExtra = true;
|
||||
|
||||
const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);
|
||||
|
||||
messages.forEach((message) => {
|
||||
firstMessage.insertAdjacentHTML("beforebegin", message.html);
|
||||
})
|
||||
updateLayout(false);
|
||||
|
||||
isLoadingExtra = false;
|
||||
}
|
||||
|
||||
messagesContainer.addEventListener("scroll", () => {
|
||||
loadExtra();
|
||||
});
|
||||
}
|
||||
setInterval(() => requestIdleCallback(updateTimes), 30000);
|
||||
|
||||
let lastMessage
|
||||
// --- Paste & drag/drop uploads ---
|
||||
const textBox = chatInputField.textarea;
|
||||
textBox.addEventListener("paste", async (e) => {
|
||||
try {
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
const dt = new DataTransfer();
|
||||
for (const item of clipboardItems) {
|
||||
for (const type of item.types.filter(t => !t.startsWith('text/'))) {
|
||||
const blob = await item.getType(type);
|
||||
dt.items.add(new File([blob], "image.png", { type }));
|
||||
}
|
||||
}
|
||||
if (dt.items.length > 0) {
|
||||
const uploadButton = chatInputField.uploadButton;
|
||||
const input = uploadButton.shadowRoot.querySelector('.file-input');
|
||||
input.files = dt.files;
|
||||
await uploadButton.uploadFiles();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to read clipboard contents: ", error);
|
||||
}
|
||||
});
|
||||
chatArea.addEventListener("drop", async (e) => {
|
||||
e.preventDefault();
|
||||
const dt = e.dataTransfer;
|
||||
if (dt.items.length > 0) {
|
||||
const uploadButton = chatInputField.uploadButton;
|
||||
const input = uploadButton.shadowRoot.querySelector('.file-input');
|
||||
input.files = dt.files;
|
||||
await uploadButton.uploadFiles();
|
||||
}
|
||||
});
|
||||
chatArea.addEventListener("dragover", e => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "link";
|
||||
});
|
||||
|
||||
function updateLayout(doScrollDown) {
|
||||
const messagesContainer = document.querySelector(".chat-messages");
|
||||
updateTimes();
|
||||
let previousUser = null;
|
||||
let previousDate = null;
|
||||
document.querySelectorAll(".message").forEach((message) => {
|
||||
if (previousUser !== message.dataset.user_uid) {
|
||||
message.classList.add("switch-user");
|
||||
previousUser = message.dataset.user_uid;
|
||||
// --- Focus input on load ---
|
||||
chatInputField.textarea.focus();
|
||||
|
||||
// --- Reply helper ---
|
||||
function replyMessage(message) {
|
||||
chatInputField.value = "markdown\n> " + (message || '') + "\n";
|
||||
chatInputField.focus();
|
||||
}
|
||||
|
||||
// --- Mention helpers ---
|
||||
function extractMentions(message) {
|
||||
return [...new Set(message.match(/@\w+/g) || [])];
|
||||
}
|
||||
function isMentionToMe(message) {
|
||||
return message.toLowerCase().includes('@' + username.toLowerCase());
|
||||
}
|
||||
function isMentionForSomeoneElse(message) {
|
||||
const mentions = extractMentions(message);
|
||||
return mentions.length > 0 && !mentions.includes('@' + username);
|
||||
}
|
||||
|
||||
// --- WebSocket events ---
|
||||
app.ws.addEventListener("refresh", (data) => {
|
||||
app.starField.showNotify(data.message);
|
||||
setTimeout(() => window.location.reload(), 4000);
|
||||
});
|
||||
app.ws.addEventListener("deployed", (data) => {
|
||||
app.starField.renderWord("Deployed", { rainbow: true, resolution: 8 });
|
||||
setTimeout(() => app.starField.shuffleAll(5000), 10000);
|
||||
});
|
||||
app.ws.addEventListener("starfield.render_word", (data) => {
|
||||
app.starField.renderWord(data.word, data);
|
||||
});
|
||||
|
||||
// --- Channel message event ---
|
||||
app.addEventListener("channel-message", (data) => {
|
||||
let display = data.text && data.text.trim() ? 'block' : 'none';
|
||||
if (data.channel_uid !== channelUid) {
|
||||
if (!isMentionForSomeoneElse(data.message)) {
|
||||
channelSidebar.notify(data);
|
||||
app.playSound("messageOtherChannel");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data.username !== username) {
|
||||
if (isMentionToMe(data.message)) {
|
||||
app.playSound("mention");
|
||||
} else if (!isMentionForSomeoneElse(data.message)) {
|
||||
app.playSound("message");
|
||||
}
|
||||
}
|
||||
const lastMessage = messagesContainer.querySelector(".message:last-child");
|
||||
const lastElement = messagesContainer.querySelector(".message-list-bottom");
|
||||
const doScrollDown = !lastMessage || isElementVisible(lastMessage);
|
||||
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);
|
||||
});
|
||||
|
||||
// --- Keyboard shortcuts ---
|
||||
let escPressed = false, gPressCount = 0, keyTimeout;
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
escPressed = true; gPressCount = 0;
|
||||
clearTimeout(keyTimeout);
|
||||
keyTimeout = setTimeout(() => { escPressed = false; }, 300);
|
||||
}
|
||||
if (event.key === 'G' && escPressed) {
|
||||
gPressCount++;
|
||||
clearTimeout(keyTimeout);
|
||||
keyTimeout = setTimeout(() => { gPressCount = 0; }, 300);
|
||||
if (gPressCount === 2) {
|
||||
gPressCount = 0; escPressed = false;
|
||||
messagesContainer.querySelector(".message:last-child")?.scrollIntoView({ block: "end", inline: "nearest" });
|
||||
setTimeout(() => chatInputField.focus(), 500);
|
||||
}
|
||||
}
|
||||
if (event.shiftKey && event.key === 'G') {
|
||||
if (chatInputField.isActive()) {
|
||||
updateLayout(true);
|
||||
setTimeout(() => chatInputField.focus(), 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Image click-to-zoom (delegated) ---
|
||||
messagesContainer.addEventListener('click', (e) => {
|
||||
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar')) return;
|
||||
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 fullImg = document.createElement('img');
|
||||
const urlObj = new URL(img.src); urlObj.search = '';
|
||||
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));
|
||||
});
|
||||
|
||||
// --- Layout update ---
|
||||
let lastMessage;
|
||||
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 {
|
||||
message.classList.remove("switch-user");
|
||||
|
||||
if (!previousDate) {
|
||||
previousDate = new Date(message.dataset.created_at);
|
||||
const currentDate = new Date(message.dataset.created_at);
|
||||
if (currentDate.getTime() - previousDate.getTime() > 1000 * 60 * 20) {
|
||||
message.classList.add("long-time");
|
||||
} 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;
|
||||
message.classList.remove("long-time");
|
||||
}
|
||||
previousDate = currentDate;
|
||||
}
|
||||
});
|
||||
lastMessage = messagesContainer.querySelector(".message:last-child");
|
||||
if (doScrollDown) {
|
||||
messagesContainer.scrollToBottom()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(updateTimes, 30000);
|
||||
|
||||
function isMentionToMe(message){
|
||||
const mentionText = '@{{ user.username.value }}';
|
||||
return message.toLowerCase().includes(mentionText);
|
||||
}
|
||||
function extractMentions(message) {
|
||||
return [...new Set(message.match(/@\w+/g) || [])];
|
||||
}
|
||||
function isMentionForSomeoneElse(message){
|
||||
const mentions = extractMentions(message);
|
||||
const mentionText = '@{{ user.username.value }}';
|
||||
return mentions.length > 0 && mentions.indexOf(mentionText) == -1;
|
||||
}
|
||||
|
||||
app.addEventListener("channel-message", (data) => {
|
||||
let display = 'block';
|
||||
if(!data.text || !data.text.trim()){
|
||||
display = "none";
|
||||
}
|
||||
if (data.channel_uid !== channelUid) {
|
||||
if(!isMentionForSomeoneElse(data.message)){
|
||||
channelSidebar.notify(data);
|
||||
app.playSound("messageOtherChannel");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (data.username !== "{{ user.username.value }}") {
|
||||
if(isMentionToMe(data.message)){
|
||||
app.playSound("mention");
|
||||
}else if (!isMentionForSomeoneElse(data.message)){
|
||||
app.playSound("message");
|
||||
}
|
||||
}
|
||||
|
||||
const messagesContainer = document.querySelector(".chat-messages");
|
||||
lastMessage = messagesContainer.querySelector(".message:last-child");
|
||||
const lastElement = messagesContainer.querySelector(".message-list-bottom");
|
||||
const doScrollDownBecauseLastMessageIsVisible = !lastMessage || isElementVisible(lastMessage);
|
||||
|
||||
const message = document.createElement("div");
|
||||
message.innerHTML = data.html;
|
||||
message.style.display = display
|
||||
document.querySelector(".chat-messages").insertBefore(message.firstChild,lastElement);
|
||||
updateLayout(doScrollDownBecauseLastMessageIsVisible);
|
||||
setTimeout(() => {
|
||||
updateLayout(doScrollDownBecauseLastMessageIsVisible)
|
||||
}, 1000);
|
||||
app.rpc.markAsRead(channelUid);
|
||||
});
|
||||
|
||||
let escPressed = false;
|
||||
let gPressCount = 0;
|
||||
let keyTimeout;
|
||||
document.addEventListener('keydown', function(event) {
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
escPressed = true;
|
||||
gPressCount = 0;
|
||||
clearTimeout(keyTimeout);
|
||||
keyTimeout = setTimeout(() => {
|
||||
escPressed = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
if (event.key === 'G' && escPressed) {
|
||||
gPressCount++;
|
||||
|
||||
clearTimeout(keyTimeout);
|
||||
keyTimeout = setTimeout(() => {
|
||||
gPressCount = 0;
|
||||
}, 300);
|
||||
if (gPressCount === 2) {
|
||||
gPressCount = 0;
|
||||
escPressed = false;
|
||||
|
||||
messagesContainer.querySelector(".message:last-child").scrollIntoView({ block: "end", inline: "nearest" });
|
||||
setTimeout(() => {
|
||||
|
||||
chatInputField.focus();
|
||||
},500)
|
||||
|
||||
}
|
||||
}
|
||||
if (event.shiftKey && event.key === 'G') {
|
||||
if(chatInputField.isActive()){
|
||||
|
||||
updateLayout(true);
|
||||
setTimeout(() => {
|
||||
chatInputField.focus();
|
||||
},500)
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
lastMessage = messagesContainer.querySelector(".message:last-child");
|
||||
if (doScrollDown) messagesContainer.scrollToBottom?.();
|
||||
}
|
||||
|
||||
messagesContainer.addEventListener('click', (e) => {
|
||||
if(e.target.tagName != 'IMG')
|
||||
return
|
||||
const img = e.target
|
||||
if(e.target.classList.contains('avatar')){
|
||||
return
|
||||
}
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.top = 0;
|
||||
overlay.style.left = 0;
|
||||
overlay.style.width = '100%';
|
||||
overlay.style.height = '100%';
|
||||
overlay.style.backgroundColor = 'rgba(0,0,0,0.9)';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.zIndex = 9999;
|
||||
// --- Utility: check if element is visible ---
|
||||
function isElementVisible(element) {
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
const fullImg = document.createElement('img');
|
||||
// --- Utility: check if scrolled past half ---
|
||||
function isScrolledPastHalf() {
|
||||
let scrollTop = messagesContainer.scrollTop;
|
||||
let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight;
|
||||
return scrollTop < scrollableHeight / 2;
|
||||
}
|
||||
|
||||
const urlObj = new URL(img.src);
|
||||
urlObj.search = ''
|
||||
fullImg.src = urlObj.toString();
|
||||
// --- Initial layout update ---
|
||||
updateLayout(true);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
updateLayout(true);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
Loading…
Reference in New Issue
Block a user