{% extends "app.html" %}
{% block header_text %}<h2 style="color:#fff">{{ name }}</h2>{% endblock %}
{% block main %}
<section class="chat-area">
<message-list class="chat-messages">
{% for message in messages %}
{% autoescape false %}
{{ message.html }}
{% endautoescape %}
{% endfor %}
<div class class="message-list-bottom"></div>
</message-list>
<chat-input live-type="false" channel="{{ channel.uid.value }}"></chat-input>
</section>
{% include "dialog_help.html" %}
{% 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();
}
}
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);
}
});
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();
}
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({ block: "end", inline: "nearest" });
chatInputField.scrollIntoView({ block: "end", 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) => {
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 doScrollDownBecauseLastMessageIsVisible = !lastMessage || isElementVisible(lastMessage);
const message = document.createElement("div");
message.innerHTML = data.html;
message.style.display = display
document.querySelector(".chat-messages").appendChild(message.firstChild);
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)
}
}
});
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;
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);
});
});
updateLayout(true);
</script>
{% endblock %}