Massive re-factor to forum code (at least the first half)

This commit is contained in:
crowetic 2024-12-12 21:49:14 -08:00
parent 24e517b299
commit f500846975
3 changed files with 278 additions and 291 deletions

View File

@ -384,6 +384,16 @@
background-color: rgba(0, 0, 0, 0.8); /* Black w/ opacity */ background-color: rgba(0, 0, 0, 0.8); /* Black w/ opacity */
} }
.remove-image-button {
background-color: #0f4c41;
color: #ffffff;
border: dotted;
border-radius: 1vh;
padding: 0.3vh 0.6vh;
margin-top: 1px;
cursor: pointer;
}
.modal-content { .modal-content {
margin: auto; margin: auto;
display: block; display: block;

View File

@ -494,7 +494,7 @@ const toggleComments = async (cardIdentifier) => {
const createModal = async () => { const createModal = async () => {
const modalHTML = ` const modalHTML = `
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;"> <div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;">
<div style="position: relative; margin: 5% auto; width: 90%; height: 90%; background: white; border-radius: 10px; overflow: hidden;"> <div style="position: relative; margin: 10% auto; width: 95%; height: 80%; background: white; border-radius: 10px; overflow: hidden;">
<iframe id="modalContent" src="" style="width: 100%; height: 100%; border: none;"></iframe> <iframe id="modalContent" src="" style="width: 100%; height: 100%; border: none;"></iframe>
<button onclick="closeModal()" style="position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;">Close</button> <button onclick="closeModal()" style="position: absolute; top: 10px; right: 10px; background: red; color: white; border: none; padding: 5px 10px; border-radius: 5px;">Close</button>
</div> </div>
@ -522,9 +522,16 @@ const closeModal = async () => {
const processLink = async (link) => { const processLink = async (link) => {
if (link.startsWith('qortal://')) { if (link.startsWith('qortal://')) {
return link.replace('qortal://', '/render/') const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
if (match) {
const firstParam = match[1].toUpperCase(); // Convert to uppercase
const remainingPath = match[2] || ""; // Rest of the URL
// Perform any asynchronous operation if necessary
await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
return `http://localhost:12391/render/${firstParam}${remainingPath}`;
} }
return link // Return the link unchanged if it doesn't start with `qortal://` }
return link; // Return unchanged if not a Qortal link
} }

View File

@ -137,7 +137,13 @@ const renderPaginationControls = async(room, totalMessages, limit) => {
// Main function to load the full content of the room, along with all main functionality ----------------------------------- // Main function to load the full content of the room, along with all main functionality -----------------------------------
const loadRoomContent = async (room) => { const loadRoomContent = async (room) => {
const forumContent = document.getElementById("forum-content"); const forumContent = document.getElementById("forum-content");
if (forumContent) {
if (!forumContent) {
console.error("Forum content container not found!");
return;
}
// Set initial content
forumContent.innerHTML = ` forumContent.innerHTML = `
<div class="room-content"> <div class="room-content">
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3> <h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
@ -159,86 +165,90 @@ const loadRoomContent = async (room) => {
</div> </div>
`; `;
const imageModalHTML = ` // Add modal for image preview
forumContent.insertAdjacentHTML(
'beforeend',
`
<div id="image-modal" class="image-modal"> <div id="image-modal" class="image-modal">
<span id="close-modal" class="close">&times;</span> <span id="close-modal" class="close">&times;</span>
<img id="modal-image" class="modal-content"> <img id="modal-image" class="modal-content">
<div id="caption" class="caption"></div> <div id="caption" class="caption"></div>
<button id="download-button" class="download-button">Download</button> <button id="download-button" class="download-button">Download</button>
</div> </div>
`; `);
forumContent.insertAdjacentHTML('beforeend', imageModalHTML);
// Initialize Quill editor for rich text input initializeQuillEditor();
const quill = new Quill('#editor', { setupModalHandlers();
setupFileInputs(room);
await loadMessagesFromQDN(room, currentPage);
};
// Initialize Quill editor
const initializeQuillEditor = () => {
new Quill('#editor', {
theme: 'snow', theme: 'snow',
modules: { modules: {
toolbar: [ toolbar: [
[{ 'font': [] }], // Add font family options [{ 'font': [] }],
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options [{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'header': [1, 2, false] }], [{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options ['bold', 'italic', 'underline'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }], [{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'], ['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options [{ 'color': [] }, { 'background': [] }],
[{ 'align': [] }], // Text alignment [{ 'align': [] }],
['clean'] // Remove formatting button ['clean']
] ]
} }
}); });
};
// Load messages from QDN for the selected room // Set up modal behavior
await loadMessagesFromQDN(room, currentPage); const setupModalHandlers = () => {
document.addEventListener("click", (event) => {
document.addEventListener("click", async (event) => {
if (event.target.classList.contains("inline-image")) { if (event.target.classList.contains("inline-image")) {
const modal = document.getElementById("image-modal"); const modal = document.getElementById("image-modal");
const modalImage = document.getElementById("modal-image"); const modalImage = document.getElementById("modal-image");
const caption = document.getElementById("caption"); const caption = document.getElementById("caption");
// const downloadButton = document.getElementById("download-button");
// Set the modal content
modalImage.src = event.target.src; modalImage.src = event.target.src;
caption.textContent = event.target.alt; caption.textContent = event.target.alt;
// Show the modal
modal.style.display = "block"; modal.style.display = "block";
} }
}); });
// Close the modal document.getElementById("close-modal").addEventListener("click", () => {
document.getElementById("close-modal").addEventListener("click", async () => {
document.getElementById("image-modal").style.display = "none"; document.getElementById("image-modal").style.display = "none";
}); });
// Hide the modal when clicking outside of the image or close button window.addEventListener("click", (event) => {
window.addEventListener("click", async (event) => {
const modal = document.getElementById("image-modal"); const modal = document.getElementById("image-modal");
if (!event.target == modal) { if (event.target === modal) {
modal.style.display = "none"; modal.style.display = "none";
} }
}); });
};
let selectedFiles = [];
let selectedImages = []; let selectedImages = [];
let selectedFiles = [];
let multiResource = [];
let attachmentIdentifiers = []; let attachmentIdentifiers = [];
let multiResource = []
// Set up file input handling
const setupFileInputs = (room) => {
const imageFileInput = document.getElementById('image-input'); const imageFileInput = document.getElementById('image-input');
const previewContainer = document.getElementById('preview-container'); const previewContainer = document.getElementById('preview-container');
const addToPublishButton = document.getElementById('add-images-to-publish-button') const addToPublishButton = document.getElementById('add-images-to-publish-button');
const randomID = await uid(); const fileInput = document.getElementById('file-input');
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`; const sendButton = document.getElementById('send-button');
imageFileInput.addEventListener('change', async (event) => { const attachmentID = generateAttachmentID(room);
// Clear previous previews to prepare for preview generation
imageFileInput.addEventListener('change', (event) => {
previewContainer.innerHTML = ''; previewContainer.innerHTML = '';
selectedImages = Array.from(event.target.files); selectedImages = [...event.target.files];
if (selectedImages.length > 0) { addToPublishButton.disabled = selectedImages.length === 0;
addToPublishButton.disabled = false;
}
selectedImages.forEach((file, index) => { selectedImages.forEach((file, index) => {
const reader = new FileReader(); const reader = new FileReader();
@ -246,161 +256,107 @@ const loadRoomContent = async (room) => {
const img = document.createElement('img'); const img = document.createElement('img');
img.src = reader.result; img.src = reader.result;
img.alt = file.name; img.alt = file.name;
img.style.width = '100px'; img.style = "width: 100px; height: 100px; object-fit: cover; border: 1px solid #ccc; border-radius: 5px;";
img.style.height = '100px';
img.style.objectFit = 'cover';
img.style.border = '1px solid #ccc';
img.style.borderRadius = '5px';
// Add remove button
const removeButton = document.createElement('button'); const removeButton = document.createElement('button');
removeButton.innerText = 'Remove'; removeButton.innerText = 'Remove';
removeButton.style.marginTop = '5px'; removeButton.classList.add('remove-image-button');
removeButton.onclick = () => { removeButton.onclick = () => {
selectedImages.splice(index, 1); selectedImages.splice(index, 1);
img.remove(); img.remove();
removeButton.remove(); removeButton.remove();
if (selectedImages.length === 0) { addToPublishButton.disabled = selectedImages.length === 0;
addToPublishButton.disabled = true;
}
}; };
const container = document.createElement('div'); const container = document.createElement('div');
container.style.display = 'flex'; container.style = "display: flex; flex-direction: column; align-items: center; margin: 5px;";
container.style.flexDirection = 'column'; container.append(img, removeButton);
container.style.alignItems = 'center'; previewContainer.append(container);
container.style.margin = '5px';
container.appendChild(img);
container.appendChild(removeButton);
previewContainer.appendChild(container);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
}); });
addToPublishButton.addEventListener('click', async () => { addToPublishButton.addEventListener('click', () => {
await addImagesToMultiPublish() processSelectedImages(selectedImages, multiResource, room);
}) selectedImages = [];
addToPublishButton.disabled = true;
// Function to add images in the preview to the multi-publish object --------------------------
const addImagesToMultiPublish = async () => {
console.log('Adding Images to multi-publish:', selectedImages);
for (let i = 0; i < selectedImages.length; i++) {
const file = selectedImages[i];
try {
multiResource.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file: file,
}); });
attachmentIdentifiers.push({ fileInput.addEventListener('change', (event) => {
name: userState.accountName, selectedFiles = [...event.target.files];
service: "FILE",
identifier: attachmentID,
filename: file.name,
mimeType: file.type
}); });
console.log(`Attachment ${file.name} placed into multiResource with attachmentID: ${attachmentID}`); sendButton.addEventListener('click', async () => {
const quill = new Quill('#editor');
// Remove the processed file
selectedImages.splice(i, 1);
i--; // Adjust the index since we removed an item
} catch (error) {
console.error(`Error processing attachment ${file.name}:`, error);
}
}
selectedImages = []
addToPublishButton.disabled = true
}
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', async (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the PUBLISH button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim(); const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0 || selectedImages.length > 0) {
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
if (selectedImages.length > 0) { if (messageHtml || selectedFiles.length > 0 || selectedImages.length > 0) {
await addImagesToMultiPublish() await handleSendMessage(room, messageHtml, selectedFiles, selectedImages, multiResource);
} }
if (selectedFiles.length === 1) {
console.log(`single file has been detected, attaching single file...`)
const singleAttachment = selectedFiles[0]
multiResource.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file: singleAttachment
})
attachmentIdentifiers.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
filename: singleAttachment.name,
filetype: singleAttachement.type
})
// Clear selectedFiles as we do not need them anymore.
document.getElementById('file-input').value = "";
selectedFiles = [];
}else if (selectedFiles.length >= 2) {
console.log(`selected files found: ${selectedFiles.length}, adding multiple files to multi-publish resource...`)
// Handle Multiple attachements utilizing multi-publish
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i];
try {
multiResource.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file: file,
}); });
attachmentIdentifiers.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
filename: file.name,
mimeType: file.type
});
console.log(`Attachment ${file.name} placed into multiResource with attachmentID: ${attachmentID}`);
// Remove the processed file
selectedFiles.splice(i, 1);
i--; // Adjust the index since we removed an item
} catch (error) {
console.error(`Error processing attachment ${file.name}:`, error);
}
}
}
// Create message object with unique identifier, HTML content, and attachments
const messageObject = {
messageHtml: messageHtml,
hasAttachment: attachmentIdentifiers.length > 0,
attachments: attachmentIdentifiers,
replyTo: replyToMessageIdentifier
}; };
// Process selected images
const processSelectedImages = async (selectedImages, multiResource, room) => {
for (const file of selectedImages) {
let attachmentID = generateAttachmentID(room, selectedImages.indexOf(file))
try { try {
// Convert message object to base64 multiResource.push({
let base64Message = await objectToBase64(messageObject); name: userState.accountName,
if (!base64Message) { service: "FILE",
console.log(`initial object creation with object failed, using btoa...`); identifier: attachmentID,
base64Message = btoa(JSON.stringify(messageObject)); file
});
attachmentIdentifiers.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
filename: file.name,
mimeType: file.type
})
} catch (error) {
console.error(`Error processing image ${file.name}:`, error);
}
}
};
// Handle send message
const handleSendMessage = async (room, messageHtml, selectedFiles, selectedImages, multiResource) => {
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${Date.now()}`;
try {
if (selectedImages.length > 0) {
await processSelectedImages(selectedImages, multiResource, room);
} }
// Put the message into the multiResource for batch-publishing. for (const file of selectedFiles) {
let attachmentID = generateAttachmentID(room, selectedFiles.indexOf(file))
multiResource.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file
});
attachmentIdentifiers.push({
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
filename: file.name,
mimeType: file.type
})
}
const messageObject = {
messageHtml,
hasAttachment: multiResource.length > 0,
attachments: attachmentIdentifiers,
replyTo: replyToMessageIdentifier || null // Add replyTo
};
const base64Message = btoa(JSON.stringify(messageObject));
multiResource.push({ multiResource.push({
name: userState.accountName, name: userState.accountName,
service: "BLOG_POST", service: "BLOG_POST",
@ -408,25 +364,38 @@ const loadRoomContent = async (room) => {
data64: base64Message data64: base64Message
}); });
console.log("Message added to multi-publish resource successfully, attempting multi-publish... "); await publishMultipleResources(multiResource);
await publishMultipleResources(multiResource) clearInputs();
showSuccessNotification();
} catch (error) {
console.error("Error sending message:", error);
}
};
// Clear the editor after sending the message, including any potential attached files and replies. // Modify clearInputs to reset replyTo
const clearInputs = () => {
const quill = new Quill('#editor');
quill.root.innerHTML = ""; quill.root.innerHTML = "";
document.getElementById('file-input').value = ""; document.getElementById('file-input').value = "";
selectedFiles = []; document.getElementById('image-input').value = "";
selectedImages = []; document.getElementById('preview-container').innerHTML = "";
multiResource = [];
replyToMessageIdentifier = null; replyToMessageIdentifier = null;
multiResource = [];
attachmentIdentifiers = [];
selectedImages = []
selectedFiles = []
const replyContainer = document.querySelector(".reply-container"); const replyContainer = document.querySelector(".reply-container");
if (replyContainer) { if (replyContainer) {
replyContainer.remove() replyContainer.remove();
} }
};
// Show success notification // Show success notification
const showSuccessNotification = () => {
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show, please be patient..."; notification.innerText = "Message published successfully! Please wait for confirmation.";
notification.style.color = "green"; notification.style.color = "green";
notification.style.marginTop = "1em"; notification.style.marginTop = "1em";
document.querySelector(".message-input-section").appendChild(notification); document.querySelector(".message-input-section").appendChild(notification);
@ -434,14 +403,16 @@ const loadRoomContent = async (room) => {
setTimeout(() => { setTimeout(() => {
notification.remove(); notification.remove();
}, 10000); }, 10000);
};
// Generate unique attachment ID
const generateAttachmentID = (room, fileIndex = null) => {
const baseID = `${messageAttachmentIdentifierPrefix}-${room}-${Date.now()}`;
return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID;
};
} catch (error) {
console.error("Error publishing message:", error);
}
}
})
}
}
const loadMessagesFromQDN = async (room, page, isPolling = false) => { const loadMessagesFromQDN = async (room, page, isPolling = false) => {
try { try {
@ -534,17 +505,17 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
let attachmentHtml = ""; let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) { if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) { for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) { if (attachment.mimeType && attachment.mimeType.startsWith('image/')) {
try { try {
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED. // Construct the image URL
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`; const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
// Add the image HTML with the direct URL // Add the image HTML with the direct URL
attachmentHtml += `<div class="attachment"> attachmentHtml += `<div class="attachment">
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/> <img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
</div>`; </div>`;
// Add the modal download button details as well, in order to pass correct information to the modal
// Set up the modal download button
const downloadButton = document.getElementById("download-button"); const downloadButton = document.getElementById("download-button");
downloadButton.onclick = () => { downloadButton.onclick = () => {
fetchAndSaveAttachment( fetchAndSaveAttachment(
@ -555,13 +526,11 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
attachment.mimeType attachment.mimeType
); );
}; };
// FOR OTHER METHOD NO LONGER USED
// attachmentHtml += imageHtml;
} catch (error) { } catch (error) {
console.error(`Failed to fetch attachment ${attachment.filename}:`, error); console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
} }
} else { } else {
// Display a button to download other attachments // Display a button to download non-image attachments
attachmentHtml += `<div class="attachment"> attachmentHtml += `<div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button> <button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`; </div>`;
@ -569,6 +538,7 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
} }
} }
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`; const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
const messageHTML = ` const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}"> <div class="message-item" data-identifier="${message.identifier}">