Bit updates to Minter and Admin boards. Still working on encrypted file downloads for the Admin Room on the forum... for whatever reason they're giving me a hassle. Just need a little more time to get it sorted.

Will also be adding additional use cases for the Admin board, and maybe a 'community board' as I really like the board concept for things like decision-making and community managmenent. I also have a really good idea for giving information on the boards via link modal concept. TBD. Will likely make the identifier changes and push announcements out on Monday. IF not sometime this weekend if time allows.
This commit is contained in:
crowetic 2024-12-20 22:07:18 -08:00
parent 57219987f3
commit 5e149039e9
8 changed files with 318 additions and 149 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -639,20 +639,20 @@ body {
transition: transform 0.2s ease-in-out;
} */
.minter-card{
background-color: #1e1e2e;
background-color: #0c1314;
flex: auto;
display: flex;
min-width: 22rem;
/* max-width: 22rem; */
max-width: calc(30% - 3rem);
color: #ffffff;
border: 1px solid #333;
border: 1px solid #6c8389;
border-radius: 12px;
padding: 1vh;
min-height: 30vh;
max-height: auto;
margin: 1vh;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
box-shadow: 0 4px 10px rgba(207, 214, 255, 0.31);
font-family: 'Arial', sans-serif;
transition: transform 0.2s ease-in-out;
flex-direction: column;
@ -961,7 +961,7 @@ body {
color: #ffffff;
border: none;
border-radius: 8px;
padding: 1vh 1.7rem;
padding: 1vh 1.1rem;
cursor: pointer;
transition: background-color 0.3s;
}
@ -975,7 +975,7 @@ body {
color: #ffffff;
border: none;
border-radius: 8px;
padding: 1.0vh 1.7rem;
padding: 1.0vh 1.1rem;
cursor: pointer;
transition: background-color 0.3s;
}

View File

@ -242,9 +242,9 @@ const fetchAllEncryptedCards = async () => {
// Fetch poll results
const pollResults = await fetchPollResults(decryptedCardData.poll);
const minterNameFromIdentifier = await extractCardsMinterName(card.identifier);
const commentCount = await getCommentCount(card.identifier);
const encryptedCommentCount = await getEncryptedCommentCount(card.identifier);
// Generate final card HTML
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, commentCount);
const finalCardHTML = await createEncryptedCardHTML(decryptedCardData, pollResults, card.identifier, encryptedCommentCount);
replaceEncryptedSkeleton(card.identifier, finalCardHTML);
} catch (error) {
console.error(`Error processing card ${card.identifier}:`, error);
@ -491,7 +491,7 @@ const publishEncryptedCard = async () => {
}
}
const getCommentCount = async (cardIdentifier) => {
const getEncryptedCommentCount = async (cardIdentifier) => {
try {
const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES',
@ -692,26 +692,63 @@ const closeLinkDisplayModal = async () => {
modalContent.src = ''; // Clear the iframe source
}
// const processQortalLinkForRendering = async (link) => {
// if (link.startsWith('qortal://')) {
// const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
// if (match) {
// const firstParam = match[1].toUpperCase(); // Convert to uppercase
// const remainingPath = match[2] || ""; // Rest of the URL
// // Perform any asynchronous operation if necessary
// await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
// return `/render/${firstParam}${remainingPath}`;
// }
// }
// return link; // Return unchanged if not a Qortal link
// }
const processQortalLinkForRendering = async (link) => {
if (link.startsWith('qortal://')) {
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
if (match) {
const firstParam = match[1].toUpperCase(); // Convert to uppercase
const remainingPath = match[2] || ""; // Rest of the URL
// Perform any asynchronous operation if necessary
await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
return `/render/${firstParam}${remainingPath}`;
const firstParam = match[1].toUpperCase();
const remainingPath = match[2] || "";
const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined
// Simulating async operation if needed
await new Promise(resolve => setTimeout(resolve, 10));
// Append theme as a query parameter
return `/render/${firstParam}${remainingPath}?theme=${themeColor}`;
}
}
return link; // Return unchanged if not a Qortal link
return link;
};
async function getMinterAvatar(minterName) {
const avatarUrl = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`;
try {
const response = await fetch(avatarUrl, { method: 'HEAD' });
if (response.ok) {
// Avatar exists, return the image HTML
return `<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">`;
} else {
// Avatar not found or no permission
return '';
}
} catch (error) {
console.error('Error checking avatar availability:', error);
return '';
}
}
// Create the overall Minter Card HTML -----------------------------------------------
const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, commentCount) => {
const { minterName, header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const minterAvatar = `/arbitrary/THUMBNAIL/${minterName}/qortal_avatar`;
const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const minterAvatar = await getMinterAvatar(minterName)
// const creatorAvatar = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const creatorAvatar = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => `
<button onclick="openLinkDisplayModal('${link}')">
${`Link ${index + 1} - ${link}`}
@ -725,50 +762,48 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
return `
<div class="admin-card">
<div class="minter-card-header">
<h2 class="support-header"> Posted By:</h2>
<img src="${creatorAvatar}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">
<h2 class="support-header"> Created By: </h2>
${creatorAvatar}
<h2>${creator}</h2>
<div class="support-header"><h5> Regarding Minter: </h5>
<img src="${minterAvatar}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">
<div class="support-header"><h5> REGARDING: </h5></div>
${minterAvatar}
<h3>${minterName}</h3>
<p>${header}</p>
</div>
<div class="info">
${content}
</div>
<div class="support-header"><h5>Informational Links:</h5></div>
<div class="support-header"><h5>LINKS</h5></div>
<div class="info-links">
${linksHTML}
</div>
<div class="results-header support-header"><h5>Resulting Support:</h5></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>
<span class="admin-yes">Admin Support: ${adminYes}</span>
<span class="admin-no">Admin Against: ${adminNo}</span>
</div>
<div class="minter-results">
<span class="minter-yes">TBD ${minterYes}</span>
<span class="minter-no">TBD ${minterNo}</span>
</div>
<div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-no">Total No: ${totalNo}</span>
<span class="minter-yes">Supporting Weight ${totalYesWeight}</span>
<span class="minter-no">Denial Weight ${totalNoWeight}</span>
</div>
</div>
<div class="support-header"><h5>Support ${minterName}?</h5></div>
<div class="support-header"><h5>SUPPORT or DENY</h5><h5 style="color: #ffae42;">${minterName}</h5>
<p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p>
</div>
<div class="actions">
<div class="actions-buttons">
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
<button class="yes" onclick="voteYesOnPoll('${poll}')">SUPPORT</button>
<button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">OPPOSE</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>
<textarea id="new-comment-${cardIdentifier}" placeholder="Input your comment..." style="width: 100%; margin-top: 10px;"></textarea>
<button onclick="postEncryptedComment('${cardIdentifier}')">Post Comment</button>
</div>
<p style="font-size: 12px; color: gray;">Published by: ${creator} on ${formattedDate}</p>
<p style="font-size: 0.75rem; margin-top: 1vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
</div>
`;
}

View File

@ -17,11 +17,13 @@ const loadMinterBoardPage = async () => {
// Add the "Minter Board" content
const mainContent = document.createElement("div");
const publishButtonColor = generateDarkPastelBackgroundBy("MinterBoardPublishButton")
const minterBoardNameColor = generateDarkPastelBackgroundBy(randomID)
mainContent.innerHTML = `
<div class="minter-board-main" style="padding: 20px; text-align: center;">
<h1 style="color: lightblue;">Minter Board</h1>
<p style="font-size: 1.25em;"> 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>
<h1 style="color: ${minterBoardNameColor};">Minter Board</h1>
<p style="font-size: 1.25em;"> Publish a Minter Card with Information, and obtain and view the support of the community. Welcome to the Minter Board!</p>
<button id="publish-card-button" class="publish-card-button" style="margin: 20px; padding: 10px; background-color: ${publishButtonColor}">Publish Minter Card</button>
<button id="refresh-cards-button" class="refresh-cards-button" style="padding: 10px;">Refresh Cards</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;">
@ -187,9 +189,11 @@ const loadCards = async () => {
// Fetch poll results
const pollResults = await fetchPollResults(cardDataResponse.poll);
const BgColor = generateDarkPastelBackgroundBy(card.name)
// Generate final card HTML
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier);
const commentCount = await countComments(card.identifier)
const finalCardHTML = await createCardHTML(cardDataResponse, pollResults, card.identifier, commentCount, BgColor);
replaceSkeleton(card.identifier, finalCardHTML);
} catch (error) {
console.error(`Error processing card ${card.identifier}:`, error);
@ -222,6 +226,7 @@ const createSkeletonCardHTML = (cardIdentifier) => {
return `
<div id="skeleton-${cardIdentifier}" class="skeleton-card" style="padding: 10px; border: 1px solid gray; margin: 10px 0;">
<div style="display: flex; align-items: center;">
<div><p style="color:rgb(174, 174, 174)">LOADING CARD...</p></div>
<div style="width: 50px; height: 50px; background-color: #ccc; border-radius: 50%;"></div>
<div style="margin-left: 10px;">
<div style="width: 120px; height: 20px; background-color: #ccc; margin-bottom: 5px;"></div>
@ -229,7 +234,7 @@ const createSkeletonCardHTML = (cardIdentifier) => {
</div>
</div>
<div style="margin-top: 10px;">
<div style="width: 100%; height: 40px; background-color: #eee;"></div>
<div style="width: 100%; height: 80px; background-color: #eee; color:rgb(17, 24, 28); padding: 0.22vh"><p>PLEASE BE PATIENT</p><p style="color: #11121c"> While data loads from QDN...</div>
</div>
</div>
`;
@ -530,6 +535,22 @@ const toggleComments = async (cardIdentifier) => {
}
};
const countComments = async (cardIdentifier) => {
try {
const response = await qortalRequest({
action: 'SEARCH_QDN_RESOURCES',
service: 'BLOG_POST',
query: `comment-${cardIdentifier}`,
mode: "ALL"
});
// Just return the count; no need to decrypt each comment here
return Array.isArray(response) ? response.length : 0;
} catch (error) {
console.error(`Error fetching comment count for ${cardIdentifier}:`, error);
return 0;
}
};
const createModal = async () => {
const modalHTML = `
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 1000;">
@ -563,22 +584,63 @@ const processLink = async (link) => {
if (link.startsWith('qortal://')) {
const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/);
if (match) {
const firstParam = match[1].toUpperCase(); // Convert to uppercase
const remainingPath = match[2] || ""; // Rest of the URL
// Perform any asynchronous operation if necessary
await new Promise(resolve => setTimeout(resolve, 10)); // Simulating async operation
return `/render/${firstParam}${remainingPath}`;
const firstParam = match[1].toUpperCase();
const remainingPath = match[2] || "";
const themeColor = window._qdnTheme || 'default'; // Fallback to 'default' if undefined
// Simulating async operation if needed
await new Promise(resolve => setTimeout(resolve, 10));
// Append theme as a query parameter
return `/render/${firstParam}${remainingPath}?theme=${themeColor}`;
}
}
return link; // Return unchanged if not a Qortal link
}
return link;
};
// Hash the name and map it to a dark pastel color
const generateDarkPastelBackgroundBy = (name) => {
// 1) Basic string hashing
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = (hash << 5) - hash + name.charCodeAt(i);
hash |= 0; // Convert to 32-bit integer
}
const safeHash = Math.abs(hash);
// 2) Restrict hue to a 'blue-ish' range (150..270 = 120 degrees total)
// We'll define a certain number of hue steps in that range.
const hueSteps = 69.69; // e.g., 12 steps
const hueIndex = safeHash % hueSteps;
// Each step is 120 / (hueSteps - 1) or so:
// but a simpler approach is 120 / hueSteps. It's okay if we don't use the extreme ends exactly.
const hueRange = 240;
const hue = 22.69 + (hueIndex * (hueRange / hueSteps));
// This yields values like 150, 160, 170, ... up to near 270
// 3) Satura­tion:
const satSteps = 13.69;
const satIndex = safeHash % satSteps;
const saturation = 18 + (satIndex * 1.333);
// 4) Lightness:
const lightSteps = 3.69;
const lightIndex = safeHash % lightSteps;
const lightness = 7 + lightIndex;
// 5) Return the HSL color string
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
};
// Create the overall Minter Card HTML -----------------------------------------------
const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCount, BgColor) => {
const { header, content, links, creator, timestamp, poll } = cardData;
const formattedDate = new Date(timestamp).toLocaleString();
const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
// const avatarUrl = `/arbitrary/THUMBNAIL/${creator}/qortal_avatar`;
const avatarHtml = await getMinterAvatar(creator)
const linksHTML = links.map((link, index) => `
<button onclick="openModal('${link}')">
${`Link ${index + 1} - ${link}`}
@ -590,21 +652,21 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
const { adminYes = 0, adminNo = 0, minterYes = 0, minterNo = 0, totalYes = 0, totalNo = 0, totalYesWeight = 0, totalNoWeight = 0 } = await calculatePollResults(pollResults, minterGroupMembers, minterAdmins)
await createModal()
return `
<div class="minter-card">
<div class="minter-card" style="background-color: ${BgColor}">
<div class="minter-card-header">
<img src="${avatarUrl}" alt="User Avatar" class="user-avatar" style="width: 50px; height: 50px; border-radius: 50%; align-self: center;">
${avatarHtml}
<h3>${creator}</h3>
<p>${header}</p>
</div>
<div class="support-header"><h5>Minter Post:</h5></div>
<div class="support-header"><h5>MINTER'S POST</h5></div>
<div class="info">
${content}
</div>
<div class="support-header"><h5>Minter Links:</h5></div>
<div class="support-header"><h5>MINTER'S LINKS</h5></div>
<div class="info-links">
${linksHTML}
</div>
<div class="results-header support-header"><h5>Current Results:</h5></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>
@ -616,14 +678,18 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
</div>
<div class="total-results">
<span class="total-yes">Total Yes: ${totalYes}</span>
<span class="total-yes">Weight: ${totalYesWeight}</span>
<span class="total-no">Total No: ${totalNo}</span>
<span class="total-no">Weight: ${totalNoWeight}</span>
</div>
</div>
<div class="support-header"><h5>Support Minter?</h5></div>
<div class="support-header"><h5>SUPPORT</h5><h5 style="color: #ffae42;">${creator}</h5>
<p style="color: #c7c7c7; font-size: .65rem; margin-top: 1vh">(click COMMENTS button to open/close card comments)</p>
</div>
<div class="actions">
<div class="actions-buttons">
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
<button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENTS</button>
<button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div>
</div>
@ -632,7 +698,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier) => {
<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>
<p style="font-size: 0.75rem; margin-top: 3vh; color: #4496a1">By: ${creator} - ${formattedDate}</p>
</div>
`;
}

View File

@ -550,7 +550,7 @@ const showSuccessNotification = () => {
// Generate unique attachment ID
const generateAttachmentID = (room, fileIndex = null) => {
const baseID = room === "admins" ? `${messageAttachmentIdentifierPrefix}-${room}-e-${Date.now()}` : `${messageAttachmentIdentifierPrefix}-${room}-${Date.now()}`;
const baseID = room === "admins" ? `${messageAttachmentIdentifierPrefix}-${room}-e-${randomID()}` : `${messageAttachmentIdentifierPrefix}-${room}-${randomID()}`;
return fileIndex !== null ? `${baseID}-${fileIndex}` : baseID;
};
@ -770,123 +770,126 @@ const buildMessageHTML = (message, fetchMessages, room, isNewMessage) => {
</div>
<button class="reply-button" data-message-identifier="${message.identifier}">Reply</button>
</div>
`;
};
`
}
const buildReplyHtml = (message, fetchMessages) => {
if (!message.replyTo) return "";
if (!message.replyTo) return ""
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo);
if (!repliedMessage) return "";
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
if (!repliedMessage) return ""
return `
<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 buildAttachmentHtml = (message, room) => {
if (!message.attachments || message.attachments.length === 0) return "";
if (!message.attachments || message.attachments.length === 0) return ""
return message.attachments.map(attachment => buildSingleAttachmentHtml(attachment, room)).join("");
};
return message.attachments.map(attachment => buildSingleAttachmentHtml(attachment, room)).join("")
}
const buildSingleAttachmentHtml = (attachment, room) => {
if (room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`;
const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`
return `
<div class="attachment">
<img src="${imageUrl}" alt="${attachment.filename}" class="inline-image"/>
</div>
`;
`
} else if
(room === "admins" && attachment.mimeType && attachment.mimeType.startsWith('image/')) {
return fetchEncryptedImageHtml(attachment)
} else {
// Non-image attachment
return `
<div class="attachment">
<button onclick="fetchAndSaveAttachment('${attachment.service}', '${attachment.name}', '${attachment.identifier}', '${attachment.filename}', '${attachment.mimeType}')">
Download ${attachment.filename}
</button>
</div>
`;
`
}
};
}
const scrollToNewMessages = (firstNewMessageIdentifier) => {
const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`);
const newMessageElement = document.querySelector(`.message-item[data-identifier="${firstNewMessageIdentifier}"]`)
if (newMessageElement) {
newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
newMessageElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
};
}
const updateLatestMessageIdentifiers = (room, mostRecentMessage) => {
latestMessageIdentifiers[room] = mostRecentMessage;
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers));
};
latestMessageIdentifiers[room] = mostRecentMessage
localStorage.setItem("latestMessageIdentifiers", JSON.stringify(latestMessageIdentifiers))
}
const handleReplyLogic = (fetchMessages) => {
const replyButtons = document.querySelectorAll(".reply-button");
const replyButtons = document.querySelectorAll(".reply-button")
replyButtons.forEach(button => {
button.addEventListener("click", () => {
const replyToMessageIdentifier = button.dataset.messageIdentifier;
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier);
const replyToMessageIdentifier = button.dataset.messageIdentifier
const repliedMessage = fetchMessages.find(m => m && m.identifier === replyToMessageIdentifier)
if (repliedMessage) {
showReplyPreview(repliedMessage);
showReplyPreview(repliedMessage)
}
});
});
};
})
})
}
const showReplyPreview = (repliedMessage) => {
replyToMessageIdentifier = repliedMessage.identifier;
replyToMessageIdentifier = repliedMessage.identifier
const replyContainer = document.createElement("div");
replyContainer.className = "reply-container";
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");
const messageInputSection = document.querySelector(".message-input-section")
if (messageInputSection) {
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild);
messageInputSection.insertBefore(replyContainer, messageInputSection.firstChild)
document.getElementById("cancel-reply").addEventListener("click", () => {
replyToMessageIdentifier = null;
replyContainer.remove();
});
replyToMessageIdentifier = null
replyContainer.remove()
})
}
}
const messageInputSection = document.querySelector(".message-input-section");
const editor = document.querySelector(".ql-editor");
const messageInputSection = document.querySelector(".message-input-section")
const editor = document.querySelector(".ql-editor")
if (messageInputSection) {
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
messageInputSection.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
if (editor) {
editor.focus();
editor.focus()
}
};
}
const updatePaginationControls = async (room, limit) => {
const totalMessages = room === "admins" ? await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`, room) : await searchAllCountOnly(`${messageIdentifierPrefix}-${room}-e`, room)
renderPaginationControls(room, totalMessages, limit);
};
const totalMessages = room === "admins" ? await searchAllCountOnly(`${messageIdentifierPrefix}-${room}-e`, room) : await searchAllCountOnly(`${messageIdentifierPrefix}-${room}`, room)
renderPaginationControls(room, totalMessages, limit)
}
// 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];
const activeRoom = document.querySelector('.room-title')?.innerText.toLowerCase().split(" ")[0]
if (activeRoom) {
await loadMessagesFromQDN(activeRoom, currentPage, true);
await loadMessagesFromQDN(activeRoom, currentPage, true)
}
}, 40000);
}, 40000)
}

View File

@ -28,9 +28,20 @@ const uid = async () => {
console.log('Generated uid:', result);
return result;
};
// a non-async version of the uid function, in case non-async functions need it. Ultimately we can probably remove uid but need to ensure no apps are using it asynchronously first. so this is kept for that purpose for now.
const randomID = () => {
console.log('randomID non-async');
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;
@ -45,7 +56,6 @@ const timestampToHumanReadableDate = async(timestamp) => {
};
// 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;
@ -119,7 +129,6 @@ const userState = {
// 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);
@ -796,39 +805,95 @@ async function loadImageHtml(service, name, identifier, filename, mimeType) {
}
const fetchAndSaveAttachment = async (service, name, identifier, filename, mimeType) => {
const url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}`;
if ((service === "FILE_PRIVATE") || (service === "MAIL_PRIVATE")) {
service = "FILE_PRIVATE" || service
try{
const encryptedBase64Data = await fetchFileBase64(service, name, identifier)
const decryptedBase64 = await decryptObject(encryptedBase64Data)
const fileBlob = new Blob([decryptedBase64], { type: mimeType });
try {
if (!filename || !mimeType) {
console.error("Filename and mimeType are required");
return;
}
let url = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?async=true&attempts=5`
if (service === "MAIL_PRIVATE") {
service = "FILE_PRIVATE";
}
if (service === "FILE_PRIVATE") {
const urlPrivate = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?encoding=base64&async=true&attempts=5`
const response = await fetch(urlPrivate,{
method: 'GET',
headers: { 'accept': 'text/plain' }
})
if (!response.ok) {
throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`)
}
const encryptedBase64Data = response
console.log("Fetched Base64 Data:", encryptedBase64Data)
// const sanitizedBase64 = encryptedBase64Data.replace(/[\r\n]+/g, '')
const decryptedData = await decryptObject(encryptedBase64Data)
console.log("Decrypted Data:", decryptedData);
const fileBlob = new Blob((decryptedData), { type: mimeType })
await qortalRequest({
action: "SAVE_FILE",
blob: fileBlob,
filename,
mimeType
mimeType,
});
console.log("Encrypted file saved successfully:", filename)
} else {
}catch (error) {
console.error("Error fetching ro saving encrypted attachment",error)
const response = await fetch(url, {
method: 'GET',
headers: {'accept': 'text/plain'}
});
if (!response.ok) {
throw new Error(`File not found (HTTP ${response.status}): ${url}`)
}
}else{
try {
const response = await fetch(url);
const blob = await response.blob();
const blob = await response.blob()
await qortalRequest({
action: "SAVE_FILE",
blob,
filename: filename,
mimeType
});
filename,
mimeType,
})
console.log("File saved successfully:", filename)
}
} catch (error) {
console.error("Error fetching or saving the attachment:", error);
console.error(
`Error fetching or saving attachment (service: ${service}, name: ${name}, identifier: ${identifier}):`,
error
);
}
};
const fetchEncryptedImageHtml = async (service, name, identifier, filename, mimeType) => {
const urlPrivate = `${baseUrl}/arbitrary/${service}/${name}/${identifier}?encoding=base64&async=true&attempts=5`
const response = await fetch(urlPrivate,{
method: 'GET',
headers: { 'accept': 'text/plain' }
})
if (!response.ok) {
throw new Error(`File not found (HTTP ${response.status}): ${urlPrivate}`)
}
//obtain the encrypted base64 of the image
const encryptedBase64Data = response
console.log("Fetched Base64 Data:", encryptedBase64Data)
//decrypt the encrypted base64 object
const decryptedData = await decryptObject(encryptedBase64Data)
console.log("Decrypted Data:", decryptedData);
//turn the decrypted object into a blob/uint8 array and specify mimetype //todo check if the uint8Array is needed or not. I am guessing not.
const fileBlob = new Blob((decryptdData), { type: mimeType })
//create the URL for the decrypted file blob
const objectUrl = URL.createObjectURL(fileBlob)
//create the HTML from the file blob URL.
const attachmentHtml = `<div class="attachment"><img src="${objectUrl}" alt="${filename}" class="inline-image"></div>`;
return attachmentHtml
}
const renderData = async (service, name, identifier) => {
console.log('renderData called');
console.log('service:', service);