created Q-Mintership-Alpha repository
This commit is contained in:
157
assets/js/BACKUP/AdminTools-backup-before-linksCardEdit.js
Normal file
157
assets/js/BACKUP/AdminTools-backup-before-linksCardEdit.js
Normal 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).`);
|
||||
}
|
157
assets/js/BACKUP/AdminTools.js-notworkingwell-nov-29-2024.js
Normal file
157
assets/js/BACKUP/AdminTools.js-notworkingwell-nov-29-2024.js
Normal 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).`);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
418
assets/js/BACKUP/Q-Mintership-11-21-withAttachments.js
Normal file
418
assets/js/BACKUP/Q-Mintership-11-21-withAttachments.js
Normal 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);
|
||||
}
|
||||
|
505
assets/js/BACKUP/Q-Mintership-Nov-29-workingWithAFewQwerks.js
Normal file
505
assets/js/BACKUP/Q-Mintership-Nov-29-workingWithAFewQwerks.js
Normal 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);
|
||||
}
|
||||
|
485
assets/js/BACKUP/Q-Mintership-with-few-bugs-Nov26-2024.js
Normal file
485
assets/js/BACKUP/Q-Mintership-with-few-bugs-Nov26-2024.js
Normal 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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
491
assets/js/BACKUP/Q-Mintership-working-embeds-nov-27.js
Normal file
491
assets/js/BACKUP/Q-Mintership-working-embeds-nov-27.js
Normal 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);
|
||||
}
|
||||
|
175
assets/js/BACKUP/backup-Dec-9-MinterBoard.js
Normal file
175
assets/js/BACKUP/backup-Dec-9-MinterBoard.js
Normal 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>
|
||||
`;
|
||||
}
|
@@ -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);
|
||||
}
|
289
assets/js/BACKUP/backup-Q-Mintership.js-startedNewStyling.js
Normal file
289
assets/js/BACKUP/backup-Q-Mintership.js-startedNewStyling.js
Normal 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);
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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);
|
||||
}
|
219
assets/js/BACKUP/backup-Q-Mintership.js-workingWithOtherStyle.js
Normal file
219
assets/js/BACKUP/backup-Q-Mintership.js-workingWithOtherStyle.js
Normal 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);
|
||||
}
|
||||
}
|
@@ -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>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@@ -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>
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
195
assets/js/BACKUP/backup-forum-styles.css
Normal file
195
assets/js/BACKUP/backup-forum-styles.css
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
319
assets/js/BACKUP/working-fewLittleIssues-0858AM-11-19-2024.js
Normal file
319
assets/js/BACKUP/working-fewLittleIssues-0858AM-11-19-2024.js
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user