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

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);
}