created Q-Mintership-Alpha repository

This commit is contained in:
2024-12-11 14:40:32 -08:00
commit 2c58e63227
87 changed files with 25356 additions and 0 deletions

167
assets/js/AdminTools.js Normal file
View File

@@ -0,0 +1,167 @@
let currentMinterToolPage = 'overview'; // Track the current page
// Load latest state for admin verification
async function verifyMinterAdminState() {
const minterGroupAdmins = await fetchMinterGroupAdmins();
return minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin);
}
document.addEventListener('DOMContentLoaded', async () => {
const isAdmin = await verifyUserIsAdmin();
if (isAdmin) {
console.log(`User is an Admin, buttons for MA Tools not removed. userState.isAdmin = ${userState.isMinterAdmin}`);
} else {
// Remove all "TOOLS" links and their related elements
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
toolsLinks.forEach(link => {
// If the link is within a button, remove the button
const buttonParent = link.closest('button');
if (buttonParent) {
buttonParent.remove();
}
// If the link is within an image card or any other element, remove that element
const cardParent = link.closest('.item.features-image');
if (cardParent) {
cardParent.remove();
}
// Finally, remove the link itself if it's not covered by the above removals
link.remove();
});
console.log(`User is NOT a Minter Admin, buttons for MA Tools removed. userState.isMinterAdmin = ${userState.isMinterAdmin}`);
// 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');
}
return;
}
// Add event listener for admin tools link if the user is an admin
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
toolsLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
if (!userState.isLoggedIn) {
await login();
}
await loadMinterAdminToolsPage();
});
});
});
async function loadMinterAdminToolsPage() {
// Remove all body content except for menu elements
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();
}
}
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
// Set the background image directly from a file
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="tools-main mbr-parallax-background cid-ttRnlSkg2R">
<div class="tools-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
<div> <h1 style="font-size: 50px; margin: 0;">MINTER ADMIN TOOLS for Admin = </h1></div>
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; 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 id="tools-submenu" class="tools-submenu">
<div class="tools-buttons">
<button id="display-pending" class="tools-button">Display Pending Approval Transactions</button>
<button id="create-group-invite" class="tools-button">Create Pending Group Invite</button>
<button id="create-promotion" class="tools-button">Create Pending Promotion</button>
</div>
<div id="tools-window" class="tools-window"></div>
</div>
</div>
`;
document.body.appendChild(mainContent);
addToolsPageEventListeners();
}
function addToolsPageEventListeners() {
document.getElementById("display-pending").addEventListener("click", async () => {
await displayPendingApprovals();
});
document.getElementById("create-group-invite").addEventListener("click", async () => {
createPendingGroupInvite();
});
document.getElementById("create-promotion").addEventListener("click", async () => {
createPendingPromotion();
});
}
// Fetch and display pending approvals
async function displayPendingApprovals() {
console.log("Fetching pending approval transactions...");
const response = await qortalRequest({
action: "SEARCH_TRANSACTIONS",
txGroupId: 694,
txType: [
"ADD_GROUP_ADMIN",
"GROUP_INVITE"
],
confirmationStatus: "UNCONFIRMED",
limit: 0,
offset: 0,
reverse: false
});
console.log("Fetched pending approvals: ", response);
const toolsWindow = document.getElementById('tools-window');
if (response && response.length > 0) {
toolsWindow.innerHTML = response.map(tx => `
<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;">
<p><strong>Transaction Type:</strong> ${tx.type}</p>
<p><strong>Amount:</strong> ${tx.amount}</p>
<p><strong>Creator Address:</strong> ${tx.creatorAddress}</p>
<p><strong>Recipient:</strong> ${tx.recipient}</p>
<p><strong>Timestamp:</strong> ${new Date(tx.timestamp).toLocaleString()}</p>
<button onclick="approveTransaction('${tx.signature}')">Approve</button>
</div>
`).join('');
} else {
toolsWindow.innerHTML = '<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;"><p>No pending approvals found.</p></div>';
}
}
// Placeholder function to create a pending group invite
async function createPendingGroupInvite() {
console.log("Creating a pending group invite...");
// Placeholder code for creating a pending group invite
alert('Pending group invite created (placeholder).');
}
// Placeholder function to create a pending promotion
async function createPendingPromotion() {
console.log("Creating a pending promotion...");
// Placeholder code for creating a pending promotion
alert('Pending promotion created (placeholder).');
}
// Placeholder function for approving a transaction
function approveTransaction(signature) {
console.log("Approving transaction with signature: ", signature);
// Placeholder code for approving transaction
alert(`Transaction with signature ${signature} approved (placeholder).`);
}

View File

@@ -0,0 +1,157 @@
let currentMinterToolPage = 'overview'; // Track the current page
// Load latest state for admin verification
async function verifyMinterAdminState() {
const minterGroupAdmins = await fetchMinterGroupAdmins();
return minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin)
}
document.addEventListener('DOMContentLoaded', async () => {
const isAdmin = await verifyUserIsAdmin();
if (isAdmin) {
console.log(`User is an Admin, buttons for MA Tools not removed. userState.isAdmin = ${userState.isMinterAdmin}`);
} else {
// Remove all "TOOLS" links and their related elements
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
toolsLinks.forEach(link => {
// If the link is within a button, remove the button
const buttonParent = link.closest('button');
if (buttonParent) {
buttonParent.remove();
}
// If the link is within an image card or any other element, remove that element
const cardParent = link.closest('.item.features-image');
if (cardParent) {
cardParent.remove();
}
// Finally, remove the link itself if it's not covered by the above removals
link.remove();
});
console.log(`User is NOT a Minter Admin, buttons for MA Tools removed. userState.isMinterAdmin = ${userState.isMinterAdmin}`);
// 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');
}
return;
}
// Add event listener for admin tools link if the user is an admin
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
toolsLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await loadMinterAdminToolsPage();
});
});
});
async function loadMinterAdminToolsPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Set the background image directly from a file
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="tools-main tools-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg');">
<div class="tools-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<span>MINTER ADMIN TOOLS (Alpha)</span>
</div>
<div id="tools-content" class="tools-content">
<div class="tools-buttons">
<button id="display-pending" class="tools-button">Display Pending Approval Transactions</button>
<button id="create-group-invite" class="tools-button">Create Pending Group Invite</button>
<button id="create-promotion" class="tools-button">Create Pending Promotion</button>
</div>
<div id="tools-window" class="tools-window"></div>
</div>
</div>
`;
document.body.appendChild(mainContent);
addToolsPageEventListeners();
}
function addToolsPageEventListeners() {
document.getElementById("display-pending").addEventListener("click", async () => {
await displayPendingApprovals();
});
document.getElementById("create-group-invite").addEventListener("click", async () => {
createPendingGroupInvite();
});
document.getElementById("create-promotion").addEventListener("click", async () => {
createPendingPromotion();
});
}
// Fetch and display pending approvals
async function displayPendingApprovals() {
console.log("Fetching pending approval transactions...");
const response = await qortalRequest({
action: "SEARCH_TRANSACTIONS",
txGroupId: 694,
txType: [
"ADD_GROUP_ADMIN",
"GROUP_INVITE"
],
confirmationStatus: "UNCONFIRMED",
limit: 0,
offset: 0,
reverse: false
});
console.log("Fetched pending approvals: ", response);
const toolsWindow = document.getElementById('tools-window');
if (response && response.length > 0) {
toolsWindow.innerHTML = response.map(tx => `
<div class="pending-approval-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;">
<p><strong>Transaction Type:</strong> ${tx.type}</p>
<p><strong>Amount:</strong> ${tx.amount}</p>
<p><strong>Creator Address:</strong> ${tx.creatorAddress}</p>
<p><strong>Recipient:</strong> ${tx.recipient}</p>
<p><strong>Timestamp:</strong> ${new Date(tx.timestamp).toLocaleString()}</p>
<button onclick="approveTransaction('${tx.signature}')">Approve</button>
</div>
`).join('');
} else {
toolsWindow.innerHTML = '<p>No pending approvals found.</p>';
}
}
// Placeholder function to create a pending group invite
async function createPendingGroupInvite() {
console.log("Creating a pending group invite...");
// Placeholder code for creating a pending group invite
alert('Pending group invite created (placeholder).');
}
// Placeholder function to create a pending promotion
async function createPendingPromotion() {
console.log("Creating a pending promotion...");
// Placeholder code for creating a pending promotion
alert('Pending promotion created (placeholder).');
}
// Placeholder function for approving a transaction
function approveTransaction(signature) {
console.log("Approving transaction with signature: ", signature);
// Placeholder code for approving transaction
alert(`Transaction with signature ${signature} approved (placeholder).`);
}

View File

@@ -0,0 +1,157 @@
let currentMinterToolPage = 'overview'; // Track the current page
// Load latest state for admin verification
async function verifyMinterAdminState() {
const minterGroupAdmins = await fetchMinterGroupAdmins();
return minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin)
}
document.addEventListener('DOMContentLoaded', async () => {
const isAdmin = await verifyUserIsAdmin();
if (isAdmin) {
console.log(`User is an Admin, buttons for MA Tools not removed. userState.isAdmin = ${userState.isMinterAdmin}`);
} else {
// Remove all "TOOLS" links and their related elements
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
toolsLinks.forEach(link => {
// If the link is within a button, remove the button
const buttonParent = link.closest('button');
if (buttonParent) {
buttonParent.remove();
}
// If the link is within an image card or any other element, remove that element
const cardParent = link.closest('.item.features-image');
if (cardParent) {
cardParent.remove();
}
// Finally, remove the link itself if it's not covered by the above removals
link.remove();
});
console.log(`User is NOT a Minter Admin, buttons for MA Tools removed. userState.isMinterAdmin = ${userState.isMinterAdmin}`);
// 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');
}
return;
}
// Add event listener for admin tools link if the user is an admin
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
toolsLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await loadMinterAdminToolsPage();
});
});
});
async function loadMinterAdminToolsPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Set the background image directly from a file
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="tools-main tools-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg');">
<div class="tools-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<span>MINTER ADMIN TOOLS (Alpha)</span>
</div>
<div id="tools-content" class="tools-content">
<div class="tools-buttons">
<button id="display-pending" class="tools-button">Display Pending Approval Transactions</button>
<button id="create-group-invite" class="tools-button">Create Pending Group Invite</button>
<button id="create-promotion" class="tools-button">Create Pending Promotion</button>
</div>
<div id="tools-window" class="tools-window"></div>
</div>
</div>
`;
document.body.appendChild(mainContent);
addToolsPageEventListeners();
}
function addToolsPageEventListeners() {
document.getElementById("display-pending").addEventListener("click", async () => {
await displayPendingApprovals();
});
document.getElementById("create-group-invite").addEventListener("click", async () => {
createPendingGroupInvite();
});
document.getElementById("create-promotion").addEventListener("click", async () => {
createPendingPromotion();
});
}
// Fetch and display pending approvals
async function displayPendingApprovals() {
console.log("Fetching pending approval transactions...");
const response = await qortalRequest({
action: "SEARCH_TRANSACTIONS",
txGroupId: 694,
txType: [
"ADD_GROUP_ADMIN",
"GROUP_INVITE"
],
confirmationStatus: "UNCONFIRMED",
limit: 0,
offset: 0,
reverse: false
});
console.log("Fetched pending approvals: ", response);
const toolsWindow = document.getElementById('tools-window');
if (response && response.length > 0) {
toolsWindow.innerHTML = response.map(tx => `
<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;">
<p><strong>Transaction Type:</strong> ${tx.type}</p>
<p><strong>Amount:</strong> ${tx.amount}</p>
<p><strong>Creator Address:</strong> ${tx.creatorAddress}</p>
<p><strong>Recipient:</strong> ${tx.recipient}</p>
<p><strong>Timestamp:</strong> ${new Date(tx.timestamp).toLocaleString()}</p>
<button onclick="approveTransaction('${tx.signature}')">Approve</button>
</div>
`).join('');
} else {
toolsWindow.innerHTML = '<p>No pending approvals found.</p>';
}
}
// Placeholder function to create a pending group invite
async function createPendingGroupInvite() {
console.log("Creating a pending group invite...");
// Placeholder code for creating a pending group invite
alert('Pending group invite created (placeholder).');
}
// Placeholder function to create a pending promotion
async function createPendingPromotion() {
console.log("Creating a pending promotion...");
// Placeholder code for creating a pending promotion
alert('Pending promotion created (placeholder).');
}
// Placeholder function for approving a transaction
function approveTransaction(signature) {
console.log("Approving transaction with signature: ", signature);
// Placeholder code for approving transaction
alert(`Transaction with signature ${signature} approved (placeholder).`);
}

View File

@@ -0,0 +1,317 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
let currentPage = 0; // Track current pagination page
// Load the latest message identifiers from local storage
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
// const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
const backgroundImage = "url('/assets/images/background.jpg')"
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<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 (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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 class="message-input-section">
<div id="toolbar" class="message-toolbar"></div>
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Clear reply reference after sending if it exists.
if (replyToMessageIdentifier) {
replyToMessageIdentifier = null;
replyContainer.remove();
}
replyToMessageIdentifier = null;
replyContainer.remove();
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages
loadMessagesFromQDN(room, currentPage);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page) {
try {
const offset = page * 10;
const limit = 10;
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, offset, limit);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
if (page === 0) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Clear messages only when loading the first page
if (page === 0) {
messagesContainer.innerHTML = "";
}
// Fetch all messages
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render messages without duplication
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
fetchMessages.forEach((message) => {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
}
});
// setTimeout(() => {
// messagesContainer.scrollTop = messagesContainer.scrollHeight;
// }, 1000);
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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>
`;
const messageInputSection = document.querySelector(".message-input-section");
if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
// Add a listener for the cancel reply button
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// Polling function to check for new messages
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
if (activeRoom) {
await loadMessagesFromQDN(activeRoom, currentPage);
}
}, 20000);
}

View File

@@ -0,0 +1,317 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
let currentPage = 0; // Track current pagination page
// Load the latest message identifiers from local storage
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
// const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
const backgroundImage = "url('/assets/images/background.jpg')"
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<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 (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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 class="message-input-section">
<div id="toolbar" class="message-toolbar"></div>
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Clear reply reference after sending if it exists.
if (replyToMessageIdentifier) {
replyToMessageIdentifier = null;
replyContainer.remove();
}
replyToMessageIdentifier = null;
replyContainer.remove();
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages
loadMessagesFromQDN(room, currentPage);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page) {
try {
const offset = page * 10;
const limit = 10;
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, offset, limit);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
if (page === 0) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Clear messages only when loading the first page
if (page === 0) {
messagesContainer.innerHTML = "";
}
// Fetch all messages
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render messages without duplication
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
fetchMessages.forEach((message) => {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
}
});
// setTimeout(() => {
// messagesContainer.scrollTop = messagesContainer.scrollHeight;
// }, 1000);
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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>
`;
const messageInputSection = document.querySelector(".message-input-section");
if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
// Add a listener for the cancel reply button
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// Polling function to check for new messages
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
if (activeRoom) {
await loadMessagesFromQDN(activeRoom, currentPage);
}
}, 20000);
}

View File

@@ -0,0 +1,423 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
// const minterGroupAdmins = await fetchMinterGroupAdmins();
// const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${userState.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 loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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="load-more-container"></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>
<button id="attach-button" class="attach-button">Attach Files</button>
</div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
let selectedFiles = [];
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments
for (const file of selectedFiles) {
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
try {
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file: file,
filename: file.name,
filetype: file.type,
});
attachmentIdentifiers.push({
identifier: attachmentID,
filename: file.name,
mimeType: file.type
});
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
} catch (error) {
console.error(`Error publishing 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
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`);
base64Message = btoa(JSON.stringify(messageObject));
}
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message, including any potential attached files and replies.
quill.root.innerHTML = "";
document.getElementById('file-input').value = "";
selectedFiles = [];
replyToMessageIdentifier = null;
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove()
}
// Show success notification
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
notification.style.color = "green";
notification.style.marginTop = "10px";
document.querySelector(".message-input-section").appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
const loadMoreContainer = document.getElementById("load-more-container");
if (loadMoreContainer) {
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
// const offset = page * 10;
const offset = page * 10;
const limit = 20;
// Get the set of existing identifiers from the messages container
const messagesContainer = document.querySelector("#messages-container");
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch only messages that are not already present in the messages container
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
if (messagesContainer) {
// If there are no messages and we're not polling, display "no messages" message
if (!response || !response.length) {
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Define `mostRecentMessage` to track the latest message during this fetch
let mostRecentMessage = null;
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return {
name: resource.name,
content: messageObject.messageHtml,
date: formattedTimestamp,
identifier: resource.identifier,
replyTo: messageObject.replyTo,
timestamp,
attachments: messageObject.attachments || [] // Include attachments if they exist
};
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]?.latestTimestamp);
let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) {
try {
// Fetch the base64 string for the image
const image = await fetchFileBase64(attachment.service, attachment.name, attachment.identifier);
// Create a data URL for the Base64 string
const dataUrl = `data:${attachment.mimeType};base64,${image}`;
// Add the image HTML with the data URL
attachmentHtml += `<div class="attachment"><img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"></div>`;
} catch (error) {
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
}
} else {
// Display a button to download other attachments
attachmentHtml += `<div class="attachment">
<button onclick="fetchAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`;
}
}
}
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
${attachmentHtml}
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Track the most recent message
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.timestamp || 0)) {
mostRecentMessage = message;
}
}
}
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = {
latestIdentifier: mostRecentMessage.identifier,
latestTimestamp: mostRecentMessage.timestamp
};
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
})
})
if (response.length >= limit) {
document.getElementById("load-more-container").style.display = 'block';
} else {
document.getElementById("load-more-container").style.display = 'none';
}
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}

View File

@@ -0,0 +1,418 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
// const minterGroupAdmins = await fetchMinterGroupAdmins();
// const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${userState.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 loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
${(existingIdentifiers.size > 10)? '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>' : ''}
<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>
<button id="attach-button" class="attach-button">Attach Files</button>
</div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
let selectedFiles = [];
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments
for (const file of selectedFiles) {
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
try {
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file: file,
filename: file.name,
filetype: file.type,
});
attachmentIdentifiers.push({
identifier: attachmentID,
filename: file.name,
mimeType: file.type
});
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
} catch (error) {
console.error(`Error publishing 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
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`);
base64Message = btoa(JSON.stringify(messageObject));
}
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message, including any potential attached files and replies.
quill.root.innerHTML = "";
document.getElementById('file-input').value = "";
selectedFiles = [];
replyToMessageIdentifier = null;
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove()
}
// Update the latest message identifier - DO NOT DO THIS ON PUBLISH, OR MESSAGE WILL NOT BE LOADED CORRECTLY.
// latestMessageIdentifiers[room] = messageIdentifier;
// localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Show success notification
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
notification.style.color = "green";
notification.style.marginTop = "10px";
document.querySelector(".message-input-section").appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
// const offset = page * 10;
const offset = 0;
const limit = 0;
// Get the set of existing identifiers from the messages container
const messagesContainer = document.querySelector("#messages-container");
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch only messages that are not already present in the messages container
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
if (messagesContainer) {
// If there are no messages and we're not polling, display "no messages" message
if (!response || !response.length) {
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Define `mostRecentMessage` to track the latest message during this fetch
let mostRecentMessage = null;
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return {
name: resource.name,
content: messageObject.messageHtml,
date: formattedTimestamp,
identifier: resource.identifier,
replyTo: messageObject.replyTo,
timestamp,
attachments: messageObject.attachments || [] // Include attachments if they exist
};
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]?.latestTimestamp);
let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) {
try {
// Fetch the base64 string for the image
const image = await fetchFileBase64(attachment.service, attachment.name, attachment.identifier);
// Create a data URL for the Base64 string
const dataUrl = `data:${attachment.mimeType};base64,${image}`;
// Add the image HTML with the data URL
attachmentHtml += `<div class="attachment"><img src="${dataUrl}" alt="${attachment.filename}" class="inline-image"></div>`;
} catch (error) {
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
}
} else {
// Display a button to download other attachments
attachmentHtml += `<div class="attachment">
<button onclick="fetchAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`;
}
}
}
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
${attachmentHtml}
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Track the most recent message
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.timestamp || 0)) {
mostRecentMessage = message;
}
}
}
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = {
latestIdentifier: mostRecentMessage.identifier,
latestTimestamp: mostRecentMessage.timestamp
};
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}

View File

@@ -0,0 +1,505 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
// 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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
//login if not already logged in.
if (!userState.isLoggedIn) {
await login();
}
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// // Remove all sections except the menu
// const allSections = document.querySelectorAll('body > section');
// allSections.forEach(section => {
// if (!section.classList.contains('menu')) {
// section.remove();
// }
// });
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();
}
}
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background mbr-fullscreen cid-ttRnlSkg2R" style="background-image: url('./assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<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: lightblue; 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>
${userState.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");
});
}
async function 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);
}
}
async function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
<button id="attach-button" class="attach-button">Attach Files</button>
</div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
await loadMessagesFromQDN(room, currentPage);
let selectedFiles = [];
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments
for (const file of selectedFiles) {
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
try {
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
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} published successfully with ID: ${attachmentID}`);
} catch (error) {
console.error(`Error publishing 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
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`);
base64Message = btoa(JSON.stringify(messageObject));
}
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message, including any potential attached files and replies.
quill.root.innerHTML = "";
document.getElementById('file-input').value = "";
selectedFiles = [];
replyToMessageIdentifier = null;
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove()
}
// Show success notification
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
notification.style.color = "green";
notification.style.marginTop = "10px";
document.querySelector(".message-input-section").appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
const loadMoreContainer = document.getElementById("load-more-container");
if (loadMoreContainer) {
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
}
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
const limit = 10;
const offset = page * limit;
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
// Get the messages container
const messagesContainer = document.querySelector("#messages-container");
if (!messagesContainer) return;
// If not polling, clear the message container and the existing identifiers for a fresh load
if (!isPolling) {
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
}
// Get the set of existing identifiers from the messages container
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch messages for the current room and page
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
if (response.length === 0) {
// If no messages are fetched and it's not polling, display "no messages" for the initial load
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Define `mostRecentMessage` to track the latest message during this fetch
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
if (existingIdentifiers.has(resource.identifier)) {
return null; // Skip messages that are already displayed
}
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return {
name: resource.name,
content: messageObject.messageHtml,
date: formattedTimestamp,
identifier: resource.identifier,
replyTo: messageObject.replyTo,
timestamp,
attachments: messageObject.attachments || [] // Include attachments if they exist
};
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) {
try {
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED.
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
// Add the image HTML with the direct URL
attachmentHtml += `<div class="attachment">
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image" style="max-width: 30%; height: auto;"/>
</div>`;
// FOR OTHER METHOD NO LONGER USED
// attachmentHtml += imageHtml;
} catch (error) {
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
}
} else {
// Display a button to download other attachments
attachmentHtml += `<div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`;
}
}
}
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
const messageHTML = `
<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>
</div>
<span class="timestamp">${message.date}</span>
</div>
${replyHtml}
${attachmentHtml}
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Update mostRecentMessage if this message is newer
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) {
mostRecentMessage = {
latestIdentifier: message.identifier,
latestTimestamp: message.timestamp
};
}
// Add the identifier to the existingIdentifiers set
existingIdentifiers.add(message.identifier);
}
}
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = mostRecentMessage;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
});
});
// Render pagination controls
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
renderPaginationControls(room, totalMessages, limit);
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}

View File

@@ -0,0 +1,485 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
// 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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
// const minterGroupAdmins = await fetchMinterGroupAdmins();
// const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${userState.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");
});
}
async function 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);
}
});
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);
}
});
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);
}
});
paginationContainer.appendChild(nextButton);
}
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
<button id="attach-button" class="attach-button">Attach Files</button>
</div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
let selectedFiles = [];
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments
for (const file of selectedFiles) {
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
try {
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "FILE",
identifier: attachmentID,
file: file,
filename: file.name,
filetype: file.type,
});
attachmentIdentifiers.push({
identifier: attachmentID,
filename: file.name,
mimeType: file.type
});
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
} catch (error) {
console.error(`Error publishing 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
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`);
base64Message = btoa(JSON.stringify(messageObject));
}
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message, including any potential attached files and replies.
quill.root.innerHTML = "";
document.getElementById('file-input').value = "";
selectedFiles = [];
replyToMessageIdentifier = null;
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove()
}
// Show success notification
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
notification.style.color = "green";
notification.style.marginTop = "10px";
document.querySelector(".message-input-section").appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
const loadMoreContainer = document.getElementById("load-more-container");
if (loadMoreContainer) {
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
// const offset = page * 10;
const limit = 10;
const offset = page * limit;
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
// Get the set of existing identifiers from the messages container
const messagesContainer = document.querySelector("#messages-container");
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch only messages that are not already present in the messages container
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
if (messagesContainer) {
// If there are no messages and we're not polling, display "no messages" message
if (!response || !response.length) {
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
if (!isPolling) {
messagesContainer.innerHTML = "";
}
// Define `mostRecentMessage` to track the latest message during this fetch
let mostRecentMessage = null;
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return {
name: resource.name,
content: messageObject.messageHtml,
date: formattedTimestamp,
identifier: resource.identifier,
replyTo: messageObject.replyTo,
timestamp,
attachments: messageObject.attachments || [] // Include attachments if they exist
};
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]?.latestTimestamp);
let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) {
try {
// Fetch the base64 string for the image
const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier);
// Create a data URL for the Base64 string - THIS CAN BE USED IF IMAGES ARE POSTED IN BASE64, ALONG WITH obtain
//const dataUrl = `data:${attachment.mimeType};base64,${image}`;
// Add the image HTML with the data URL
attachmentHtml += imageHtml;
} catch (error) {
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
}
} else {
// Display a button to download other attachments
attachmentHtml += `<div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`;
}
}
}
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
${attachmentHtml}
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Track the most recent message
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.timestamp || 0)) {
mostRecentMessage = message;
}
}
}
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = {
latestIdentifier: mostRecentMessage.identifier,
latestTimestamp: mostRecentMessage.timestamp
};
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
})
})
// if (response.length >= limit) {
// document.getElementById("load-more-container").style.display = 'block';
// } else {
// document.getElementById("load-more-container").style.display = 'none';
// }
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
renderPaginationControls(room, totalMessages, limit);
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}

View File

@@ -0,0 +1,494 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
// 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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; 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>User: ${userState.accountName || 'Guest'}</span>
</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${userState.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");
});
}
async function 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);
}
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
<button id="attach-button" class="attach-button">Attach Files</button>
</div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
let selectedFiles = [];
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments
for (const file of selectedFiles) {
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
try {
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
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} published successfully with ID: ${attachmentID}`);
} catch (error) {
console.error(`Error publishing 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
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`);
base64Message = btoa(JSON.stringify(messageObject));
}
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message, including any potential attached files and replies.
quill.root.innerHTML = "";
document.getElementById('file-input').value = "";
selectedFiles = [];
replyToMessageIdentifier = null;
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove()
}
// Show success notification
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
notification.style.color = "green";
notification.style.marginTop = "10px";
document.querySelector(".message-input-section").appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
const loadMoreContainer = document.getElementById("load-more-container");
if (loadMoreContainer) {
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
}
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
const limit = 10;
const offset = page * limit;
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
// Get the messages container
const messagesContainer = document.querySelector("#messages-container");
if (!messagesContainer) return;
// If not polling, clear the message container and the existing identifiers for a fresh load
if (!isPolling) {
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
}
// Get the set of existing identifiers from the messages container
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch messages for the current room and page
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
if (response.length === 0) {
// If no messages are fetched and it's not polling, display "no messages" for the initial load
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Define `mostRecentMessage` to track the latest message during this fetch
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
if (existingIdentifiers.has(resource.identifier)) {
return null; // Skip messages that are already displayed
}
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return {
name: resource.name,
content: messageObject.messageHtml,
date: formattedTimestamp,
identifier: resource.identifier,
replyTo: messageObject.replyTo,
timestamp,
attachments: messageObject.attachments || [] // Include attachments if they exist
};
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) {
try {
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED.
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
// Add the image HTML with the direct URL
attachmentHtml += `<div class="attachment">
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image" style="max-width: 100%; height: auto;"/>
</div>`;
// FOR OTHER METHOD NO LONGER USED
// attachmentHtml += imageHtml;
} catch (error) {
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
}
} else {
// Display a button to download other attachments
attachmentHtml += `<div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`;
}
}
}
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header" style="display: flex; align-items: center;">
<img src="${avatarUrl}" alt="${message.name}'s Avatar" class="user-avatar" style="width: 40px; height: 40px; border-radius: 50%; margin-right: 10px;">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
${attachmentHtml}
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Update mostRecentMessage if this message is newer
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) {
mostRecentMessage = {
latestIdentifier: message.identifier,
latestTimestamp: message.timestamp
};
}
// Add the identifier to the existingIdentifiers set
existingIdentifiers.add(message.identifier);
}
}
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = mostRecentMessage;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
});
});
// Render pagination controls
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
renderPaginationControls(room, totalMessages, limit);
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}

View File

@@ -0,0 +1,491 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
// 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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
// const minterGroupAdmins = await fetchMinterGroupAdmins();
// const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; align-items: center; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${userState.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");
});
}
async function 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);
}
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
<button id="attach-button" class="attach-button">Attach Files</button>
</div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
let selectedFiles = [];
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments
for (const file of selectedFiles) {
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
try {
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
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} published successfully with ID: ${attachmentID}`);
} catch (error) {
console.error(`Error publishing 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
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`);
base64Message = btoa(JSON.stringify(messageObject));
}
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message, including any potential attached files and replies.
quill.root.innerHTML = "";
document.getElementById('file-input').value = "";
selectedFiles = [];
replyToMessageIdentifier = null;
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove()
}
// Show success notification
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
notification.style.color = "green";
notification.style.marginTop = "10px";
document.querySelector(".message-input-section").appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
const loadMoreContainer = document.getElementById("load-more-container");
if (loadMoreContainer) {
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
}
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
const limit = 10;
const offset = page * limit;
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
// Get the messages container
const messagesContainer = document.querySelector("#messages-container");
if (!messagesContainer) return;
// If not polling, clear the message container and the existing identifiers for a fresh load
if (!isPolling) {
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
}
// Get the set of existing identifiers from the messages container
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch messages for the current room and page
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
if (response.length === 0) {
// If no messages are fetched and it's not polling, display "no messages" for the initial load
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Define `mostRecentMessage` to track the latest message during this fetch
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
if (existingIdentifiers.has(resource.identifier)) {
return null; // Skip messages that are already displayed
}
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return {
name: resource.name,
content: messageObject.messageHtml,
date: formattedTimestamp,
identifier: resource.identifier,
replyTo: messageObject.replyTo,
timestamp,
attachments: messageObject.attachments || [] // Include attachments if they exist
};
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) {
try {
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED.
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
// Add the image HTML with the direct URL
attachmentHtml += `<div class="attachment">
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image" style="max-width: 100%; height: auto;"/>
</div>`;
// FOR OTHER METHOD NO LONGER USED
// attachmentHtml += imageHtml;
} catch (error) {
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
}
} else {
// Display a button to download other attachments
attachmentHtml += `<div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`;
}
}
}
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
${attachmentHtml}
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Update mostRecentMessage if this message is newer
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) {
mostRecentMessage = {
latestIdentifier: message.identifier,
latestTimestamp: message.timestamp
};
}
// Add the identifier to the existingIdentifiers set
existingIdentifiers.add(message.identifier);
}
}
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = mostRecentMessage;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
});
});
// Render pagination controls
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
renderPaginationControls(room, totalMessages, limit);
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}

View File

@@ -0,0 +1,175 @@
// const cardIdentifier = `minter-board-card-${Date.now()}`;
const cardIdentifier = `test-board-card-${await uid()}`;
document.addEventListener("DOMContentLoaded", async () => {
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();
}
await loadMinterBoardPage();
});
});
});
async function loadMinterBoardPage() {
// Clear existing content on the page
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();
}
}
// Add the "Minter Board" content
const mainContent = document.createElement("div");
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">Minter Board</h1>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<h3>Create a New Minter Card</h3>
<form id="publish-card-form">
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information..." required></textarea>
<label for="card-links">Links (qortal://...):</label>
<div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link">
</div>
<button type="button" id="add-link-button">Add Another Link</button>
<button type="submit" style="margin-top: 10px;">Publish Card</button>
<button type="button" id="cancel-publish" style="margin-top: 10px;">Cancel</button>
</form>
</div>
</div>
`;
document.body.appendChild(mainContent);
document.getElementById("publish-card-button").addEventListener("click", () => {
document.getElementById("publish-card-view").style.display = "block";
document.getElementById("cards-container").style.display = "none";
});
document.getElementById("cancel-publish").addEventListener("click", () => {
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "block";
});
document.getElementById("add-link-button").addEventListener("click", () => {
const linksContainer = document.getElementById("links-container");
const newLinkInput = document.createElement("input");
newLinkInput.type = "text";
newLinkInput.className = "card-link";
newLinkInput.placeholder = "Enter QDN link";
linksContainer.appendChild(newLinkInput);
});
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault();
await publishCard();
});
await loadCards();
}
async function publishCard() {
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"));
if (!header || !content) {
alert("Header and content are required!");
return;
}
const cardData = {
header,
content,
links,
creator: userState.accountName,
timestamp: new Date().toISOString(),
};
try {
const base64CardData = btoa(JSON.stringify(cardData));
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: cardIdentifier,
data64: base64CardData,
});
alert("Card published successfully!");
document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "block";
await loadCards();
} catch (error) {
console.error("Error publishing card:", error);
alert("Failed to publish card.");
}
}
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
identifierPrefix: "minter-board-card-",
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = "";
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = JSON.parse(atob(cardDataResponse));
const cardHTML = createCardHTML(cardData);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
function createCardHTML(cardData) {
const { header, content, links, creator, timestamp } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const linksHTML = links.map(link => `<a href="${link}" target="_blank">${link}</a>`).join("<br>");
return `
<div class="card" style="border: 1px solid lightblue; padding: 20px; margin-bottom: 20px; background-color: #2a2a2a; color: lightblue;">
<h3>${header}</h3>
<p>${content}</p>
<div>${linksHTML}</div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
</div>
`;
}

View File

@@ -0,0 +1,302 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
let currentPage = 0; // Track current pagination page
// Load the latest message identifiers from local storage
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background image
const mainContent = document.createElement('div');
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
mainContent.innerHTML = `
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<span>MINTERSHIP FORUM (Alpha)</span>
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<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 (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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 class="message-input-section">
<div id="toolbar" class="message-toolbar"></div>
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages
loadMessagesFromQDN(room, currentPage);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page) {
try {
const offset = page * 10;
const limit = 10;
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, offset, limit);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
if (page === 0) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
let messagesHTML = messagesContainer.innerHTML;
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
fetchMessages.forEach(async (message) => {
if (message) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
messagesHTML += `
<div class="message-item">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
}
});
messagesContainer.innerHTML = messagesHTML;
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 1000);
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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; font-weight: bold;">Cancel</button>
</div>
`;
const messageInputSection = document.querySelector(".message-input-section");
if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
// Add a listener for the cancel reply button
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// Polling function to check for new messages
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
if (activeRoom) {
await loadMessagesFromQDN(activeRoom, currentPage);
}
}, 10000);
}

View File

@@ -0,0 +1,289 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
// Load the latest message identifiers from local storage
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background image
const mainContent = document.createElement('div');
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
mainContent.innerHTML = `
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue;">MINTERSHIP FORUM (Alpha)</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
<button class="room-button" id="general-room">General Room</button>
<div class="user-info" style="float: right; color: lightblue; margin-right: 50px;">User: ${userState.accountName || 'Guest'}</div>
</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", () => {
loadRoomContent("minters");
});
if (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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 class="message-input-section">
<div id="toolbar" class="message-toolbar"></div>
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
// const quill = new Quill('#editor', {
// theme: 'snow',
// modules: {
// toolbar: '#toolbar' // Link to the external toolbar element
// }
// });
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }],
['clean'] // remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages
loadMessagesFromQDN(room);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
}
}
// Load messages for any given room
async function loadMessagesFromQDN(room) {
try {
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
return;
}
let messagesHTML = "";
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
fetchMessages.forEach(async (message) => {
if (message) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
messagesHTML += `
<div class="message-item">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
}
});
messagesContainer.innerHTML = messagesHTML;
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 1000);
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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; font-weight: bold;">Cancel</button>
</div>
`;
const messageInputSection = document.querySelector(".message-input-section");
if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
// Add a listener for the cancel reply button
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// Polling function to check for new messages
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
if (activeRoom) {
await loadMessagesFromQDN(activeRoom);
}
}, 10000);
}

View File

@@ -0,0 +1,192 @@
const messageIdentifierPrefix = `mintership-forum-message`
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLink = document.querySelector('a[href="MINTERSHIP-FORUM"]');
if (mintershipForumLink) {
mintershipForumLink.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
});
}
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background image
const mainContent = document.createElement('div');
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
mainContent.innerHTML = `
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header">MINTERSHIP FORUM (Alpha)</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
<button class="room-button" id="general-room">General Room</button>
<div class="user-info" style="float: right; color: white; margin-right: 20px;">User: ${userState.accountName || 'Guest'}</div>
</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", () => {
loadRoomContent("minters");
});
if (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
forumContent.innerHTML = `
<div class="room-content">
<h3 class="room-title">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
<div id="messages-container" class="messages-container"></div>
<div class="message-input-section">
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow'
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false
};
try {
// Convert message object to base64
const base64Message = await objectToBase64(messageObject);
if (!messageObject) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
// Reload messages
loadMessagesFromQDN(room);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
}
}
// Helper function to load messages from QDN for a specific room
async function loadMessagesFromQDN(room) {
try {
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
return;
}
let messagesHTML = "";
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
fetchMessages.forEach(async (message) => {
if (message) {
messagesHTML += `
<div class="message-item">
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button">Reply</button>
</div>
`;
}
});
messagesContainer.innerHTML = messagesHTML;
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 5000);
}
} catch (error) {
console.error("Error loading messages from QDN:", error);
}
}

View File

@@ -0,0 +1,282 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
// Load the latest message identifiers from local storage
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background image
const mainContent = document.createElement('div');
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
mainContent.innerHTML = `
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue;">MINTERSHIP FORUM (Alpha)</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
<button class="room-button" id="general-room">General Room</button>
<div class="user-info" style="float: right; color: lightblue; margin-right: 50px;">User: ${userState.accountName || 'Guest'}</div>
</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", () => {
loadRoomContent("minters");
});
if (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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 class="message-input-section" style="background-color: black; padding: 10px; position: relative;">
<div id="toolbar" class="message-toolbar" style="margin-bottom: 10px;"></div>
<div id="editor" class="message-input" style="height: 150px;"></div>
<button id="send-button" class="send-button" style="margin-top: 10px;">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }],
['clean'] // remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages
loadMessagesFromQDN(room);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
}
}
// Load messages for any given room
async function loadMessagesFromQDN(room) {
try {
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
return;
}
let messagesHTML = "";
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
fetchMessages.forEach(async (message) => {
if (message) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 10px; padding-left: 10px;">
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
messagesHTML += `
<div class="message-item">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
}
});
messagesContainer.innerHTML = messagesHTML;
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 1000);
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
const replyContainer = document.createElement("div");
replyContainer.className = "reply-container";
replyContainer.innerHTML = `
<div class="reply-preview" style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; background-color: black; color: white;">
<strong>Replying to:</strong> ${repliedMessage.content}
<button id="cancel-reply" style="float: right;">Cancel</button>
</div>
`;
const messageInputSection = document.querySelector(".message-input-section");
if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
// Add a listener for the cancel reply button
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// Polling function to check for new messages
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
if (activeRoom) {
await loadMessagesFromQDN(activeRoom);
}
}, 10000);
}

View File

@@ -0,0 +1,274 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
// Load the latest message identifiers from local storage
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
// if (mintershipForumLink) {
// mintershipForumLink.addEventListener('click', async (event) => {
// event.preventDefault();
// await login(); // Assuming login is an async function
// await loadForumPage();
// startPollingForNewMessages(); // Start polling for new messages after loading the forum page
// });
// }
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background image
const mainContent = document.createElement('div');
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
mainContent.innerHTML = `
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue;">MINTERSHIP FORUM (Alpha)</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
<button class="room-button" id="general-room">General Room</button>
<div class="user-info" style="float: right; color: lightblue; margin-right: 50px;">User: ${userState.accountName || 'Guest'}</div>
</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", () => {
loadRoomContent("minters");
});
if (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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 class="message-input-section">
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow'
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
const base64Message = await objectToBase64(messageObject);
if (!messageObject) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages
loadMessagesFromQDN(room);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
}
}
// Helper function to load messages from QDN for a specific room
async function loadMessagesFromQDN(room) {
try {
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
return;
}
let messagesHTML = "";
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
fetchMessages.forEach(async (message) => {
if (message) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 10px; padding-left: 10px;">
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
messagesHTML += `
<div class="message-item">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
}
});
messagesContainer.innerHTML = messagesHTML;
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 1000);
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
const replyContainer = document.createElement("div");
replyContainer.className = "reply-container";
replyContainer.innerHTML = `
<div class="reply-preview" style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;">
<strong>Replying to:</strong> ${repliedMessage.content}
<button id="cancel-reply" style="float: right;">Cancel</button>
</div>
`;
const messageInputSection = document.querySelector(".message-input-section");
if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
// Add a listener for the cancel reply button
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// Polling function to check for new messages
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
if (activeRoom) {
await loadMessagesFromQDN(activeRoom);
}
}, 10000);
}

View File

@@ -0,0 +1,219 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLink = document.querySelector('a[href="MINTERSHIP-FORUM"]');
if (mintershipForumLink) {
mintershipForumLink.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
});
}
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background image
const mainContent = document.createElement('div');
const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
mainContent.innerHTML = `
<div class="forum-main" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue;">MINTERSHIP FORUM (Alpha)</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
<button class="room-button" id="general-room">General Room</button>
<div class="user-info" style="float: right; color: lightblue; margin-right: 50px;">User: ${userState.accountName || 'Guest'}</div>
</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", () => {
loadRoomContent("minters");
});
if (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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 class="message-input-section">
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow'
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
const base64Message = await objectToBase64(messageObject);
if (!messageObject) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Reload messages
loadMessagesFromQDN(room);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
}
}
// Helper function to load messages from QDN for a specific room
async function loadMessagesFromQDN(room) {
try {
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, 0, false);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
return;
}
let messagesHTML = "";
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
fetchMessages.forEach(async (message) => {
if (message) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 10px; padding-left: 10px;">
<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>
`;
}
}
messagesHTML += `
<div class="message-item">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
}
});
messagesContainer.innerHTML = messagesHTML;
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 5000);
// Add event listeners to reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", (event) => {
replyToMessageIdentifier = event.target.getAttribute("data-message-identifier");
console.log("Replying to message with identifier:", replyToMessageIdentifier);
});
});
}
} catch (error) {
console.error("Error loading messages from QDN:", error);
}
}

View File

@@ -0,0 +1,464 @@
const cardIdentifierPrefix = "test-board-card";
let isExistingCard = false
let existingCard = {}
document.addEventListener("DOMContentLoaded", async () => {
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();
}
await loadMinterBoardPage();
});
});
});
async function loadMinterBoardPage() {
// Clear existing content on the page
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();
}
}
// Add the "Minter Board" content
const mainContent = document.createElement("div");
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">Minter Board</h1>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<h3>Create or Update Your Minter Card</h3>
<form id="publish-card-form">
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information..." required></textarea>
<label for="card-links">Links (qortal://...):</label>
<div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link">
</div>
<button type="button" id="add-link-button">Add Another Link</button>
<button type="submit" style="margin-top: 10px;">Publish Card</button>
<button type="button" id="cancel-publish" style="margin-top: 10px;">Cancel</button>
</form>
</div>
</div>
`;
document.body.appendChild(mainContent);
document.getElementById("publish-card-button").addEventListener("click", async () => {
existingCard = await fetchExistingCard();
if (existingCard) {
const updateCard = confirm("You already have a card. Do you want to update it?");
isExistingCard = true
if (updateCard) {
loadCardIntoForm(existingCard);
document.getElementById("publish-card-view").style.display = "block";
document.getElementById("cards-container").style.display = "none";
}
} else {
document.getElementById("publish-card-view").style.display = "block";
document.getElementById("cards-container").style.display = "none";
}
});
document.getElementById("cancel-publish").addEventListener("click", () => {
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "block";
});
document.getElementById("add-link-button").addEventListener("click", () => {
const linksContainer = document.getElementById("links-container");
const newLinkInput = document.createElement("input");
newLinkInput.type = "text";
newLinkInput.className = "card-link";
newLinkInput.placeholder = "Enter QDN link";
linksContainer.appendChild(newLinkInput);
});
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault();
await publishCard();
});
await loadCards();
}
async function fetchExistingCard() {
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
nameListFilter: userState.accountName,
query: cardIdentifierPrefix,
});
existingCard = response.find(card => card.name === userState.accountName);
if (existingCard) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: existingCard.name,
service: "BLOG_POST",
identifier: existingCard.identifier,
});
return cardDataResponse;
}
return null;
} catch (error) {
console.error("Error fetching existing card:", error);
return null;
}
}
function loadCardIntoForm(cardData) {
document.getElementById("card-header").value = cardData.header;
document.getElementById("card-content").value = cardData.content;
const linksContainer = document.getElementById("links-container");
linksContainer.innerHTML = ""; // Clear previous links
cardData.links.forEach(link => {
const linkInput = document.createElement("input");
linkInput.type = "text";
linkInput.className = "card-link";
linkInput.value = link;
linksContainer.appendChild(linkInput);
});
}
async function publishCard() {
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"));
if (!header || !content) {
alert("Header and content are required!");
return;
}
const cardIdentifier = isExistingCard ? existingCard.identifier : `${cardIdentifierPrefix}-${await uid()}`;
const pollName = `${cardIdentifier}-poll`;
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
const cardData = {
header,
content,
links,
creator: userState.accountName,
timestamp: Date.now(),
poll: pollName,
};
// new Date().toISOString()
try {
let base64CardData = await objectToBase64(cardData);
if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CardData = btoa(JSON.stringify(cardData));
}
// const base64CardData = btoa(JSON.stringify(cardData));
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: cardIdentifier,
data64: base64CardData,
});
await qortalRequest({
action: "CREATE_POLL",
pollName,
pollDescription,
pollOptions: ["Yes", "No", "Comment"],
pollOwnerAddress: userState.accountAddress,
});
alert("Card and poll published successfully!");
document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "block";
await loadCards();
} catch (error) {
console.error("Error publishing card or poll:", error);
alert("Failed to publish card and poll.");
}
}
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = "";
const pollResultsCache = {};
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
const calculatePollResults = (pollData, minterGroupMembers) => {
const memberAddresses = minterGroupMembers.map(member => member.member);
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
pollData.votes.forEach(vote => {
const voterAddress = vote.voterPublicKey;
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
if (vote.optionIndex === 1) {
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
} else if (vote.optionIndex === 0) {
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
}
});
const totalYes = adminYes + minterYes;
const totalNo = adminNo + minterNo;
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
};
const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
const commentText = commentInput.value.trim();
if (!commentText) {
alert('Comment cannot be empty!');
return;
}
const commentData = {
content: commentText,
creator: userState.accountName,
timestamp: Date.now(),
};
const commentIdentifier = `${cardIdentifier}-comment-${await uid()}`;
try {
const base64CommentData = await objectToBase64(commentData);
if (!base64CommentData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CommentData = btoa(JSON.stringify(commentData));
}
// const base64CommentData = btoa(JSON.stringify(commentData));
await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName,
service: 'BLOG_POST',
identifier: commentIdentifier,
data64: base64CommentData,
});
alert('Comment posted successfully!');
commentInput.value = ''; // Clear input
await displayComments(cardIdentifier); // Refresh comments
} catch (error) {
console.error('Error posting comment:', error);
alert('Failed to post comment.');
}
};
const fetchCommentsForCard = async (cardIdentifier) => {
try {
const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES',
service: 'BLOG_POST',
query: `${cardIdentifier}-comment`,
});
return response;
} catch (error) {
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
return [];
}
};
const displayComments = async (cardIdentifier) => {
const comments = await fetchCommentsForCard(cardIdentifier);
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
commentsContainer.innerHTML = comments.map(comment => `
<div class="comment" style="border: 1px solid gray; margin: 10px 0; padding: 10px; background: #1c1c1c;">
<p><strong>${comment.creator}</strong>:</p>
<p>${comment.content}</p>
<p>${timestampToHumanReadableDate(comment.timestamp)}</p>
</div>
`).join('');
};
const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
await displayComments(cardIdentifier);
commentsSection.style.display = 'block';
} else {
commentsSection.style.display = 'none';
}
};
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = "";
const pollResultsCache = {};
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
function toggleFullContent(cardIdentifier, fullContent) {
const contentPreview = document.getElementById(`content-preview-${cardIdentifier}`);
const toggleButton = document.getElementById(`toggle-content-${cardIdentifier}`);
const isExpanded = contentPreview.getAttribute("data-expanded") === "true";
if (isExpanded) {
// Collapse the content
contentPreview.innerText = `${fullContent.substring(0, 150)}...`;
toggleButton.innerText = "Display Full Text";
contentPreview.setAttribute("data-expanded", "false");
} else {
// Expand the content
contentPreview.innerText = fullContent;
toggleButton.innerText = "Show Less";
contentPreview.setAttribute("data-expanded", "true");
}
}
async function createCardHTML(cardData, pollResults) {
const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const linksHTML = links.map((link, index) => `
<button onclick="window.open('${link}', '_blank')">
${`Link ${index + 1} - ${link}`}
</button>
`).join("");
const minterGroupMembers = await fetchMinterGroupMembers();
const { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo } =
calculatePollResults(pollResults, minterGroupMembers);
const trimmedContent = content.length > 150 ? `${content.substring(0, 150)}...` : content;
return `
<div class="minter-card">
<div class="minter-card-header">
<h3>${creator}</h3>
<p>${header}</p>
</div>
<div class="info">
<div><h5>Minter's Message</h5></div>
<div id="content-preview-${cardData.identifier}" class="content-preview">
${trimmedContent}
</div>
${
content.length > 150
? `<button id="toggle-content-${cardData.identifier}" class="toggle-content-button" onclick="toggleFullContent('${cardData.identifier}', '${content}')">Display Full Content</button>`
: ""
}
</div>
<div class="info-links">
${linksHTML}
</div>
<div class="minter-card-results">
<div class="admin-results">
<span class="admin-yes">Admin Yes: ${adminYes}</span>
<span class="admin-no">Admin No: ${adminNo}</span>
</div>
<div class="minter-results">
<span class="minter-yes">Minter Yes: ${minterYes}</span>
<span class="minter-no">Minter No: ${minterNo}</span>
</div>
<div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-no">Total No: ${totalNo}</span>
</div>
</div>
<div class="actions">
<div><h5>Support Minter</h5></div>
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
<button class="comment" onclick="toggleComments('${cardData.identifier}')">COMMENT</button>
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
</div>
<div id="comments-section-${cardData.identifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardData.identifier}" class="comments-container"></div>
<textarea id="new-comment-${cardData.identifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
<button onclick="postComment('${cardData.identifier}')">Post Comment</button>
</div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
</div>
`
}

View File

@@ -0,0 +1,461 @@
const cardIdentifierPrefix = "test-board-card";
let isExistingCard = false
let existingCard = {}
document.addEventListener("DOMContentLoaded", async () => {
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();
}
await loadMinterBoardPage();
});
});
});
async function loadMinterBoardPage() {
// Clear existing content on the page
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();
}
}
// Add the "Minter Board" content
const mainContent = document.createElement("div");
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">Minter Board</h1>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<h3>Create or Update Your Minter Card</h3>
<form id="publish-card-form">
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information..." required></textarea>
<label for="card-links">Links (qortal://...):</label>
<div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link">
</div>
<button type="button" id="add-link-button">Add Another Link</button>
<button type="submit" style="margin-top: 10px;">Publish Card</button>
<button type="button" id="cancel-publish" style="margin-top: 10px;">Cancel</button>
</form>
</div>
</div>
`;
document.body.appendChild(mainContent);
document.getElementById("publish-card-button").addEventListener("click", async () => {
existingCard = await fetchExistingCard();
if (existingCard) {
const updateCard = confirm("You already have a card. Do you want to update it?");
isExistingCard = true
if (updateCard) {
loadCardIntoForm(existingCard);
document.getElementById("publish-card-view").style.display = "block";
document.getElementById("cards-container").style.display = "none";
}
} else {
document.getElementById("publish-card-view").style.display = "block";
document.getElementById("cards-container").style.display = "none";
}
});
document.getElementById("cancel-publish").addEventListener("click", () => {
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "block";
});
document.getElementById("add-link-button").addEventListener("click", () => {
const linksContainer = document.getElementById("links-container");
const newLinkInput = document.createElement("input");
newLinkInput.type = "text";
newLinkInput.className = "card-link";
newLinkInput.placeholder = "Enter QDN link";
linksContainer.appendChild(newLinkInput);
});
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault();
await publishCard();
});
await loadCards();
}
async function fetchExistingCard() {
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
nameListFilter: userState.accountName,
query: cardIdentifierPrefix,
});
existingCard = response.find(card => card.name === userState.accountName);
if (existingCard) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: existingCard.name,
service: "BLOG_POST",
identifier: existingCard.identifier,
});
return cardDataResponse;
}
return null;
} catch (error) {
console.error("Error fetching existing card:", error);
return null;
}
}
function loadCardIntoForm(cardData) {
document.getElementById("card-header").value = cardData.header;
document.getElementById("card-content").value = cardData.content;
const linksContainer = document.getElementById("links-container");
linksContainer.innerHTML = ""; // Clear previous links
cardData.links.forEach(link => {
const linkInput = document.createElement("input");
linkInput.type = "text";
linkInput.className = "card-link";
linkInput.value = link;
linksContainer.appendChild(linkInput);
});
}
async function publishCard() {
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"));
if (!header || !content) {
alert("Header and content are required!");
return;
}
const cardIdentifier = isExistingCard ? existingCard.identifier : `${cardIdentifierPrefix}-${await uid()}`;
const pollName = `${cardIdentifier}-poll`;
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
const cardData = {
header,
content,
links,
creator: userState.accountName,
timestamp: Date.now(),
poll: pollName,
};
// new Date().toISOString()
try {
let base64CardData = await objectToBase64(cardData);
if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CardData = btoa(JSON.stringify(cardData));
}
// const base64CardData = btoa(JSON.stringify(cardData));
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: cardIdentifier,
data64: base64CardData,
});
await qortalRequest({
action: "CREATE_POLL",
pollName,
pollDescription,
pollOptions: ["Yes", "No", "Comment"],
pollOwnerAddress: userState.accountAddress,
});
alert("Card and poll published successfully!");
document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "block";
await loadCards();
} catch (error) {
console.error("Error publishing card or poll:", error);
alert("Failed to publish card and poll.");
}
}
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = "";
const pollResultsCache = {};
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
const calculatePollResults = (pollData, minterGroupMembers) => {
const memberAddresses = minterGroupMembers.map(member => member.member);
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
pollData.votes.forEach(vote => {
const voterAddress = vote.voterPublicKey;
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
if (vote.optionIndex === 1) {
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
} else if (vote.optionIndex === 0) {
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
}
});
const totalYes = adminYes + minterYes;
const totalNo = adminNo + minterNo;
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
};
const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
const commentText = commentInput.value.trim();
if (!commentText) {
alert('Comment cannot be empty!');
return;
}
const commentData = {
content: commentText,
creator: userState.accountName,
timestamp: Date.now(),
};
const commentIdentifier = `${cardIdentifier}-comment-${await uid()}`;
try {
const base64CommentData = await objectToBase64(commentData);
if (!base64CommentData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CommentData = btoa(JSON.stringify(commentData));
}
// const base64CommentData = btoa(JSON.stringify(commentData));
await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName,
service: 'BLOG_POST',
identifier: commentIdentifier,
data64: base64CommentData,
});
alert('Comment posted successfully!');
commentInput.value = ''; // Clear input
await displayComments(cardIdentifier); // Refresh comments
} catch (error) {
console.error('Error posting comment:', error);
alert('Failed to post comment.');
}
};
const fetchCommentsForCard = async (cardIdentifier) => {
try {
const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES',
service: 'BLOG_POST',
query: `${cardIdentifier}-comment`,
});
return response;
} catch (error) {
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
return [];
}
};
const displayComments = async (cardIdentifier) => {
const comments = await fetchCommentsForCard(cardIdentifier);
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
commentsContainer.innerHTML = comments.map(comment => `
<div class="comment" style="border: 1px solid gray; margin: 10px 0; padding: 10px; background: #1c1c1c;">
<p><strong>${comment.creator}</strong>:</p>
<p>${comment.content}</p>
<p>${timestampToHumanReadableDate(comment.timestamp)}</p>
</div>
`).join('');
};
const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
await displayComments(cardIdentifier);
commentsSection.style.display = 'block';
} else {
commentsSection.style.display = 'none';
}
};
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = "";
const pollResultsCache = {};
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
function toggleFullContent(cardIdentifier, fullContent) {
const contentPreview = document.getElementById(`content-preview-${cardIdentifier}`);
const toggleButton = document.getElementById(`toggle-content-${cardIdentifier}`);
if (contentPreview.innerText.length > 150) {
// Collapse the content
contentPreview.innerText = `${fullContent.substring(0, 150)}...`;
toggleButton.innerText = "Display Full Content";
} else {
// Expand the content
contentPreview.innerText = fullContent;
toggleButton.innerText = "Show Less";
}
}
async function createCardHTML(cardData, pollResults) {
const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const linksHTML = links.map((link, index) => `
<button onclick="window.open('${link}', '_blank')">
${`Link ${index + 1} - ${link}`}
</button>
`).join("");
const minterGroupMembers = await fetchMinterGroupMembers();
const { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo } =
calculatePollResults(pollResults, minterGroupMembers);
const trimmedContent = content.length > 150 ? `${content.substring(0, 150)}...` : content;
return `
<div class="minter-card">
<div class="minter-card-header">
<h3>${creator}</h3>
<p>${header}</p>
</div>
<div class="info">
<div><h5>Minter's Message</h5></div>
<div id="content-preview-${cardData.identifier}" class="content-preview">
${trimmedContent}
</div>
${
content.length > 150
? `<button id="toggle-content-${cardData.identifier}" class="toggle-content-button" onclick="toggleFullContent('${cardData.identifier}', '${content}')">Display Full Content</button>`
: ""
}
</div>
<div class="info-links">
${linksHTML}
</div>
<div class="minter-card-results">
<div class="admin-results">
<span class="admin-yes">Admin Yes: ${adminYes}</span>
<span class="admin-no">Admin No: ${adminNo}</span>
</div>
<div class="minter-results">
<span class="minter-yes">Minter Yes: ${minterYes}</span>
<span class="minter-no">Minter No: ${minterNo}</span>
</div>
<div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-no">Total No: ${totalNo}</span>
</div>
</div>
<div class="actions">
<div><h5>Support Minter</h5></div>
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
<button class="comment" onclick="toggleComments('${cardData.identifier}')">COMMENT</button>
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
</div>
<div id="comments-section-${cardData.identifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardData.identifier}" class="comments-container"></div>
<textarea id="new-comment-${cardData.identifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
<button onclick="postComment('${cardData.identifier}')">Post Comment</button>
</div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
</div>
`
}

View File

@@ -0,0 +1,450 @@
const cardIdentifierPrefix = "test-board-card";
let isExistingCard = false
let existingCard = {}
document.addEventListener("DOMContentLoaded", async () => {
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();
}
await loadMinterBoardPage();
});
});
});
async function loadMinterBoardPage() {
// Clear existing content on the page
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();
}
}
// Add the "Minter Board" content
const mainContent = document.createElement("div");
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">Minter Board</h1>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<h3>Create or Update Your Minter Card</h3>
<form id="publish-card-form">
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information..." required></textarea>
<label for="card-links">Links (qortal://...):</label>
<div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link">
</div>
<button type="button" id="add-link-button">Add Another Link</button>
<button type="submit" style="margin-top: 10px;">Publish Card</button>
<button type="button" id="cancel-publish" style="margin-top: 10px;">Cancel</button>
</form>
</div>
</div>
`;
document.body.appendChild(mainContent);
document.getElementById("publish-card-button").addEventListener("click", async () => {
const existingCard = await fetchExistingCard();
if (existingCard) {
const updateCard = confirm("You already have a card. Do you want to update it?");
isExistingCard = true
if (updateCard) {
loadCardIntoForm(existingCard);
document.getElementById("publish-card-view").style.display = "block";
document.getElementById("cards-container").style.display = "none";
}
} else {
document.getElementById("publish-card-view").style.display = "block";
document.getElementById("cards-container").style.display = "none";
}
});
document.getElementById("cancel-publish").addEventListener("click", () => {
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "block";
});
document.getElementById("add-link-button").addEventListener("click", () => {
const linksContainer = document.getElementById("links-container");
const newLinkInput = document.createElement("input");
newLinkInput.type = "text";
newLinkInput.className = "card-link";
newLinkInput.placeholder = "Enter QDN link";
linksContainer.appendChild(newLinkInput);
});
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault();
await publishCard();
});
await loadCards();
}
async function fetchExistingCard() {
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
nameListFilter: userState.accountName,
query: cardIdentifierPrefix,
});
existingCard = response.find(card => card.name === userState.accountName);
if (existingCard) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: existingCard.name,
service: "BLOG_POST",
identifier: existingCard.identifier,
});
return cardDataResponse;
}
return null;
} catch (error) {
console.error("Error fetching existing card:", error);
return null;
}
}
function loadCardIntoForm(cardData) {
document.getElementById("card-header").value = cardData.header;
document.getElementById("card-content").value = cardData.content;
const linksContainer = document.getElementById("links-container");
linksContainer.innerHTML = ""; // Clear previous links
cardData.links.forEach(link => {
const linkInput = document.createElement("input");
linkInput.type = "text";
linkInput.className = "card-link";
linkInput.value = link;
linksContainer.appendChild(linkInput);
});
}
async function publishCard() {
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"));
if (!header || !content) {
alert("Header and content are required!");
return;
}
const cardIdentifier = isExistingCard ? existingCard.identifier : `${cardIdentifierPrefix}-${await uid()}`;
const pollName = `${cardIdentifier}-poll`;
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
const cardData = {
header,
content,
links,
creator: userState.accountName,
timestamp: Date.now(),
poll: pollName,
};
// new Date().toISOString()
try {
let base64CardData = await objectToBase64(cardData);
if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CardData = btoa(JSON.stringify(cardData));
}
// const base64CardData = btoa(JSON.stringify(cardData));
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: cardIdentifier,
data64: base64CardData,
});
await qortalRequest({
action: "CREATE_POLL",
pollName,
pollDescription,
pollOptions: ["Yes", "No", "Comment"],
pollOwnerAddress: userState.accountAddress,
});
alert("Card and poll published successfully!");
document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "block";
await loadCards();
} catch (error) {
console.error("Error publishing card or poll:", error);
alert("Failed to publish card and poll.");
}
}
const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
const commentText = commentInput.value.trim();
if (!commentText) {
alert('Comment cannot be empty!');
return;
}
const commentData = {
content: commentText,
creator: userState.accountName,
timestamp: new Date().toISOString(),
};
const commentIdentifier = `${cardIdentifier}-comment-${await uid()}`;
try {
const base64CommentData = btoa(JSON.stringify(commentData));
await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName,
service: 'BLOG_POST',
identifier: commentIdentifier,
data64: base64CommentData,
});
alert('Comment posted successfully!');
commentInput.value = ''; // Clear input
await displayComments(cardIdentifier); // Refresh comments
} catch (error) {
console.error('Error posting comment:', error);
alert('Failed to post comment.');
}
};
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = "";
const pollResultsCache = {};
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
const calculatePollResults = (pollData, minterGroupMembers) => {
const memberAddresses = minterGroupMembers.map(member => member.member);
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
pollData.votes.forEach(vote => {
const voterAddress = vote.voterPublicKey;
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
if (vote.optionIndex === 1) {
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
} else if (vote.optionIndex === 0) {
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
}
});
const totalYes = adminYes + minterYes;
const totalNo = adminNo + minterNo;
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
};
const fetchCommentsForCard = async (cardIdentifier) => {
try {
const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES',
service: 'BLOG_POST',
identifier: `${cardIdentifier}-comments`,
});
return response;
} catch (error) {
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
return [];
}
};
const displayComments = async (cardIdentifier) => {
const comments = await fetchCommentsForCard(cardIdentifier);
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
commentsContainer.innerHTML = comments.map(comment => `
<div class="comment" style="border: 1px solid gray; margin: 10px 0; padding: 10px; background: #1c1c1c;">
<p><strong>${comment.creator}</strong>:</p>
<p>${comment.content}</p>
</div>
`).join('');
};
const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
await displayComments(cardIdentifier);
commentsSection.style.display = 'block';
} else {
commentsSection.style.display = 'none';
}
};
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = "";
const pollResultsCache = {};
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
function toggleFullContent(cardIdentifier, fullContent) {
const contentPreview = document.getElementById(`content-preview-${cardIdentifier}`);
const toggleButton = document.getElementById(`toggle-content-${cardIdentifier}`);
if (contentPreview.innerText.length > 150) {
// Collapse the content
contentPreview.innerText = `${fullContent.substring(0, 150)}...`;
toggleButton.innerText = "Display Full Content";
} else {
// Expand the content
contentPreview.innerText = fullContent;
toggleButton.innerText = "Show Less";
}
}
async function createCardHTML(cardData, pollResults) {
const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const linksHTML = links.map((link, index) => `
<button onclick="window.open('${link}', '_blank')">
${`Link ${index + 1}`}
</button>
`).join("");
const minterGroupMembers = await fetchMinterGroupMembers();
const { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo } =
calculatePollResults(pollResults, minterGroupMembers);
const trimmedContent = content.length > 150 ? `${content.substring(0, 150)}...` : content;
return `
<div class="card">
<div class="card-header">
<h3>${creator}</h3>
<p>${header}</p>
</div>
<div class="results">
<div class="admin-yes">Admin Yes: ${adminYes}</div>
<div class="admin-no">Admin No: ${adminNo}</div>
<div class="minter-yes">Minter Yes: ${minterYes}</div>
<div class="minter-no">Minter No: ${minterNo}</div>
<div class="total-yes">Total Yes: ${totalYes}</div>
<div class="total-no">Total No: ${totalNo}</div>
</div>
<div class="info">
<div id="content-preview-${cardData.identifier}" class="content-preview">
${trimmedContent}
</div>
${
content.length > 150
? `<button id="toggle-content-${cardData.identifier}" class="toggle-content-button" onclick="toggleFullContent('${cardData.identifier}', '${content}')">Display Full Content</button>`
: ""
}
</div>
<div class="info-links">
<button>Minter Introduction</button>
${linksHTML}
</div>
<div class="actions">
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
<button class="comment" onclick="toggleComments('${cardData.identifier}')">COMMENT</button>
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
</div>
<div id="comments-section-${cardData.identifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardData.identifier}" class="comments-container"></div>
<textarea id="new-comment-${cardData.identifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
<button onclick="postComment('${cardData.identifier}')">Post Comment</button>
</div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
</div>
`;
}

View File

@@ -0,0 +1,195 @@
/* forum-styles.css */
.forum-main {
color: #ffffff;
background-color: #000000;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100vw;
box-sizing: border-box;
background-size: cover;
background-position: center;
}
.forum-header {
width: 100%;
padding: 2vh;
background-color: #000000;
color: #add8e6; /* Light blue color */
text-align: center;
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 0;
}
.forum-submenu {
width: 100%;
padding: 1vh 2vh;
background: rgba(0, 0, 0, 0.8);
text-align: center;
margin-top: 0;
}
.forum-rooms {
display: flex;
justify-content: center;
gap: 2vh; /* Increased gap for better spacing */
margin-top: 0;
flex-wrap: wrap;
}
.room-button {
background-color: #317e78;
color: #ffffff;
border: 2px solid #317e78;
border-radius: 2vh;
padding: 1vh 2vh;
font-size: 1.1rem;
cursor: pointer;
}
.room-button:hover {
background-color: #19403d;
}
.forum-content {
flex-grow: 1;
width: 90%;
padding: 3vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
box-sizing: border-box;
border: 3px solid #ffffff; /* Increased border width */
}
.room-content {
background: rgba(0, 0, 0, 0.6);
padding: 2vh;
border-radius: 1vh;
width: 100%;
box-sizing: border-box;
text-align: center;
}
.room-title {
color: #add8e6; /* Light blue color for room name */
text-align: center;
margin-bottom: 2vh;
font-size: 2rem;
}
.message-item {
background: #1c1c1c;
color: #ffffff;
padding: 1vh;
margin-bottom: 1vh;
border-radius: 0.8vh;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
border: 1px solid #ffffff;
}
.message-header {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 1vh;
font-size: 1.25rem;
color: white
}
.message-header.username {
color: #228ec0;
}
.username {
font-weight: bold;
color:#228ec0
}
.timestamp {
font-style: italic;
color: rgb(157, 167, 151)
}
.message-text {
margin: 0;
font-size: 1.25rem;
}
.reply-button {
align-self: flex-end;
margin-top: 1vh;
background-color: #167089;
color: #ffffff;
border: none;
border-radius: 1vh;
padding: 0.3vh 0.6vh;
cursor: pointer;
}
.reply-button:hover {
background-color: #19403d;
}
/* forum-styles.css additions */
.message-input-section {
display: flex;
flex-direction: column;
align-items: stretch;
box-sizing: border-box;
width: 100%;
gap: 1vh; /* Spacing between toolbar and editor */
background-color: black;
padding: 1vh;
}
.ql-editor {
flex-grow: 1;
text-size: 1.25rem;
}
.message-input {
flex-grow: 1;
padding: 2vh;
border-radius: 1vh;
border: 1px solid #cccccc;
font-size: 1.25rem;
/* margin-right: 8vh; */
box-sizing: border-box;
min-height: 15vh;
}
.send-button {
background-color: #13a97c;
color: #ffffff;
border: none;
border-radius: 1vh;
padding: 2vh 4vh;
cursor: pointer;
}
.send-button:hover {
background-color: #19403d;
}
.messages-container {
width: 100%;
margin-bottom: 5vh; /* Ensure space above input section */
overflow-y: auto;
padding-bottom: 1vh;
box-sizing: border-box;
}

View File

@@ -0,0 +1,508 @@
const cardIdentifierPrefix = "test-board-card";
let isExistingCard = false;
let existingCardData = {};
let existingCardInfo ={};
document.addEventListener("DOMContentLoaded", async () => {
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();
}
await loadMinterBoardPage();
});
});
});
async function loadMinterBoardPage() {
// Clear existing content on the page
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();
}
}
// Add the "Minter Board" content
const mainContent = document.createElement("div");
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">Minter Board</h1>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<h3>Create or Update Your Minter Card</h3>
<form id="publish-card-form">
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information about why you deserve to be a minter..." required></textarea>
<label for="card-links">Links (qortal://...):</label>
<div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link">
</div>
<button type="button" id="add-link-button">Add Another Link</button>
<button type="submit" id="submit-publish-button">Publish Card</button>
<button type="button" id="cancel-publish-button">Cancel</button>
</form>
</div>
</div>
`;
document.body.appendChild(mainContent);
document.getElementById("publish-card-button").addEventListener("click", async () => {
try {
const {cardData, cardIdentifier} = await fetchExistingCard();
if (cardIdentifier) {
// Update existing card
const updateCard = confirm("A card already exists. Do you want to update it?");
isExistingCard = true;
if (updateCard) {
// Load existing card into the form for editing
loadCardIntoForm(cardData);
alert("Edit your existing card and publish.");
} else {
// Allow creating a new card for testing purposes
alert("You can now create a new card for testing.");
isExistingCard = false;
existingCardData = {}; // Reset to allow new card creation
document.getElementById("publish-card-form").reset();
}
} else {
alert("No existing card found. Create a new card.");
isExistingCard = false;
}
// Show the form for publishing a card
const publishCardView = document.getElementById("publish-card-view");
publishCardView.style.display = "flex";
document.getElementById("cards-container").style.display = "none";
} catch (error) {
console.error("Error checking for existing card:", error);
alert("Failed to check for existing card. Please try again.");
}
});
document.getElementById("cancel-publish-button").addEventListener("click", () => {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.style.display = "flex"; // Restore visibility
const publishCardView = document.getElementById("publish-card-view");
publishCardView.style.display = "none"; // Hide the publish form
});
document.getElementById("add-link-button").addEventListener("click", () => {
const linksContainer = document.getElementById("links-container");
const newLinkInput = document.createElement("input");
newLinkInput.type = "text";
newLinkInput.className = "card-link";
newLinkInput.placeholder = "Enter QDN link";
linksContainer.appendChild(newLinkInput);
});
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault();
await publishCard();
});
await loadCards();
}
async function fetchExistingCard() {
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: userState.accountName,
identifier: cardIdentifierPrefix
});
existingCardInfo = response;
if (existingCard) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: existingCardInfo.name,
service: "BLOG_POST",
identifier: existingCardInfo.identifier,
});
const existingCardIdentifier = existingCardInfo.identifier;
existingCardData = cardDataResponse
return {cardDataResponse, existingCardIdentifier};
}
return null;
} catch (error) {
console.error("Error fetching existing card:", error);
return null;
}
}
function loadCardIntoForm(cardData) {
document.getElementById("card-header").value = cardData.header;
document.getElementById("card-content").value = cardData.content;
const linksContainer = document.getElementById("links-container");
linksContainer.innerHTML = ""; // Clear previous links
cardData.links.forEach(link => {
const linkInput = document.createElement("input");
linkInput.type = "text";
linkInput.className = "card-link";
linkInput.value = link;
linksContainer.appendChild(linkInput);
});
}
async function publishCard() {
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"));
if (!header || !content) {
alert("Header and content are required!");
return;
}
const cardIdentifier = isExistingCard ? existingCardInfo.identifier : `${cardIdentifierPrefix}-${await uid()}`;
const pollName = `${cardIdentifier}-poll`;
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
const cardData = {
header,
content,
links,
creator: userState.accountName,
timestamp: Date.now(),
poll: pollName,
};
// new Date().toISOString()
try {
let base64CardData = await objectToBase64(cardData);
if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CardData = btoa(JSON.stringify(cardData));
}
// const base64CardData = btoa(JSON.stringify(cardData));
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: cardIdentifier,
data64: base64CardData,
});
await qortalRequest({
action: "CREATE_POLL",
pollName,
pollDescription,
pollOptions: ["Allow", "Deny"],
pollOwnerAddress: userState.accountAddress,
});
alert("Card and poll published successfully!");
document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "flex";
await loadCards();
} catch (error) {
console.error("Error publishing card or poll:", error);
alert("Failed to publish card and poll.");
}
}
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = ""
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
// Cache poll results
const pollResults = await fetchPollResults(cardData.poll)
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
const calculatePollResults = (pollData, minterGroupMembers) => {
const memberAddresses = minterGroupMembers.map(member => member.member);
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
pollData.votes.forEach(vote => {
const voterAddress = vote.voterPublicKey;
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
if (vote.optionIndex === 1) {
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
} else if (vote.optionIndex === 0) {
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
}
});
const totalYes = adminYes + minterYes;
const totalNo = adminNo + minterNo;
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
};
const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
const commentText = commentInput.value.trim();
if (!commentText) {
alert('Comment cannot be empty!');
return;
}
const commentData = {
content: commentText,
creator: userState.accountName,
timestamp: Date.now(),
};
const commentIdentifier = `${cardIdentifier}-comment-${await uid()}`;
try {
const base64CommentData = await objectToBase64(commentData);
if (!base64CommentData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CommentData = btoa(JSON.stringify(commentData));
}
// const base64CommentData = btoa(JSON.stringify(commentData));
await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName,
service: 'BLOG_POST',
identifier: commentIdentifier,
data64: base64CommentData,
});
alert('Comment posted successfully!');
commentInput.value = ''; // Clear input
await displayComments(cardIdentifier); // Refresh comments
} catch (error) {
console.error('Error posting comment:', error);
alert('Failed to post comment.');
}
};
const fetchCommentsForCard = async (cardIdentifier) => {
try {
const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES',
service: 'BLOG_POST',
query: `${cardIdentifier}-comment`,
});
return response;
} catch (error) {
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
return [];
}
};
const displayComments = async (cardIdentifier) => {
try {
const comments = await fetchCommentsForCard(cardIdentifier);
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
// Clear previous comments
commentsContainer.innerHTML = '';
// Fetch and display each comment
for (const comment of comments) {
const commentDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: comment.name,
service: "BLOG_POST",
identifier: comment.identifier,
});
const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp);
const commentHTML = `
<div class="comment" style="border: 1px solid gray; margin: 10px 0; padding: 10px; background: #1c1c1c;">
<p><strong>${commentDataResponse.creator}</strong>:</p>
<p>${commentDataResponse.content}</p>
<p>${timestamp}</p>
</div>
`;
commentsContainer.insertAdjacentHTML('beforeend', commentHTML);
}
} catch (error) {
console.error(`Error displaying comments for ${cardIdentifier}:`, error);
alert("Failed to load comments. Please try again.");
}
};
const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
await displayComments(cardIdentifier);
commentsSection.style.display = 'block';
} else {
commentsSection.style.display = 'none';
}
};
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = "";
const pollResultsCache = {};
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
function toggleFullContent(cardIdentifier, fullContent) {
const contentPreview = document.getElementById(`content-preview-${cardIdentifier}`);
const toggleButton = document.getElementById(`toggle-content-${cardIdentifier}`);
const isExpanded = contentPreview.getAttribute("data-expanded") === "true";
if (isExpanded) {
// Collapse the content
contentPreview.innerText = `${fullContent.substring(0, 150)}...`;
toggleButton.innerText = "Display Full Text";
contentPreview.setAttribute("data-expanded", "false");
} else {
// Expand the content
contentPreview.innerText = fullContent;
toggleButton.innerText = "Show Less";
contentPreview.setAttribute("data-expanded", "true");
}
}
async function createCardHTML(cardData, pollResults, cardIdentifier) {
const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const linksHTML = links.map((link, index) => `
<button onclick="window.open('${link}', '_blank')">
${`Link ${index + 1} - ${link}`}
</button>
`).join("");
const minterGroupMembers = await fetchMinterGroupMembers();
const { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo } =
calculatePollResults(pollResults, minterGroupMembers);
const trimmedContent = content.length > 150 ? `${content.substring(0, 150)}...` : content;
return `
<div class="minter-card">
<div class="minter-card-header">
<h3>${creator}</h3>
<p>${header}</p>
</div>
<div class="info">
<div id="content-preview-${cardIdentifier}" class="content-preview">
${trimmedContent}
</div>
${
content.length > 150
? `<button id="toggle-content-${cardIdentifier}" class="toggle-content-button" onclick="toggleFullContent('${cardIdentifier}', '${content}')">Display Full Content</button>`
: ""
}
</div>
<div class="info-links">
${linksHTML}
</div>
<div class="minter-card-results">
<div><h5>Current Support Results</h5></div>
<div class="admin-results">
<span class="admin-yes">Admin Yes: ${adminYes}</span>
<span class="admin-no">Admin No: ${adminNo}</span>
</div>
<div class="minter-results">
<span class="minter-yes">Minter Yes: ${minterYes}</span>
<span class="minter-no">Minter No: ${minterNo}</span>
</div>
<div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-no">Total No: ${totalNo}</span>
</div>
</div>
<div class="actions">
<div><h5>Support Minter</h5></div> <!-- Move this heading above the buttons -->
<div class="button-group">
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
<button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENT</button>
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
</div>
</div>
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
<button onclick="postComment('${cardIdentifier}')">Post Comment</button>
</div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
</div>
`;
}

View File

@@ -0,0 +1,329 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<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 (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
${(existingIdentifiers.size > 10)? '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>' : ''}
<div class="message-input-section">
<div id="toolbar" class="message-toolbar"></div>
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
// Clear reply reference after sending if it exists.
if (replyToMessageIdentifier) {
replyToMessageIdentifier = null;
replyContainer.remove();
}
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
const offset = page * 10;
const limit = 10;
// Get the set of existing identifiers from the messages container
const messagesContainer = document.querySelector("#messages-container");
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch only messages that are not already present in the messages container
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
if (messagesContainer) {
// If there are no messages and we're not polling, display "no messages" message
if (!response || !response.length) {
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
fetchMessages.forEach((message) => {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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>
`;
}
}
let mostRecentMessage = null;
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Track the most recent message
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage.timestamp)) {
mostRecentMessage = message;
}
}
});
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = {
latestIdentifier: mostRecentMessage.identifier,
latestTimestamp: mostRecentMessage.timestamp
};
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}

View File

@@ -0,0 +1,319 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
let currentPage = 0; // Track current pagination page
// Load the latest message identifiers from local storage
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
// const backgroundImage = document.querySelector('.header1')?.style.backgroundImage;
const backgroundImage = "url('/assets/images/background.jpg')"
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: ${backgroundImage}; background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<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 (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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 class="message-input-section">
<div id="toolbar" class="message-toolbar"></div>
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Clear reply reference after sending if it exists.
if (replyToMessageIdentifier) {
replyToMessageIdentifier = null;
replyContainer.remove();
}
replyToMessageIdentifier = null;
replyContainer.remove();
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages
loadMessagesFromQDN(room, currentPage);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page) {
try {
const offset = page * 10;
const limit = 10;
const response = await searchAllResources(`${messageIdentifierPrefix}-${room}`, limit, 0, false);
const qdnMessages = response;
console.log("Messages fetched successfully:", qdnMessages);
const messagesContainer = document.querySelector("#messages-container");
if (messagesContainer) {
if (!qdnMessages || !qdnMessages.length) {
if (page === 0) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Fetch all messages
const fetchMessages = await Promise.all(qdnMessages.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render messages without duplication
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
fetchMessages.forEach((message) => {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
}
});
// Only scroll to bottom if user is not interacting with the input section
const messageInputSection = document.querySelector(".message-input-section");
if (document.activeElement !== messageInputSection) {
setTimeout(() => {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}, 1000);
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
}
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// Polling function to check for new messages
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
if (activeRoom) {
await loadMessagesFromQDN(activeRoom, currentPage);
}
}, 20000);
}

497
assets/js/MinterBoard.js Normal file
View File

@@ -0,0 +1,497 @@
const cardIdentifierPrefix = "test-board-card";
let isExistingCard = false;
let existingCardData = {};
let existingCardIdentifier ={};
document.addEventListener("DOMContentLoaded", async () => {
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();
}
await loadMinterBoardPage();
});
});
});
async function loadMinterBoardPage() {
// Clear existing content on the page
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();
}
}
// Add the "Minter Board" content
const mainContent = document.createElement("div");
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">Minter Board</h1>
<p> The Minter Board is a place to publish information about yourself in order to obtain support from existing Minters and Minter Admins on the Qortal network. You may publish a header, content, and links to other QDN-published content in order to support you in your mission. Minter Admins and Existing Minters will then support you (or not) by way of a vote on your card. Card details you publish, along with existing poll results, and comments from others, will be displayed here. Good Luck on your Qortal journey to becoming a minter!</p>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
<form id="publish-card-form">
<h3>Create or Update Your Minter Card</h3>
<label for="card-header">Header:</label>
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
<label for="card-content">Content:</label>
<textarea id="card-content" placeholder="Enter detailed information about why you deserve to be a minter..." required></textarea>
<label for="card-links">Links (qortal://...):</label>
<div id="links-container">
<input type="text" class="card-link" placeholder="Enter QDN link">
</div>
<button type="button" id="add-link-button">Add Another Link</button>
<button type="submit" id="submit-publish-button">Publish Card</button>
<button type="button" id="cancel-publish-button">Cancel</button>
</form>
</div>
</div>
`;
document.body.appendChild(mainContent);
document.getElementById("publish-card-button").addEventListener("click", async () => {
try {
await fetchExistingCard();
const cardIdentifier = existingCardIdentifier
if (cardIdentifier) {
// Update existing card
const updateCard = confirm("A card already exists. Do you want to update it?");
isExistingCard = true;
if (updateCard) {
// Load existing card into the form for editing
loadCardIntoForm(existingCardData);
alert("Edit your existing card and publish.");
} else {
// Allow creating a new card for testing purposes
alert("You can now create a new card for testing.");
isExistingCard = false;
existingCardData = {}; // Reset to allow new card creation
document.getElementById("publish-card-form").reset();
}
} else {
alert("No existing card found. Create a new card.");
isExistingCard = false;
}
// Show the form for publishing a card
const publishCardView = document.getElementById("publish-card-view");
publishCardView.style.display = "flex";
document.getElementById("cards-container").style.display = "none";
} catch (error) {
console.error("Error checking for existing card:", error);
alert("Failed to check for existing card. Please try again.");
}
});
document.getElementById("cancel-publish-button").addEventListener("click", () => {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.style.display = "flex"; // Restore visibility
const publishCardView = document.getElementById("publish-card-view");
publishCardView.style.display = "none"; // Hide the publish form
});
document.getElementById("add-link-button").addEventListener("click", () => {
const linksContainer = document.getElementById("links-container");
const newLinkInput = document.createElement("input");
newLinkInput.type = "text";
newLinkInput.className = "card-link";
newLinkInput.placeholder = "Enter QDN link";
linksContainer.appendChild(newLinkInput);
});
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
event.preventDefault();
await publishCard();
});
await loadCards();
}
async function fetchExistingCard() {
try {
// Step 1: Perform the search
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: userState.accountName, // Scoped to the user
identifier: cardIdentifierPrefix,
});
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`);
// Step 2: Check if the response is an array and not empty
if (!response || !Array.isArray(response) || response.length === 0) {
console.log("No cards found for the current user.");
return null;
}
const validCards = response.filter(card => validateCardStructure(card));
if (validCards.length > 0) {
// Sort by most recent timestamp
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0];
// Step 3: Find the object with the most recent timestamp
// const mostRecentCard = response.reduce((latest, card) => {
// const currentTimestamp = card.created || card.updated || 0; // Ensure there's a timestamp
// const latestTimestamp = latest.created || latest.updated || 0;
// return currentTimestamp > latestTimestamp ? card : latest;
// }, {});
// console.log(`Most recent card found: ${JSON.stringify(mostRecentCard, null, 2)}`);
// // Step 4: Validate the card and fetch its full data
// if (!mostRecentCard.identifier) {
// console.error("Identifier is missing in the most recent card.");
// return null;
// }
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: userState.accountName, // User's account name
service: "BLOG_POST",
identifier: mostRecentCard.identifier,
});
existingCardIdentifier = mostRecentCard.identifier
existingCardData = cardDataResponse
console.log("Full card data fetched successfully:", cardDataResponse);
return cardDataResponse; // Return full card data
}
console.log("No valid cards found.");
return null;
} catch (error) {
console.error("Error fetching existing card:", error);
return null;
}
}
const validateCardStructure = (card) => {
return (
typeof card === "object" &&
card.name &&
card.service === "BLOG_POST" &&
card.identifier && !card.identifier.includes("comment") &&
card.created
);
}
function loadCardIntoForm(cardData) {
document.getElementById("card-header").value = cardData.header;
document.getElementById("card-content").value = cardData.content;
const linksContainer = document.getElementById("links-container");
linksContainer.innerHTML = ""; // Clear previous links
cardData.links.forEach(link => {
const linkInput = document.createElement("input");
linkInput.type = "text";
linkInput.className = "card-link";
linkInput.value = link;
linksContainer.appendChild(linkInput);
});
}
async function publishCard() {
const header = document.getElementById("card-header").value.trim();
const content = document.getElementById("card-content").value.trim();
const links = Array.from(document.querySelectorAll(".card-link"))
.map(input => input.value.trim())
.filter(link => link.startsWith("qortal://"));
if (!header || !content) {
alert("Header and content are required!");
return;
}
const cardIdentifier = isExistingCard ? existingCardInfo.identifier : `${cardIdentifierPrefix}-${await uid()}`;
const pollName = `${cardIdentifier}-poll`;
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
const cardData = {
header,
content,
links,
creator: userState.accountName,
timestamp: Date.now(),
poll: pollName,
};
// new Date().toISOString()
try {
let base64CardData = await objectToBase64(cardData);
if (!base64CardData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CardData = btoa(JSON.stringify(cardData));
}
// const base64CardData = btoa(JSON.stringify(cardData));
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: cardIdentifier,
data64: base64CardData,
});
await qortalRequest({
action: "CREATE_POLL",
pollName,
pollDescription,
pollOptions: ['Yes, No'],
pollOwnerAddress: userState.accountAddress,
});
alert("Card and poll published successfully!");
document.getElementById("publish-card-form").reset();
document.getElementById("publish-card-view").style.display = "none";
document.getElementById("cards-container").style.display = "flex";
await loadCards();
} catch (error) {
console.error("Error publishing card or poll:", error);
alert("Failed to publish card and poll.");
}
}
async function loadCards() {
const cardsContainer = document.getElementById("cards-container");
cardsContainer.innerHTML = "<p>Loading cards...</p>";
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: cardIdentifierPrefix,
});
if (!response || response.length === 0) {
cardsContainer.innerHTML = "<p>No cards found.</p>";
return;
}
cardsContainer.innerHTML = ""
const pollResultsCache = {};
for (const card of response) {
const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: card.name,
service: "BLOG_POST",
identifier: card.identifier,
});
const cardData = cardDataResponse;
if (!cardData || !cardData.poll) {
console.warn(`Skipping card with missing poll data: ${JSON.stringify(cardData)}`);
continue; // Skip to the next card
}
// Cache poll results
if (!pollResultsCache[cardData.poll]) {
try {
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
} catch (error) {
console.warn(`Failed to fetch poll results for poll: ${cardData.poll}`, error);
pollResultsCache[cardData.poll] = null; // Store as null to avoid repeated attempts
}
}
const pollResults = pollResultsCache[cardData.poll];
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
}
} catch (error) {
console.error("Error loading cards:", error);
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
}
}
const calculatePollResults = (pollData, minterGroupMembers) => {
const memberAddresses = minterGroupMembers.map(member => member.member);
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
pollData.votes.forEach(vote => {
const voterAddress = getAddressFromPublicKey(vote.voterPublicKey);
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
if (vote.optionIndex === 1) {
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
} else if (vote.optionIndex === 0) {
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
}
});
const totalYes = adminYes + minterYes;
const totalNo = adminNo + minterNo;
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
};
const postComment = async (cardIdentifier) => {
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
const commentText = commentInput.value.trim();
if (!commentText) {
alert('Comment cannot be empty!');
return;
}
const commentData = {
content: commentText,
creator: userState.accountName,
timestamp: Date.now(),
};
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
try {
const base64CommentData = await objectToBase64(commentData);
if (!base64CommentData) {
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
base64CommentData = btoa(JSON.stringify(commentData));
}
// const base64CommentData = btoa(JSON.stringify(commentData));
await qortalRequest({
action: 'PUBLISH_QDN_RESOURCE',
name: userState.accountName,
service: 'BLOG_POST',
identifier: commentIdentifier,
data64: base64CommentData,
});
alert('Comment posted successfully!');
commentInput.value = ''; // Clear input
await displayComments(cardIdentifier); // Refresh comments
} catch (error) {
console.error('Error posting comment:', error);
alert('Failed to post comment.');
}
};
const fetchCommentsForCard = async (cardIdentifier) => {
try {
const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES',
service: 'BLOG_POST',
query: `comment-${cardIdentifier}`,
});
return response;
} catch (error) {
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
return [];
}
};
const displayComments = async (cardIdentifier) => {
try {
const comments = await fetchCommentsForCard(cardIdentifier);
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
// Clear previous comments
commentsContainer.innerHTML = '';
// Fetch and display each comment
for (const comment of comments) {
const commentDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: comment.name,
service: "BLOG_POST",
identifier: comment.identifier,
});
const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp);
const commentHTML = `
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
<p><strong><u>${commentDataResponse.creator}</strong>:</p></u>
<p>${commentDataResponse.content}</p>
<p><i>${timestamp}</p></i>
</div>
`;
commentsContainer.insertAdjacentHTML('beforeend', commentHTML);
}
} catch (error) {
console.error(`Error displaying comments for ${cardIdentifier}:`, error);
alert("Failed to load comments. Please try again.");
}
};
const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
await displayComments(cardIdentifier);
commentsSection.style.display = 'block';
} else {
commentsSection.style.display = 'none';
}
};
async function createCardHTML(cardData, pollResults, cardIdentifier) {
const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const linksHTML = links.map((link, index) => `
<button onclick="window.open('${link}', '_blank')">
${`Link ${index + 1} - ${link}`}
</button>
`).join("");
const minterGroupMembers = await fetchMinterGroupMembers();
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0 } =
calculatePollResults(pollResults, minterGroupMembers) || {};
return `
<div class="minter-card">
<div class="minter-card-header">
<h3>${creator}</h3>
<p>${header}</p>
</div>
<div class="support-header"><h5>Minter Post:</h5></div>
<div class="info">
${content}
</div>
<div class="support-header"><h5>Minter Links:</h5></div>
<div class="info-links">
${linksHTML}
</div>
<div class="results-header support-header"><h5>Current Results:</h5></div>
<div class="minter-card-results">
<div class="admin-results">
<span class="admin-yes">Admin Yes: ${adminYes}</span>
<span class="admin-no">Admin No: ${adminNo}</span>
</div>
<div class="minter-results">
<span class="minter-yes">Minter Yes: ${minterYes}</span>
<span class="minter-no">Minter No: ${minterNo}</span>
</div>
<div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-no">Total No: ${totalNo}</span>
</div>
</div>
<div class="support-header"><h5>Support Minter?</h5></div>
<div class="actions">
<div class="actions-buttons">
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
<button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENTS</button>
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
</div>
</div>
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
<button onclick="postComment('${cardIdentifier}')">Post Comment</button>
</div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
</div>
`;
}

569
assets/js/Q-Mintership.js Normal file
View File

@@ -0,0 +1,569 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
// 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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
//login if not already logged in.
if (!userState.isLoggedIn) {
await login();
}
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// // Remove all sections except the menu
// const allSections = document.querySelectorAll('body > section');
// allSections.forEach(section => {
// if (!section.classList.contains('menu')) {
// section.remove();
// }
// });
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();
}
}
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="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: lightblue; 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>
${userState.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");
});
}
async function 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);
}
}
async function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
<button id="attach-button" class="attach-button">Attach Files</button>
</div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
const imageModalHTML = `
<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>
`;
forumContent.insertAdjacentHTML('beforeend', imageModalHTML);
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
await loadMessagesFromQDN(room, currentPage);
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");
// const downloadButton = document.getElementById("download-button");
// Set the modal content
modalImage.src = event.target.src;
caption.textContent = event.target.alt;
// Set download button link - This has been moved to the Message Rendering Section of the code.
// downloadButton.onclick = () => {
// fetchAndSaveAttachment(
// "FILE",
// event.target.dataset.name,
// event.target.dataset.identifier,
// event.target.dataset.filename,
// event.target.dataset.mimeType
// );
// };
// Show the modal
modal.style.display = "block";
}
});
// Close the modal
document.getElementById("close-modal").addEventListener("click", () => {
document.getElementById("image-modal").style.display = "none";
});
// Hide the modal when clicking outside of the image or close button
window.addEventListener("click", (event) => {
const modal = document.getElementById("image-modal");
if (event.target == modal) {
modal.style.display = "none";
}
});
let selectedFiles = [];
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments
for (const file of selectedFiles) {
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
try {
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
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} published successfully with ID: ${attachmentID}`);
} catch (error) {
console.error(`Error publishing 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
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`);
base64Message = btoa(JSON.stringify(messageObject));
}
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName,
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message, including any potential attached files and replies.
quill.root.innerHTML = "";
document.getElementById('file-input').value = "";
selectedFiles = [];
replyToMessageIdentifier = null;
const replyContainer = document.querySelector(".reply-container");
if (replyContainer) {
replyContainer.remove()
}
// Show success notification
const notification = document.createElement('div');
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
notification.style.color = "green";
notification.style.marginTop = "10px";
document.querySelector(".message-input-section").appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
const loadMoreContainer = document.getElementById("load-more-container");
if (loadMoreContainer) {
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
}
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
const limit = 10;
const offset = page * limit;
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
// Get the messages container
const messagesContainer = document.querySelector("#messages-container");
if (!messagesContainer) return;
// If not polling, clear the message container and the existing identifiers for a fresh load
if (!isPolling) {
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
}
// Get the set of existing identifiers from the messages container
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch messages for the current room and page
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
if (response.length === 0) {
// If no messages are fetched and it's not polling, display "no messages" for the initial load
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Define `mostRecentMessage` to track the latest message during this fetch
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
if (existingIdentifiers.has(resource.identifier)) {
return null; // Skip messages that are already displayed
}
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return {
name: resource.name,
content: messageObject.messageHtml,
date: formattedTimestamp,
identifier: resource.identifier,
replyTo: messageObject.replyTo,
timestamp,
attachments: messageObject.attachments || [] // Include attachments if they exist
};
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
for (const message of fetchMessages) {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) {
try {
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED.
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
// Add the image HTML with the direct URL
attachmentHtml += `<div class="attachment">
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
</div>`;
// Add the modal download button details as well, in order to pass correct information to the modal
const downloadButton = document.getElementById("download-button");
downloadButton.onclick = () => {
fetchAndSaveAttachment(
attachment.service,
attachment.name,
attachment.identifier,
attachment.filename,
attachment.mimeType
);
};
// FOR OTHER METHOD NO LONGER USED
// attachmentHtml += imageHtml;
} catch (error) {
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
}
} else {
// Display a button to download other attachments
attachmentHtml += `<div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`;
}
}
}
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
const messageHTML = `
<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>
</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>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Update mostRecentMessage if this message is newer
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) {
mostRecentMessage = {
latestIdentifier: message.identifier,
latestTimestamp: message.timestamp
};
}
// Add the identifier to the existingIdentifiers set
existingIdentifiers.add(message.identifier);
}
}
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = mostRecentMessage;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
});
});
// Render pagination controls
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
renderPaginationControls(room, totalMessages, limit);
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}

712
assets/js/QortalApi.js Normal file
View File

@@ -0,0 +1,712 @@
// Set the forumAdminGroups variable
let adminGroups = ["Q-Mintership-admin", "dev-group", "Mintership-Forum-Admins"];
// Settings to allow non-devmode development with 'live-server' module
let baseUrl = '';
let isOutsideOfUiDevelopment = false;
if (typeof qortalRequest === 'function') {
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.');
isOutsideOfUiDevelopment = false;
baseUrl = '';
} else {
console.log('qortalRequest is not available as a function. Setting baseUrl to localhost.');
isOutsideOfUiDevelopment = true;
baseUrl = "http://localhost:12391";
};
// USEFUL UTILITIES ----- ----- -----
// Generate a short random ID to utilize at the end of unique identifiers.
const uid = async () => {
console.log('uid function called');
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 6; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
};
console.log('Generated uid:', result);
return result;
};
// Turn a unix timestamp into a human-readable date
const timestampToHumanReadableDate = async(timestamp) => {
console.log('timestampToHumanReadableDate called');
const date = new Date(timestamp);
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear() - 2000;
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const formattedDate = `${month}.${day}.${year}@${hours}:${minutes}:${seconds}`;
console.log('Formatted date:', formattedDate);
return formattedDate;
};
// Base64 encode a string
const base64EncodeString = async (str) => {
console.log('base64EncodeString called');
const encodedString = btoa(String.fromCharCode.apply(null, new Uint8Array(new TextEncoder().encode(str).buffer)));
console.log('Encoded string:', encodedString);
return encodedString;
};
const objectToBase64 = async (obj) => {
// Step 1: Convert the object to a JSON string
const jsonString = JSON.stringify(obj);
// Step 2: Create a Blob from the JSON string
const blob = new Blob([jsonString], { type: 'application/json' });
// Step 3: Create a FileReader to read the Blob as a base64-encoded string
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (typeof reader.result === 'string') {
// Remove 'data:application/json;base64,' prefix
const base64 = reader.result.replace('data:application/json;base64,', '');
console.log(`base64 resolution: ${base64}`);
resolve(base64);
} else {
reject(new Error('Failed to read the Blob as a base64-encoded string'));
}
};
reader.onerror = () => {
reject(reader.error);
};
reader.readAsDataURL(blob);
});
};
// User state util
const userState = {
isLoggedIn: false,
accountName: null,
accountAddress: null,
isAdmin: false,
isMinterAdmin: false,
isForumAdmin: false
};
// USER-RELATED QORTAL CALLS ------------------------------------------
// Obtain the address of the authenticated user checking userState.accountAddress first.
const getUserAddress = async () => {
console.log('getUserAddress called');
try {
if (userState.accountAddress) {
console.log('User address found in state:', userState.accountAddress);
return userState.accountAddress;
};
const userAccount = await qortalRequest({ action: "GET_USER_ACCOUNT" });
if (userAccount) {
console.log('Account address:', userAccount.address);
userState.accountAddress = userAccount.address;
console.log('Account address added to state:', userState.accountAddress);
return userState.accountAddress;
};
} catch (error) {
console.error('Error fetching account address:', error);
throw error;
};
};
const fetchOwnerAddressFromName = async (name) => {
console.log('fetchOwnerAddressFromName called');
console.log('name:', name);
try {
const response = await fetch(`${baseUrl}/names/${name}`, {
headers: { 'Accept': 'application/json' },
method: 'GET',
});
const data = await response.json();
console.log('Fetched owner address:', data.owner);
return data.owner;
} catch (error) {
console.error('Error fetching owner address:', error);
return null;
};
};
const verifyUserIsAdmin = async () => {
console.log('verifyUserIsAdmin called (QortalApi.js)');
try {
const accountAddress = userState.accountAddress || await getUserAddress();
userState.accountAddress = accountAddress;
const userGroups = await getUserGroups(accountAddress);
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isAdmin = await userGroups.some(group => adminGroups.includes(group.groupName))
const isMinterAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin)
if (isMinterAdmin) {
userState.isMinterAdmin = true
}
if (isAdmin) {
userState.isAdmin = true;
userState.isForumAdmin = true
}
return userState.isAdmin;
} catch (error) {
console.error('Error verifying user admin status:', error);
throw error;
}
};
const verifyAddressIsAdmin = async (address) => {
console.log('verifyAddressIsAdmin called');
console.log('address:', address);
try {
if (!address) {
console.log('No address provided');
return false;
};
const userGroups = await getUserGroups(address);
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isAdmin = await userGroups.some(group => adminGroups.includes(group.groupName))
const isMinterAdmin = minterGroupAdmins.members.some(admin => admin.member === address && admin.isAdmin)
if ((isMinterAdmin) || (isAdmin)) {
return true;
} else {
return false
};
} catch (error) {
console.error('Error verifying address admin status:', error);
throw error
}
};
const getNameInfo = async (name) => {
console.log('getNameInfo called');
console.log('name:', name);
try {
const response = await fetch(`${baseUrl}/names/${name}`);
const data = await response.json();
console.log('Fetched name info:', data);
return {
name: data.name,
reducedName: data.reducedName,
owner: data.owner,
data: data.data,
registered: data.registered,
updated: data.updated,
isForSale: data.isForSale,
salePrice: data.salePrice
};
} catch (error) {
console.log('Error fetching name info:', error);
return null;
}
};
const getPublicKeyByName = async (name) => {
console.log('getPublicKeyByName called');
console.log('name:', name);
try {
const nameInfo = await getNameInfo(name);
const address = nameInfo.owner;
const publicKey = await getPublicKeyFromAddress(address);
console.log(`Found public key: for name: ${name}`, publicKey);
return publicKey;
} catch (error) {
console.log('Error obtaining public key from name:', error);
return null;
}
};
const getPublicKeyFromAddress = async (address) => {
console.log('getPublicKeyFromAddress called');
console.log('address:', address);
try {
const response = await fetch(`${baseUrl}/addresses/${address}`,{
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const data = await response.json();
const publicKey = data.publicKey;
console.log('Fetched public key:', publicKey);
return publicKey;
} catch (error) {
console.log('Error fetching public key from address:', error);
return null;
}
};
const getAddressFromPublicKey = async (publicKey) => {
console.log('getAddressFromPublicKey called');
try {
const response = await fetch(`${baseUrl}/addresses/convert/${publicKey}`,{
method: 'GET',
headers: { 'Accept': 'text/plain' }
});
const data = await response.text();
const address = data;
console.log('Converted Address:', address);
return address;
} catch (error) {
console.log('Error converting public key to address:', error);
return null;
}
};
const login = async () => {
console.log('login called');
try {
if (userState.accountName && (userState.isAdmin || userState.isLoggedIn) && userState.accountAddress) {
console.log(`Account name found in userState: '${userState.accountName}', no need to call API...skipping API call.`);
return userState.accountName;
}
const accountAddress = userState.accountAddress || await getUserAddress();
const accountNames = await qortalRequest({
action: "GET_ACCOUNT_NAMES",
address: accountAddress,
});
if (accountNames) {
userState.isLoggedIn = true;
userState.accountName = accountNames[0].name;
userState.accountAddress = accountAddress;
console.log('All account names:', accountNames);
console.log('Main name (in state):', userState.accountName);
console.log('User has been logged in successfully!');
return userState.accountName;
} else {
throw new Error("No account names found. Are you logged in? Do you have a registered name?");
}
} catch (error) {
console.error('Error fetching account names:', error);
throw error;
}
};
const getNamesFromAddress = async (address) => {
try {
const response = await fetch(`${baseUrl}/names/address/${address}?limit=20`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const names = await response.json();
return names.length > 0 ? names[0].name : address; // Return name if found, else return address
} catch (error) {
console.error(`Error fetching names for address ${address}:`, error);
return address;
}
};
// QORTAL GROUP-RELATED CALLS ------------------------------------------
const getUserGroups = async (userAddress) => {
console.log('getUserGroups called');
console.log('userAddress:', userAddress);
try {
if (!userAddress && userState.accountAddress) {
console.log('No address passed to getUserGroups call... using address from state...');
userAddress = userState.accountAddress;
}
const response = await fetch(`${baseUrl}/groups/member/${userAddress}`, {
method: 'GET',
headers: { 'accept': 'application/json' }
});
const data = await response.json();
console.log('Fetched user groups:', data);
return data;
} catch (error) {
console.error('Error fetching user groups:', error);
throw error;
}
};
const fetchMinterGroupAdmins = async () => {
console.log('calling for minter admins')
const response = await fetch(`${baseUrl}/groups/members/694?onlyAdmins=true&limit=0&reverse=true`,{
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const admins = await response.json();
console.log('Fetched minter admins', admins);
return admins;
}
const fetchMinterGroupMembers = async () => {
try {
const response = await fetch(`${baseUrl}/groups/members/694?limit=0`, {
method: 'GET',
headers: { 'Accept': 'application/json' },
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
// Ensure the structure of the response is as expected
if (!Array.isArray(data.members)) {
throw new Error("Expected 'members' to be an array but got a different structure");
}
return data.members; // Assuming 'members' is the key in the response JSON
} catch (error) {
console.error("Error fetching minter group members:", error);
return []; // Return an empty array to prevent further errors
}
};
const fetchAllGroups = async (limit) => {
console.log('fetchAllGroups called');
console.log('limit:', limit);
if (!limit) {
limit = 2000;
}
try {
const response = await fetch(`${baseUrl}/groups?limit=${limit}&reverse=true`);
const data = await response.json();
console.log('Fetched all groups:', data);
return data;
} catch (error) {
console.error('Error fetching all groups:', error);
}
};
// QDN data calls
const searchLatestDataByIdentifier = async (identifier) => {
console.log('fetchAllDataByIdentifier called');
console.log('identifier:', identifier);
try {
const response = await fetch(`${baseUrl}/arbitrary/resources/search?service=DOCUMENT&identifier=${identifier}&includestatus=true&mode=ALL&limit=0&reverse=true`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const latestData = await response.json();
console.log('Fetched latest data by identifier:', latestData);
return latestData;
} catch (error) {
console.error('Error fetching latest published data:', error);
return null;
}
};
const publishMultipleResources = async (resources, publicKeys = null, isPrivate = false) => {
console.log('publishMultipleResources called');
console.log('resources:', resources);
const request = {
action: 'PUBLISH_MULTIPLE_QDN_RESOURCES',
resources: resources,
};
if (isPrivate && publicKeys) {
request.encrypt = true;
request.publicKeys = publicKeys;
};
try {
const response = await qortalRequest(request);
console.log('Multiple resources published successfully:', response);
} catch (error) {
console.error('Error publishing multiple resources:', error);
};
};
const searchResourcesWithMetadata = async (query, limit) => {
console.log('searchResourcesWithMetadata called');
try {
if (limit == 0) {
limit = 0;
} else if (!limit || (limit < 10 && limit != 0)) {
limit = 200;
}
const response = await fetch(`${baseUrl}/arbitrary/resources/search?query=${query}&mode=ALL&includestatus=true&includemetadata=true&limit=${limit}&reverse=true`, {
method: 'GET',
headers: { 'accept': 'application/json' }
});
const data = await response.json();
console.log('Search results with metadata:', data);
return data;
} catch (error) {
console.error('Error searching for resources with metadata:', error);
throw error;
}
};
const searchAllResources = async (query, limit, after, reverse=false) => {
console.log('searchAllResources called. Query:', query, 'Limit:', limit,'Reverse:', reverse);
try {
if (limit == 0) {
limit = 0;
}
if (!limit || (limit < 10 && limit != 0)) {
limit = 200;
}
if (after == null || after === undefined) {
after = 0;
}
const response = await fetch(`${baseUrl}/arbitrary/resources/search?query=${query}&mode=ALL&after=${after}&includestatus=false&includemetadata=false&limit=${limit}&reverse=${reverse}`, {
method: 'GET',
headers: { 'accept': 'application/json' }
});
const data = await response.json();
console.log('Search results with metadata:', data);
return data;
} catch (error) {
console.error('Error searching for resources with metadata:', error);
throw error;
}
};
const searchAllWithOffset = async (query, limit, offset) =>{
try {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: query,
limit: limit,
offset: offset,
mode: "ALL",
reverse: false
});
return response
} catch (error) {
console.error("Error during SEARCH_QDN_RESOURCES:", error);
return [];
}
}
const searchAllCountOnly = async (query) => {
try {
let offset = 0;
const limit = 100; // Chunk size for fetching
let totalCount = 0;
let hasMore = true;
// Fetch in chunks to accumulate the count
while (hasMore) {
const response = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: "BLOG_POST",
query: query,
limit: limit,
offset: offset,
mode: "ALL",
reverse: false
});
if (response && response.length > 0) {
totalCount += response.length;
offset += limit;
console.log(`Fetched ${response.length} items, total count: ${totalCount}, current offset: ${offset}`);
} else {
hasMore = false;
}
}
return totalCount;
} catch (error) {
console.error("Error during SEARCH_QDN_RESOURCES:", error);
throw error;
}
}
const searchResourcesWithStatus = async (query, limit, status = 'local') => {
console.log('searchResourcesWithStatus called');
console.log('query:', query);
console.log('limit:', limit);
console.log('status:', status);
try {
// Set default limit if not provided or too low
if (!limit || limit < 10) {
limit = 200;
}
// Make API request
const response = await fetch(`${baseUrl}/arbitrary/resources/search?query=${query}&includestatus=true&limit=${limit}&reverse=true`, {
method: 'GET',
headers: { 'accept': 'application/json' }
});
const data = await response.json();
// Filter based on status if provided
if (status) {
if (status === 'notLocal') {
const notDownloaded = data.filter((resource) => resource.status.status === 'published');
console.log('notDownloaded:', notDownloaded);
return notDownloaded;
} else if (status === 'local') {
const downloaded = data.filter((resource) =>
resource.status.status === 'ready' ||
resource.status.status === 'downloaded' ||
resource.status.status === 'building' ||
(resource.status.status && resource.status.status !== 'published')
);
return downloaded;
}
}
// Return all data if no specific status is provided
console.log('Returning all data...');
return data;
} catch (error) {
console.error('Error searching for resources with metadata:', error);
throw error;
}
};
const getResourceMetadata = async (service, name, identifier) => {
console.log('getResourceMetadata called');
console.log('service:', service);
console.log('name:', name);
console.log('identifier:', identifier);
try {
const response = await fetch(`${baseUrl}/arbitrary/metadata/${service}/${name}/${identifier}`, {
method: 'GET',
headers: { 'accept': 'application/json' }
});
const data = await response.json();
console.log('Fetched resource metadata:', data);
return data;
} catch (error) {
console.error('Error fetching resource metadata:', error);
throw error;
}
};
const fetchFileBase64 = async (service, name, identifier) => {
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}/?encoding=base64`;
try {
const response = await fetch(url,{
method: 'GET',
headers: { 'accept': 'text/plain' }
});
return response;
} catch (error) {
console.error("Error fetching the image file:", error);
}
};
async function loadImageHtml(service, name, identifier, filename, mimeType) {
try {
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
// Fetch the file as a blob
const response = await fetch(url);
// Convert the response to a Blob
const fileBlob = new Blob([response], { type: mimeType });
// Create an Object URL from the Blob
const objectUrl = URL.createObjectURL(fileBlob);
// Use the Object URL as the image source
const attachmentHtml = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`;
return attachmentHtml;
} catch (error) {
console.error("Error fetching the image:", error);
}
}
const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeType) => {
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
try {
const response = await fetch(url);
const blob = await response.blob();
await qortalRequest({
action: "SAVE_FILE",
blob,
filename: filename,
mimeType
});
} catch (error) {
console.error("Error fetching or saving the attachment:", error);
}
}
const renderData = async (service, name, identifier) => {
console.log('renderData called');
console.log('service:', service);
console.log('name:', name);
console.log('identifier:', identifier);
try {
const response = await fetch(`${baseUrl}/render/${service}/${name}?identifier=${identifier}`, {
method: 'GET',
headers: { 'accept': '*/*' }
});
// If the response is not OK (status 200-299), throw an error
if (!response.ok) {
throw new Error('Error rendering data');
}
const responseText = await response.text();
// Check if the response includes <!DOCTYPE> indicating it's an HTML document
if (responseText.includes('<!DOCTYPE')) {
throw new Error('Received HTML response');
}
const data = JSON.parse(responseText);
console.log('Rendered data:', data);
return data;
} catch (error) {
console.error('Error rendering data:', error);
// Return the custom message when theres an error or invalid data
return 'Requested data is either missing or still being obtained from QDN... please try again in a short time.';
}
};
const getProductDetails = async (service, name, identifier) => {
console.log('getProductDetails called');
console.log('service:', service);
console.log('name:', name);
console.log('identifier:', identifier);
try {
const response = await fetch(`${baseUrl}/arbitrary/metadata/${service}/${name}/${identifier}`, {
method: 'GET',
headers: { 'accept': 'application/json' }
});
const data = await response.json();
console.log('Fetched product details:', data);
return data;
} catch (error) {
console.error('Error fetching product details:', error);
throw error;
}
};
// Qortal poll-related calls ----------------------------------------------------------------------
const fetchPollResults = async (pollName) => {
try {
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
const pollData = await response.json();
return pollData;
} catch (error) {
console.error(`Error fetching poll results for ${pollName}:`, error);
return null;
}
};
// export {
// userState,
// adminGroups,
// searchResourcesWithMetadata,
// searchResourcesWithStatus,
// getResourceMetadata,
// renderData,
// getProductDetails,
// getUserGroups,
// getUserAddress,
// login,
// timestampToHumanReadableDate,
// base64EncodeString,
// verifyUserIsAdmin,
// fetchAllDataByIdentifier,
// fetchOwnerAddressFromName,
// verifyAddressIsAdmin,
// uid,
// fetchAllGroups,
// getNameInfo,
// publishMultipleResources,
// getPublicKeyByName,
// objectToBase64,
// fetchMinterGroupAdmins
// };

195
assets/js/css-scratch.css Normal file
View File

@@ -0,0 +1,195 @@
/* forum-styles.css */
.forum-main {
color: #ffffff;
background-color: #000000;
padding: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 100vw;
box-sizing: border-box;
background-size: cover;
background-position: center;
}
.forum-header {
width: 100%;
padding: 2vh;
background-color: #000000;
color: #add8e6; /* Light blue color */
text-align: center;
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 0;
}
.forum-submenu {
width: 100%;
padding: 1vh 2vh;
background: rgba(0, 0, 0, 0.8);
text-align: center;
margin-top: 0;
}
.forum-rooms {
display: flex;
justify-content: center;
gap: 2vh; /* Increased gap for better spacing */
margin-top: 0;
flex-wrap: wrap;
}
.room-button {
background-color: #317e78;
color: #ffffff;
border: 2px solid #317e78;
border-radius: 2vh;
padding: 1vh 2vh;
font-size: 1.1rem;
cursor: pointer;
}
.room-button:hover {
background-color: #19403d;
}
.forum-content {
flex-grow: 1;
width: 90%;
padding: 3vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
box-sizing: border-box;
border: 3px solid #ffffff; /* Increased border width */
}
.room-content {
background: rgba(0, 0, 0, 0.6);
padding: 2vh;
border-radius: 1vh;
width: 100%;
box-sizing: border-box;
text-align: center;
}
.room-title {
color: #add8e6; /* Light blue color for room name */
text-align: center;
margin-bottom: 2vh;
font-size: 2rem;
}
.message-item {
background: #1c1c1c;
color: #ffffff;
padding: 1vh;
margin-bottom: 1vh;
border-radius: 0.8vh;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
border: 1px solid #ffffff;
}
.message-header {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 1vh;
font-size: 1.25rem;
color: white
}
.message-header.username {
color: #228ec0;
}
.username {
font-weight: bold;
color:#228ec0
}
.timestamp {
font-style: italic;
color: rgb(157, 167, 151)
}
.message-text {
margin: 0;
font-size: 1.25rem;
}
.reply-button {
align-self: flex-end;
margin-top: 1vh;
background-color: #167089;
color: #ffffff;
border: none;
border-radius: 1vh;
padding: 0.3vh 0.6vh;
cursor: pointer;
}
.reply-button:hover {
background-color: #19403d;
}
/* forum-styles.css additions */
.message-input-section {
display: flex;
flex-direction: column;
align-items: stretch;
box-sizing: border-box;
width: 100%;
gap: 1vh; /* Spacing between toolbar and editor */
background-color: black;
padding: 1vh;
}
.ql-editor {
flex-grow: 1;
text-size: 1.25rem;
}
.message-input {
flex-grow: 1;
padding: 2vh;
border-radius: 1vh;
border: 1px solid #cccccc;
font-size: 1.25rem;
/* margin-right: 8vh; */
box-sizing: border-box;
min-height: 15vh;
}
.send-button {
background-color: #13a97c;
color: #ffffff;
border: none;
border-radius: 1vh;
padding: 2vh 4vh;
cursor: pointer;
}
.send-button:hover {
background-color: #19403d;
}
.messages-container {
width: 100%;
margin-bottom: 5vh; /* Ensure space above input section */
overflow-y: auto;
padding-bottom: 1vh;
box-sizing: border-box;
}

305
assets/js/scratch-2.js Normal file
View File

@@ -0,0 +1,305 @@
const messageIdentifierPrefix = `mintership-forum-message`;
let replyToMessageIdentifier = null;
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
let currentPage = 0; // Track current pagination page
// Load the latest message identifiers from local storage
if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
}
document.addEventListener("DOMContentLoaded", async () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<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 (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
<div class="message-input-section">
<div id="toolbar" class="message-toolbar"></div>
<div id="editor" class="message-input"></div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "") {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
// Create message object with unique identifier and HTML content
const messageObject = {
messageHtml: messageHtml,
hasAttachment: false,
replyTo: replyToMessageIdentifier
};
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null; // Clear reply reference after sending
// Clear reply reference after sending if it exists.
if (replyToMessageIdentifier) {
replyToMessageIdentifier = null;
replyContainer.remove();
}
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages
loadMessagesFromQDN(room, currentPage);
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page) {
try {
const offset = page * 10;
const limit = 10;
// Get the set of existing identifiers from the messages container
const messagesContainer = document.querySelector("#messages-container");
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch only messages that are not already present in the messages container
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
if (messagesContainer) {
if (!response || !response.length) {
if (page === 0) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Fetch all messages
const fetchMessages = await Promise.all(response.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render messages without duplication
fetchMessages.forEach((message) => {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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 isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
}
});
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
}
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// Polling function to check for new messages
function startPollingForNewMessages() {
setInterval(async () => {
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
if (activeRoom) {
await loadMessagesFromQDN(activeRoom, currentPage);
}
}, 20000);
}

392
assets/js/scratch.js Normal file
View File

@@ -0,0 +1,392 @@
const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
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 () => {
// Identify the link for 'Mintership Forum'
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
mintershipForumLinks.forEach(link => {
link.addEventListener('click', async (event) => {
event.preventDefault();
await login(); // Assuming login is an async function
await loadForumPage();
loadRoomContent("general"); // Automatically load General Room on forum load
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
});
});
});
async function loadForumPage() {
// Remove all sections except the menu
const allSections = document.querySelectorAll('body > section');
allSections.forEach(section => {
if (!section.classList.contains('menu')) {
section.remove();
}
});
// Check if user is an admin
const minterGroupAdmins = await fetchMinterGroupAdmins();
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
const mainContent = document.createElement('div');
mainContent.innerHTML = `
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
</div>
<div class="forum-submenu">
<div class="forum-rooms">
<button class="room-button" id="minters-room">Minters Room</button>
${isUserAdmin ? '<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 (isUserAdmin) {
document.getElementById("admins-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("admins");
});
}
document.getElementById("general-room").addEventListener("click", () => {
currentPage = 0;
loadRoomContent("general");
});
}
function loadRoomContent(room) {
const forumContent = document.getElementById("forum-content");
if (forumContent) {
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>
${(existingIdentifiers.size > 10)? '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>' : ''}
<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>
<button id="attach-button" class="attach-button">Attach Files</button>
</div>
<button id="send-button" class="send-button">Send</button>
</div>
</div>
`;
// Initialize Quill editor for rich text input
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'font': [] }], // Add font family options
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline'], // Text formatting options
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'blockquote', 'code-block'],
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
[{ 'align': [] }], // Text alignment
['clean'] // Remove formatting button
]
}
});
// Load messages from QDN for the selected room
loadMessagesFromQDN(room, currentPage);
let selectedFiles = [];
// Add event listener to handle file selection
document.getElementById('file-input').addEventListener('change', (event) => {
selectedFiles = Array.from(event.target.files);
});
// Add event listener for the send button
document.getElementById("send-button").addEventListener("click", async () => {
const messageHtml = quill.root.innerHTML.trim();
if (messageHtml !== "" || selectedFiles.length > 0) {
const randomID = await uid();
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
let attachmentIdentifiers = [];
// Handle attachments
for (const file of selectedFiles) {
const attachmentID = `${messageAttachmentIdentifierPrefix}-${randomID}-${file.name}`;
try {
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "IMAGE", // Adjust based on file type
identifier: attachmentID,
file: file
});
attachmentIdentifiers.push(attachmentID);
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
} catch (error) {
console.error(`Error publishing 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
};
if (!messageObject.attachments) {
messageObject.attachments = [];
}
messageObject.attachments.push({
identifier: attachmentIdentifier,
filename: file.name,
mimeType: file.type
});
try {
// Convert message object to base64
let base64Message = await objectToBase64(messageObject);
if (!base64Message) {
console.log(`initial object creation with object failed, using btoa...`)
base64Message = btoa(JSON.stringify(messageObject));
}
console.log("Message Object:", messageObject);
console.log("Base64 Encoded Message:", base64Message);
// Publish message to QDN
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: userState.accountName, // Publisher must own the registered name
service: "BLOG_POST",
identifier: messageIdentifier,
data64: base64Message
});
console.log("Message published successfully");
// Clear the editor after sending the message
quill.root.innerHTML = "";
replyToMessageIdentifier = null;
document.getElementById('file-input').value = "";
selectedFiles = [];
// Clear reply reference after sending if it exists.
if (replyToMessageIdentifier) {
replyToMessageIdentifier = null;
replyContainer.remove();
}
// Update the latest message identifier
latestMessageIdentifiers[room] = messageIdentifier;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
// Reload messages - - - - - - - - - - - - - - - - - - CHANGE THIS TO DISPLAY THE LAST MESSAGE IN THE CONTAINER INSTEAD OF RELOADING ALL MESSAGES, AND DISPLAY NOTIFICATION OF SUCCESSFUL PUBLISH.
} catch (error) {
console.error("Error publishing message:", error);
}
}
});
// Add event listener for the load more button
document.getElementById("load-more-button").addEventListener("click", () => {
currentPage++;
loadMessagesFromQDN(room, currentPage);
});
}
}
// Load messages for any given room with pagination
async function loadMessagesFromQDN(room, page, isPolling = false) {
try {
const offset = page * 10;
const limit = 10;
// Get the set of existing identifiers from the messages container
const messagesContainer = document.querySelector("#messages-container");
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
// Fetch only messages that are not already present in the messages container
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
if (messagesContainer) {
// If there are no messages and we're not polling, display "no messages" message
if (!response || !response.length) {
if (page === 0 && !isPolling) {
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
}
return;
}
// Fetch all messages that haven't been fetched before
const fetchMessages = await Promise.all(response.map(async (resource) => {
try {
console.log(`Fetching message with identifier: ${resource.identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resource.name,
service: "BLOG_POST",
identifier: resource.identifier,
});
console.log("Fetched message response:", messageResponse);
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
const messageObject = messageResponse;
const timestamp = resource.updated || resource.created;
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
} catch (error) {
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
return null;
}
}));
// Render new messages without duplication
fetchMessages.forEach((message) => {
if (message && !existingIdentifiers.has(message.identifier)) {
let replyHtml = "";
if (message.replyTo) {
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (repliedMessage) {
replyHtml = `
<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>
`;
}
}
let mostRecentMessage = null;
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
let attachmentHtml = "";
if (message.attachments && message.attachments.length > 0) {
for (const attachment of message.attachments) {
if (attachment.mimeType.startsWith('image/')) {
// Display images inline
const url = `/arbitrary/${attachment.service}/${message.name}/${attachment.identifier}`;
attachmentHtml += `<div class="attachment"><img src="${url}" alt="${attachment.filename}" class="inline-image"></div>`;
} else {
// Display a button to download other attachments
attachmentHtml += `<div class="attachment">
<button onclick="fetchAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
</div>`;
}
}
}
const messageHTML = `
<div class="message-item" data-identifier="${message.identifier}">
${replyHtml}
<div class="message-header">
<span class="username">${message.name}</span>
<span class="timestamp">${message.date}</span>
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
</div>
${attachmentHtml}
<div class="message-text">${message.content}</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
// Append new message to the end of the container
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
// Track the most recent message
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage.timestamp)) {
mostRecentMessage = message;
}
}
});
// Update latestMessageIdentifiers for the room
if (mostRecentMessage) {
latestMessageIdentifiers[room] = {
latestIdentifier: mostRecentMessage.identifier,
latestTimestamp: mostRecentMessage.timestamp
};
}
// Add event listeners to the reply buttons
const replyButtons = document.querySelectorAll(".reply-button");
replyButtons.forEach(button => {
button.addEventListener("click", () => {
replyToMessageIdentifier = button.dataset.messageIdentifier;
// Find the message being replied to
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
if (repliedMessage) {
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);
// Add a listener for the cancel reply button
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();
}
}
});
});
}
} catch (error) {
console.error('Error loading messages from QDN:', error);
}
}
// 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);
}
}, 20000);
}