Q-Mintership-Alpha/assets/js/Q-Mintership.js
crowetic 5e149039e9 Bit updates to Minter and Admin boards. Still working on encrypted file downloads for the Admin Room on the forum... for whatever reason they're giving me a hassle. Just need a little more time to get it sorted.
Will also be adding additional use cases for the Admin board, and maybe a 'community board' as I really like the board concept for things like decision-making and community managmenent. I also have a really good idea for giving information on the boards via link modal concept. TBD. Will likely make the identifier changes and push announcements out on Monday. IF not sometime this weekend if time allows.
2024-12-20 22:07:18 -08:00

896 lines
31 KiB
JavaScript

const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
let adminPublicKeys = []
// NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically.
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
let currentPage = 0; // Track current pagination page
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
console.log("DOMContentLoaded fired!");
// --- GENERAL LINKS (MINTERSHIP-FORUM and MINTER-BOARD) ---
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
if (!userState.isLoggedIn) {
await login();
}
await loadForumPage();
loadRoomContent("general");
startPollingForNewMessages();
});
});
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]');
minterBoardLinks.forEach(link => {
link.addEventListener("click", async (event) => {
event.preventDefault();
if (!userState.isLoggedIn) {
await login();
}
if (typeof loadMinterBoardPage === "undefined") {
console.log("loadMinterBoardPage not found, loading script dynamically...");
await loadScript("./assets/js/MinterBoard.js");
}
await loadMinterBoardPage();
});
});
// --- ADMIN CHECK ---
await verifyUserIsAdmin();
if (userState.isAdmin) {
console.log(`User is an Admin. Admin-specific buttons will remain visible.`);
// DATA-BOARD Links for Admins
const minterDataBoardLinks = document.querySelectorAll('a[href="ADMINBOARD"]');
minterDataBoardLinks.forEach(link => {
link.addEventListener("click", async (event) => {
event.preventDefault();
if (!userState.isLoggedIn) {
await login();
}
if (typeof loadAdminBoardPage === "undefined") {
console.log("loadAdminBoardPage function not found, loading script dynamically...");
await loadScript("./assets/js/AdminBoard.js");
}
await loadAdminBoardPage();
});
});
// TOOLS Links for Admins
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
toolsLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
if (!userState.isLoggedIn) {
await login();
}
if (typeof loadMinterAdminToolsPage === "undefined") {
console.log("loadMinterAdminToolsPage function not found, loading script dynamically...");
await loadScript("./assets/js/AdminTools.js");
}
await loadMinterAdminToolsPage();
});
});
} else {
console.log("User is NOT an Admin. Removing admin-specific links.");
// Remove all admin-specific links and their parents
const toolsLinks = document.querySelectorAll('a[href="TOOLS"], a[href="ADMINBOARD"]');
toolsLinks.forEach(link => {
const buttonParent = link.closest('button');
if (buttonParent) buttonParent.remove();
const cardParent = link.closest('.item.features-image');
if (cardParent) cardParent.remove();
link.remove();
});
// Center the remaining card if it exists
const remainingCard = document.querySelector('.features7 .row .item.features-image');
if (remainingCard) {
remainingCard.classList.remove('col-lg-6', 'col-md-6');
remainingCard.classList.add('col-12', 'text-center');
}
}
console.log("All DOMContentLoaded tasks completed.");
});
async function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Main load function to clear existing HTML and load the forum page -----------------------------------------------------
const loadForumPage = async () => {
// remove everything that isn't the menu from the body to use js to generate page content.
const bodyChildren = document.body.children;
for (let i = bodyChildren.length - 1; i >= 0; i--) {
const child = bodyChildren[i];
if (!child.classList.contains('menu')) {
child.remove();
}
}
if (typeof userState.isAdmin === 'undefined') {
try {
// Fetch and verify the admin status asynchronously
userState.isAdmin = await verifyUserIsAdmin();
} catch (error) {
console.error('Error verifying admin status:', error);
userState.isAdmin = false; // Default to non-admin if there's an issue
}
}
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
const isAdmin = userState.isAdmin;
// Create the forum layout, including a header, sub-menu, and keeping the original background image: style="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background cid-ttRnlSkg2R">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: white; display: flex; align-items: center; justify-content: center;">
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
<span>${userState.accountName || 'Guest'}</span>
</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
<button class="room-button" id="general-room">General Room</button>
</div>
</div>
<div id="forum-content" class="forum-content"></div>
</div>
`;
document.body.appendChild(mainContent);
// Add event listeners to room buttons
document.getElementById("minters-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("minters");
});
if (userState.isAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("general");
});
}
// Function to add the pagination buttons and related control mechanisms ------------------------
const renderPaginationControls = (room, totalMessages, limit) => {
const paginationContainer = document.getElementById("pagination-container");
if (!paginationContainer) return;
paginationContainer.innerHTML = ""; // Clear existing buttons
const totalPages = Math.ceil(totalMessages / limit);
// Add "Previous" button
if (currentPage > 0) {
const prevButton = document.createElement("button");
prevButton.innerText = "Previous";
prevButton.addEventListener("click", () => {
if (currentPage > 0) {
currentPage--;
loadMessagesFromQDN(room, currentPage, false);
}
});
paginationContainer.appendChild(prevButton);
}
// Add numbered page buttons
for (let i = 0; i < totalPages; i++) {
const pageButton = document.createElement("button");
pageButton.innerText = i + 1;
pageButton.className = i === currentPage ? "active-page" : "";
pageButton.addEventListener("click", () => {
if (i !== currentPage) {
currentPage = i;
loadMessagesFromQDN(room, currentPage, false);
}
});
paginationContainer.appendChild(pageButton);
}
// Add "Next" button
if (currentPage < totalPages - 1) {
const nextButton = document.createElement("button");
nextButton.innerText = "Next";
nextButton.addEventListener("click", () => {
if (currentPage < totalPages - 1) {
currentPage++;
loadMessagesFromQDN(room, currentPage, false);
}
});
paginationContainer.appendChild(nextButton);
}
}
// Main function to load the full content of the room, along with all main functionality -----------------------------------
const loadRoomContent = async (room) => {
const forumContent = document.getElementById("forum-content");
if (!forumContent) {
console.error("Forum content container not found!");
return;
}
// Set initial content
forumContent.innerHTML = `
<div class="room-content">
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
<div id="messages-container" class="messages-container"></div>
<div id="pagination-container" class="pagination-container" style="margin-top: 20px; text-align: center;"></div>
<div class="message-input-section">
<div id="toolbar" class="message-toolbar"></div>
<div id="editor" class="message-input"></div>
<div class="attachment-section">
<input type="file" id="file-input" class="file-input" multiple>
<label for="file-input" class="custom-file-input-button">Select Files</label>
<input type="file" id="image-input" class="image-input" multiple accept="image/*">
<label for="image-input" class="custom-image-input-button">Select IMAGES w/Preview</label>
<button id="add-images-to-publish-button" disabled>Add Images to Multi-Publish</button>
<div id="preview-container" style="display: flex; flex-wrap: wrap; gap: 10px;"></div>
</div>
<button id="send-button" class="send-button">Publish</button>
</div>
</div>
`;
// Add modal for image preview
forumContent.insertAdjacentHTML(
'beforeend',
`
<div id="image-modal" class="image-modal">
<span id="close-modal" class="close">&times;</span>
<img id="modal-image" class="modal-content">
<div id="caption" class="caption"></div>
<button id="download-button" class="download-button">Download</button>
</div>
`);
initializeQuillEditor();
setupModalHandlers();
setupFileInputs(room);
await loadMessagesFromQDN(room, currentPage);
};
// Initialize Quill editor
const initializeQuillEditor = () => {
new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }],
[{ 'align': [] }],
['clean']
]
}
});
};
// Set up modal behavior
const setupModalHandlers = () => {
document.addEventListener("click", (event) => {
if (event.target.classList.contains("inline-image")) {
const modal = document.getElementById("image-modal");
const modalImage = document.getElementById("modal-image");
const caption = document.getElementById("caption");
modalImage.src = event.target.src;
caption.textContent = event.target.alt;
modal.style.display = "block";
}
});
document.getElementById("close-modal").addEventListener("click", () => {
document.getElementById("image-modal").style.display = "none";
});
window.addEventListener("click", (event) => {
const modal = document.getElementById("image-modal");
if (event.target === modal) {
modal.style.display = "none";
}
});
};
let selectedImages = [];
let selectedFiles = [];
let multiResource = [];
let attachmentIdentifiers = [];
// Set up file input handling
const setupFileInputs = (room) => {
const imageFileInput = document.getElementById('image-input');
const previewContainer = document.getElementById('preview-container');
const addToPublishButton = document.getElementById('add-images-to-publish-button');
const fileInput = document.getElementById('file-input');
const sendButton = document.getElementById('send-button');
const attachmentID = generateAttachmentID(room);
imageFileInput.addEventListener('change', (event) => {
previewContainer.innerHTML = '';
selectedImages = [...event.target.files];
addToPublishButton.disabled = selectedImages.length === 0;
selectedImages.forEach((file, index) => {
const reader = new FileReader();
reader.onload = () => {
const img = document.createElement('img');
img.src = reader.result;
img.alt = file.name;
img.style = "width: 100px; height: 100px; object-fit: cover; border: 1px solid #ccc; border-radius: 5px;";
const removeButton = document.createElement('button');
removeButton.innerText = 'Remove';
removeButton.classList.add('remove-image-button');
removeButton.onclick = () => {
selectedImages.splice(index, 1);
img.remove();
removeButton.remove();
addToPublishButton.disabled = selectedImages.length === 0;
};
const container = document.createElement('div');
container.style = "display: flex; flex-direction: column; align-items: center; margin: 5px;";
container.append(img, removeButton);
previewContainer.append(container);
};
reader.readAsDataURL(file);
});
});
addToPublishButton.addEventListener('click', () => {
processSelectedImages(selectedImages, multiResource, room);
selectedImages = [];
addToPublishButton.disabled = true;
});
fileInput.addEventListener('change', (event) => {
selectedFiles = [...event.target.files];
});
sendButton.addEventListener('click', async () => {
const quill = new Quill('#editor');
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml || selectedFiles.length > 0 || selectedImages.length > 0) {
await handleSendMessage(room, messageHtml, selectedFiles, selectedImages, multiResource);
}
});
};
// Process selected images
const processSelectedImages = async (selectedImages, multiResource, room) => {
for (const file of selectedImages) {
const attachmentID = generateAttachmentID(room, selectedImages.indexOf(file));
multiResource.push({
name: userState.accountName,
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
identifier: attachmentID,
file: file, // Use encrypted file for admins
});
attachmentIdentifiers.push({
name: userState.accountName,
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
identifier: attachmentID,
filename: file.name,
mimeType: file.type,
});
}
};
// Handle send message
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
const messageIdentifier = room === "admins"
? `${messageIdentifierPrefix}-${room}-e-${Date.now()}`
: `${messageIdentifierPrefix}-${room}-${Date.now()}`;
const adminPublicKeys = room === "admins" && userState.isAdmin
? await fetchAdminGroupsMembersPublicKeys()
: [];
try {
// Process selected images
if (selectedImages.length > 0) {
await processSelectedImages(selectedImages, multiResource, room);
}
// Process selected files
if (selectedFiles && selectedFiles.length > 0) {
for (const file of selectedFiles) {
const attachmentID = generateAttachmentID(room, selectedFiles.indexOf(file));
multiResource.push({
name: userState.accountName,
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
identifier: attachmentID,
file: file, // Use encrypted file for admins
});
attachmentIdentifiers.push({
name: userState.accountName,
service: room === "admins" ? "FILE_PRIVATE" : "FILE",
identifier: attachmentID,
filename: file.name,
mimeType: file.type,
});
}
}
// Build the message object
const messageObject = {
messageHtml,
hasAttachment: multiResource.length > 0,
attachments: attachmentIdentifiers,
replyTo: replyToMessageIdentifier || null, // Include replyTo if applicable
};
// Encode the message object
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
base64Message = btoa(JSON.stringify(messageObject));
}
if (room === "admins" && userState.isAdmin) {
console.log("Encrypting message for admins...");
multiResource.push({
name: userState.accountName,
service: "MAIL_PRIVATE",
identifier: messageIdentifier,
data64: base64Message,
});
} else {
multiResource.push({
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message,
});
}
// Publish resources
if (room === "admins") {
if (!userState.isAdmin || adminPublicKeys.length === 0) {
console.error("User is not an admin or no admin public keys found. Aborting publish.");
window.alert("You are not authorized to post in the Admin room.");
return;
}
console.log("Publishing encrypted resources for Admin room...");
await publishMultipleResources(multiResource, adminPublicKeys, true);
} else {
console.log("Publishing resources for non-admin room...");
await publishMultipleResources(multiResource);
}
// Clear inputs and show success notification
clearInputs();
showSuccessNotification();
} catch (error) {
console.error("Error sending message:", error);
}
};
// Modify clearInputs to reset replyTo
const clearInputs = () => {
const quill = new Quill('#editor');
quill.root.innerHTML = "";
document.getElementById('file-input').value = "";
document.getElementById('image-input').value = "";
document.getElementById('preview-container').innerHTML = "";
replyToMessageIdentifier = null;
multiResource = [];
attachmentIdentifiers = [];
selectedImages = []
selectedFiles = []
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove();
}
};
// Show success notification
const showSuccessNotification = () => {
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Please wait for confirmation.";
notification.style.color = "green";
notification.style.marginTop = "1em";
document.querySelector(".message-input-section").appendChild(notification);
alert(`Successfully Published! Please note that messages will not display until after they are CONFIRMED, be patient!`)
setTimeout(() => {
notification.remove();
}, 10000);
};
// Generate unique attachment ID
const generateAttachmentID = (room, fileIndex = null) => {
const baseID = room === "admins" ? `${messageAttachmentIdentifierPrefix}-${room}-e-${randomID()}` : `${messageAttachmentIdentifierPrefix}-${room}-${randomID()}`;
return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID;
};
// const decryptFile = async (encryptedData) => {
// const publicKey = await getPublicKeyByName(userState.accountName)
// const response = await qortalRequest({
// action: 'DECRYPT_DATA',
// encryptedData, // has to be in base64 format
// // publicKey: publicKey // requires the public key of the opposite user with whom you've created the encrypted data.
// });
// const decryptedObject = response
// return decryptedObject
// }
// --- REFACTORED LOAD MESSAGES AND HELPER FUNCTIONS ---
const loadMessagesFromQDN = async (room, page, isPolling = false) => {
try {
const limit = 10;
const offset = page * limit;
console.log(`Loading messages from QDN: room=${room}, page=${page}, offset=${offset}, limit=${limit}`);
const messagesContainer = document.querySelector("#messages-container");
if (!messagesContainer) return;
prepareMessageContainer(messagesContainer, isPolling);
const { service, query } = getServiceAndQuery(room);
const response = await fetchResourceList(service, query, limit, offset, room);
console.log(`Fetched ${response.length} message(s) for page ${page}.`);
if (handleNoMessagesScenario(isPolling, page, response, messagesContainer)) {
return;
}
// Re-establish existing identifiers after preparing container
existingIdentifiers = new Set(
Array.from(messagesContainer.querySelectorAll('.message-item'))
.map(item => item.dataset.identifier)
);
let mostRecentMessage = getCurrentMostRecentMessage(room);
const fetchMessages = await fetchAllMessages(response, service, room);
const { firstNewMessageIdentifier, updatedMostRecentMessage } = renderNewMessages(
fetchMessages,
existingIdentifiers,
messagesContainer,
room,
mostRecentMessage
);
if (firstNewMessageIdentifier && !isPolling) {
scrollToNewMessages(firstNewMessageIdentifier);
}
if (updatedMostRecentMessage) {
updateLatestMessageIdentifiers(room, updatedMostRecentMessage);
}
handleReplyLogic(fetchMessages);
await updatePaginationControls(room, limit);
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
};
/** Helper Functions (Arrow Functions) **/
const prepareMessageContainer = (messagesContainer, isPolling) => {
if (!isPolling) {
messagesContainer.innerHTML = "";
existingIdentifiers.clear();
}
};
const getServiceAndQuery = (room) => {
const service = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST";
const query = (room === "admins")
? `${messageIdentifierPrefix}-${room}-e`
: `${messageIdentifierPrefix}-${room}`;
return { service, query };
};
const fetchResourceList = async (service, query, limit, offset, room) => {
return await searchAllWithOffset(service, query, limit, offset, room);
};
const handleNoMessagesScenario = (isPolling, page, response, messagesContainer) => {
if (response.length === 0) {
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return true;
}
return false;
};
const getCurrentMostRecentMessage = (room) => {
return latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
};
const fetchAllMessages = async (response, service, room) => {
return Promise.all(response.map(resource => fetchFullMessage(resource, service, room)));
};
const fetchFullMessage = async (resource, service, room) => {
try {
// Skip if already displayed
if (existingIdentifiers.has(resource.identifier)) {
return null;
}
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service,
identifier: resource.identifier,
...(room === "admins" ? { encoding: "base64" } : {}),
});
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
const messageObject = await processMessageObject(messageResponse, room);
return {
name: resource.name,
content: messageObject?.messageHtml || "<em>Message content missing</em>",
date: formattedTimestamp,
identifier: resource.identifier,
replyTo: messageObject?.replyTo || null,
timestamp,
attachments: messageObject?.attachments || [],
};
} catch (error) {
console.error(`Failed to fetch message ${resource.identifier}: ${error.message}`);
return {
name: resource.name,
content: "<em>Error loading message</em>",
date: "Unknown",
identifier: resource.identifier,
replyTo: null,
timestamp: resource.updated || resource.created,
attachments: [],
};
}
};
const processMessageObject = async (messageResponse, room) => {
if (room !== "admins") {
return messageResponse;
}
try {
const decryptedData = await decryptAndParseObject(messageResponse);
return decryptedData
} catch (error) {
console.error(`Failed to decrypt admin message: ${error.message}`);
return null;
}
};
const renderNewMessages = (fetchMessages, existingIdentifiers, messagesContainer, room, mostRecentMessage) => {
let firstNewMessageIdentifier = null;
let updatedMostRecentMessage = mostRecentMessage;
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
const isNewMessage = isMessageNew(message, mostRecentMessage);
if (isNewMessage && !firstNewMessageIdentifier) {
firstNewMessageIdentifier = message.identifier;
}
const messageHTML = buildMessageHTML(message, fetchMessages, room, isNewMessage);
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
if (!updatedMostRecentMessage || new Date(message.timestamp) > new Date(updatedMostRecentMessage?.latestTimestamp || 0)) {
updatedMostRecentMessage = {
latestIdentifier: message.identifier,
latestTimestamp: message.timestamp,
};
}
existingIdentifiers.add(message.identifier);
}
}
return { firstNewMessageIdentifier, updatedMostRecentMessage };
};
const isMessageNew = (message, mostRecentMessage) => {
return !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
};
const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => {
const replyHtml = buildReplyHtml(message, fetchMessages);
const attachmentHtml = buildAttachmentHtml(message, room);
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
return `
<div class="message-item" data-identifier="${message.identifier}">
<div class="message-header" style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center;">
<img src="${avatarUrl}" alt="Avatar" class="user-avatar" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 10px;">
<span class="username">${message.name}</span>
${isNewMessage ? `<span class="new-indicator" style="margin-left: 10px; color: red; font-weight: bold;">NEW</span>` : ''}
</div>
<span class="timestamp">${message.date}</span>
</div>
${replyHtml}
<div class="message-text">${message.content}</div>
<div class="attachments-gallery">
${attachmentHtml}
</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`
}
const buildReplyHtml = (message, fetchMessages) => {
if (!message.replyTo) return ""
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
if (!repliedMessage) return ""
return `
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
<div class="reply-content">${repliedMessage.content}</div>
</div>
`
}
const buildAttachmentHtml = (message, room) => {
if (!message.attachments || message.attachments.length === 0) return ""
return message.attachments.map(attachment => buildSingleAttachmentHtml(attachment, room)).join("")
}
const buildSingleAttachmentHtml = (attachment, room) => {
if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`
return `
<div class="attachment">
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
</div>
`
} else if
(room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
return fetchEncryptedImageHtml(attachment)
} else {
return `
<div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">
Download ${attachment.filename}
</button>
</div>
`
}
}
const scrollToNewMessages = (firstNewMessageIdentifier) => {
const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`)
if (newMessageElement) {
newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}
const updateLatestMessageIdentifiers = (room, mostRecentMessage) => {
latestMessageIdentifiers[room] = mostRecentMessage
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers))
}
const handleReplyLogic = (fetchMessages) => {
const replyButtons = document.querySelectorAll(".reply-button")
replyButtons.forEach(button => {
button.addEventListener("click", () => {
const replyToMessageIdentifier = button.dataset.messageIdentifier
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier)
if (repliedMessage) {
showReplyPreview(repliedMessage)
}
})
})
}
const showReplyPreview = (repliedMessage) => {
replyToMessageIdentifier = repliedMessage.identifier
const replyContainer = document.createElement("div")
replyContainer.className = "reply-container"
replyContainer.innerHTML = `
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
<strong>Replying to:</strong> ${repliedMessage.content}
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
</div>
`
if (!document.querySelector(".reply-container")) {
const messageInputSection = document.querySelector(".message-input-section")
if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild)
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null
replyContainer.remove()
})
}
}
const messageInputSection = document.querySelector(".message-input-section")
const editor = document.querySelector(".ql-editor")
if (messageInputSection) {
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
if (editor) {
editor.focus()
}
}
const updatePaginationControls = async (room, limit) => {
const totalMessages = room === "admins" ? await searchAllCountOnly(`${messageIdentifierPrefix}-${room}-e`, room) : await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`, room)
renderPaginationControls(room, totalMessages, limit)
}
// Polling function to check for new messages without clearing existing ones
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0]
if (activeRoom) {
await loadMessagesFromQDN(activeRoom, currentPage, true)
}
}, 40000)
}