const messageIdentifierPrefix = `mintership-forum-message` const messageAttachmentIdentifierPrefix = `mintership-forum-attachment` // Kakashi Note: Version label rendering is owned by index.html so release changes happen in one centralized location. // NOTE - SET adminGroups in QortalApi.js to enable admin access to forum for specific groups. Minter Admins will be fetched automatically. let replyToMessageIdentifier = null let latestMessageIdentifiers = {} // To keep track of the latest message in each room let currentPage = 0 // Track current pagination page let existingIdentifiers = new Set() // Keep track of existing identifiers to not pull them more than once. 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") ) } const BOARD_ROUTE_WAIT_INTERVAL_MS = 120 const BOARD_ROUTE_WAIT_TIMEOUT_MS = 90000 const BOARD_ROUTE_HIGHLIGHT_CLASS = "board-route-target" let qMintershipRouteState = { board: "", cardIdentifier: "", section: "", hash: "", } let qMintershipRouteFocusRequestId = 0 let qMintershipRouteHighlightTarget = null let qMintershipRouteHighlightTimer = null const normalizeBoardRouteKey = (value = "") => { const normalized = String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]/g, "") if (!normalized) return "" const aliasMap = { admin: "admin", adminboard: "admin", databoard: "admin", encryptedboard: "admin", stats: "stats", statistics: "stats", nominatorstats: "stats", minter: "minter", minters: "minter", minterboard: "minter", ar: "ar", mam: "ar", addremove: "ar", addremoveadmin: "ar", addremoveboard: "ar", } return aliasMap[normalized] || "" } const safeDecodeRouteSegment = (value = "") => { const raw = String(value || "").trim() if (!raw) return "" try { return decodeURIComponent(raw) } catch (error) { return raw } } const buildBoardRouteHash = ({ board = "", cardIdentifier = "", section = "", } = {}) => { const normalizedBoard = normalizeBoardRouteKey(board) if (!normalizedBoard) return "" const routeSegments = [normalizedBoard] const safeCardIdentifier = String(cardIdentifier || "").trim() const safeSection = String(section || "").trim() if (safeCardIdentifier) { routeSegments.push(encodeURIComponent(safeCardIdentifier)) } if (safeSection) { routeSegments.push(encodeURIComponent(safeSection)) } return `#/${routeSegments.join("/")}` } const setBoardRouteState = (route = {}) => { qMintershipRouteState.board = normalizeBoardRouteKey(route.board) qMintershipRouteState.cardIdentifier = String( route.cardIdentifier || "" ).trim() qMintershipRouteState.section = String(route.section || "").trim() qMintershipRouteState.hash = String(route.hash || "").trim() } const clearBoardRouteHighlight = () => { if (qMintershipRouteHighlightTimer) { window.clearTimeout(qMintershipRouteHighlightTimer) qMintershipRouteHighlightTimer = null } if (qMintershipRouteHighlightTarget) { qMintershipRouteHighlightTarget.classList.remove( BOARD_ROUTE_HIGHLIGHT_CLASS ) qMintershipRouteHighlightTarget = null } } const clearBoardRouteHash = () => { qMintershipRouteFocusRequestId += 1 setBoardRouteState({}) clearBoardRouteHighlight() if (window.location.hash) { window.history.replaceState( null, "", `${window.location.pathname}${window.location.search}` ) } } const parseBoardRouteHash = (rawHash = window.location.hash) => { const strippedHash = String(rawHash || "") .trim() .replace(/^#/, "") if (!strippedHash) return null let board = "" let cardIdentifier = "" let section = "" if (strippedHash.startsWith("/")) { const [pathPart, queryPart = ""] = strippedHash .replace(/^\//, "") .split("?") const pathSegments = pathPart .split("/") .filter(Boolean) .map((segment) => safeDecodeRouteSegment(segment)) board = normalizeBoardRouteKey(pathSegments[0] || "") cardIdentifier = pathSegments[1] || "" section = pathSegments[2] || "" if (queryPart) { const params = new URLSearchParams(queryPart) board = normalizeBoardRouteKey(params.get("board") || board) cardIdentifier = params.get("card") || params.get("cardIdentifier") || cardIdentifier section = params.get("section") || section } } else { const queryString = strippedHash.startsWith("?") ? strippedHash.slice(1) : strippedHash const params = new URLSearchParams(queryString) board = normalizeBoardRouteKey(params.get("board")) cardIdentifier = params.get("card") || params.get("cardIdentifier") || "" section = params.get("section") || "" } cardIdentifier = String(cardIdentifier || "").trim() section = String(section || "").trim() if (!board && !cardIdentifier && !section) { return null } return { board, cardIdentifier, section, hash: rawHash, } } const isBoardPageMounted = (boardKey = "") => { switch (normalizeBoardRouteKey(boardKey)) { case "minter": return Boolean( document.getElementById("display-mode-select") && document.getElementById("cards-container") && document.querySelector(".minter-board-main") ) case "admin": return Boolean( document.getElementById("encrypted-cards-container") && document.querySelector(".minter-board-main") ) case "ar": return Boolean( document.querySelector(".add-remove-admin-main") && document.getElementById("cards-container") ) case "stats": return Boolean( document.querySelector(".stats-board-main") && document.getElementById("stats-board-content") ) default: return false } } const ensureBoardPageScriptLoaded = async (boardKey = "") => { switch (normalizeBoardRouteKey(boardKey)) { case "minter": if (typeof loadMinterBoardPage === "undefined") { await loadScript("./assets/js/MinterBoard.js") } return typeof loadMinterBoardPage === "function" case "admin": if (typeof loadAdminBoardPage === "undefined") { await loadScript("./assets/js/AdminBoard.js") } return typeof loadAdminBoardPage === "function" case "ar": if (typeof loadAddRemoveAdminPage === "undefined") { await loadScript("./assets/js/ARBoard.js") } return typeof loadAddRemoveAdminPage === "function" case "stats": if (typeof loadStatsPage === "undefined") { await loadScript("./assets/js/StatsBoard.js") } return typeof loadStatsPage === "function" default: return false } } const loadBoardPageForRoute = async (boardKey = "") => { const normalizedBoard = normalizeBoardRouteKey(boardKey) if (!normalizedBoard) return false try { const scriptReady = await ensureBoardPageScriptLoaded(normalizedBoard) if (!scriptReady) { console.warn(`Board route script could not be loaded for ${normalizedBoard}`) return false } if (normalizedBoard === "minter") { await loadMinterBoardPage() } else if (normalizedBoard === "admin") { await loadAdminBoardPage() } else if (normalizedBoard === "ar") { await loadAddRemoveAdminPage() } else if (normalizedBoard === "stats") { await loadStatsPage() } return true } catch (error) { console.error(`Error loading board route for ${normalizedBoard}:`, error) return false } } const waitForBoardCardElement = async ( cardIdentifier = "", timeoutMs = BOARD_ROUTE_WAIT_TIMEOUT_MS, requestId = qMintershipRouteFocusRequestId ) => { const safeCardIdentifier = String(cardIdentifier || "").trim() if (!safeCardIdentifier) return null const targetId = `card-shell-${safeCardIdentifier}` const startTime = Date.now() while (Date.now() - startTime < timeoutMs) { if (requestId !== qMintershipRouteFocusRequestId) { return null } const target = document.getElementById(targetId) if (target) { return target } await qBoardDelay(BOARD_ROUTE_WAIT_INTERVAL_MS) } return document.getElementById(targetId) } const ensureMinterBoardListRouteState = async ( cardIdentifier = "", section = "", requestId = qMintershipRouteFocusRequestId ) => { const safeCardIdentifier = String(cardIdentifier || "").trim() if (!safeCardIdentifier) return if (requestId !== qMintershipRouteFocusRequestId) { return } const detail = document.getElementById( `minter-list-detail-${safeCardIdentifier}` ) if (!detail) return const showComments = ["all", "comments", "full"].includes( String(section || "").trim().toLowerCase() ) const controls = Array.from( document.querySelectorAll(`[aria-controls="${detail.id}"]`) ) const desiredButton = controls.find( (control) => String(control?.dataset?.showComments || "false").toLowerCase() === String(showComments).toLowerCase() ) || controls[0] || null if (detail.hidden) { if (typeof toggleMinterListDetails === "function" && desiredButton) { await toggleMinterListDetails(safeCardIdentifier, desiredButton) } else { detail.hidden = false if (typeof setMinterListCommentsVisibility === "function") { await setMinterListCommentsVisibility(safeCardIdentifier, showComments) } } } else if (typeof setMinterListCommentsVisibility === "function") { await setMinterListCommentsVisibility(safeCardIdentifier, showComments) } await qBoardDelay(0) } const highlightBoardRouteTarget = (targetEl) => { if (!targetEl) return clearBoardRouteHighlight() qMintershipRouteHighlightTarget = targetEl targetEl.classList.add(BOARD_ROUTE_HIGHLIGHT_CLASS) try { if (typeof targetEl.focus === "function") { targetEl.setAttribute("tabindex", "-1") targetEl.focus({ preventScroll: true }) } } catch (error) { console.warn("Unable to focus route target element:", error) } qMintershipRouteHighlightTimer = window.setTimeout(() => { if (qMintershipRouteHighlightTarget === targetEl) { targetEl.classList.remove(BOARD_ROUTE_HIGHLIGHT_CLASS) qMintershipRouteHighlightTarget = null qMintershipRouteHighlightTimer = null } }, 2500) } const focusBoardRoute = async (route = {}) => { const routeBoard = normalizeBoardRouteKey(route.board) const cardIdentifier = String(route.cardIdentifier || "").trim() const section = String(route.section || "").trim() const routeHash = String(route.hash || "").trim() || buildBoardRouteHash(route) const requestId = ++qMintershipRouteFocusRequestId setBoardRouteState({ board: routeBoard, cardIdentifier, section, hash: routeHash, }) if (!routeBoard) { return } if (!userState.isLoggedIn) { await login() } if (!userState.isLoggedIn) { return } if (!isBoardPageMounted(routeBoard) || qMintershipActiveBoard !== routeBoard) { const loaded = await loadBoardPageForRoute(routeBoard) if (!loaded || requestId !== qMintershipRouteFocusRequestId) { return } } if (routeBoard === "stats") { if (section) { if (typeof focusStatsBoardSection === "function") { await focusStatsBoardSection(section, { behavior: "smooth", }) } } else { if (typeof setStatsBoardSectionActiveState === "function") { setStatsBoardSectionActiveState("nominator") } try { window.scrollTo({ top: 0, behavior: "auto" }) } catch (error) { window.scrollTo(0, 0) } } return } if (!cardIdentifier) { return } const targetEl = await waitForBoardCardElement( cardIdentifier, BOARD_ROUTE_WAIT_TIMEOUT_MS, requestId ) if (!targetEl || requestId !== qMintershipRouteFocusRequestId) { console.warn(`Could not find card "${cardIdentifier}" for route ${routeHash}`) return } if (routeBoard === "minter") { await ensureMinterBoardListRouteState(cardIdentifier, section, requestId) if (requestId !== qMintershipRouteFocusRequestId) { return } } highlightBoardRouteTarget(targetEl) try { targetEl.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest", }) } catch (error) { console.warn("Unable to scroll to routed card:", error) targetEl.scrollIntoView() } } const handleBoardRouteFromHash = async (rawHash = window.location.hash) => { const route = parseBoardRouteHash(rawHash) if (!route) { return } await focusBoardRoute(route) } const openQortalLinkInNewTab = async (qortalLink = "") => { if (typeof qMintershipOpenQortalLinkInNewTab === "function") { await qMintershipOpenQortalLinkInNewTab(qortalLink) return } if (typeof qortalRequest !== "function") { return } const normalizedQortalLink = String(qortalLink || "").trim() if (!normalizedQortalLink) { return } try { await qortalRequest({ action: "OPEN_NEW_TAB", qortalLink: normalizedQortalLink, }) } catch (error) { console.error( "Unable to open Qortal link in a new tab:", normalizedQortalLink, error ) } } const attachQortalOpenNewTabHandlers = () => { const qortalExternalLinks = document.querySelectorAll( ".q-minter-link[data-qortal-link]" ) qortalExternalLinks.forEach((link) => { link.addEventListener("click", async (event) => { event.preventDefault() event.stopPropagation() if (typeof event.stopImmediatePropagation === "function") { event.stopImmediatePropagation() } const qortalLink = link.getAttribute("data-qortal-link")?.trim() if (!qortalLink) { return } await openQortalLinkInNewTab(qortalLink) }) }) } document.addEventListener("DOMContentLoaded", async () => { console.log("DOMContentLoaded fired!") createScrollToTopButton() // --- GENERAL LINKS (MINTERSHIP-FORUM and MINTER-BOARD) --- const mintershipForumLinks = document.querySelectorAll( 'a[href="MINTERSHIP-FORUM"]' ) mintershipForumLinks.forEach((link) => { link.addEventListener("click", async (event) => { event.preventDefault() if (!userState.isLoggedIn) { await login() } clearBoardRouteHash() await loadForumPage() loadRoomContent("general") startPollingForNewMessages() createScrollToTopButton() }) }) const minterBoardLinks = document.querySelectorAll( 'a[href="MINTER-BOARD"], a[href="MINTERS"]' ) minterBoardLinks.forEach((link) => { link.addEventListener("click", async (event) => { event.preventDefault() if (!userState.isLoggedIn) { await login() } clearBoardRouteHash() if (typeof loadMinterBoardPage === "undefined") { console.log( "loadMinterBoardPage not found, loading script dynamically..." ) await loadScript("./assets/js/MinterBoard.js") } await loadMinterBoardPage() }) }) const addRemoveAdminLinks = document.querySelectorAll( 'a[href="ADDREMOVEADMIN"]' ) addRemoveAdminLinks.forEach((link) => { link.addEventListener("click", async (event) => { event.preventDefault() // Possibly require user to login if not logged if (!userState.isLoggedIn) { await login() } clearBoardRouteHash() if (typeof loadAddRemoveAdminPage === "undefined") { console.log( "loadAddRemoveAdminPage not found, loading script dynamically..." ) await loadScript("./assets/js/ARBoard.js") } await loadAddRemoveAdminPage() }) }) const statsLinks = document.querySelectorAll('a[href="STATS"]') statsLinks.forEach((link) => { link.addEventListener("click", async (event) => { event.preventDefault() if (!userState.isLoggedIn) { await login() } clearBoardRouteHash() if (typeof loadStatsPage === "undefined") { console.log("loadStatsPage not found, loading script dynamically...") await loadScript("./assets/js/StatsBoard.js") } await loadStatsPage() }) }) attachQortalOpenNewTabHandlers() // --- 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 || userState.isForumAdmin || userState.isMinterAdmin) { console.log(`User is an Admin. Admin-specific buttons will remain visible.`) // DATA-BOARD Links for Admins const minterDataBoardLinks = document.querySelectorAll( 'a[href="ADMINBOARD"]' ) minterDataBoardLinks.forEach((link) => { link.addEventListener("click", async (event) => { event.preventDefault() if (!userState.isLoggedIn) { await login() } clearBoardRouteHash() if (typeof loadAdminBoardPage === "undefined") { console.log( "loadAdminBoardPage function not found, loading script dynamically..." ) await loadScript("./assets/js/AdminBoard.js") } await loadAdminBoardPage() }) }) // TOOLS Links for Admins const toolsLinks = document.querySelectorAll('a[href="TOOLS"]') toolsLinks.forEach((link) => { link.addEventListener("click", async (event) => { event.preventDefault() if (!userState.isLoggedIn) { await login() } clearBoardRouteHash() if (typeof loadMinterAdminToolsPage === "undefined") { console.log( "loadMinterAdminToolsPage function not found, loading script dynamically..." ) await loadScript("./assets/js/AdminTools.js") } await loadMinterAdminToolsPage() }) }) } else { console.log("User is NOT an Admin. Removing admin-specific links.") // Remove all admin-specific links and their parents const toolsLinks = document.querySelectorAll( 'a[href="TOOLS"], a[href="ADMINBOARD"]' ) toolsLinks.forEach((link) => { const buttonParent = link.closest("button") if (buttonParent) buttonParent.remove() const cardParent = link.closest(".item.features-image") if (cardParent) cardParent.remove() link.remove() }) // Center the remaining card if it exists const remainingCard = document.querySelector( ".features7 .row .item.features-image" ) if (remainingCard) { remainingCard.classList.remove("col-lg-6", "col-md-6") remainingCard.classList.add("col-12", "text-center") } } window.addEventListener("hashchange", () => { void handleBoardRouteFromHash(window.location.hash) }) const initialRoute = parseBoardRouteHash(window.location.hash) if (initialRoute) { await handleBoardRouteFromHash(initialRoute.hash) } console.log("All DOMContentLoaded tasks completed.") }) async function loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement("script") script.src = src script.onload = resolve script.onerror = reject document.head.appendChild(script) }) } // Main load function to clear existing HTML and load the forum page ----------------------------------------------------- const loadForumPage = async () => { clearQMintershipBodyContent() qMintershipActiveBoard = "forum" if (typeof userState.isAdmin === "undefined" || !userState.isAdmin) { try { // Fetch and verify the admin status asynchronously userState.isAdmin = await verifyUserIsAdmin() } catch (error) { console.error("Error verifying admin status:", error) userState.isAdmin = false // Default to non-admin if there's an issue } } const avatarUrl = `/arbitrary/THUMBNAIL/${userState.accountName}/qortal_avatar` const isAdmin = userState.isAdmin // Create the forum layout, including a header, sub-menu, and keeping the original background image: style="background-image: url('/assets/images/background.jpg');"> const mainContent = document.createElement("div") mainContent.innerHTML = `
No messages found. Be the first to post!
` } return true } return false } const getCurrentMostRecentMessage = (room) => { return latestMessageIdentifiers[room]?.latestTimestamp ? latestMessageIdentifiers[room] : null } // 1) Convert fetchAllMessages to fully async const fetchAllMessages = async (response, service, room) => { // Instead of returning Promise.all(...) directly, // we explicitly map each resource to a try/catch block. const messages = await Promise.all( response.map(async (resource) => { try { const msg = await fetchFullMessage(resource, service, room) return msg // This might be null if you do that check in fetchFullMessage } catch (err) { console.error( `Skipping resource ${resource.identifier} due to error:`, err ) // Return null so it doesn't break everything return null } }) ) // Filter out any that are null/undefined (missing or errored) return messages.filter(Boolean) } // 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)) { return null } console.log(`Fetching message with identifier: ${resource.identifier}`) const messageResponse = await qortalRequest({ action: "FETCH_QDN_RESOURCE", name: resource.name, service, identifier: resource.identifier, ...(room === "admins" ? { encoding: "base64" } : {}), }) const timestamp = resource.updated || resource.created const formattedTimestamp = await timestampToHumanReadableDate(timestamp) const messageObject = await processMessageObject(messageResponse, room) const builtMsg = { name: resource.name, content: messageObject?.messageHtml || "Message content missing", date: formattedTimestamp, identifier: resource.identifier, replyTo: messageObject?.replyTo || null, 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 { name: resource.name, content: "Error loading message", date: "Unknown", identifier: resource.identifier, replyTo: null, timestamp: resource.updated || resource.created, attachments: [], } } } 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 || "Message content missing", 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: "Error loading message", date: "Unknown", identifier, replyTo: null, timestamp: null, attachments: [], } } } const processMessageObject = async (messageResponse, room) => { if (room !== "admins") { return messageResponse } try { const decryptedData = await decryptAndParseObject(messageResponse) return decryptedData } catch (error) { console.error(`Failed to decrypt admin message: ${error.message}`) return null } } const renderNewMessages = async ( fetchMessages, existingIdentifiers, messagesContainer, room, mostRecentMessage ) => { let firstNewMessageIdentifier = null let updatedMostRecentMessage = mostRecentMessage for (const message of fetchMessages) { if (message && !existingIdentifiers.has(message.identifier)) { const isNewMessage = isMessageNew(message, mostRecentMessage) if (isNewMessage && !firstNewMessageIdentifier) { firstNewMessageIdentifier = message.identifier } const messageHTML = await buildMessageHTML( message, fetchMessages, room, isNewMessage ) messagesContainer.insertAdjacentHTML("beforeend", messageHTML) if ( !updatedMostRecentMessage || new Date(message.timestamp) > new Date(updatedMostRecentMessage?.latestTimestamp || 0) ) { updatedMostRecentMessage = { latestIdentifier: message.identifier, latestTimestamp: message.timestamp, } } existingIdentifiers.add(message.identifier) } } return { firstNewMessageIdentifier, updatedMostRecentMessage } } const isMessageNew = (message, mostRecentMessage) => { return ( !mostRecentMessage || new Date(message.timestamp) > new Date(mostRecentMessage?.latestTimestamp) ) } const buildMessageHTML = async (message, fetchMessages, room, isNewMessage) => { const replyHtml = await buildReplyHtml(message, room) const attachmentHtml = await buildAttachmentHtml(message, room) const avatarUrl = `/arbitrary/THUMBNAIL/${encodeURIComponent( message.name )}/qortal_avatar` const safeName = qEscapeHtml(message.name) const safeDate = qEscapeHtml(message.date) // Kakashi Note: Forum messages are sanitized before render so rich text remains readable without allowing injected scripts. const safeMessageContent = qSanitizeRichHtml(message.content) return ` ` } const buildReplyHtml = async (message, room) => { // 1) If no replyTo, skip if (!message.replyTo) return "" // 2) Decide which QDN service for this room const replyService = room === "admins" ? "MAIL_PRIVATE" : "BLOG_POST" const replyIdentifier = message.replyTo // 3) Check if we already have a *saved* message const savedRepliedToMessage = messagesById[replyIdentifier] console.log("savedRepliedToMessage", savedRepliedToMessage) // 4) If we do, try to process/decrypt it if (savedRepliedToMessage) { if (savedRepliedToMessage) { // We successfully processed the cached message console.log("Using saved message data for reply:", savedRepliedToMessage) const safeReplyName = qEscapeHtml(savedRepliedToMessage.name) const safeReplyDate = qEscapeHtml(savedRepliedToMessage.date) const safeReplyContent = qSanitizeRichHtml(savedRepliedToMessage.content) return ` ` } else { // The cached message is invalid console.log( "Saved message found but processMessageObject returned null. Falling back..." ) } } // 5) Fallback approach: If we don't have it in memory OR the cached version was invalid try { const replyData = await searchSimple(replyService, replyIdentifier, "", 1) if (!replyData || !replyData.name) { console.log("No data found via searchSimple. Skipping reply rendering.") return "" } // We'll use replyData to fetch the actual message from QDN const replyName = replyData.name const replyTimestamp = replyData.updated || replyData.created console.log( "message not found in workable form, using searchSimple result =>", replyData ) // This fetches and decrypts the actual message const repliedMessage = await fetchReplyData( replyService, replyName, replyIdentifier, room, replyTimestamp ) if (!repliedMessage) return "" // Now store the final message in the map for next time storeMessageInMap(repliedMessage) // Return final HTML const safeReplyName = qEscapeHtml(repliedMessage.name) const safeReplyDate = qEscapeHtml(repliedMessage.date) const safeReplyContent = qSanitizeRichHtml(repliedMessage.content) return ` ` } catch (error) { throw error } } const buildAttachmentHtml = async (message, room) => { if (!message.attachments || message.attachments.length === 0) { return "" } // Map over attachments -> array of Promises const attachmentsHtmlPromises = message.attachments.map((attachment) => buildSingleAttachmentHtml(attachment, room) ) // Wait for all Promises to resolve -> array of HTML strings const attachmentsHtmlArray = await Promise.all(attachmentsHtmlPromises) // Join them into a single string return attachmentsHtmlArray.join("") } const buildSingleAttachmentHtml = async (attachment, room) => { // Kakashi Note: Attachment metadata is escaped and passed through data-* attributes for safe button handlers. const safeService = qEscapeAttr(attachment.service) const safeName = qEscapeAttr(attachment.name) const safeIdentifier = qEscapeAttr(attachment.identifier) const safeFilenameAttr = qEscapeAttr(attachment.filename) const safeFilenameText = qEscapeHtml(attachment.filename) const safeMimeType = qEscapeAttr(attachment.mimeType) if ( room !== "admins" && attachment.mimeType && attachment.mimeType.startsWith("image/") ) { const imageUrl = `/arbitrary/${encodeURIComponent( attachment.service )}/${encodeURIComponent(attachment.name)}/${encodeURIComponent( attachment.identifier )}` return ` ` } else if ( room === "admins" && attachment.mimeType && attachment.mimeType.startsWith("image/") ) { // const imageUrl = `/arbitrary/${attachment.service}/${attachment.name}/${attachment.identifier}`; // const decryptedBase64 = await fetchEncryptedImageBase64(attachment.service, attachment.name, attachment.identifier, attachment.mimeType) // const dataUrl = `data:image/${attachment.mimeType};base64,${decryptedBase64}` //