|
{% 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 %}
|