const Q_MINTERSHIP_HUB_NOTIFICATION_STATE = { supportChecked: false, supported: false, permissionGranted: false, lastRegisteredAddress: "", lastRegisteredName: "", autoPromptedAddress: "", registrationInFlight: false, } const Q_MINTERSHIP_HUB_NOTIFICATION_STORAGE_KEY = "qmintership-hub-notification-declined-v1" const Q_MINTERSHIP_HUB_NOTIFICATION_LINK = "qortal://APP/Q-Mintership" const Q_MINTERSHIP_HUB_NOTIFICATION_IMAGE = "/arbitrary/THUMBNAIL/Q-Mintership/qortal_avatar?async=true" const Q_MINTERSHIP_HUB_NOTIFICATION_EVENT_PREFIX = "Mintership-notification-event-v1" const Q_MINTERSHIP_HUB_NOTIFICATION_TOKEN_PREFIX = "qmintership" const Q_MINTERSHIP_FORUM_MESSAGE_IDENTIFIER_PREFIX = "mintership-forum-message" const Q_MINTERSHIP_ADMIN_CARD_IDENTIFIER_PREFIX = "card-MAC" const Q_MINTERSHIP_AR_CARD_IDENTIFIER_PREFIX = "QM-AR-card" const Q_MINTERSHIP_COMMENT_IDENTIFIER_PREFIX = "comment-" const Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES = { minterNominee: { en: "{name} posted a nomination update for you", }, minterNominator: { en: "{name} posted a nomination update you created", }, minterReply: { en: "{name} replied to your nomination discussion", }, forumReply: { en: "{name} replied to your forum post", }, adminSubject: { en: "{name} published an Admin Board proposal about you", }, adminReply: { en: "{name} replied to your Admin Board comment", }, arSubject: { en: "{name} published an Add/Remove Admin proposal about you", }, arReply: { en: "{name} replied to your Add/Remove Admin comment", }, } const normalizeHubNotificationName = (value = "") => String(value ?? "") .trim() .toLowerCase() const normalizeHubNotificationScope = (value = "") => String(value ?? "") .trim() .toLowerCase() const normalizeHubNotificationRole = (value = "") => String(value ?? "") .trim() .toLowerCase() const readHubNotificationDeclinedMap = () => { try { const raw = localStorage.getItem(Q_MINTERSHIP_HUB_NOTIFICATION_STORAGE_KEY) if (!raw) { return {} } const parsed = JSON.parse(raw) return parsed && typeof parsed === "object" ? parsed : {} } catch (error) { console.warn("Unable to load Hub notification preferences:", error) return {} } } const writeHubNotificationDeclinedMap = (nextMap = {}) => { try { localStorage.setItem( Q_MINTERSHIP_HUB_NOTIFICATION_STORAGE_KEY, JSON.stringify(nextMap || {}) ) } catch (error) { console.warn("Unable to save Hub notification preferences:", error) } } const hashHubNotificationValue = async (value = "") => { const normalizedValue = normalizeHubNotificationName(value) if (!normalizedValue) { return "" } const subtle = typeof crypto !== "undefined" ? crypto.subtle : null if (!subtle || typeof TextEncoder === "undefined") { return normalizedValue } try { const encoded = new TextEncoder().encode(normalizedValue) const digest = await subtle.digest("SHA-256", encoded) return Array.from(new Uint8Array(digest)) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") } catch (error) { console.warn("Unable to hash Hub notification value:", error) return normalizedValue } } const buildHubNotificationToken = async ( scope = "", role = "", value = "" ) => { const normalizedScope = normalizeHubNotificationScope(scope) const normalizedRole = normalizeHubNotificationRole(role) const hashedValue = await hashHubNotificationValue(value) if (!normalizedScope || !normalizedRole || !hashedValue) { return "" } return `${Q_MINTERSHIP_HUB_NOTIFICATION_TOKEN_PREFIX}:${normalizedScope}:${normalizedRole}:${hashedValue}` } const buildHubNotificationCardToken = (scope = "", value = "") => { const normalizedScope = normalizeHubNotificationScope(scope) const normalizedValue = normalizeHubNotificationName(value) if (!normalizedScope || !normalizedValue) { return "" } return `${Q_MINTERSHIP_HUB_NOTIFICATION_TOKEN_PREFIX}:${normalizedScope}:card:${normalizedValue}` } const buildHubNotificationDescription = async (entries = []) => { const tokens = [] for (const entry of Array.isArray(entries) ? entries : []) { if (!entry) { continue } const scope = normalizeHubNotificationScope( entry.scope || entry.board || "" ) const kind = normalizeHubNotificationRole(entry.kind || entry.type || "") const role = normalizeHubNotificationRole(entry.role || "") const value = String( entry.value || entry.identifier || entry.cardIdentifier || entry.name || "" ).trim() let token = "" if (kind === "card" || role === "card") { token = buildHubNotificationCardToken(scope, value) } else if (scope && role && value) { token = await buildHubNotificationToken(scope, role, value) } if (token) { tokens.push(token) } } return Array.from(new Set(tokens)).join(" ") } const buildMinterHubNotificationDescription = async (event = {}) => { const eventContext = typeof getMinterNotificationEventContext === "function" ? getMinterNotificationEventContext(event) : {} const cardIdentifier = String(event.cardIdentifier || "").trim() const tokens = [] if (cardIdentifier) { tokens.push(buildHubNotificationCardToken("minter", cardIdentifier)) } const nomineeName = String( event.nomineeName || eventContext.nomineeName || "" ).trim() const nominatorName = String( event.nominatorName || eventContext.nominatorName || "" ).trim() const replyAuthorName = String( event.replyAuthorName || eventContext.replyAuthorName || "" ).trim() const scopedTokens = await buildHubNotificationDescription([ { scope: "minter", role: "nominee", value: nomineeName }, { scope: "minter", role: "nominator", value: nominatorName }, { scope: "minter", role: "reply", value: replyAuthorName }, ]) if (scopedTokens) { tokens.push(scopedTokens) } return Array.from( new Set(tokens.flatMap((token) => String(token || "").split(" ")).filter(Boolean)) ).join(" ") } const refreshHubNotificationPrompt = () => { const prompt = document.getElementById("hub-notification-prompt") if (!prompt) { return } const address = String(userState.accountAddress || "").trim() const hasRegisteredName = Boolean(String(userState.accountName || "").trim()) if ( !address || !hasRegisteredName || !Q_MINTERSHIP_HUB_NOTIFICATION_STATE.supported || Q_MINTERSHIP_HUB_NOTIFICATION_STATE.permissionGranted ) { prompt.hidden = true prompt.innerHTML = "" return } const title = "Enable Hub notifications" const subtitle = "Get push alerts in Qortal Hub when forum replies, proposal cards, or nomination updates are published." const buttonLabel = "Enable Hub notifications" prompt.innerHTML = `
${qEscapeHtml(title)} ${qEscapeHtml(subtitle)}
` prompt.hidden = false } const registerHubNotificationSubscriptions = async () => { const address = String(userState.accountAddress || "").trim() const name = String(userState.accountName || "").trim() if (!address || !name || !Q_MINTERSHIP_HUB_NOTIFICATION_STATE.permissionGranted) { refreshHubNotificationPrompt() return false } const normalizedAddress = address const normalizedName = normalizeHubNotificationName(name) if ( Q_MINTERSHIP_HUB_NOTIFICATION_STATE.registrationInFlight || (Q_MINTERSHIP_HUB_NOTIFICATION_STATE.lastRegisteredAddress === normalizedAddress && Q_MINTERSHIP_HUB_NOTIFICATION_STATE.lastRegisteredName === normalizedName) ) { refreshHubNotificationPrompt() return true } Q_MINTERSHIP_HUB_NOTIFICATION_STATE.registrationInFlight = true try { const notificationDefinitions = [ { notificationId: `qmintership-hub-minter-nominee-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.minterNominee, service: "BLOG_POST", identifier: Q_MINTERSHIP_HUB_NOTIFICATION_EVENT_PREFIX, scope: "minter", role: "nominee", }, { notificationId: `qmintership-hub-minter-nominator-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.minterNominator, service: "BLOG_POST", identifier: Q_MINTERSHIP_HUB_NOTIFICATION_EVENT_PREFIX, scope: "minter", role: "nominator", }, { notificationId: `qmintership-hub-minter-reply-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.minterReply, service: "BLOG_POST", identifier: Q_MINTERSHIP_HUB_NOTIFICATION_EVENT_PREFIX, scope: "minter", role: "reply", }, { notificationId: `qmintership-hub-forum-reply-blog-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.forumReply, service: "BLOG_POST", identifier: Q_MINTERSHIP_FORUM_MESSAGE_IDENTIFIER_PREFIX, scope: "forum", role: "reply", }, { notificationId: `qmintership-hub-forum-reply-mail-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.forumReply, service: "MAIL_PRIVATE", identifier: Q_MINTERSHIP_FORUM_MESSAGE_IDENTIFIER_PREFIX, scope: "forum", role: "reply", }, { notificationId: `qmintership-hub-admin-subject-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.adminSubject, service: "MAIL_PRIVATE", identifier: Q_MINTERSHIP_ADMIN_CARD_IDENTIFIER_PREFIX, scope: "admin", role: "subject", }, { notificationId: `qmintership-hub-admin-reply-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.adminReply, service: "MAIL_PRIVATE", identifier: Q_MINTERSHIP_COMMENT_IDENTIFIER_PREFIX, scope: "admin", role: "reply", }, { notificationId: `qmintership-hub-ar-subject-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.arSubject, service: "BLOG_POST", identifier: Q_MINTERSHIP_AR_CARD_IDENTIFIER_PREFIX, scope: "ar", role: "subject", }, { notificationId: `qmintership-hub-ar-reply-${normalizedName}`, message: Q_MINTERSHIP_HUB_NOTIFICATION_MESSAGES.arReply, service: "BLOG_POST", identifier: Q_MINTERSHIP_COMMENT_IDENTIFIER_PREFIX, scope: "ar", role: "reply", }, ] const notifications = ( await Promise.all( notificationDefinitions.map(async (definition) => { const description = await buildHubNotificationToken( definition.scope, definition.role, normalizedName ) if ( !definition.notificationId || !definition.service || !definition.identifier || !description ) { return null } return { notificationId: definition.notificationId, link: Q_MINTERSHIP_HUB_NOTIFICATION_LINK, image: Q_MINTERSHIP_HUB_NOTIFICATION_IMAGE, message: definition.message, filters: { service: definition.service, identifier: definition.identifier, description, excludeBlocked: true, mode: "ALL", }, } }) ) ).filter(Boolean) try { await qortalRequest({ action: "NOTIFICATION_REMOVE", }) } catch (error) { console.warn("Unable to clear existing Hub notification registrations:", error) } await qortalRequest({ action: "NOTIFICATION_ADD", notifications, }) Q_MINTERSHIP_HUB_NOTIFICATION_STATE.lastRegisteredAddress = normalizedAddress Q_MINTERSHIP_HUB_NOTIFICATION_STATE.lastRegisteredName = normalizedName refreshHubNotificationPrompt() return true } catch (error) { console.error("Failed to register Hub notifications:", error) return false } finally { Q_MINTERSHIP_HUB_NOTIFICATION_STATE.registrationInFlight = false } } const requestHubNotificationPermission = async () => { const address = String(userState.accountAddress || "").trim() if (!address) { return false } const supportChecked = Q_MINTERSHIP_HUB_NOTIFICATION_STATE.supportChecked if (!supportChecked) { await initializeHubNotifications({ prompt: false }) } if (!Q_MINTERSHIP_HUB_NOTIFICATION_STATE.supported) { refreshHubNotificationPrompt() return false } try { const result = await qortalRequest({ action: "NOTIFICATION_PERMISSION", }) if (!result) { throw new Error("Hub notification permission was not granted.") } const declinedMap = readHubNotificationDeclinedMap() delete declinedMap[address] writeHubNotificationDeclinedMap(declinedMap) Q_MINTERSHIP_HUB_NOTIFICATION_STATE.permissionGranted = true await registerHubNotificationSubscriptions() refreshHubNotificationPrompt() return true } catch (error) { const declinedMap = readHubNotificationDeclinedMap() declinedMap[address] = true writeHubNotificationDeclinedMap(declinedMap) Q_MINTERSHIP_HUB_NOTIFICATION_STATE.permissionGranted = false console.error("Notification permission declined or errored:", error) refreshHubNotificationPrompt() return false } } const initializeHubNotifications = async ({ prompt = true } = {}) => { const address = String(userState.accountAddress || "").trim() const name = String(userState.accountName || "").trim() if (!address || !name || typeof qortalRequest !== "function") { refreshHubNotificationPrompt() return false } const previousAddress = Q_MINTERSHIP_HUB_NOTIFICATION_STATE.lastRegisteredAddress const previousName = Q_MINTERSHIP_HUB_NOTIFICATION_STATE.lastRegisteredName if ( previousAddress && (previousAddress !== address || previousName !== normalizeHubNotificationName(name)) ) { Q_MINTERSHIP_HUB_NOTIFICATION_STATE.permissionGranted = false Q_MINTERSHIP_HUB_NOTIFICATION_STATE.autoPromptedAddress = "" } if (!Q_MINTERSHIP_HUB_NOTIFICATION_STATE.supportChecked) { try { const actions = await qortalRequest({ action: "SHOW_ACTIONS" }) Q_MINTERSHIP_HUB_NOTIFICATION_STATE.supported = Array.isArray(actions) && actions.includes("NOTIFICATION_PERMISSION") } catch (error) { Q_MINTERSHIP_HUB_NOTIFICATION_STATE.supported = false } finally { Q_MINTERSHIP_HUB_NOTIFICATION_STATE.supportChecked = true } } if (!Q_MINTERSHIP_HUB_NOTIFICATION_STATE.supported) { refreshHubNotificationPrompt() return false } const declinedMap = readHubNotificationDeclinedMap() const wasDeclined = Boolean(declinedMap[address]) if (prompt && !wasDeclined && !Q_MINTERSHIP_HUB_NOTIFICATION_STATE.permissionGranted) { if (Q_MINTERSHIP_HUB_NOTIFICATION_STATE.autoPromptedAddress !== address) { Q_MINTERSHIP_HUB_NOTIFICATION_STATE.autoPromptedAddress = address await requestHubNotificationPermission() return true } } if (Q_MINTERSHIP_HUB_NOTIFICATION_STATE.permissionGranted) { await registerHubNotificationSubscriptions() } refreshHubNotificationPrompt() return Q_MINTERSHIP_HUB_NOTIFICATION_STATE.permissionGranted }