This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well.

This commit is contained in:
crowetic 2024-12-26 20:06:51 -08:00
parent 0fc471a1b8
commit 2192f7c855
11 changed files with 424 additions and 45 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,5 @@
// NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards. // NOTE - Change isTestMode to false prior to actual release ---- !important - You may also change identifier if you want to not show older cards.
const isEncryptedTestMode = true const isEncryptedTestMode = false
const encryptedCardIdentifierPrefix = "card-MAC" const encryptedCardIdentifierPrefix = "card-MAC"
let isExistingEncryptedCard = false let isExistingEncryptedCard = false
let existingDecryptedCardData = {} let existingDecryptedCardData = {}
@ -9,6 +9,7 @@ let existingCardMinterNames = []
let isTopic = false let isTopic = false
let attemptLoadAdminDataCount = 0 let attemptLoadAdminDataCount = 0
let adminMemberCount = 0 let adminMemberCount = 0
let adminPublicKeys = []
console.log("Attempting to load AdminBoard.js"); console.log("Attempting to load AdminBoard.js");
@ -112,7 +113,7 @@ const loadAdminBoardPage = async () => {
const updateOrSaveAdminGroupsDataLocally = async () => { const updateOrSaveAdminGroupsDataLocally = async () => {
try { try {
// Fetch the array of admin public keys // Fetch the array of admin public keys
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys(); const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
// Build an object containing the count and the array // Build an object containing the count and the array
const adminData = { const adminData = {
@ -120,12 +121,14 @@ const updateOrSaveAdminGroupsDataLocally = async () => {
publicKeys: verifiedAdminPublicKeys publicKeys: verifiedAdminPublicKeys
}; };
// Stringify and save to localStorage adminPublicKeys = verifiedAdminPublicKeys
localStorage.setItem('savedAdminData', JSON.stringify(adminData));
console.log('Admin public keys saved locally:', adminData); // Stringify and save to localStorage
localStorage.setItem('savedAdminData', JSON.stringify(adminData))
console.log('Admin public keys saved locally:', adminData)
} catch (error) { } catch (error) {
console.error('Error fetching/storing admin public keys:', error); console.error('Error fetching/storing admin public keys:', error)
attemptLoadAdminDataCount++ attemptLoadAdminDataCount++
} }
}; };
@ -146,7 +149,10 @@ const loadOrFetchAdminGroupsData = async () => {
adminMemberCount = parsedData.keysCount adminMemberCount = parsedData.keysCount
adminPublicKeys = parsedData.publicKeys adminPublicKeys = parsedData.publicKeys
console.log(`Loaded admins 'keysCount'=${adminMemberCount}, adminKeys=`, adminPublicKeys) console.log(typeof adminPublicKeys); // Should be "object"
console.log(Array.isArray(adminPublicKeys))
console.log(`Loaded admins 'keysCount'=${adminMemberCount}, publicKeys=`, adminPublicKeys)
attemptLoadAdminDataCount = 0 attemptLoadAdminDataCount = 0
return parsedData; // and return { adminMemberCount, adminKeys } to the caller return parsedData; // and return { adminMemberCount, adminKeys } to the caller
@ -526,7 +532,22 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
base64CardData = btoa(JSON.stringify(cardData)); base64CardData = btoa(JSON.stringify(cardData));
} }
const verifiedAdminPublicKeys = (adminPublicKeys) ? adminPublicKeys : loadOrFetchAdminGroupsData().publicKeys let verifiedAdminPublicKeys = adminPublicKeys
if ((!verifiedAdminPublicKeys) || verifiedAdminPublicKeys.length <= 5 || !Array.isArray(verifiedAdminPublicKeys)) {
console.log(`adminPublicKeys variable failed check, attempting to load from localStorage`,adminPublicKeys)
const savedAdminData = localStorage.getItem('savedAdminData')
const parsedAdminData = JSON.parse(savedAdminData)
const loadedAdminKeys = parsedAdminData.publicKeys
if ((!loadedAdminKeys) || (!Array.isArray(loadedAdminKeys)) || (loadedAdminKeys.length === 0)){
console.log('loaded admin keys from localStorage failed, falling back to API call...')
verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
}
verifiedAdminPublicKeys = loadedAdminKeys
}
await qortalRequest({ await qortalRequest({
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
@ -605,8 +626,9 @@ const postEncryptedComment = async (cardIdentifier) => {
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`; const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0)) { if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0) || (!adminPublicKeys)) {
const verifiedAdminPublicKeys = await loadOrFetchAdminGroupsData().publicKeys console.log('adminPpublicKeys variable failed checks, calling for admin public keys from API (comment)',adminPublicKeys)
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
adminPublicKeys = verifiedAdminPublicKeys adminPublicKeys = verifiedAdminPublicKeys
} }
@ -731,12 +753,24 @@ const calculateAdminBoardPollResults = async (pollData, minterGroupMembers, mint
} }
const toggleEncryptedComments = async (cardIdentifier) => { const toggleEncryptedComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`); const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`)
if (commentsSection.style.display === 'none' || !commentsSection.style.display) { const commentButton = document.getElementById(`comment-button-${cardIdentifier}`)
if (!commentsSection || !commentButton) return;
const count = commentButton.dataset.commentCount;
const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display);
if (isHidden) {
// Show comments
commentButton.textContent = "LOADING...";
await displayEncryptedComments(cardIdentifier); await displayEncryptedComments(cardIdentifier);
commentsSection.style.display = 'block'; commentsSection.style.display = 'block';
// Change the button text to 'HIDE COMMENTS'
commentButton.textContent = 'HIDE COMMENTS';
} else { } else {
// Hide comments
commentsSection.style.display = 'none'; commentsSection.style.display = 'none';
commentButton.textContent = `COMMENTS (${count})`;
} }
}; };
@ -847,7 +881,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
showTopic = false showTopic = false
} }
} }
const cardColorCode = showTopic ? '#0e1b15' : '#151f28' const cardColorCode = showTopic ? '#0e1b15' : '#151f28'
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? ` const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
@ -895,7 +929,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
<div class="actions"> <div class="actions">
<div class="actions-buttons"> <div class="actions-buttons">
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button> <button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
<button class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button> <button id="comment-button-${cardIdentifier}" data-comment-count="${commentCount}" class="comment" onclick="toggleEncryptedComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button> <button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div> </div>
</div> </div>

View File

@ -117,6 +117,78 @@ const loadMinterBoardPage = async () => {
await loadCards(); await loadCards();
} }
const extractMinterCardsMinterName = async (cardIdentifier) => {
// Ensure the identifier starts with the prefix
if (!cardIdentifier.startsWith(`${cardIdentifierPrefix}-`)) {
throw new Error('Invalid identifier format or prefix mismatch');
}
// Split the identifier into parts
const parts = cardIdentifier.split('-');
// Ensure the format has at least 3 parts
if (parts.length < 3) {
throw new Error('Invalid identifier format');
}
try {
const nameFromIdentifier = await searchSimple('BLOG_POST', cardIdentifier, "", 1)
const minterName = await nameFromIdentifier.name
return minterName
} catch (error) {
throw error
}
}
const processMinterCards = async (validMinterCards) => {
const latestCardsMap = new Map()
// Step 1: Filter and keep the most recent card per identifier
validMinterCards.forEach(card => {
const timestamp = card.updated || card.created || 0
const existingCard = latestCardsMap.get(card.identifier)
if (!existingCard || timestamp > (existingCard.updated || existingCard.created || 0)) {
latestCardsMap.set(card.identifier, card)
}
})
// Step 2: Extract unique cards
const uniqueValidCards = Array.from(latestCardsMap.values())
// Step 3: Group by minterName and select the most recent card per minterName
const minterNameMap = new Map()
for (const card of validMinterCards) {
const minterName = await extractMinterCardsMinterName(card.identifier)
const existingCard = minterNameMap.get(minterName)
const cardTimestamp = card.updated || card.created || 0
const existingTimestamp = existingCard?.updated || existingCard?.created || 0
// Keep only the most recent card for each minterName
if (!existingCard || cardTimestamp > existingTimestamp) {
minterNameMap.set(minterName, card)
}
}
// Step 4: Filter cards to ensure each minterName is included only once
const finalCards = []
const seenMinterNames = new Set()
for (const [minterName, card] of minterNameMap.entries()) {
if (!seenMinterNames.has(minterName)) {
finalCards.push(card)
seenMinterNames.add(minterName) // Mark the minterName as seen
}
}
// Step 5: Sort by the most recent timestamp
finalCards.sort((a, b) => {
const timestampA = a.updated || a.created || 0
const timestampB = b.updated || b.created || 0
return timestampB - timestampA
})
return finalCards
}
//Main function to load the Minter Cards ---------------------------------------- //Main function to load the Minter Cards ----------------------------------------
const loadCards = async () => { const loadCards = async () => {
const cardsContainer = document.getElementById("cards-container"); const cardsContainer = document.getElementById("cards-container");
@ -150,22 +222,24 @@ const loadCards = async () => {
return; return;
} }
const finalCards = await processMinterCards(validCards)
// Sort cards by timestamp descending (newest first) // Sort cards by timestamp descending (newest first)
validCards.sort((a, b) => { // validCards.sort((a, b) => {
const timestampA = a.updated || a.created || 0; // const timestampA = a.updated || a.created || 0;
const timestampB = b.updated || b.created || 0; // const timestampB = b.updated || b.created || 0;
return timestampB - timestampA; // return timestampB - timestampA;
}); // });
// Display skeleton cards immediately // Display skeleton cards immediately
cardsContainer.innerHTML = ""; cardsContainer.innerHTML = "";
validCards.forEach(card => { finalCards.forEach(card => {
const skeletonHTML = createSkeletonCardHTML(card.identifier); const skeletonHTML = createSkeletonCardHTML(card.identifier);
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML); cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
}); });
// Fetch and update each card // Fetch and update each card
validCards.forEach(async card => { finalCards.forEach(async card => {
try { try {
const cardDataResponse = await qortalRequest({ const cardDataResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE", action: "FETCH_QDN_RESOURCE",
@ -528,11 +602,23 @@ const displayComments = async (cardIdentifier) => {
// Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled -------------------- // Toggle comments from being shown or not, with passed cardIdentifier for comments being toggled --------------------
const toggleComments = async (cardIdentifier) => { const toggleComments = async (cardIdentifier) => {
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`); const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
if (commentsSection.style.display === 'none' || !commentsSection.style.display) { const commentButton = document.getElementById(`comment-button-${cardIdentifier}`)
if (!commentsSection || !commentButton) return;
const count = commentButton.dataset.commentCount;
const isHidden = (commentsSection.style.display === 'none' || !commentsSection.style.display);
if (isHidden) {
// Show comments
commentButton.textContent = "LOADING...";
await displayComments(cardIdentifier); await displayComments(cardIdentifier);
commentsSection.style.display = 'block'; commentsSection.style.display = 'block';
// Change the button text to 'HIDE COMMENTS'
commentButton.textContent = 'HIDE COMMENTS';
} else { } else {
// Hide comments
commentsSection.style.display = 'none'; commentsSection.style.display = 'none';
commentButton.textContent = `COMMENTS (${count})`;
} }
}; };
@ -610,14 +696,13 @@ const generateDarkPastelBackgroundBy = (name) => {
const safeHash = Math.abs(hash); const safeHash = Math.abs(hash);
// 2) Restrict hue to a 'blue-ish' range (150..270 = 120 degrees total) // 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 hueSteps = 69.69;
const hueIndex = safeHash % hueSteps; 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 = 288;
const hueRange = 240; const hue = 140 + (hueIndex * (hueRange / hueSteps));
const hue = 22.69 + (hueIndex * (hueRange / hueSteps));
// This yields values like 150, 160, 170, ... up to near 270
// 3) Satura­tion: // 3) Satura­tion:
const satSteps = 13.69; const satSteps = 13.69;
@ -690,7 +775,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
<div class="actions"> <div class="actions">
<div class="actions-buttons"> <div class="actions-buttons">
<button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button> <button class="yes" onclick="voteYesOnPoll('${poll}')">YES</button>
<button class="comment" onclick="toggleComments('${cardIdentifier}')">COMMENTS (${commentCount})</button> <button class="comment" id="comment-button-${cardIdentifier}" data-comment-count="${commentCount}" onclick="toggleComments('${cardIdentifier}')">COMMENTS (${commentCount})</button>
<button class="no" onclick="voteNoOnPoll('${poll}')">NO</button> <button class="no" onclick="voteNoOnPoll('${poll}')">NO</button>
</div> </div>
</div> </div>

View File

@ -1,6 +1,5 @@
const messageIdentifierPrefix = `mintership-forum-message`; const messageIdentifierPrefix = `mintership-forum-message`;
const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`; const messageAttachmentIdentifierPrefix = `mintership-forum-attachment`;
let adminPublicKeys = []
// NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically. // NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically.
@ -9,7 +8,58 @@ let latestMessageIdentifiers = {}; // To keep track of the latest message in eac
let currentPage = 0; // Track current pagination page let currentPage = 0; // Track current pagination page
let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once. let existingIdentifiers = new Set(); // Keep track of existing identifiers to not pull them more than once.
let messagesById = {}
let messageOrder =[]
const MAX_MESSAGES = 2000
// Key = message.identifier
// Value = { ...the message object with timestamp, name, content, etc. }
// If there is a previous latest message identifiers, use them. Otherwise, use an empty. // If there is a previous latest message identifiers, use them. Otherwise, use an empty.
const storeMessageInMap = (msg) => {
if (!msg?.identifier || !msg || !msg?.timestamp) return
messagesById[msg.identifier] = msg
// We will keep an array 'messageOrder' to store the messages and limit the size they take
messageOrder.push({ identifier: msg.identifier, timestamp: msg.timestamp })
messageOrder.sort((a, b) => a.timestamp - b.timestamp);
while (messageOrder.length > MAX_MESSAGES) {
// Remove oldest from the front
const oldest = messageOrder.shift();
// Delete from the map as well
delete messagesById[oldest.identifier];
}
}
function saveMessagesToLocalStorage() {
try {
const data = { messagesById, messageOrder };
localStorage.setItem("forumMessages", JSON.stringify(data));
console.log("Saved messages to localStorage. Count:", messageOrder.length);
} catch (error) {
console.error("Error saving to localStorage:", error);
}
}
function loadMessagesFromLocalStorage() {
try {
const stored = localStorage.getItem("forumMessages");
if (!stored) {
console.log("No saved messages in localStorage.");
return;
}
const parsed = JSON.parse(stored);
if (parsed.messagesById && parsed.messageOrder) {
messagesById = parsed.messagesById;
messageOrder = parsed.messageOrder;
console.log(`Loaded ${messageOrder.length} messages from localStorage.`);
}
} catch (error) {
console.error("Error loading messages from localStorage:", error);
}
}
if (localStorage.getItem("latestMessageIdentifiers")) { if (localStorage.getItem("latestMessageIdentifiers")) {
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers")); latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
} }
@ -49,6 +99,21 @@ document.addEventListener("DOMContentLoaded", async () => {
// --- ADMIN CHECK --- // --- ADMIN CHECK ---
await verifyUserIsAdmin(); await verifyUserIsAdmin();
if (userState.isAdmin && (localStorage.getItem('savedAdminData'))) {
console.log('saved admin data found (Q-Mintership.js), loading...')
const adminData = localStorage.getItem('savedAdminData')
const parsedAdminData = JSON.parse(adminData)
if (!adminPublicKeys || adminPublicKeys.length === 0 || !Array.isArray(adminPublicKeys)) {
console.log('no adminPublicKey variable data found and/or data did not pass checks, using fetched localStorage data...',adminPublicKeys)
if (parsedAdminData.publicKeys.length === 0 || !parsedAdminData.publicKeys || !Array.isArray(parsedAdminData.publicKeys)) {
console.log('loaded data from localStorage also did not pass checks... fetching from API...',parsedAdminData.publicKeys)
adminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
} else {
adminPublicKeys = parsedAdminData.publicKeys
}
}
}
if (userState.isAdmin) { if (userState.isAdmin) {
console.log(`User is an Admin. Admin-specific buttons will remain visible.`); console.log(`User is an Admin. Admin-specific buttons will remain visible.`);
@ -285,7 +350,17 @@ const loadRoomContent = async (room) => {
initializeQuillEditor(); initializeQuillEditor();
setupModalHandlers(); setupModalHandlers();
setupFileInputs(room); setupFileInputs(room);
await loadMessagesFromQDN(room, currentPage); //TODO - maybe turn this into its own function and put it as a button? But for now it's fine to just load the latest message's position by default I think.
const latestId = latestMessageIdentifiers[room]?.latestIdentifier;
if (latestId) {
const page = await findMessagePage(room, latestId, 10)
currentPage = page;
await loadMessagesFromQDN(room, currentPage)
scrollToMessage(latestId.latestIdentifier)
} else{
await loadMessagesFromQDN(room, currentPage)
}
;
}; };
// Initialize Quill editor // Initialize Quill editor
@ -572,6 +647,21 @@ const generateAttachmentID = (room, fileIndex = null) => {
// --- REFACTORED LOAD MESSAGES AND HELPER FUNCTIONS --- // --- REFACTORED LOAD MESSAGES AND HELPER FUNCTIONS ---
const findMessagePage = async (room, identifier, limit) => {
const { service, query } = getServiceAndQuery(room)
const allMessages = await searchAllWithOffset(service, query, 0, 0, room)
const idx = allMessages.findIndex(msg => msg.identifier === identifier);
if (idx === -1) {
// Not found, default to last page or page=0
return 0;
}
return Math.floor(idx / limit)
}
const loadMessagesFromQDN = async (room, page, isPolling = false) => { const loadMessagesFromQDN = async (room, page, isPolling = false) => {
try { try {
const limit = 10; const limit = 10;
@ -601,6 +691,12 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
let mostRecentMessage = getCurrentMostRecentMessage(room); let mostRecentMessage = getCurrentMostRecentMessage(room);
const fetchMessages = await fetchAllMessages(response, service, room); const fetchMessages = await fetchAllMessages(response, service, room);
for (const msg of fetchMessages) {
if (!msg) continue;
storeMessageInMap(msg);
}
const { firstNewMessageIdentifier, updatedMostRecentMessage } = await renderNewMessages( const { firstNewMessageIdentifier, updatedMostRecentMessage } = await renderNewMessages(
fetchMessages, fetchMessages,
existingIdentifiers, existingIdentifiers,
@ -625,6 +721,13 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
} }
}; };
function scrollToMessage(identifier) {
const targetElement = document.querySelector(`.message-item[data-identifier="${identifier}"]`);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
/** Helper Functions (Arrow Functions) **/ /** Helper Functions (Arrow Functions) **/
const prepareMessageContainer = (messagesContainer, isPolling) => { const prepareMessageContainer = (messagesContainer, isPolling) => {
@ -684,6 +787,13 @@ const fetchAllMessages = async (response, service, room) => {
// 2) fetchFullMessage is already async. We keep it async/await-based // 2) fetchFullMessage is already async. We keep it async/await-based
const fetchFullMessage = async (resource, service, room) => { const fetchFullMessage = async (resource, service, room) => {
// 1) Skip if we already have it in memory
if (messagesById[resource.identifier]) {
// Possibly also check if the local data is "up to date," //TODO when adding 'edit' ability to messages, will also need to verify timestamp in saved data.
// but if you trust your local data, skip the fetch entirely.
console.log(`Skipping fetch. Found in local store: ${resource.identifier}`);
return messagesById[resource.identifier];
}
try { try {
// Skip if already displayed // Skip if already displayed
if (existingIdentifiers.has(resource.identifier)) { if (existingIdentifiers.has(resource.identifier)) {
@ -703,7 +813,7 @@ const fetchFullMessage = async (resource, service, room) => {
const formattedTimestamp = await timestampToHumanReadableDate(timestamp); const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
const messageObject = await processMessageObject(messageResponse, room); const messageObject = await processMessageObject(messageResponse, room);
return { const builtMsg = {
name: resource.name, name: resource.name,
content: messageObject?.messageHtml || "<em>Message content missing</em>", content: messageObject?.messageHtml || "<em>Message content missing</em>",
date: formattedTimestamp, date: formattedTimestamp,
@ -712,6 +822,11 @@ const fetchFullMessage = async (resource, service, room) => {
timestamp, timestamp,
attachments: messageObject?.attachments || [], attachments: messageObject?.attachments || [],
}; };
// 3) Store it in the map so we skip future fetches
storeMessageInMap(builtMsg);
return builtMsg;
} catch (error) { } catch (error) {
console.error(`Failed to fetch message ${resource.identifier}: ${error.message}`); console.error(`Failed to fetch message ${resource.identifier}: ${error.message}`);
return { return {
@ -726,6 +841,46 @@ const fetchFullMessage = async (resource, service, room) => {
} }
}; };
const fetchReplyData = async (service, name, identifier, room, replyTimestamp) => {
try {
console.log(`Fetching message with identifier: ${identifier}`);
const messageResponse = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name,
service,
identifier,
...(room === "admins" ? { encoding: "base64" } : {}),
})
console.log('reply response',messageResponse)
const messageObject = await processMessageObject(messageResponse, room)
console.log('reply message object',messageObject)
const formattedTimestamp = await timestampToHumanReadableDate(replyTimestamp)
return {
name,
content: messageObject?.messageHtml || "<em>Message content missing</em>",
date: formattedTimestamp,
identifier,
replyTo: messageObject?.replyTo || null,
timestamp: replyTimestamp,
attachments: messageObject?.attachments || [],
};
} catch (error) {
console.error(`Failed to fetch message ${identifier}: ${error.message}`)
return {
name,
content: "<em>Error loading message</em>",
date: "Unknown",
identifier,
replyTo: null,
timestamp: null,
attachments: [],
}
}
}
const processMessageObject = async (messageResponse, room) => { const processMessageObject = async (messageResponse, room) => {
if (room !== "admins") { if (room !== "admins") {
@ -774,7 +929,7 @@ const isMessageNew = (message, mostRecentMessage) => {
}; };
const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => { const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
const replyHtml = await buildReplyHtml(message, fetchMessages); const replyHtml = await buildReplyHtml(message, room);
const attachmentHtml = await buildAttachmentHtml(message, room); const attachmentHtml = await buildAttachmentHtml(message, room);
const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`; const avatarUrl = `/arbitrary/THUMBNAIL/${message.name}/qortal_avatar`;
@ -798,18 +953,57 @@ const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => {
` `
} }
const buildReplyHtml = async (message, fetchMessages) => { const buildReplyHtml = async (message, room) => {
if (!message.replyTo) return "" if (!message.replyTo) return ""
const replyService = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST";
const replyIdentifier = message.replyTo
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo) const savedRepliedToMessage = messagesById[message.replyTo];
if (!repliedMessage) return ""
return ` if (savedRepliedToMessage) {
<div class="reply-message" style="border-left: 2px solid #ccc; margin-bottom: 0.5vh; padding-left: 1vh;"> const processedMessage = await processMessageObject(savedRepliedToMessage, room)
<div class="reply-header">In reply to: <span class="reply-username">${repliedMessage.name}</span> <span class="reply-timestamp">${repliedMessage.date}</span></div> console.log('message is saved in saved message data, returning from that',savedRepliedToMessage)
<div class="reply-content">${repliedMessage.content}</div> return `
</div> <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">${processedMessage.name}</span>
<span class="reply-timestamp">${processedMessage.date}</span>
</div>
<div class="reply-content">${processedMessage.content}</div>
</div>
`;
}
try {
const replyData = await searchSimple(replyService, replyIdentifier, "", 1)
if ((!replyData) || (!replyData.name)){
// No result found. You can either return an empty string or handle differently
console.log("No data found via searchSimple. Skipping reply rendering.");
return "";
}
const replyName = await replyData.name
const replyTimestamp = await replyData.updated || await replyData.created
console.log('message not found in saved message data, using searchSimple', replyData)
// const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
// const repliedMessageIdentifier = message.replyTo
const repliedMessage = await fetchReplyData(replyService, replyName, replyIdentifier, room, replyTimestamp)
storeMessageInMap(repliedMessage)
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>
`
} catch (error) {
throw (error)
}
} }
const buildAttachmentHtml = async (message, room) => { const buildAttachmentHtml = async (message, room) => {

View File

@ -651,9 +651,58 @@ const searchAllWithOffset = async (service, query, limit, offset, room) => {
console.error("Error during SEARCH_QDN_RESOURCES:", error); console.error("Error during SEARCH_QDN_RESOURCES:", error);
return []; // Return empty array on error return []; // Return empty array on error
} }
};
// NOTE - This function does a search and will return EITHER AN ARRAY OR A SINGLE OBJECT. if you want to guarantee a single object, pass 1 as limit. i.e. await searchSimple(service, identifier, "", 1) will return a single object.
const searchSimple = async (service, identifier, name, limit = 20) => {
try {
let urlSuffix = `service=${service}&identifier=${identifier}&name=${name}&limit=${limit}`;
if (name && !identifier) {
console.log('name only searchSimple', name);
urlSuffix = `service=${service}&name=${name}&limit=${limit}`;
} else if (!name && identifier) {
console.log('identifier only searchSimple', identifier);
urlSuffix = `service=${service}&identifier=${identifier}&limit=${limit}`;
} else if (!name && !identifier) {
console.error(`name: ${name} AND identifier: ${identifier} not passed. Must include at least one...`);
return null;
} else {
console.log(`name: ${name} AND identifier: ${identifier} passed, searching by both...`);
}
const response = await fetch(`${baseUrl}/arbitrary/resources/searchsimple?${urlSuffix}`, {
method: 'GET',
headers: { 'accept': 'application/json' }
});
const data = await response.json();
if (!Array.isArray(data)) {
console.log("searchSimple: data is not an array?", data);
return null;
}
if (data.length === 0) {
console.log("searchSimple: no results found");
return null; // Return null when no items
}
if (data.length === 1 || limit === 1) {
console.log("searchSimple: single result returned", data[0]);
return data[0]; // Return just the single object
}
console.log("searchSimple: multiple results returned", data);
return data;
} catch (error) {
console.error("error during searchSimple", error);
throw error;
}
}; };
const searchAllCountOnly = async (query, room) => { const searchAllCountOnly = async (query, room) => {
try { try {
let offset = 0; let offset = 0;

View File

@ -68,7 +68,7 @@
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt=""> <img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
</a> </a>
</span> </span>
<span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.6b<br></a></span> <span class="navbar-caption-wrap"><a class="navbar-caption text-primary display-4" href="index.html">Q-Mintership Alpha v0.61b<br></a></span>
</div> </div>
<ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul> <ul class="navbar-nav nav-dropdown" data-app-modern-menu="true"><li class="nav-item"><a class="nav-link link text-primary display-7" href="MINTERSHIP-FORUM"></a></li></ul>
@ -197,6 +197,23 @@
<section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6"> <section data-bs-version="5.1" class="content7 boldm5 cid-uufIRKtXOO" id="content7-6">
<div class="container">
<div class="row">
<div class="col-12 col-lg-7 card">
<div class="title-wrapper">
<h2 class="mbr-section-title mbr-fonts-style display-2">
Yet More Fixes + Updates 12-26-2024</h2>
</div>
</div>
<div class="col-12 col-lg-5 card">
<div class="text-wrapper">
<p class="mbr-text mbr-fonts-style display-7">
Another update has been accomplished, this time to version 0.61beta. This version includes many changes and performance improvements. Further performance improvements will be coming soon. This change includes a cache for the published data on the forum. Messages of up to 2000 in number, will be stored locally in browser storage, that way if the message has already been loaded by that computer, it will not have to pull the data again from QDN. It will be stored in encrypted format for the Admin room. This same caching will be applied to the Minter and Admin boards in the future. Also, reply issues that were present before should be resolved, all replies, regardless of when they were published, will now show their previews in the message pane as they are supposed to. Previously if a reply was on another page, it would not load this preview. The encrypted portions of the app now include a method of caching the admin public keys, for faster publishing. The Minter and Admin boards have a new comment loading display when the comments button is clicked to let users know that data is being loaded, on top of the existing comment count. Other new features and additional performance improvements are in planning. Also, the issue preventing comments from those that had not already loaded the forum, in the Admin Board, has been resolved as well. </p>
</div>
</div>
</div>
</div>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-lg-7 card"> <div class="col-12 col-lg-7 card">
@ -298,7 +315,7 @@
</div> </div>
<a class="link-wrap" href="#"> <a class="link-wrap" href="#">
<p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.6beta</p> <p class="mbr-link mbr-fonts-style display-4">Q-Mintership v0.61beta</p>
</a> </a>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">