Convert all JavaScript classes in app.js and schedule.js from implicit globals to ES module exports, add import statements in template scripts for the app instance, and mark all script tags with type="module" to enable module scoping. Also fix a potential null reference in web.html by using optional chaining on lastMessage.scrollIntoView and hoist the lastMessage variable to outer scope for proper closure behavior.
194 lines
6.3 KiB
HTML
194 lines
6.3 KiB
HTML
{% extends "app.html" %}
|
|
|
|
{% block main %}
|
|
<section class="chat-area" id="chat">
|
|
<div class="chat-header">
|
|
<h2>{{ name }}</h2>
|
|
</div>
|
|
<div class="chat-messages">
|
|
{% for message in messages %}
|
|
{% autoescape false %}
|
|
{{ message.html }}
|
|
{% endautoescape %}
|
|
{% endfor %}
|
|
</div>
|
|
<chat-window style="display:none" class="chat-area"></chat-window>
|
|
<div class="chat-input">
|
|
<textarea placeholder="Type a message..." rows="2"></textarea>
|
|
<upload-button channel="{{ channel.uid.value }}"></upload-button>
|
|
</div>
|
|
</section>
|
|
|
|
<script type="module">
|
|
import { app } from "/app.js";
|
|
|
|
const channelUid = "{{ channel.uid.value }}";
|
|
|
|
function getInputField(){
|
|
return document.querySelector("textarea")
|
|
}
|
|
|
|
function initInputField(textBox) {
|
|
textBox.addEventListener('change', (e) => {
|
|
e.preventDefault();
|
|
this.dispatchEvent(new CustomEvent('change', { detail: e.target.value, bubbles: true }));
|
|
});
|
|
|
|
textBox.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
const message = e.target.value.trim();
|
|
if (message) {
|
|
app.rpc.sendMessage(channelUid, message);
|
|
e.target.value = '';
|
|
}
|
|
}
|
|
});
|
|
textBox.focus();
|
|
}
|
|
|
|
function replyMessage(message) {
|
|
const field = getInputField()
|
|
field.value = "```markdown\n> " + (message || '') + "\n```\n";
|
|
field.focus();
|
|
}
|
|
|
|
function updateTimes() {
|
|
document.querySelectorAll(".time").forEach((container) => {
|
|
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) => {
|
|
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();
|
|
});
|
|
|
|
let lastMessage
|
|
|
|
function updateLayout(doScrollDown) {
|
|
const messagesContainer = document.querySelector(".chat-messages");
|
|
updateTimes();
|
|
let previousUser = null;
|
|
document.querySelectorAll(".message").forEach((message) => {
|
|
if (previousUser !== message.dataset.user_uid) {
|
|
message.classList.add("switch-user");
|
|
previousUser = message.dataset.user_uid;
|
|
} else {
|
|
message.classList.remove("switch-user");
|
|
}
|
|
});
|
|
lastMessage = messagesContainer.querySelector(".message:last-child");
|
|
if (doScrollDown) {
|
|
lastMessage?.scrollIntoView({ inline: "nearest" });
|
|
}
|
|
}
|
|
|
|
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) => {
|
|
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 doScrollDownBecauseLastMessageIsVisible = !lastMessage || isElementVisible(lastMessage);
|
|
|
|
const message = document.createElement("div");
|
|
message.innerHTML = data.html;
|
|
document.querySelector(".chat-messages").appendChild(message.firstChild);
|
|
updateLayout(doScrollDownBecauseLastMessageIsVisible);
|
|
setTimeout(() => {
|
|
updateLayout(doScrollDownBecauseLastMessageIsVisible)
|
|
}, 1000);
|
|
});
|
|
|
|
initInputField(getInputField());
|
|
updateLayout(true);
|
|
</script>
|
|
{% endblock %}
|