created Q-Mintership-Alpha repository
This commit is contained in:
167
assets/js/AdminTools.js
Normal file
167
assets/js/AdminTools.js
Normal file
@@ -0,0 +1,167 @@
|
||||
let currentMinterToolPage = 'overview'; // Track the current page
|
||||
|
||||
// Load latest state for admin verification
|
||||
async function verifyMinterAdminState() {
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
return minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const isAdmin = await verifyUserIsAdmin();
|
||||
|
||||
if (isAdmin) {
|
||||
console.log(`User is an Admin, buttons for MA Tools not removed. userState.isAdmin = ${userState.isMinterAdmin}`);
|
||||
} else {
|
||||
// Remove all "TOOLS" links and their related elements
|
||||
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
|
||||
toolsLinks.forEach(link => {
|
||||
// If the link is within a button, remove the button
|
||||
const buttonParent = link.closest('button');
|
||||
if (buttonParent) {
|
||||
buttonParent.remove();
|
||||
}
|
||||
|
||||
// If the link is within an image card or any other element, remove that element
|
||||
const cardParent = link.closest('.item.features-image');
|
||||
if (cardParent) {
|
||||
cardParent.remove();
|
||||
}
|
||||
|
||||
// Finally, remove the link itself if it's not covered by the above removals
|
||||
link.remove();
|
||||
});
|
||||
|
||||
console.log(`User is NOT a Minter Admin, buttons for MA Tools removed. userState.isMinterAdmin = ${userState.isMinterAdmin}`);
|
||||
|
||||
// Center the remaining card if it exists
|
||||
const remainingCard = document.querySelector('.features7 .row .item.features-image');
|
||||
if (remainingCard) {
|
||||
remainingCard.classList.remove('col-lg-6', 'col-md-6');
|
||||
remainingCard.classList.add('col-12', 'text-center');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Add event listener for admin tools link if the user is an admin
|
||||
const toolsLinks = document.querySelectorAll('a[href="TOOLS"]');
|
||||
toolsLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadMinterAdminToolsPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function loadMinterAdminToolsPage() {
|
||||
// Remove all body content except for menu elements
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains('menu')) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
|
||||
|
||||
// Set the background image directly from a file
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="tools-main mbr-parallax-background cid-ttRnlSkg2R">
|
||||
<div class="tools-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
|
||||
<div> <h1 style="font-size: 50px; margin: 0;">MINTER ADMIN TOOLS for Admin = </h1></div>
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
<span>${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tools-submenu" class="tools-submenu">
|
||||
<div class="tools-buttons">
|
||||
<button id="display-pending" class="tools-button">Display Pending Approval Transactions</button>
|
||||
<button id="create-group-invite" class="tools-button">Create Pending Group Invite</button>
|
||||
<button id="create-promotion" class="tools-button">Create Pending Promotion</button>
|
||||
</div>
|
||||
<div id="tools-window" class="tools-window"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
addToolsPageEventListeners();
|
||||
}
|
||||
|
||||
function addToolsPageEventListeners() {
|
||||
document.getElementById("display-pending").addEventListener("click", async () => {
|
||||
await displayPendingApprovals();
|
||||
});
|
||||
|
||||
document.getElementById("create-group-invite").addEventListener("click", async () => {
|
||||
createPendingGroupInvite();
|
||||
});
|
||||
|
||||
document.getElementById("create-promotion").addEventListener("click", async () => {
|
||||
createPendingPromotion();
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch and display pending approvals
|
||||
async function displayPendingApprovals() {
|
||||
console.log("Fetching pending approval transactions...");
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_TRANSACTIONS",
|
||||
txGroupId: 694,
|
||||
txType: [
|
||||
"ADD_GROUP_ADMIN",
|
||||
"GROUP_INVITE"
|
||||
],
|
||||
confirmationStatus: "UNCONFIRMED",
|
||||
limit: 0,
|
||||
offset: 0,
|
||||
reverse: false
|
||||
});
|
||||
|
||||
console.log("Fetched pending approvals: ", response);
|
||||
|
||||
const toolsWindow = document.getElementById('tools-window');
|
||||
if (response && response.length > 0) {
|
||||
toolsWindow.innerHTML = response.map(tx => `
|
||||
<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;">
|
||||
<p><strong>Transaction Type:</strong> ${tx.type}</p>
|
||||
<p><strong>Amount:</strong> ${tx.amount}</p>
|
||||
<p><strong>Creator Address:</strong> ${tx.creatorAddress}</p>
|
||||
<p><strong>Recipient:</strong> ${tx.recipient}</p>
|
||||
<p><strong>Timestamp:</strong> ${new Date(tx.timestamp).toLocaleString()}</p>
|
||||
<button onclick="approveTransaction('${tx.signature}')">Approve</button>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
toolsWindow.innerHTML = '<div class="message-item" style="border: 1px solid lightblue; padding: 10px; margin-bottom: 10px;"><p>No pending approvals found.</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder function to create a pending group invite
|
||||
async function createPendingGroupInvite() {
|
||||
console.log("Creating a pending group invite...");
|
||||
// Placeholder code for creating a pending group invite
|
||||
alert('Pending group invite created (placeholder).');
|
||||
}
|
||||
|
||||
// Placeholder function to create a pending promotion
|
||||
async function createPendingPromotion() {
|
||||
console.log("Creating a pending promotion...");
|
||||
// Placeholder code for creating a pending promotion
|
||||
alert('Pending promotion created (placeholder).');
|
||||
}
|
||||
|
||||
// Placeholder function for approving a transaction
|
||||
function approveTransaction(signature) {
|
||||
console.log("Approving transaction with signature: ", signature);
|
||||
// Placeholder code for approving transaction
|
||||
alert(`Transaction with signature ${signature} approved (placeholder).`);
|
||||
}
|
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);
|
||||
}
|
497
assets/js/MinterBoard.js
Normal file
497
assets/js/MinterBoard.js
Normal file
@@ -0,0 +1,497 @@
|
||||
const cardIdentifierPrefix = "test-board-card";
|
||||
let isExistingCard = false;
|
||||
let existingCardData = {};
|
||||
let existingCardIdentifier ={};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const minterBoardLinks = document.querySelectorAll('a[href="MINTER-BOARD"], a[href="MINTERS"]');
|
||||
|
||||
minterBoardLinks.forEach(link => {
|
||||
link.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadMinterBoardPage();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadMinterBoardPage() {
|
||||
// Clear existing content on the page
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains("menu")) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Add the "Minter Board" content
|
||||
const mainContent = document.createElement("div");
|
||||
mainContent.innerHTML = `
|
||||
<div class="minter-board-main" style="padding: 20px; text-align: center;">
|
||||
<h1 style="color: lightblue;">Minter Board</h1>
|
||||
<p> The Minter Board is a place to publish information about yourself in order to obtain support from existing Minters and Minter Admins on the Qortal network. You may publish a header, content, and links to other QDN-published content in order to support you in your mission. Minter Admins and Existing Minters will then support you (or not) by way of a vote on your card. Card details you publish, along with existing poll results, and comments from others, will be displayed here. Good Luck on your Qortal journey to becoming a minter!</p>
|
||||
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px;">Publish Minter Card</button>
|
||||
<div id="cards-container" class="cards-container" style="margin-top: 20px;"></div>
|
||||
<div id="publish-card-view" class="publish-card-view" style="display: none; text-align: left; padding: 20px;">
|
||||
<form id="publish-card-form">
|
||||
<h3>Create or Update Your Minter Card</h3>
|
||||
<label for="card-header">Header:</label>
|
||||
<input type="text" id="card-header" maxlength="100" placeholder="Enter card header" required>
|
||||
<label for="card-content">Content:</label>
|
||||
<textarea id="card-content" placeholder="Enter detailed information about why you deserve to be a minter..." required></textarea>
|
||||
<label for="card-links">Links (qortal://...):</label>
|
||||
<div id="links-container">
|
||||
<input type="text" class="card-link" placeholder="Enter QDN link">
|
||||
</div>
|
||||
<button type="button" id="add-link-button">Add Another Link</button>
|
||||
<button type="submit" id="submit-publish-button">Publish Card</button>
|
||||
<button type="button" id="cancel-publish-button">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
document.getElementById("publish-card-button").addEventListener("click", async () => {
|
||||
try {
|
||||
await fetchExistingCard();
|
||||
const cardIdentifier = existingCardIdentifier
|
||||
if (cardIdentifier) {
|
||||
// Update existing card
|
||||
const updateCard = confirm("A card already exists. Do you want to update it?");
|
||||
isExistingCard = true;
|
||||
|
||||
if (updateCard) {
|
||||
// Load existing card into the form for editing
|
||||
loadCardIntoForm(existingCardData);
|
||||
alert("Edit your existing card and publish.");
|
||||
} else {
|
||||
// Allow creating a new card for testing purposes
|
||||
alert("You can now create a new card for testing.");
|
||||
isExistingCard = false;
|
||||
existingCardData = {}; // Reset to allow new card creation
|
||||
document.getElementById("publish-card-form").reset();
|
||||
}
|
||||
} else {
|
||||
alert("No existing card found. Create a new card.");
|
||||
isExistingCard = false;
|
||||
}
|
||||
|
||||
// Show the form for publishing a card
|
||||
const publishCardView = document.getElementById("publish-card-view");
|
||||
publishCardView.style.display = "flex";
|
||||
document.getElementById("cards-container").style.display = "none";
|
||||
} catch (error) {
|
||||
console.error("Error checking for existing card:", error);
|
||||
alert("Failed to check for existing card. Please try again.");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("cancel-publish-button").addEventListener("click", () => {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.style.display = "flex"; // Restore visibility
|
||||
const publishCardView = document.getElementById("publish-card-view");
|
||||
publishCardView.style.display = "none"; // Hide the publish form
|
||||
});
|
||||
|
||||
document.getElementById("add-link-button").addEventListener("click", () => {
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
const newLinkInput = document.createElement("input");
|
||||
newLinkInput.type = "text";
|
||||
newLinkInput.className = "card-link";
|
||||
newLinkInput.placeholder = "Enter QDN link";
|
||||
linksContainer.appendChild(newLinkInput);
|
||||
});
|
||||
|
||||
document.getElementById("publish-card-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
await publishCard();
|
||||
});
|
||||
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
async function fetchExistingCard() {
|
||||
try {
|
||||
// Step 1: Perform the search
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: userState.accountName, // Scoped to the user
|
||||
identifier: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
console.log(`SEARCH_QDN_RESOURCES response: ${JSON.stringify(response, null, 2)}`);
|
||||
|
||||
// Step 2: Check if the response is an array and not empty
|
||||
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||
console.log("No cards found for the current user.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const validCards = response.filter(card => validateCardStructure(card));
|
||||
|
||||
if (validCards.length > 0) {
|
||||
// Sort by most recent timestamp
|
||||
const mostRecentCard = validCards.sort((a, b) => b.created - a.created)[0];
|
||||
|
||||
// Step 3: Find the object with the most recent timestamp
|
||||
// const mostRecentCard = response.reduce((latest, card) => {
|
||||
// const currentTimestamp = card.created || card.updated || 0; // Ensure there's a timestamp
|
||||
// const latestTimestamp = latest.created || latest.updated || 0;
|
||||
// return currentTimestamp > latestTimestamp ? card : latest;
|
||||
// }, {});
|
||||
|
||||
// console.log(`Most recent card found: ${JSON.stringify(mostRecentCard, null, 2)}`);
|
||||
|
||||
// // Step 4: Validate the card and fetch its full data
|
||||
// if (!mostRecentCard.identifier) {
|
||||
// console.error("Identifier is missing in the most recent card.");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: userState.accountName, // User's account name
|
||||
service: "BLOG_POST",
|
||||
identifier: mostRecentCard.identifier,
|
||||
});
|
||||
|
||||
existingCardIdentifier = mostRecentCard.identifier
|
||||
existingCardData = cardDataResponse
|
||||
|
||||
console.log("Full card data fetched successfully:", cardDataResponse);
|
||||
|
||||
return cardDataResponse; // Return full card data
|
||||
}
|
||||
|
||||
console.log("No valid cards found.");
|
||||
return null;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching existing card:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const validateCardStructure = (card) => {
|
||||
return (
|
||||
typeof card === "object" &&
|
||||
card.name &&
|
||||
card.service === "BLOG_POST" &&
|
||||
card.identifier && !card.identifier.includes("comment") &&
|
||||
card.created
|
||||
);
|
||||
}
|
||||
|
||||
function loadCardIntoForm(cardData) {
|
||||
document.getElementById("card-header").value = cardData.header;
|
||||
document.getElementById("card-content").value = cardData.content;
|
||||
|
||||
const linksContainer = document.getElementById("links-container");
|
||||
linksContainer.innerHTML = ""; // Clear previous links
|
||||
cardData.links.forEach(link => {
|
||||
const linkInput = document.createElement("input");
|
||||
linkInput.type = "text";
|
||||
linkInput.className = "card-link";
|
||||
linkInput.value = link;
|
||||
linksContainer.appendChild(linkInput);
|
||||
});
|
||||
}
|
||||
|
||||
async function publishCard() {
|
||||
const header = document.getElementById("card-header").value.trim();
|
||||
const content = document.getElementById("card-content").value.trim();
|
||||
const links = Array.from(document.querySelectorAll(".card-link"))
|
||||
.map(input => input.value.trim())
|
||||
.filter(link => link.startsWith("qortal://"));
|
||||
|
||||
if (!header || !content) {
|
||||
alert("Header and content are required!");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardIdentifier = isExistingCard ? existingCardInfo.identifier : `${cardIdentifierPrefix}-${await uid()}`;
|
||||
const pollName = `${cardIdentifier}-poll`;
|
||||
const pollDescription = `Mintership Board Poll for ${userState.accountName}`;
|
||||
|
||||
const cardData = {
|
||||
header,
|
||||
content,
|
||||
links,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
poll: pollName,
|
||||
};
|
||||
// new Date().toISOString()
|
||||
try {
|
||||
|
||||
let base64CardData = await objectToBase64(cardData);
|
||||
if (!base64CardData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CardData = btoa(JSON.stringify(cardData));
|
||||
}
|
||||
// const base64CardData = btoa(JSON.stringify(cardData));
|
||||
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: cardIdentifier,
|
||||
data64: base64CardData,
|
||||
});
|
||||
|
||||
await qortalRequest({
|
||||
action: "CREATE_POLL",
|
||||
pollName,
|
||||
pollDescription,
|
||||
pollOptions: ['Yes, No'],
|
||||
pollOwnerAddress: userState.accountAddress,
|
||||
});
|
||||
|
||||
alert("Card and poll published successfully!");
|
||||
document.getElementById("publish-card-form").reset();
|
||||
document.getElementById("publish-card-view").style.display = "none";
|
||||
document.getElementById("cards-container").style.display = "flex";
|
||||
await loadCards();
|
||||
} catch (error) {
|
||||
console.error("Error publishing card or poll:", error);
|
||||
alert("Failed to publish card and poll.");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCards() {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
cardsContainer.innerHTML = "<p>Loading cards...</p>";
|
||||
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: cardIdentifierPrefix,
|
||||
});
|
||||
|
||||
if (!response || response.length === 0) {
|
||||
cardsContainer.innerHTML = "<p>No cards found.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = ""
|
||||
const pollResultsCache = {};
|
||||
|
||||
for (const card of response) {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: card.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: card.identifier,
|
||||
});
|
||||
|
||||
const cardData = cardDataResponse;
|
||||
|
||||
if (!cardData || !cardData.poll) {
|
||||
console.warn(`Skipping card with missing poll data: ${JSON.stringify(cardData)}`);
|
||||
continue; // Skip to the next card
|
||||
}
|
||||
|
||||
// Cache poll results
|
||||
if (!pollResultsCache[cardData.poll]) {
|
||||
try {
|
||||
pollResultsCache[cardData.poll] = await fetchPollResults(cardData.poll);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch poll results for poll: ${cardData.poll}`, error);
|
||||
pollResultsCache[cardData.poll] = null; // Store as null to avoid repeated attempts
|
||||
}
|
||||
}
|
||||
|
||||
const pollResults = pollResultsCache[cardData.poll];
|
||||
const cardHTML = await createCardHTML(cardData, pollResults, card.identifier);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", cardHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading cards:", error);
|
||||
cardsContainer.innerHTML = "<p>Failed to load cards.</p>";
|
||||
}
|
||||
}
|
||||
|
||||
const calculatePollResults = (pollData, minterGroupMembers) => {
|
||||
const memberAddresses = minterGroupMembers.map(member => member.member);
|
||||
let adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0;
|
||||
|
||||
pollData.votes.forEach(vote => {
|
||||
const voterAddress = getAddressFromPublicKey(vote.voterPublicKey);
|
||||
const isAdmin = minterGroupMembers.some(member => member.member === voterAddress && member.isAdmin);
|
||||
|
||||
if (vote.optionIndex === 1) {
|
||||
isAdmin ? adminYes++ : memberAddresses.includes(voterAddress) ? minterYes++ : null;
|
||||
} else if (vote.optionIndex === 0) {
|
||||
isAdmin ? adminNo++ : memberAddresses.includes(voterAddress) ? minterNo++ : null;
|
||||
}
|
||||
});
|
||||
|
||||
const totalYes = adminYes + minterYes;
|
||||
const totalNo = adminNo + minterNo;
|
||||
|
||||
return { adminYes, adminNo, minterYes, minterNo, totalYes, totalNo };
|
||||
};
|
||||
|
||||
const postComment = async (cardIdentifier) => {
|
||||
const commentInput = document.getElementById(`new-comment-${cardIdentifier}`);
|
||||
const commentText = commentInput.value.trim();
|
||||
if (!commentText) {
|
||||
alert('Comment cannot be empty!');
|
||||
return;
|
||||
}
|
||||
|
||||
const commentData = {
|
||||
content: commentText,
|
||||
creator: userState.accountName,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
|
||||
|
||||
try {
|
||||
const base64CommentData = await objectToBase64(commentData);
|
||||
if (!base64CommentData) {
|
||||
console.log(`initial base64 object creation with objectToBase64 failed, using btoa...`);
|
||||
base64CommentData = btoa(JSON.stringify(commentData));
|
||||
}
|
||||
// const base64CommentData = btoa(JSON.stringify(commentData));
|
||||
await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: userState.accountName,
|
||||
service: 'BLOG_POST',
|
||||
identifier: commentIdentifier,
|
||||
data64: base64CommentData,
|
||||
});
|
||||
alert('Comment posted successfully!');
|
||||
commentInput.value = ''; // Clear input
|
||||
await displayComments(cardIdentifier); // Refresh comments
|
||||
} catch (error) {
|
||||
console.error('Error posting comment:', error);
|
||||
alert('Failed to post comment.');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCommentsForCard = async (cardIdentifier) => {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: 'SEARCH_QDN_RESOURCES',
|
||||
service: 'BLOG_POST',
|
||||
query: `comment-${cardIdentifier}`,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching comments for ${cardIdentifier}:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const displayComments = async (cardIdentifier) => {
|
||||
try {
|
||||
const comments = await fetchCommentsForCard(cardIdentifier);
|
||||
const commentsContainer = document.getElementById(`comments-container-${cardIdentifier}`);
|
||||
|
||||
// Clear previous comments
|
||||
commentsContainer.innerHTML = '';
|
||||
|
||||
// Fetch and display each comment
|
||||
for (const comment of comments) {
|
||||
const commentDataResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: comment.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: comment.identifier,
|
||||
});
|
||||
const timestamp = await timestampToHumanReadableDate(commentDataResponse.timestamp);
|
||||
const commentHTML = `
|
||||
<div class="comment" style="border: 1px solid gray; margin: 1vh 0; padding: 1vh; background: #1c1c1c;">
|
||||
<p><strong><u>${commentDataResponse.creator}</strong>:</p></u>
|
||||
<p>${commentDataResponse.content}</p>
|
||||
<p><i>${timestamp}</p></i>
|
||||
</div>
|
||||
`;
|
||||
commentsContainer.insertAdjacentHTML('beforeend', commentHTML);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error displaying comments for ${cardIdentifier}:`, error);
|
||||
alert("Failed to load comments. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const toggleComments = async (cardIdentifier) => {
|
||||
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
|
||||
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
|
||||
await displayComments(cardIdentifier);
|
||||
commentsSection.style.display = 'block';
|
||||
} else {
|
||||
commentsSection.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
async function createCardHTML(cardData, pollResults, cardIdentifier) {
|
||||
const { header, content, links, creator, timestamp, poll } = cardData;
|
||||
const formattedDate = new Date(timestamp).toLocaleString();
|
||||
const linksHTML = links.map((link, index) => `
|
||||
<button onclick="window.open('${link}', '_blank')">
|
||||
${`Link ${index + 1} - ${link}`}
|
||||
</button>
|
||||
`).join("");
|
||||
|
||||
const minterGroupMembers = await fetchMinterGroupMembers();
|
||||
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0 } =
|
||||
calculatePollResults(pollResults, minterGroupMembers) || {};
|
||||
|
||||
|
||||
return `
|
||||
<div class="minter-card">
|
||||
<div class="minter-card-header">
|
||||
<h3>${creator}</h3>
|
||||
<p>${header}</p>
|
||||
</div>
|
||||
<div class="support-header"><h5>Minter Post:</h5></div>
|
||||
<div class="info">
|
||||
${content}
|
||||
</div>
|
||||
<div class="support-header"><h5>Minter Links:</h5></div>
|
||||
<div class="info-links">
|
||||
${linksHTML}
|
||||
</div>
|
||||
<div class="results-header support-header"><h5>Current Results:</h5></div>
|
||||
<div class="minter-card-results">
|
||||
<div class="admin-results">
|
||||
<span class="admin-yes">Admin Yes: ${adminYes}</span>
|
||||
<span class="admin-no">Admin No: ${adminNo}</span>
|
||||
</div>
|
||||
<div class="minter-results">
|
||||
<span class="minter-yes">Minter Yes: ${minterYes}</span>
|
||||
<span class="minter-no">Minter No: ${minterNo}</span>
|
||||
</div>
|
||||
<div class="total-results">
|
||||
<span class="total-yes">Total Yes: ${totalYes}</span>
|
||||
<span class="total-no">Total No: ${totalNo}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="support-header"><h5>Support Minter?</h5></div>
|
||||
<div class="actions">
|
||||
<div class="actions-buttons">
|
||||
<button class="yes" onclick="voteOnPoll('${poll}', 'Yes')">YES</button>
|
||||
<button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENTS</button>
|
||||
<button class="no" onclick="voteOnPoll('${poll}', 'No')">NO</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="comments-section-${cardIdentifier}" class="comments-section" style="display: none; margin-top: 20px;">
|
||||
<div id="comments-container-${cardIdentifier}" class="comments-container"></div>
|
||||
<textarea id="new-comment-${cardIdentifier}" placeholder="Write a comment..." style="width: 100%; margin-top: 10px;"></textarea>
|
||||
<button onclick="postComment('${cardIdentifier}')">Post Comment</button>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
569
assets/js/Q-Mintership.js
Normal file
569
assets/js/Q-Mintership.js
Normal file
@@ -0,0 +1,569 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
// NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically.
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
//login if not already logged in.
|
||||
if (!userState.isLoggedIn) {
|
||||
await login();
|
||||
}
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// // Remove all sections except the menu
|
||||
// const allSections = document.querySelectorAll('body > section');
|
||||
// allSections.forEach(section => {
|
||||
// if (!section.classList.contains('menu')) {
|
||||
// section.remove();
|
||||
// }
|
||||
// });
|
||||
const bodyChildren = document.body.children;
|
||||
for (let i = bodyChildren.length - 1; i >= 0; i--) {
|
||||
const child = bodyChildren[i];
|
||||
if (!child.classList.contains('menu')) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar`;
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background cid-ttRnlSkg2R">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: center; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">
|
||||
<span>${userState.accountName || 'Guest'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${userState.isAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (userState.isAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
async function renderPaginationControls(room, totalMessages, limit) {
|
||||
const paginationContainer = document.getElementById("pagination-container");
|
||||
if (!paginationContainer) return;
|
||||
|
||||
paginationContainer.innerHTML = ""; // Clear existing buttons
|
||||
|
||||
const totalPages = Math.ceil(totalMessages / limit);
|
||||
|
||||
// Add "Previous" button
|
||||
if (currentPage > 0) {
|
||||
const prevButton = document.createElement("button");
|
||||
prevButton.innerText = "Previous";
|
||||
prevButton.addEventListener("click", () => {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(prevButton);
|
||||
}
|
||||
|
||||
// Add numbered page buttons
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageButton = document.createElement("button");
|
||||
pageButton.innerText = i + 1;
|
||||
pageButton.className = i === currentPage ? "active-page" : "";
|
||||
pageButton.addEventListener("click", () => {
|
||||
if (i !== currentPage) {
|
||||
currentPage = i;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(pageButton);
|
||||
}
|
||||
|
||||
// Add "Next" button
|
||||
if (currentPage < totalPages - 1) {
|
||||
const nextButton = document.createElement("button");
|
||||
nextButton.innerText = "Next";
|
||||
nextButton.addEventListener("click", () => {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage, false);
|
||||
}
|
||||
});
|
||||
paginationContainer.appendChild(nextButton);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<div id="pagination-container" class="pagination-container" style="margin-top: 20px; text-align: center;"></div>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const imageModalHTML = `
|
||||
<div id="image-modal" class="image-modal">
|
||||
<span id="close-modal" class="close">×</span>
|
||||
<img id="modal-image" class="modal-content">
|
||||
<div id="caption" class="caption"></div>
|
||||
<button id="download-button" class="download-button">Download</button>
|
||||
</div>
|
||||
`;
|
||||
forumContent.insertAdjacentHTML('beforeend', imageModalHTML);
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
await loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target.classList.contains("inline-image")) {
|
||||
const modal = document.getElementById("image-modal");
|
||||
const modalImage = document.getElementById("modal-image");
|
||||
const caption = document.getElementById("caption");
|
||||
// const downloadButton = document.getElementById("download-button");
|
||||
|
||||
// Set the modal content
|
||||
modalImage.src = event.target.src;
|
||||
caption.textContent = event.target.alt;
|
||||
|
||||
// Set download button link - This has been moved to the Message Rendering Section of the code.
|
||||
// downloadButton.onclick = () => {
|
||||
// fetchAndSaveAttachment(
|
||||
// "FILE",
|
||||
// event.target.dataset.name,
|
||||
// event.target.dataset.identifier,
|
||||
// event.target.dataset.filename,
|
||||
// event.target.dataset.mimeType
|
||||
// );
|
||||
// };
|
||||
|
||||
// Show the modal
|
||||
modal.style.display = "block";
|
||||
}
|
||||
});
|
||||
|
||||
// Close the modal
|
||||
document.getElementById("close-modal").addEventListener("click", () => {
|
||||
document.getElementById("image-modal").style.display = "none";
|
||||
});
|
||||
|
||||
// Hide the modal when clicking outside of the image or close button
|
||||
window.addEventListener("click", (event) => {
|
||||
const modal = document.getElementById("image-modal");
|
||||
if (event.target == modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${room}-${randomID}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
file: file,
|
||||
});
|
||||
attachmentIdentifiers.push({
|
||||
name: userState.accountName,
|
||||
service: "FILE",
|
||||
identifier: attachmentID,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`);
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName,
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message, including any potential attached files and replies.
|
||||
quill.root.innerHTML = "";
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
replyToMessageIdentifier = null;
|
||||
const replyContainer = document.querySelector(".reply-container");
|
||||
if (replyContainer) {
|
||||
replyContainer.remove()
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
const notification = document.createElement('div');
|
||||
notification.innerText = "Message published successfully! Message will take a confirmation to show.";
|
||||
notification.style.color = "green";
|
||||
notification.style.marginTop = "10px";
|
||||
document.querySelector(".message-input-section").appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Add event listener for the load more button
|
||||
const loadMoreContainer = document.getElementById("load-more-container");
|
||||
if (loadMoreContainer) {
|
||||
loadMoreContainer.innerHTML = '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>';
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
const limit = 10;
|
||||
const offset = page * limit;
|
||||
console.log(`Loading messages for room: ${room}, page: ${page}, offset: ${offset}, limit: ${limit}`);
|
||||
|
||||
// Get the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
if (!messagesContainer) return;
|
||||
|
||||
// If not polling, clear the message container and the existing identifiers for a fresh load
|
||||
if (!isPolling) {
|
||||
messagesContainer.innerHTML = ""; // Clear the messages container before loading new page
|
||||
existingIdentifiers.clear(); // Clear the existing identifiers set for fresh page load
|
||||
}
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch messages for the current room and page
|
||||
const response = await searchAllWithOffset(`${messageIdentifierPrefix}-${room}`, limit, offset);
|
||||
console.log(`Fetched messages count: ${response.length} for page: ${page}`);
|
||||
|
||||
if (response.length === 0) {
|
||||
// If no messages are fetched and it's not polling, display "no messages" for the initial load
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Define `mostRecentMessage` to track the latest message during this fetch
|
||||
let mostRecentMessage = latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null;
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
if (existingIdentifiers.has(resource.identifier)) {
|
||||
return null; // Skip messages that are already displayed
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return {
|
||||
name: resource.name,
|
||||
content: messageObject.messageHtml,
|
||||
date: formattedTimestamp,
|
||||
identifier: resource.identifier,
|
||||
replyTo: messageObject.replyTo,
|
||||
timestamp,
|
||||
attachments: messageObject.attachments || [] // Include attachments if they exist
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
for (const message of fetchMessages) {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// OTHER METHOD NOT BEING USED HERE. WE CAN LOAD THE IMAGE DIRECTLY SINCE IT WILL BE PUBLISHED UNENCRYPTED/UNENCODED.
|
||||
// const imageHtml = await loadImageHtml(attachment.service, attachment.name, attachment.identifier, attachment.filename, attachment.mimeType);
|
||||
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
|
||||
|
||||
// Add the image HTML with the direct URL
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
|
||||
</div>`;
|
||||
// Add the modal download button details as well, in order to pass correct information to the modal
|
||||
const downloadButton = document.getElementById("download-button");
|
||||
downloadButton.onclick = () => {
|
||||
fetchAndSaveAttachment(
|
||||
attachment.service,
|
||||
attachment.name,
|
||||
attachment.identifier,
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
);
|
||||
};
|
||||
// FOR OTHER METHOD NO LONGER USED
|
||||
// attachmentHtml += imageHtml;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch attachment ${attachment.filename}:`, error);
|
||||
}
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
<div class="message-header" style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img src="${avatarUrl}" alt="Avatar" class="user-avatar" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 10px;">
|
||||
<span class="username">${message.name}</span>
|
||||
</div>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
</div>
|
||||
${replyHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<div class="attachments-gallery">
|
||||
${attachmentHtml}
|
||||
</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
|
||||
// Update mostRecentMessage if this message is newer
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp || 0)) {
|
||||
mostRecentMessage = {
|
||||
latestIdentifier: message.identifier,
|
||||
latestTimestamp: message.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
// Add the identifier to the existingIdentifiers set
|
||||
existingIdentifiers.add(message.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = mostRecentMessage;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
}
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Render pagination controls
|
||||
const totalMessages = await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`);
|
||||
renderPaginationControls(room, totalMessages, limit);
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
712
assets/js/QortalApi.js
Normal file
712
assets/js/QortalApi.js
Normal file
@@ -0,0 +1,712 @@
|
||||
// Set the forumAdminGroups variable
|
||||
let adminGroups = ["Q-Mintership-admin", "dev-group", "Mintership-Forum-Admins"];
|
||||
|
||||
// Settings to allow non-devmode development with 'live-server' module
|
||||
let baseUrl = '';
|
||||
let isOutsideOfUiDevelopment = false;
|
||||
|
||||
if (typeof qortalRequest === 'function') {
|
||||
console.log('qortalRequest is available as a function. Setting development mode to false and baseUrl to nothing.');
|
||||
isOutsideOfUiDevelopment = false;
|
||||
baseUrl = '';
|
||||
} else {
|
||||
console.log('qortalRequest is not available as a function. Setting baseUrl to localhost.');
|
||||
isOutsideOfUiDevelopment = true;
|
||||
baseUrl = "http://localhost:12391";
|
||||
};
|
||||
|
||||
// USEFUL UTILITIES ----- ----- -----
|
||||
// Generate a short random ID to utilize at the end of unique identifiers.
|
||||
const uid = async () => {
|
||||
console.log('uid function called');
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
};
|
||||
console.log('Generated uid:', result);
|
||||
return result;
|
||||
};
|
||||
// Turn a unix timestamp into a human-readable date
|
||||
const timestampToHumanReadableDate = async(timestamp) => {
|
||||
console.log('timestampToHumanReadableDate called');
|
||||
const date = new Date(timestamp);
|
||||
const day = date.getDate();
|
||||
const month = date.getMonth() + 1;
|
||||
const year = date.getFullYear() - 2000;
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const seconds = date.getSeconds();
|
||||
|
||||
const formattedDate = `${month}.${day}.${year}@${hours}:${minutes}:${seconds}`;
|
||||
console.log('Formatted date:', formattedDate);
|
||||
return formattedDate;
|
||||
};
|
||||
// Base64 encode a string
|
||||
const base64EncodeString = async (str) => {
|
||||
console.log('base64EncodeString called');
|
||||
const encodedString = btoa(String.fromCharCode.apply(null, new Uint8Array(new TextEncoder().encode(str).buffer)));
|
||||
console.log('Encoded string:', encodedString);
|
||||
return encodedString;
|
||||
};
|
||||
|
||||
const objectToBase64 = async (obj) => {
|
||||
// Step 1: Convert the object to a JSON string
|
||||
const jsonString = JSON.stringify(obj);
|
||||
// Step 2: Create a Blob from the JSON string
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
// Step 3: Create a FileReader to read the Blob as a base64-encoded string
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
// Remove 'data:application/json;base64,' prefix
|
||||
const base64 = reader.result.replace('data:application/json;base64,', '');
|
||||
console.log(`base64 resolution: ${base64}`);
|
||||
resolve(base64);
|
||||
} else {
|
||||
reject(new Error('Failed to read the Blob as a base64-encoded string'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject(reader.error);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
// User state util
|
||||
const userState = {
|
||||
isLoggedIn: false,
|
||||
accountName: null,
|
||||
accountAddress: null,
|
||||
isAdmin: false,
|
||||
isMinterAdmin: false,
|
||||
isForumAdmin: false
|
||||
};
|
||||
|
||||
// USER-RELATED QORTAL CALLS ------------------------------------------
|
||||
// Obtain the address of the authenticated user checking userState.accountAddress first.
|
||||
const getUserAddress = async () => {
|
||||
console.log('getUserAddress called');
|
||||
try {
|
||||
if (userState.accountAddress) {
|
||||
console.log('User address found in state:', userState.accountAddress);
|
||||
return userState.accountAddress;
|
||||
};
|
||||
const userAccount = await qortalRequest({ action: "GET_USER_ACCOUNT" });
|
||||
if (userAccount) {
|
||||
console.log('Account address:', userAccount.address);
|
||||
userState.accountAddress = userAccount.address;
|
||||
console.log('Account address added to state:', userState.accountAddress);
|
||||
return userState.accountAddress;
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching account address:', error);
|
||||
throw error;
|
||||
};
|
||||
};
|
||||
|
||||
const fetchOwnerAddressFromName = async (name) => {
|
||||
console.log('fetchOwnerAddressFromName called');
|
||||
console.log('name:', name);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/names/${name}`, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
method: 'GET',
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Fetched owner address:', data.owner);
|
||||
return data.owner;
|
||||
} catch (error) {
|
||||
console.error('Error fetching owner address:', error);
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
const verifyUserIsAdmin = async () => {
|
||||
console.log('verifyUserIsAdmin called (QortalApi.js)');
|
||||
try {
|
||||
const accountAddress = userState.accountAddress || await getUserAddress();
|
||||
userState.accountAddress = accountAddress;
|
||||
const userGroups = await getUserGroups(accountAddress);
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isAdmin = await userGroups.some(group => adminGroups.includes(group.groupName))
|
||||
const isMinterAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin)
|
||||
if (isMinterAdmin) {
|
||||
userState.isMinterAdmin = true
|
||||
}
|
||||
if (isAdmin) {
|
||||
userState.isAdmin = true;
|
||||
userState.isForumAdmin = true
|
||||
}
|
||||
return userState.isAdmin;
|
||||
} catch (error) {
|
||||
console.error('Error verifying user admin status:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const verifyAddressIsAdmin = async (address) => {
|
||||
console.log('verifyAddressIsAdmin called');
|
||||
console.log('address:', address);
|
||||
try {
|
||||
if (!address) {
|
||||
console.log('No address provided');
|
||||
return false;
|
||||
};
|
||||
const userGroups = await getUserGroups(address);
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isAdmin = await userGroups.some(group => adminGroups.includes(group.groupName))
|
||||
const isMinterAdmin = minterGroupAdmins.members.some(admin => admin.member === address && admin.isAdmin)
|
||||
if ((isMinterAdmin) || (isAdmin)) {
|
||||
return true;
|
||||
} else {
|
||||
return false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error verifying address admin status:', error);
|
||||
throw error
|
||||
}
|
||||
};
|
||||
|
||||
const getNameInfo = async (name) => {
|
||||
console.log('getNameInfo called');
|
||||
console.log('name:', name);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/names/${name}`);
|
||||
const data = await response.json();
|
||||
console.log('Fetched name info:', data);
|
||||
return {
|
||||
name: data.name,
|
||||
reducedName: data.reducedName,
|
||||
owner: data.owner,
|
||||
data: data.data,
|
||||
registered: data.registered,
|
||||
updated: data.updated,
|
||||
isForSale: data.isForSale,
|
||||
salePrice: data.salePrice
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('Error fetching name info:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getPublicKeyByName = async (name) => {
|
||||
console.log('getPublicKeyByName called');
|
||||
console.log('name:', name);
|
||||
try {
|
||||
const nameInfo = await getNameInfo(name);
|
||||
const address = nameInfo.owner;
|
||||
const publicKey = await getPublicKeyFromAddress(address);
|
||||
console.log(`Found public key: for name: ${name}`, publicKey);
|
||||
return publicKey;
|
||||
} catch (error) {
|
||||
console.log('Error obtaining public key from name:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getPublicKeyFromAddress = async (address) => {
|
||||
console.log('getPublicKeyFromAddress called');
|
||||
console.log('address:', address);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/addresses/${address}`,{
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
const publicKey = data.publicKey;
|
||||
console.log('Fetched public key:', publicKey);
|
||||
return publicKey;
|
||||
} catch (error) {
|
||||
console.log('Error fetching public key from address:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getAddressFromPublicKey = async (publicKey) => {
|
||||
console.log('getAddressFromPublicKey called');
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/addresses/convert/${publicKey}`,{
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'text/plain' }
|
||||
});
|
||||
const data = await response.text();
|
||||
const address = data;
|
||||
console.log('Converted Address:', address);
|
||||
return address;
|
||||
} catch (error) {
|
||||
console.log('Error converting public key to address:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const login = async () => {
|
||||
console.log('login called');
|
||||
try {
|
||||
if (userState.accountName && (userState.isAdmin || userState.isLoggedIn) && userState.accountAddress) {
|
||||
console.log(`Account name found in userState: '${userState.accountName}', no need to call API...skipping API call.`);
|
||||
return userState.accountName;
|
||||
}
|
||||
|
||||
const accountAddress = userState.accountAddress || await getUserAddress();
|
||||
const accountNames = await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address: accountAddress,
|
||||
});
|
||||
|
||||
if (accountNames) {
|
||||
userState.isLoggedIn = true;
|
||||
userState.accountName = accountNames[0].name;
|
||||
userState.accountAddress = accountAddress;
|
||||
console.log('All account names:', accountNames);
|
||||
console.log('Main name (in state):', userState.accountName);
|
||||
console.log('User has been logged in successfully!');
|
||||
return userState.accountName;
|
||||
} else {
|
||||
throw new Error("No account names found. Are you logged in? Do you have a registered name?");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching account names:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getNamesFromAddress = async (address) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/names/address/${address}?limit=20`, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const names = await response.json();
|
||||
return names.length > 0 ? names[0].name : address; // Return name if found, else return address
|
||||
} catch (error) {
|
||||
console.error(`Error fetching names for address ${address}:`, error);
|
||||
return address;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// QORTAL GROUP-RELATED CALLS ------------------------------------------
|
||||
const getUserGroups = async (userAddress) => {
|
||||
console.log('getUserGroups called');
|
||||
console.log('userAddress:', userAddress);
|
||||
try {
|
||||
if (!userAddress && userState.accountAddress) {
|
||||
console.log('No address passed to getUserGroups call... using address from state...');
|
||||
userAddress = userState.accountAddress;
|
||||
}
|
||||
const response = await fetch(`${baseUrl}/groups/member/${userAddress}`, {
|
||||
method: 'GET',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Fetched user groups:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user groups:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMinterGroupAdmins = async () => {
|
||||
console.log('calling for minter admins')
|
||||
const response = await fetch(`${baseUrl}/groups/members/694?onlyAdmins=true&limit=0&reverse=true`,{
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const admins = await response.json();
|
||||
console.log('Fetched minter admins', admins);
|
||||
return admins;
|
||||
}
|
||||
|
||||
const fetchMinterGroupMembers = async () => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/groups/members/694?limit=0`, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Ensure the structure of the response is as expected
|
||||
if (!Array.isArray(data.members)) {
|
||||
throw new Error("Expected 'members' to be an array but got a different structure");
|
||||
}
|
||||
|
||||
return data.members; // Assuming 'members' is the key in the response JSON
|
||||
} catch (error) {
|
||||
console.error("Error fetching minter group members:", error);
|
||||
return []; // Return an empty array to prevent further errors
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const fetchAllGroups = async (limit) => {
|
||||
console.log('fetchAllGroups called');
|
||||
console.log('limit:', limit);
|
||||
if (!limit) {
|
||||
limit = 2000;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/groups?limit=${limit}&reverse=true`);
|
||||
const data = await response.json();
|
||||
console.log('Fetched all groups:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching all groups:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// QDN data calls
|
||||
const searchLatestDataByIdentifier = async (identifier) => {
|
||||
console.log('fetchAllDataByIdentifier called');
|
||||
console.log('identifier:', identifier);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/arbitrary/resources/search?service=DOCUMENT&identifier=${identifier}&includestatus=true&mode=ALL&limit=0&reverse=true`, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const latestData = await response.json();
|
||||
console.log('Fetched latest data by identifier:', latestData);
|
||||
return latestData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching latest published data:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const publishMultipleResources = async (resources, publicKeys = null, isPrivate = false) => {
|
||||
console.log('publishMultipleResources called');
|
||||
console.log('resources:', resources);
|
||||
|
||||
const request = {
|
||||
action: 'PUBLISH_MULTIPLE_QDN_RESOURCES',
|
||||
resources: resources,
|
||||
};
|
||||
|
||||
if (isPrivate && publicKeys) {
|
||||
request.encrypt = true;
|
||||
request.publicKeys = publicKeys;
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await qortalRequest(request);
|
||||
console.log('Multiple resources published successfully:', response);
|
||||
} catch (error) {
|
||||
console.error('Error publishing multiple resources:', error);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const searchResourcesWithMetadata = async (query, limit) => {
|
||||
console.log('searchResourcesWithMetadata called');
|
||||
try {
|
||||
if (limit == 0) {
|
||||
limit = 0;
|
||||
} else if (!limit || (limit < 10 && limit != 0)) {
|
||||
limit = 200;
|
||||
}
|
||||
const response = await fetch(`${baseUrl}/arbitrary/resources/search?query=${query}&mode=ALL&includestatus=true&includemetadata=true&limit=${limit}&reverse=true`, {
|
||||
method: 'GET',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Search results with metadata:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error searching for resources with metadata:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const searchAllResources = async (query, limit, after, reverse=false) => {
|
||||
console.log('searchAllResources called. Query:', query, 'Limit:', limit,'Reverse:', reverse);
|
||||
try {
|
||||
if (limit == 0) {
|
||||
limit = 0;
|
||||
}
|
||||
if (!limit || (limit < 10 && limit != 0)) {
|
||||
limit = 200;
|
||||
}
|
||||
if (after == null || after === undefined) {
|
||||
after = 0;
|
||||
}
|
||||
const response = await fetch(`${baseUrl}/arbitrary/resources/search?query=${query}&mode=ALL&after=${after}&includestatus=false&includemetadata=false&limit=${limit}&reverse=${reverse}`, {
|
||||
method: 'GET',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Search results with metadata:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error searching for resources with metadata:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const searchAllWithOffset = async (query, limit, offset) =>{
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: query,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
mode: "ALL",
|
||||
reverse: false
|
||||
});
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error("Error during SEARCH_QDN_RESOURCES:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const searchAllCountOnly = async (query) => {
|
||||
try {
|
||||
let offset = 0;
|
||||
const limit = 100; // Chunk size for fetching
|
||||
let totalCount = 0;
|
||||
let hasMore = true;
|
||||
|
||||
// Fetch in chunks to accumulate the count
|
||||
while (hasMore) {
|
||||
const response = await qortalRequest({
|
||||
action: "SEARCH_QDN_RESOURCES",
|
||||
service: "BLOG_POST",
|
||||
query: query,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
mode: "ALL",
|
||||
reverse: false
|
||||
});
|
||||
|
||||
if (response && response.length > 0) {
|
||||
totalCount += response.length;
|
||||
offset += limit;
|
||||
console.log(`Fetched ${response.length} items, total count: ${totalCount}, current offset: ${offset}`);
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
return totalCount;
|
||||
} catch (error) {
|
||||
console.error("Error during SEARCH_QDN_RESOURCES:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const searchResourcesWithStatus = async (query, limit, status = 'local') => {
|
||||
console.log('searchResourcesWithStatus called');
|
||||
console.log('query:', query);
|
||||
console.log('limit:', limit);
|
||||
console.log('status:', status);
|
||||
try {
|
||||
// Set default limit if not provided or too low
|
||||
if (!limit || limit < 10) {
|
||||
limit = 200;
|
||||
}
|
||||
// Make API request
|
||||
const response = await fetch(`${baseUrl}/arbitrary/resources/search?query=${query}&includestatus=true&limit=${limit}&reverse=true`, {
|
||||
method: 'GET',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
// Filter based on status if provided
|
||||
if (status) {
|
||||
if (status === 'notLocal') {
|
||||
const notDownloaded = data.filter((resource) => resource.status.status === 'published');
|
||||
console.log('notDownloaded:', notDownloaded);
|
||||
return notDownloaded;
|
||||
} else if (status === 'local') {
|
||||
const downloaded = data.filter((resource) =>
|
||||
resource.status.status === 'ready' ||
|
||||
resource.status.status === 'downloaded' ||
|
||||
resource.status.status === 'building' ||
|
||||
(resource.status.status && resource.status.status !== 'published')
|
||||
);
|
||||
return downloaded;
|
||||
}
|
||||
}
|
||||
// Return all data if no specific status is provided
|
||||
console.log('Returning all data...');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error searching for resources with metadata:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getResourceMetadata = async (service, name, identifier) => {
|
||||
console.log('getResourceMetadata called');
|
||||
console.log('service:', service);
|
||||
console.log('name:', name);
|
||||
console.log('identifier:', identifier);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/arbitrary/metadata/${service}/${name}/${identifier}`, {
|
||||
method: 'GET',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Fetched resource metadata:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching resource metadata:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFileBase64 = async (service, name, identifier) => {
|
||||
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}/?encoding=base64`;
|
||||
try {
|
||||
const response = await fetch(url,{
|
||||
method: 'GET',
|
||||
headers: { 'accept': 'text/plain' }
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error fetching the image file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
async function loadImageHtml(service, name, identifier, filename, mimeType) {
|
||||
try {
|
||||
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
|
||||
// Fetch the file as a blob
|
||||
const response = await fetch(url);
|
||||
// Convert the response to a Blob
|
||||
const fileBlob = new Blob([response], { type: mimeType });
|
||||
// Create an Object URL from the Blob
|
||||
const objectUrl = URL.createObjectURL(fileBlob);
|
||||
// Use the Object URL as the image source
|
||||
const attachmentHtml = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`;
|
||||
|
||||
return attachmentHtml;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching the image:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeType) => {
|
||||
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const blob = await response.blob();
|
||||
await qortalRequest({
|
||||
action: "SAVE_FILE",
|
||||
blob,
|
||||
filename: filename,
|
||||
mimeType
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching or saving the attachment:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const renderData = async (service, name, identifier) => {
|
||||
console.log('renderData called');
|
||||
console.log('service:', service);
|
||||
console.log('name:', name);
|
||||
console.log('identifier:', identifier);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/render/${service}/${name}?identifier=${identifier}`, {
|
||||
method: 'GET',
|
||||
headers: { 'accept': '*/*' }
|
||||
});
|
||||
// If the response is not OK (status 200-299), throw an error
|
||||
if (!response.ok) {
|
||||
throw new Error('Error rendering data');
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
// Check if the response includes <!DOCTYPE> indicating it's an HTML document
|
||||
if (responseText.includes('<!DOCTYPE')) {
|
||||
throw new Error('Received HTML response');
|
||||
}
|
||||
|
||||
const data = JSON.parse(responseText);
|
||||
console.log('Rendered data:', data);
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error rendering data:', error);
|
||||
// Return the custom message when there’s an error or invalid data
|
||||
return 'Requested data is either missing or still being obtained from QDN... please try again in a short time.';
|
||||
}
|
||||
};
|
||||
|
||||
const getProductDetails = async (service, name, identifier) => {
|
||||
console.log('getProductDetails called');
|
||||
console.log('service:', service);
|
||||
console.log('name:', name);
|
||||
console.log('identifier:', identifier);
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/arbitrary/metadata/${service}/${name}/${identifier}`, {
|
||||
method: 'GET',
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log('Fetched product details:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching product details:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Qortal poll-related calls ----------------------------------------------------------------------
|
||||
|
||||
const fetchPollResults = async (pollName) => {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/polls/votes/${pollName}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const pollData = await response.json();
|
||||
return pollData;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching poll results for ${pollName}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// export {
|
||||
// userState,
|
||||
// adminGroups,
|
||||
// searchResourcesWithMetadata,
|
||||
// searchResourcesWithStatus,
|
||||
// getResourceMetadata,
|
||||
// renderData,
|
||||
// getProductDetails,
|
||||
// getUserGroups,
|
||||
// getUserAddress,
|
||||
// login,
|
||||
// timestampToHumanReadableDate,
|
||||
// base64EncodeString,
|
||||
// verifyUserIsAdmin,
|
||||
// fetchAllDataByIdentifier,
|
||||
// fetchOwnerAddressFromName,
|
||||
// verifyAddressIsAdmin,
|
||||
// uid,
|
||||
// fetchAllGroups,
|
||||
// getNameInfo,
|
||||
// publishMultipleResources,
|
||||
// getPublicKeyByName,
|
||||
// objectToBase64,
|
||||
// fetchMinterGroupAdmins
|
||||
// };
|
195
assets/js/css-scratch.css
Normal file
195
assets/js/css-scratch.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;
|
||||
}
|
||||
|
||||
|
||||
|
305
assets/js/scratch-2.js
Normal file
305
assets/js/scratch-2.js
Normal file
@@ -0,0 +1,305 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
|
||||
// Load the latest message identifiers from local storage
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "") {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
|
||||
// Create message object with unique identifier and HTML content
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: false,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null; // Clear reply reference after sending
|
||||
// Clear reply reference after sending if it exists.
|
||||
if (replyToMessageIdentifier) {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
}
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
// Reload messages
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
const existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch only messages that are not already present in the messages container
|
||||
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
|
||||
|
||||
if (messagesContainer) {
|
||||
if (!response || !response.length) {
|
||||
if (page === 0) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all messages
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render messages without duplication
|
||||
fetchMessages.forEach((message) => {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
392
assets/js/scratch.js
Normal file
392
assets/js/scratch.js
Normal file
@@ -0,0 +1,392 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
|
||||
|
||||
let replyToMessageIdentifier = null;
|
||||
let latestMessageIdentifiers = {}; // To keep track of the latest message in each room
|
||||
let currentPage = 0; // Track current pagination page
|
||||
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
|
||||
|
||||
// If there is a previous latest message identifiers, use them. Otherwise, use an empty.
|
||||
if (localStorage.getItem("latestMessageIdentifiers")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Identify the link for 'Mintership Forum'
|
||||
const mintershipForumLinks = document.querySelectorAll('a[href="MINTERSHIP-FORUM"]');
|
||||
|
||||
mintershipForumLinks.forEach(link => {
|
||||
link.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
await login(); // Assuming login is an async function
|
||||
await loadForumPage();
|
||||
loadRoomContent("general"); // Automatically load General Room on forum load
|
||||
startPollingForNewMessages(); // Start polling for new messages after loading the forum page
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadForumPage() {
|
||||
// Remove all sections except the menu
|
||||
const allSections = document.querySelectorAll('body > section');
|
||||
allSections.forEach(section => {
|
||||
if (!section.classList.contains('menu')) {
|
||||
section.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if user is an admin
|
||||
const minterGroupAdmins = await fetchMinterGroupAdmins();
|
||||
const isUserAdmin = minterGroupAdmins.members.some(admin => admin.member === userState.accountAddress && admin.isAdmin) || await verifyUserIsAdmin();
|
||||
|
||||
// Create the forum layout, including a header, sub-menu, and keeping the original background imagestyle="background-image: url('/assets/images/background.jpg');">
|
||||
const mainContent = document.createElement('div');
|
||||
mainContent.innerHTML = `
|
||||
<div class="forum-main mbr-parallax-background" style="background-image: url('/assets/images/background.jpg'); background-size: cover; background-position: center; min-height: 100vh; width: 100vw;">
|
||||
<div class="forum-header" style="color: lightblue; display: flex; justify-content: space-between; align-items: center; padding: 10px;">
|
||||
<div class="user-info" style="border: 1px solid lightblue; padding: 5px; color: lightblue;">User: ${userState.accountName || 'Guest'}</div>
|
||||
</div>
|
||||
<div class="forum-submenu">
|
||||
<div class="forum-rooms">
|
||||
<button class="room-button" id="minters-room">Minters Room</button>
|
||||
${isUserAdmin ? '<button class="room-button" id="admins-room">Admins Room</button>' : ''}
|
||||
<button class="room-button" id="general-room">General Room</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum-content" class="forum-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(mainContent);
|
||||
|
||||
// Add event listeners to room buttons
|
||||
document.getElementById("minters-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("minters");
|
||||
});
|
||||
if (isUserAdmin) {
|
||||
document.getElementById("admins-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("admins");
|
||||
});
|
||||
}
|
||||
document.getElementById("general-room").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadRoomContent("general");
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoomContent(room) {
|
||||
const forumContent = document.getElementById("forum-content");
|
||||
if (forumContent) {
|
||||
forumContent.innerHTML = `
|
||||
<div class="room-content">
|
||||
<h3 class="room-title" style="color: lightblue;">${room.charAt(0).toUpperCase() + room.slice(1)} Room</h3>
|
||||
<div id="messages-container" class="messages-container"></div>
|
||||
${(existingIdentifiers.size > 10)? '<button id="load-more-button" class="load-more-button" style="margin-top: 10px;">Load More</button>' : ''}
|
||||
<div class="message-input-section">
|
||||
<div id="toolbar" class="message-toolbar"></div>
|
||||
<div id="editor" class="message-input"></div>
|
||||
<div class="attachment-section">
|
||||
<input type="file" id="file-input" class="file-input" multiple>
|
||||
<button id="attach-button" class="attach-button">Attach Files</button>
|
||||
</div>
|
||||
<button id="send-button" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Quill editor for rich text input
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }], // Add font family options
|
||||
[{ 'size': ['small', false, 'large', 'huge'] }], // Add font size options
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'], // Text formatting options
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
['link', 'blockquote', 'code-block'],
|
||||
[{ 'color': [] }, { 'background': [] }], // Text color and background color options
|
||||
[{ 'align': [] }], // Text alignment
|
||||
['clean'] // Remove formatting button
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Load messages from QDN for the selected room
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
|
||||
let selectedFiles = [];
|
||||
|
||||
// Add event listener to handle file selection
|
||||
document.getElementById('file-input').addEventListener('change', (event) => {
|
||||
selectedFiles = Array.from(event.target.files);
|
||||
});
|
||||
|
||||
// Add event listener for the send button
|
||||
document.getElementById("send-button").addEventListener("click", async () => {
|
||||
const messageHtml = quill.root.innerHTML.trim();
|
||||
if (messageHtml !== "" || selectedFiles.length > 0) {
|
||||
const randomID = await uid();
|
||||
const messageIdentifier = `${messageIdentifierPrefix}-${room}-${randomID}`;
|
||||
let attachmentIdentifiers = [];
|
||||
|
||||
// Handle attachments
|
||||
for (const file of selectedFiles) {
|
||||
const attachmentID = `${messageAttachmentIdentifierPrefix}-${randomID}-${file.name}`;
|
||||
try {
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "IMAGE", // Adjust based on file type
|
||||
identifier: attachmentID,
|
||||
file: file
|
||||
});
|
||||
attachmentIdentifiers.push(attachmentID);
|
||||
console.log(`Attachment ${file.name} published successfully with ID: ${attachmentID}`);
|
||||
} catch (error) {
|
||||
console.error(`Error publishing attachment ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create message object with unique identifier, HTML content, and attachments
|
||||
const messageObject = {
|
||||
messageHtml: messageHtml,
|
||||
hasAttachment: attachmentIdentifiers.length > 0,
|
||||
attachments: attachmentIdentifiers,
|
||||
replyTo: replyToMessageIdentifier
|
||||
};
|
||||
if (!messageObject.attachments) {
|
||||
messageObject.attachments = [];
|
||||
}
|
||||
messageObject.attachments.push({
|
||||
identifier: attachmentIdentifier,
|
||||
filename: file.name,
|
||||
mimeType: file.type
|
||||
});
|
||||
|
||||
try {
|
||||
// Convert message object to base64
|
||||
let base64Message = await objectToBase64(messageObject);
|
||||
if (!base64Message) {
|
||||
console.log(`initial object creation with object failed, using btoa...`)
|
||||
base64Message = btoa(JSON.stringify(messageObject));
|
||||
}
|
||||
|
||||
console.log("Message Object:", messageObject);
|
||||
console.log("Base64 Encoded Message:", base64Message);
|
||||
|
||||
// Publish message to QDN
|
||||
await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: userState.accountName, // Publisher must own the registered name
|
||||
service: "BLOG_POST",
|
||||
identifier: messageIdentifier,
|
||||
data64: base64Message
|
||||
});
|
||||
console.log("Message published successfully");
|
||||
|
||||
// Clear the editor after sending the message
|
||||
quill.root.innerHTML = "";
|
||||
replyToMessageIdentifier = null;
|
||||
document.getElementById('file-input').value = "";
|
||||
selectedFiles = [];
|
||||
|
||||
// Clear reply reference after sending if it exists.
|
||||
if (replyToMessageIdentifier) {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
}
|
||||
|
||||
// Update the latest message identifier
|
||||
latestMessageIdentifiers[room] = messageIdentifier;
|
||||
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
|
||||
|
||||
// Reload messages - - - - - - - - - - - - - - - - - - CHANGE THIS TO DISPLAY THE LAST MESSAGE IN THE CONTAINER INSTEAD OF RELOADING ALL MESSAGES, AND DISPLAY NOTIFICATION OF SUCCESSFUL PUBLISH.
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error publishing message:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener for the load more button
|
||||
document.getElementById("load-more-button").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadMessagesFromQDN(room, currentPage);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load messages for any given room with pagination
|
||||
async function loadMessagesFromQDN(room, page, isPolling = false) {
|
||||
try {
|
||||
const offset = page * 10;
|
||||
const limit = 10;
|
||||
|
||||
// Get the set of existing identifiers from the messages container
|
||||
const messagesContainer = document.querySelector("#messages-container");
|
||||
existingIdentifiers = new Set(Array.from(messagesContainer.querySelectorAll('.message-item')).map(item => item.dataset.identifier));
|
||||
|
||||
// Fetch only messages that are not already present in the messages container
|
||||
const response = await searchAllWithoutDuplicates(`${messageIdentifierPrefix}-${room}`, limit, offset, existingIdentifiers);
|
||||
|
||||
if (messagesContainer) {
|
||||
// If there are no messages and we're not polling, display "no messages" message
|
||||
if (!response || !response.length) {
|
||||
if (page === 0 && !isPolling) {
|
||||
messagesContainer.innerHTML = `<p>No messages found. Be the first to post!</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all messages that haven't been fetched before
|
||||
const fetchMessages = await Promise.all(response.map(async (resource) => {
|
||||
try {
|
||||
console.log(`Fetching message with identifier: ${resource.identifier}`);
|
||||
const messageResponse = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: resource.name,
|
||||
service: "BLOG_POST",
|
||||
identifier: resource.identifier,
|
||||
});
|
||||
|
||||
console.log("Fetched message response:", messageResponse);
|
||||
|
||||
// No need to decode, as qortalRequest returns the decoded data if no 'encoding: base64' is set.
|
||||
const messageObject = messageResponse;
|
||||
const timestamp = resource.updated || resource.created;
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
return { name: resource.name, content: messageObject.messageHtml, date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject.replyTo };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message with identifier ${resource.identifier}. Error: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
// Render new messages without duplication
|
||||
fetchMessages.forEach((message) => {
|
||||
if (message && !existingIdentifiers.has(message.identifier)) {
|
||||
let replyHtml = "";
|
||||
if (message.replyTo) {
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
|
||||
if (repliedMessage) {
|
||||
replyHtml = `
|
||||
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;">
|
||||
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div>
|
||||
<div class="reply-content">${repliedMessage.content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
let mostRecentMessage = null;
|
||||
const isNewMessage = !latestMessageIdentifiers[room] || new Date(message.date) > new Date(latestMessageIdentifiers[room]);
|
||||
|
||||
let attachmentHtml = "";
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
if (attachment.mimeType.startsWith('image/')) {
|
||||
// Display images inline
|
||||
const url = `/arbitrary/${attachment.service}/${message.name}/${attachment.identifier}`;
|
||||
attachmentHtml += `<div class="attachment"><img src="${url}" alt="${attachment.filename}" class="inline-image"></div>`;
|
||||
} else {
|
||||
// Display a button to download other attachments
|
||||
attachmentHtml += `<div class="attachment">
|
||||
<button onclick="fetchAttachment('${attachment.service}', '${message.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">Download ${attachment.filename}</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHTML = `
|
||||
<div class="message-item" data-identifier="${message.identifier}">
|
||||
${replyHtml}
|
||||
<div class="message-header">
|
||||
<span class="username">${message.name}</span>
|
||||
<span class="timestamp">${message.date}</span>
|
||||
${isNewMessage ? '<span class="new-tag" style="color: red; font-weight: bold; margin-left: 10px;">NEW</span>' : ''}
|
||||
</div>
|
||||
${attachmentHtml}
|
||||
<div class="message-text">${message.content}</div>
|
||||
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Append new message to the end of the container
|
||||
messagesContainer.insertAdjacentHTML('beforeend', messageHTML);
|
||||
// Track the most recent message
|
||||
if (!mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage.timestamp)) {
|
||||
mostRecentMessage = message;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Update latestMessageIdentifiers for the room
|
||||
if (mostRecentMessage) {
|
||||
latestMessageIdentifiers[room] = {
|
||||
latestIdentifier: mostRecentMessage.identifier,
|
||||
latestTimestamp: mostRecentMessage.timestamp
|
||||
};
|
||||
}
|
||||
// Add event listeners to the reply buttons
|
||||
const replyButtons = document.querySelectorAll(".reply-button");
|
||||
|
||||
replyButtons.forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
replyToMessageIdentifier = button.dataset.messageIdentifier;
|
||||
// Find the message being replied to
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
|
||||
|
||||
if (repliedMessage) {
|
||||
const replyContainer = document.createElement("div");
|
||||
replyContainer.className = "reply-container";
|
||||
replyContainer.innerHTML = `
|
||||
<div class="reply-preview" style="border: 1px solid #ccc; padding: 1vh; margin-bottom: 1vh; background-color: black; color: white;">
|
||||
<strong>Replying to:</strong> ${repliedMessage.content}
|
||||
<button id="cancel-reply" style="float: right; color: red; background-color: black; font-weight: bold;">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!document.querySelector(".reply-container")) {
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
|
||||
|
||||
// Add a listener for the cancel reply button
|
||||
document.getElementById("cancel-reply").addEventListener("click", () => {
|
||||
replyToMessageIdentifier = null;
|
||||
replyContainer.remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
const messageInputSection = document.querySelector(".message-input-section");
|
||||
const editor = document.querySelector(".ql-editor");
|
||||
|
||||
if (messageInputSection) {
|
||||
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
if (editor) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading messages from QDN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Polling function to check for new messages without clearing existing ones
|
||||
function startPollingForNewMessages() {
|
||||
setInterval(async () => {
|
||||
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0];
|
||||
if (activeRoom) {
|
||||
await loadMessagesFromQDN(activeRoom, currentPage, true);
|
||||
}
|
||||
}, 20000);
|
||||
}
|
||||
|
Reference in New Issue
Block a user