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:
parent
0fc471a1b8
commit
2192f7c855
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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.
|
||||
const isEncryptedTestMode = true
|
||||
const isEncryptedTestMode = false
|
||||
const encryptedCardIdentifierPrefix = "card-MAC"
|
||||
let isExistingEncryptedCard = false
|
||||
let existingDecryptedCardData = {}
|
||||
@ -9,6 +9,7 @@ let existingCardMinterNames = []
|
||||
let isTopic = false
|
||||
let attemptLoadAdminDataCount = 0
|
||||
let adminMemberCount = 0
|
||||
let adminPublicKeys = []
|
||||
|
||||
console.log("Attempting to load AdminBoard.js");
|
||||
|
||||
@ -112,7 +113,7 @@ const loadAdminBoardPage = async () => {
|
||||
const updateOrSaveAdminGroupsDataLocally = async () => {
|
||||
try {
|
||||
// Fetch the array of admin public keys
|
||||
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys();
|
||||
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
||||
|
||||
// Build an object containing the count and the array
|
||||
const adminData = {
|
||||
@ -120,12 +121,14 @@ const updateOrSaveAdminGroupsDataLocally = async () => {
|
||||
publicKeys: verifiedAdminPublicKeys
|
||||
};
|
||||
|
||||
// Stringify and save to localStorage
|
||||
localStorage.setItem('savedAdminData', JSON.stringify(adminData));
|
||||
adminPublicKeys = verifiedAdminPublicKeys
|
||||
|
||||
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) {
|
||||
console.error('Error fetching/storing admin public keys:', error);
|
||||
console.error('Error fetching/storing admin public keys:', error)
|
||||
attemptLoadAdminDataCount++
|
||||
}
|
||||
};
|
||||
@ -146,7 +149,10 @@ const loadOrFetchAdminGroupsData = async () => {
|
||||
adminMemberCount = parsedData.keysCount
|
||||
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
|
||||
|
||||
return parsedData; // and return { adminMemberCount, adminKeys } to the caller
|
||||
@ -526,7 +532,22 @@ const publishEncryptedCard = async (isTopicModePassed = false) => {
|
||||
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({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
@ -605,8 +626,9 @@ const postEncryptedComment = async (cardIdentifier) => {
|
||||
|
||||
const commentIdentifier = `comment-${cardIdentifier}-${await uid()}`;
|
||||
|
||||
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0)) {
|
||||
const verifiedAdminPublicKeys = await loadOrFetchAdminGroupsData().publicKeys
|
||||
if (!Array.isArray(adminPublicKeys) || (adminPublicKeys.length === 0) || (!adminPublicKeys)) {
|
||||
console.log('adminPpublicKeys variable failed checks, calling for admin public keys from API (comment)',adminPublicKeys)
|
||||
const verifiedAdminPublicKeys = await fetchAdminGroupsMembersPublicKeys()
|
||||
adminPublicKeys = verifiedAdminPublicKeys
|
||||
}
|
||||
|
||||
@ -731,12 +753,24 @@ const calculateAdminBoardPollResults = async (pollData, minterGroupMembers, mint
|
||||
}
|
||||
|
||||
const toggleEncryptedComments = async (cardIdentifier) => {
|
||||
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`);
|
||||
if (commentsSection.style.display === 'none' || !commentsSection.style.display) {
|
||||
const commentsSection = document.getElementById(`comments-section-${cardIdentifier}`)
|
||||
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);
|
||||
commentsSection.style.display = 'block';
|
||||
// Change the button text to 'HIDE COMMENTS'
|
||||
commentButton.textContent = 'HIDE COMMENTS';
|
||||
} else {
|
||||
// Hide comments
|
||||
commentsSection.style.display = 'none';
|
||||
commentButton.textContent = `COMMENTS (${count})`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -847,7 +881,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
showTopic = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const cardColorCode = showTopic ? '#0e1b15' : '#151f28'
|
||||
|
||||
const minterOrTopicHtml = ((showTopic) || (isUndefinedUser)) ? `
|
||||
@ -895,7 +929,7 @@ const createEncryptedCardHTML = async (cardData, pollResults, cardIdentifier, co
|
||||
<div class="actions">
|
||||
<div class="actions-buttons">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -117,6 +117,78 @@ const loadMinterBoardPage = async () => {
|
||||
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 ----------------------------------------
|
||||
const loadCards = async () => {
|
||||
const cardsContainer = document.getElementById("cards-container");
|
||||
@ -150,22 +222,24 @@ const loadCards = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const finalCards = await processMinterCards(validCards)
|
||||
|
||||
// Sort cards by timestamp descending (newest first)
|
||||
validCards.sort((a, b) => {
|
||||
const timestampA = a.updated || a.created || 0;
|
||||
const timestampB = b.updated || b.created || 0;
|
||||
return timestampB - timestampA;
|
||||
});
|
||||
// validCards.sort((a, b) => {
|
||||
// const timestampA = a.updated || a.created || 0;
|
||||
// const timestampB = b.updated || b.created || 0;
|
||||
// return timestampB - timestampA;
|
||||
// });
|
||||
|
||||
// Display skeleton cards immediately
|
||||
cardsContainer.innerHTML = "";
|
||||
validCards.forEach(card => {
|
||||
finalCards.forEach(card => {
|
||||
const skeletonHTML = createSkeletonCardHTML(card.identifier);
|
||||
cardsContainer.insertAdjacentHTML("beforeend", skeletonHTML);
|
||||
});
|
||||
|
||||
// Fetch and update each card
|
||||
validCards.forEach(async card => {
|
||||
finalCards.forEach(async card => {
|
||||
try {
|
||||
const cardDataResponse = await qortalRequest({
|
||||
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 --------------------
|
||||
const toggleComments = async (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);
|
||||
commentsSection.style.display = 'block';
|
||||
// Change the button text to 'HIDE COMMENTS'
|
||||
commentButton.textContent = 'HIDE COMMENTS';
|
||||
} else {
|
||||
// Hide comments
|
||||
commentsSection.style.display = 'none';
|
||||
commentButton.textContent = `COMMENTS (${count})`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -610,14 +696,13 @@ const generateDarkPastelBackgroundBy = (name) => {
|
||||
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 hueSteps = 69.69;
|
||||
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
|
||||
|
||||
const hueRange = 288;
|
||||
const hue = 140 + (hueIndex * (hueRange / hueSteps));
|
||||
|
||||
|
||||
// 3) Saturation:
|
||||
const satSteps = 13.69;
|
||||
@ -690,7 +775,7 @@ const createCardHTML = async (cardData, pollResults, cardIdentifier, commentCoun
|
||||
<div class="actions">
|
||||
<div class="actions-buttons">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,5 @@
|
||||
const messageIdentifierPrefix = `mintership-forum-message`;
|
||||
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.
|
||||
|
||||
@ -9,7 +8,58 @@ let latestMessageIdentifiers = {}; // To keep track of the latest message in eac
|
||||
let currentPage = 0; // Track current pagination page
|
||||
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.
|
||||
|
||||
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")) {
|
||||
latestMessageIdentifiers = JSON.parse(localStorage.getItem("latestMessageIdentifiers"));
|
||||
}
|
||||
@ -49,6 +99,21 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
// --- ADMIN CHECK ---
|
||||
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) {
|
||||
console.log(`User is an Admin. Admin-specific buttons will remain visible.`);
|
||||
|
||||
@ -285,7 +350,17 @@ const loadRoomContent = async (room) => {
|
||||
initializeQuillEditor();
|
||||
setupModalHandlers();
|
||||
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
|
||||
@ -572,6 +647,21 @@ const generateAttachmentID = (room, fileIndex = null) => {
|
||||
|
||||
// --- 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) => {
|
||||
try {
|
||||
const limit = 10;
|
||||
@ -601,6 +691,12 @@ const loadMessagesFromQDN = async (room, page, isPolling = false) => {
|
||||
let mostRecentMessage = getCurrentMostRecentMessage(room);
|
||||
|
||||
const fetchMessages = await fetchAllMessages(response, service, room);
|
||||
|
||||
for (const msg of fetchMessages) {
|
||||
if (!msg) continue;
|
||||
storeMessageInMap(msg);
|
||||
}
|
||||
|
||||
const { firstNewMessageIdentifier, updatedMostRecentMessage } = await renderNewMessages(
|
||||
fetchMessages,
|
||||
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) **/
|
||||
|
||||
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
|
||||
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 {
|
||||
// Skip if already displayed
|
||||
if (existingIdentifiers.has(resource.identifier)) {
|
||||
@ -703,7 +813,7 @@ const fetchFullMessage = async (resource, service, room) => {
|
||||
const formattedTimestamp = await timestampToHumanReadableDate(timestamp);
|
||||
const messageObject = await processMessageObject(messageResponse, room);
|
||||
|
||||
return {
|
||||
const builtMsg = {
|
||||
name: resource.name,
|
||||
content: messageObject?.messageHtml || "<em>Message content missing</em>",
|
||||
date: formattedTimestamp,
|
||||
@ -712,6 +822,11 @@ const fetchFullMessage = async (resource, service, room) => {
|
||||
timestamp,
|
||||
attachments: messageObject?.attachments || [],
|
||||
};
|
||||
|
||||
// 3) Store it in the map so we skip future fetches
|
||||
storeMessageInMap(builtMsg);
|
||||
|
||||
return builtMsg;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message ${resource.identifier}: ${error.message}`);
|
||||
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) => {
|
||||
if (room !== "admins") {
|
||||
@ -774,7 +929,7 @@ const isMessageNew = (message, mostRecentMessage) => {
|
||||
};
|
||||
|
||||
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 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 ""
|
||||
const replyService = (room === "admins") ? "MAIL_PRIVATE" : "BLOG_POST";
|
||||
const replyIdentifier = message.replyTo
|
||||
|
||||
const repliedMessage = fetchMessages.find(m => m && m.identifier === message.replyTo)
|
||||
if (!repliedMessage) return ""
|
||||
const savedRepliedToMessage = messagesById[message.replyTo];
|
||||
|
||||
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>
|
||||
`
|
||||
if (savedRepliedToMessage) {
|
||||
const processedMessage = await processMessageObject(savedRepliedToMessage, room)
|
||||
console.log('message is saved in saved message data, returning from that',savedRepliedToMessage)
|
||||
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">${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) => {
|
||||
|
@ -651,9 +651,58 @@ const searchAllWithOffset = async (service, query, limit, offset, room) => {
|
||||
console.error("Error during SEARCH_QDN_RESOURCES:", 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) => {
|
||||
try {
|
||||
let offset = 0;
|
||||
|
21
index.html
21
index.html
@ -68,7 +68,7 @@
|
||||
<img src="assets/images/again-edited-qortal-minting-icon-156x156.png" alt="">
|
||||
</a>
|
||||
</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>
|
||||
<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">
|
||||
|
||||
<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="row">
|
||||
<div class="col-12 col-lg-7 card">
|
||||
@ -298,7 +315,7 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
|
Loading…
Reference in New Issue
Block a user