From bfbedac30529b20c8deb34c37f5241ad349d469f Mon Sep 17 00:00:00 2001 From: Bernard Gatt Date: Mon, 24 Nov 2025 14:46:13 +0100 Subject: [PATCH 1/8] Added the ability to move between message steps with different display types --- src/managers/message-component-manager.js | 20 ++- src/managers/message-manager.js | 177 +++++++++++++++++++++- 2 files changed, 184 insertions(+), 13 deletions(-) diff --git a/src/managers/message-component-manager.js b/src/managers/message-component-manager.js index 6000c1f..80937e8 100644 --- a/src/managers/message-component-manager.js +++ b/src/managers/message-component-manager.js @@ -17,7 +17,7 @@ export function isElementLoaded(elementId) { } } -export function loadEmbedComponent(elementId, url, message, options) { +export function loadEmbedComponent(elementId, url, message, options, stepName = null) { var element = safelyFetchElement(elementId); if (element) { var messageElementId = getMessageElementId(message.instanceId); @@ -35,7 +35,7 @@ export function loadEmbedComponent(elementId, url, message, options) { element.style.height = "0px"; } element.innerHTML = embed(url, message, messageProperties); - attachIframeLoadEvent(messageElementId, options); + attachIframeLoadEvent(messageElementId, options, stepName); } else { log(`Message could not be embedded, elementId ${elementId} not found.`); } @@ -85,24 +85,28 @@ export function resizeComponent(message, size) { } } -export function loadOverlayComponent(url, message, options) { +export function loadOverlayComponent(url, message, options, stepName = null) { document.body.insertAdjacentHTML('afterbegin', component(url, message)); - attachIframeLoadEvent(getMessageElementId(message.instanceId), options); + attachIframeLoadEvent(getMessageElementId(message.instanceId), options, stepName); } -function attachIframeLoadEvent(elementId, options) { +function attachIframeLoadEvent(elementId, options, stepName = null) { const iframe = document.getElementById(elementId); if (iframe) { iframe.onload = function() { - sendOptionsToIframe(elementId, options); // Send the options when iframe loads + sendOptionsToIframe(elementId, options, stepName); // Send the options when iframe loads }; } } -function sendOptionsToIframe(iframeId, options) { +export function sendOptionsToIframe(iframeId, options, stepName = null) { const iframe = document.getElementById(iframeId); if (iframe && iframe.contentWindow) { - iframe.contentWindow.postMessage({ options: options }, '*'); + const message = { options: options }; + if (stepName) { + options.stepId = stepName; + } + iframe.contentWindow.postMessage(message, '*'); } } diff --git a/src/managers/message-manager.js b/src/managers/message-manager.js index d9d2d34..5dc6827 100644 --- a/src/managers/message-manager.js +++ b/src/managers/message-manager.js @@ -14,7 +14,8 @@ import { resizeComponent, elementHasHeight, isElementLoaded, - changeOverlayTitle + changeOverlayTitle, + sendOptionsToIframe } from "./message-component-manager"; import { resolveMessageProperties } from "./gist-properties-manager"; import { positions, addPageElement } from "./page-component-manager"; @@ -114,7 +115,7 @@ async function resetOverlayState(hideFirst, message) { Gist.overlayInstanceId = null; } -function loadMessageComponent(message, elementId = null) { +function loadMessageComponent(message, elementId = null, stepName = null) { if (elementId && isElementLoaded(elementId)) { log(`Message ${message.messageId} already showing in element ${elementId}.`); return null; @@ -130,20 +131,25 @@ function loadMessageComponent(message, elementId = null) { properties: message.properties, customAttributes: Object.fromEntries(getAllCustomAttributes()) } + var url = `${settings.GIST_VIEW_ENDPOINT[Gist.config.env]}/index.html` window.addEventListener('message', handleGistEvents); window.addEventListener('touchstart', handleTouchStartEvents); if (elementId) { if (positions.includes(elementId)) { addPageElement(elementId); } - loadEmbedComponent(elementId, url, message, options); + loadEmbedComponent(elementId, url, message, options, stepName); } else { - loadOverlayComponent(url, message, options); + loadOverlayComponent(url, message, options, stepName); } return message; } +function getMessageElementId(instanceId) { + return `gist-${instanceId}`; +} + async function reportMessageView(message) { log(`Message shown, logging view for: ${message.messageId}`); @@ -175,7 +181,6 @@ function updateMessageByInstanceId(instanceId, message) { Gist.currentMessages.push(message); } - function handleTouchStartEvents() { // Added this to avoid errors in the console } @@ -253,6 +258,32 @@ async function handleGistEvents(e) { } break; } + } else if (url && url.protocol === "inapp:") { + var inappAction = url.href.replace("inapp://", "").split('?')[0]; + switch (inappAction) { + case "changeMessage": + var displaySettings = e.data.gist.parameters.options?.displaySettings; + var messageStepName = e.data.gist.parameters.options?.messageStepName; + + if (displaySettings && hasDisplayChanged(currentMessage, displaySettings)) { + log(`Display settings changed, reloading message`); + // Hide visually without side effects + await hideMessageVisually(currentMessage); + + // Apply new display settings + applyDisplaySettings(currentMessage, displaySettings); + + // Re-show message with new settings + await reloadMessageWithNewDisplay(currentMessage, messageStepName); + } else { + // Just send stepName to iframe for navigation + if (messageStepName) { + log(`Navigating to step: ${messageStepName}`); + sendOptionsToIframe(getMessageElementId(currentMessage.instanceId), {}, messageStepName); + } + } + break; + } } } catch { // If the action is not a URL, we don't need to do anything. @@ -297,6 +328,142 @@ async function handleGistEvents(e) { } } +// Reload message with new display settings +async function reloadMessageWithNewDisplay(message, stepName) { + // Set firstLoad to false to prevent duplicate logging and event triggering + message.firstLoad = false; + + // Update Gist.overlayInstanceId based on new display type + if (message.overlay) { + Gist.overlayInstanceId = message.instanceId; + } else { + Gist.overlayInstanceId = null; + } + + // Determine elementId based on display type + var elementId = message.elementId || null; + + // Add page element if it's an overlay position + if (elementId && positions.includes(elementId)) { + addPageElement(elementId); + } + + // Reload the message component with new settings + loadMessageComponent(message, elementId, stepName); + + // Show the component immediately since firstLoad is false + if (message.overlay) { + showOverlayComponent(message); + } else if (elementId) { + showEmbedComponent(elementId); + } +} + +// Helper function to map overlay positions to element IDs +function mapOverlayPositionToElementId(overlayPosition) { + const positionMap = { + "topLeft": "x-gist-floating-top-left", + "topCenter": "x-gist-floating-top", + "topRight": "x-gist-floating-top-right", + "bottomLeft": "x-gist-floating-bottom-left", + "bottomCenter": "x-gist-floating-bottom", + "bottomRight": "x-gist-floating-bottom-right" + }; + return positionMap[overlayPosition] || "x-gist-floating-bottom"; +} + +// Helper function to determine current display type +function getCurrentDisplayType(message) { + if (message.overlay) { + return "modal"; + } else if (message.elementId && positions.includes(message.elementId)) { + return "overlay"; + } else if (message.elementId) { + return "inline"; + } + return "modal"; // default +} + +// Helper function to check if display settings have changed +function hasDisplayChanged(currentMessage, displaySettings) { + const currentDisplayType = getCurrentDisplayType(currentMessage); + const newDisplayType = displaySettings.displayType; + + // Check if display type changed + if (currentDisplayType !== newDisplayType) { + return true; + } + + // Check if position changed within the same display type + if (newDisplayType === "modal") { + const currentPosition = currentMessage.position || "center"; + const newPosition = displaySettings.modalPosition || "center"; + if (currentPosition !== newPosition) { + return true; + } + } else if (newDisplayType === "overlay") { + const newElementId = mapOverlayPositionToElementId(displaySettings.overlayPosition); + if (currentMessage.elementId !== newElementId) { + return true; + } + } else if (newDisplayType === "inline") { + if (currentMessage.elementId !== displaySettings.elementSelector) { + return true; + } + } + + return false; +} + +// Visual-only hide without side effects +async function hideMessageVisually(message) { + if (message.overlay) { + await hideOverlayComponent(); + removeOverlayComponent(); + } else { + hideEmbedComponent(message.elementId); + } + // Note: We don't call removeMessageByInstanceId or clear Gist.overlayInstanceId + // to keep the message in memory for re-rendering +} + +// Apply display settings to message +function applyDisplaySettings(message, displaySettings) { + // Ensure message.properties.gist exists + if (!message.properties) { + message.properties = {}; + } + if (!message.properties.gist) { + message.properties.gist = {}; + } + + // Apply display type specific settings + if (displaySettings.displayType === "modal") { + message.overlay = true; // Note: overlay property = true for modals + message.elementId = null; + message.position = displaySettings.modalPosition || "center"; + } else if (displaySettings.displayType === "overlay") { + message.overlay = false; + message.elementId = mapOverlayPositionToElementId(displaySettings.overlayPosition); + message.position = null; + } else if (displaySettings.displayType === "inline") { + message.overlay = false; + message.elementId = displaySettings.elementSelector; + message.position = null; + } + + // Apply other settings + if (displaySettings.maxWidth !== undefined) { + message.properties.gist.messageWidth = displaySettings.maxWidth; + } + if (displaySettings.overlayColor !== undefined) { + message.properties.gist.overlayColor = displaySettings.overlayColor; + } + if (displaySettings.dismissOutsideClick !== undefined) { + message.properties.gist.exitClick = displaySettings.dismissOutsideClick; + } +} + async function logUserMessageViewLocally(message) { log(`Logging user message view locally for: ${message.queueId}`); if (isMessageBroadcast(message)) { From 12d356b9010c81d4cc04ba307ff8ffcc82962b22 Mon Sep 17 00:00:00 2001 From: Bernard Gatt Date: Tue, 25 Nov 2025 10:57:55 +0100 Subject: [PATCH 2/8] Added SDK capabilities and some other fixes to multi-step --- src/managers/message-component-manager.js | 10 +++++++++- src/managers/message-manager.js | 14 +------------- src/managers/page-component-manager.js | 5 +++++ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/managers/message-component-manager.js b/src/managers/message-component-manager.js index 80937e8..d25e7f4 100644 --- a/src/managers/message-component-manager.js +++ b/src/managers/message-component-manager.js @@ -99,10 +99,18 @@ function attachIframeLoadEvent(elementId, options, stepName = null) { } } +// SDK capabilities that can be communicated to the renderer +const SDK_CAPABILITIES = [ + 'MultiStepDisplayTypes' +]; + export function sendOptionsToIframe(iframeId, options, stepName = null) { const iframe = document.getElementById(iframeId); if (iframe && iframe.contentWindow) { - const message = { options: options }; + const message = { + options: options, + capabilities: SDK_CAPABILITIES + }; if (stepName) { options.stepId = stepName; } diff --git a/src/managers/message-manager.js b/src/managers/message-manager.js index 5dc6827..e8c8059 100644 --- a/src/managers/message-manager.js +++ b/src/managers/message-manager.js @@ -14,8 +14,7 @@ import { resizeComponent, elementHasHeight, isElementLoaded, - changeOverlayTitle, - sendOptionsToIframe + changeOverlayTitle } from "./message-component-manager"; import { resolveMessageProperties } from "./gist-properties-manager"; import { positions, addPageElement } from "./page-component-manager"; @@ -146,11 +145,6 @@ function loadMessageComponent(message, elementId = null, stepName = null) { return message; } -function getMessageElementId(instanceId) { - return `gist-${instanceId}`; -} - - async function reportMessageView(message) { log(`Message shown, logging view for: ${message.messageId}`); var response = {}; @@ -275,12 +269,6 @@ async function handleGistEvents(e) { // Re-show message with new settings await reloadMessageWithNewDisplay(currentMessage, messageStepName); - } else { - // Just send stepName to iframe for navigation - if (messageStepName) { - log(`Navigating to step: ${messageStepName}`); - sendOptionsToIframe(getMessageElementId(currentMessage.instanceId), {}, messageStepName); - } } break; } diff --git a/src/managers/page-component-manager.js b/src/managers/page-component-manager.js index e528844..d281aff 100644 --- a/src/managers/page-component-manager.js +++ b/src/managers/page-component-manager.js @@ -3,6 +3,11 @@ import { log } from "../utilities/log"; export var positions = ["x-gist-top", "x-gist-floating-top", "x-gist-bottom", "x-gist-floating-bottom", "x-gist-floating-bottom-left", "x-gist-floating-bottom-right", "x-gist-floating-top-left", "x-gist-floating-top-right"]; export function addPageElement(position) { + // Check if element already exists + if (document.getElementById(position)) { + return; + } + const element = document.createElement("div"); element.id = position From 17fa99a23214b6cab7b793db4f4c1653e297fed4 Mon Sep 17 00:00:00 2001 From: Bernard Gatt Date: Tue, 25 Nov 2025 13:50:54 +0100 Subject: [PATCH 3/8] Fixed a few issues with multi-step --- src/managers/message-manager.js | 37 ++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/managers/message-manager.js b/src/managers/message-manager.js index e8c8059..c0c69ab 100644 --- a/src/managers/message-manager.js +++ b/src/managers/message-manager.js @@ -191,21 +191,27 @@ async function handleGistEvents(e) { log(`Engine render for message: ${currentMessage.messageId} timer elapsed in ${timeElapsed.toFixed(3)} seconds`); setMessageLoaded(currentMessage.queueId); currentMessage.currentRoute = e.data.gist.parameters.route; - if (currentMessage.firstLoad) { + + // Show component for first load or display change reload + if (currentMessage.firstLoad || currentMessage.isDisplayChange) { if (currentMessage.overlay) { showOverlayComponent(currentMessage); } else { showEmbedComponent(currentMessage.elementId); } - Gist.messageShown(currentMessage); - if (messageProperties.persistent) { - log(`Persistent message shown, skipping logging view`); - } else { - await reportMessageView(currentMessage); + // Only trigger events for actual first load, not display changes + if (currentMessage.firstLoad && !currentMessage.isDisplayChange) { + Gist.messageShown(currentMessage); + if (messageProperties.persistent) { + log(`Persistent message shown, skipping logging view`); + } else { + await reportMessageView(currentMessage); + } } currentMessage.firstLoad = false; + currentMessage.isDisplayChange = false; } updateMessageByInstanceId(currentInstanceId, currentMessage); break; @@ -318,8 +324,10 @@ async function handleGistEvents(e) { // Reload message with new display settings async function reloadMessageWithNewDisplay(message, stepName) { - // Set firstLoad to false to prevent duplicate logging and event triggering - message.firstLoad = false; + // Mark as display change reload to show component when routeLoaded is received + // but without triggering messageShown event or logging view + message.isDisplayChange = true; + message.renderStartTime = new Date().getTime(); // Update Gist.overlayInstanceId based on new display type if (message.overlay) { @@ -337,14 +345,8 @@ async function reloadMessageWithNewDisplay(message, stepName) { } // Reload the message component with new settings + // Component will be shown when routeLoaded event is received loadMessageComponent(message, elementId, stepName); - - // Show the component immediately since firstLoad is false - if (message.overlay) { - showOverlayComponent(message); - } else if (elementId) { - showEmbedComponent(elementId); - } } // Helper function to map overlay positions to element IDs @@ -377,6 +379,11 @@ function hasDisplayChanged(currentMessage, displaySettings) { const currentDisplayType = getCurrentDisplayType(currentMessage); const newDisplayType = displaySettings.displayType; + // If the new display type is undefined, we don't need to check if it has changed. + if (newDisplayType === undefined) { + return false; + } + // Check if display type changed if (currentDisplayType !== newDisplayType) { return true; From 314b569014d967a02e74e5a8ead21280ff0dfcdd Mon Sep 17 00:00:00 2001 From: Bernard Gatt Date: Wed, 26 Nov 2025 09:37:12 +0100 Subject: [PATCH 4/8] Prevents duplicate messages to be displayed at the same time --- src/managers/message-manager.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/managers/message-manager.js b/src/managers/message-manager.js index c0c69ab..64e719c 100644 --- a/src/managers/message-manager.js +++ b/src/managers/message-manager.js @@ -26,6 +26,10 @@ import { setMessageLoaded } from './message-user-queue-manager'; export async function showMessage(message) { if (Gist.isDocumentVisible) { + if (isQueueIdAlreadyShowing(message.queueId)) { + log(`Message with queueId ${message.queueId} is already showing.`); + return null; + } if (Gist.overlayInstanceId) { log(`Message ${Gist.overlayInstanceId} already showing.`); return null; @@ -50,6 +54,10 @@ export async function showMessage(message) { export async function embedMessage(message, elementId) { if (Gist.isDocumentVisible) { + if (isQueueIdAlreadyShowing(message.queueId)) { + log(`Message with queueId ${message.queueId} is already showing.`); + return null; + } message.instanceId = uuidv4(); message.overlay = false; message.firstLoad = true; @@ -166,6 +174,13 @@ export function fetchMessageByInstanceId(instanceId) { return Gist.currentMessages.find(message => message.instanceId === instanceId); } +function isQueueIdAlreadyShowing(queueId) { + if (!queueId) { + return false; + } + return Gist.currentMessages.some(message => message.queueId === queueId); +} + function removeMessageByInstanceId(instanceId) { Gist.currentMessages = Gist.currentMessages.filter(message => message.instanceId !== instanceId) } From 7f3b8f369a259d0b76b8041121bf37c7bb4c1b6d Mon Sep 17 00:00:00 2001 From: Bernard Gatt Date: Fri, 28 Nov 2025 12:52:53 +0100 Subject: [PATCH 5/8] Persisted messages now have saved state --- src/gist.js | 3 +- src/managers/message-manager.js | 188 +++++---------------- src/managers/message-user-queue-manager.js | 40 +++++ src/managers/queue-manager.js | 20 ++- src/utilities/local-storage.js | 2 +- src/utilities/message-utils.js | 127 ++++++++++++++ 6 files changed, 234 insertions(+), 146 deletions(-) create mode 100644 src/utilities/message-utils.js diff --git a/src/gist.js b/src/gist.js index 56c720f..2bbf3ed 100644 --- a/src/gist.js +++ b/src/gist.js @@ -3,7 +3,8 @@ import { log } from "./utilities/log"; import { clearExpiredFromLocalStore } from "./utilities/local-storage"; import { startQueueListener, checkMessageQueue, stopSSEListener } from "./managers/queue-manager"; import { setUserToken, clearUserToken, useGuestSession } from "./managers/user-manager"; -import { showMessage, embedMessage, hideMessage, removePersistentMessage, fetchMessageByInstanceId, logBroadcastDismissedLocally } from "./managers/message-manager"; +import { showMessage, embedMessage, hideMessage, removePersistentMessage, logBroadcastDismissedLocally } from "./managers/message-manager"; +import { fetchMessageByInstanceId } from "./utilities/message-utils"; import { setUserLocale } from "./managers/locale-manager"; import { setCustomAttribute, clearCustomAttributes, removeCustomAttribute } from "./managers/custom-attribute-manager"; import { setupPreview } from "./utilities/preview-mode"; diff --git a/src/managers/message-manager.js b/src/managers/message-manager.js index 64e719c..b67d522 100644 --- a/src/managers/message-manager.js +++ b/src/managers/message-manager.js @@ -20,9 +20,17 @@ import { resolveMessageProperties } from "./gist-properties-manager"; import { positions, addPageElement } from "./page-component-manager"; import { getAllCustomAttributes } from "./custom-attribute-manager"; import { checkMessageQueue } from "./queue-manager"; -import { isMessageBroadcast, markBroadcastAsSeen, markBroadcastAsDismissed } from './message-broadcast-manager'; -import { markUserQueueMessageAsSeen } from './message-user-queue-manager'; +import { isMessageBroadcast, markBroadcastAsSeen, markBroadcastAsDismissed, isShowAlwaysBroadcast } from './message-broadcast-manager'; +import { markUserQueueMessageAsSeen, saveMessageState, clearMessageState } from './message-user-queue-manager'; import { setMessageLoaded } from './message-user-queue-manager'; +import { + fetchMessageByInstanceId, + isQueueIdAlreadyShowing, + removeMessageByInstanceId, + updateMessageByInstanceId, + hasDisplayChanged, + applyDisplaySettings +} from '../utilities/message-utils'; export async function showMessage(message) { if (Gist.isDocumentVisible) { @@ -34,7 +42,8 @@ export async function showMessage(message) { log(`Message ${Gist.overlayInstanceId} already showing.`); return null; } else { - var properties = resolveMessageProperties(message) + var properties = resolveMessageProperties(message); + message.instanceId = uuidv4(); message.overlay = true; message.firstLoad = true; @@ -44,7 +53,9 @@ export async function showMessage(message) { Gist.overlayInstanceId = message.instanceId; Gist.currentMessages.push(message); - return loadMessageComponent(message); + // Use saved step if available (set by queue manager) + const savedStep = message.savedStepName || null; + return loadMessageComponent(message, null, savedStep); } } else { log("Document hidden, not showing message now."); @@ -58,6 +69,7 @@ export async function embedMessage(message, elementId) { log(`Message with queueId ${message.queueId} is already showing.`); return null; } + message.instanceId = uuidv4(); message.overlay = false; message.firstLoad = true; @@ -67,7 +79,9 @@ export async function embedMessage(message, elementId) { message.renderStartTime = new Date().getTime(); Gist.currentMessages.push(message); - return loadMessageComponent(message, elementId); + // Use saved step if available (set by queue manager) + const savedStep = message.savedStepName || null; + return loadMessageComponent(message, elementId, savedStep); } else { log("Document hidden, not showing message now."); return null; @@ -95,6 +109,8 @@ export async function removePersistentMessage(message) { log(`Persistent message dismissed, logging view`); await logUserMessageViewLocally(message); await reportMessageView(message); + // Clear saved message state when persistent message is removed + await clearMessageState(message.queueId); } } else { log(`Message with instance id: ${message.instanceId} not found`); @@ -170,26 +186,6 @@ async function reportMessageView(message) { } } -export function fetchMessageByInstanceId(instanceId) { - return Gist.currentMessages.find(message => message.instanceId === instanceId); -} - -function isQueueIdAlreadyShowing(queueId) { - if (!queueId) { - return false; - } - return Gist.currentMessages.some(message => message.queueId === queueId); -} - -function removeMessageByInstanceId(instanceId) { - Gist.currentMessages = Gist.currentMessages.filter(message => message.instanceId !== instanceId) -} - -function updateMessageByInstanceId(instanceId, message) { - removeMessageByInstanceId(instanceId); - Gist.currentMessages.push(message); -} - function handleTouchStartEvents() { // Added this to avoid errors in the console } @@ -273,26 +269,6 @@ async function handleGistEvents(e) { } break; } - } else if (url && url.protocol === "inapp:") { - var inappAction = url.href.replace("inapp://", "").split('?')[0]; - switch (inappAction) { - case "changeMessage": - var displaySettings = e.data.gist.parameters.options?.displaySettings; - var messageStepName = e.data.gist.parameters.options?.messageStepName; - - if (displaySettings && hasDisplayChanged(currentMessage, displaySettings)) { - log(`Display settings changed, reloading message`); - // Hide visually without side effects - await hideMessageVisually(currentMessage); - - // Apply new display settings - applyDisplaySettings(currentMessage, displaySettings); - - // Re-show message with new settings - await reloadMessageWithNewDisplay(currentMessage, messageStepName); - } - break; - } } } catch { // If the action is not a URL, we don't need to do anything. @@ -300,6 +276,28 @@ async function handleGistEvents(e) { break; } + case "changeMessageStep": { + var displaySettings = e.data.gist.parameters.displaySettings; + var messageStepName = e.data.gist.parameters.messageStepName; + + // Save message state (step + display settings) for persistent messages or show-always broadcasts + if (messageProperties.persistent || isShowAlwaysBroadcast(currentMessage)) { + await saveMessageState(currentMessage.queueId, messageStepName, displaySettings); + } + + if (displaySettings && hasDisplayChanged(currentMessage, displaySettings)) { + log(`Display settings changed, reloading message`); + // Hide visually without side effects + await hideMessageVisually(currentMessage); + + // Apply new display settings + applyDisplaySettings(currentMessage, displaySettings); + + // Re-show message with new settings + await reloadMessageWithNewDisplay(currentMessage, messageStepName); + } + break; + } case "routeChanged": { currentMessage.currentRoute = e.data.gist.parameters.route; currentMessage.renderStartTime = new Date().getTime(); @@ -364,67 +362,6 @@ async function reloadMessageWithNewDisplay(message, stepName) { loadMessageComponent(message, elementId, stepName); } -// Helper function to map overlay positions to element IDs -function mapOverlayPositionToElementId(overlayPosition) { - const positionMap = { - "topLeft": "x-gist-floating-top-left", - "topCenter": "x-gist-floating-top", - "topRight": "x-gist-floating-top-right", - "bottomLeft": "x-gist-floating-bottom-left", - "bottomCenter": "x-gist-floating-bottom", - "bottomRight": "x-gist-floating-bottom-right" - }; - return positionMap[overlayPosition] || "x-gist-floating-bottom"; -} - -// Helper function to determine current display type -function getCurrentDisplayType(message) { - if (message.overlay) { - return "modal"; - } else if (message.elementId && positions.includes(message.elementId)) { - return "overlay"; - } else if (message.elementId) { - return "inline"; - } - return "modal"; // default -} - -// Helper function to check if display settings have changed -function hasDisplayChanged(currentMessage, displaySettings) { - const currentDisplayType = getCurrentDisplayType(currentMessage); - const newDisplayType = displaySettings.displayType; - - // If the new display type is undefined, we don't need to check if it has changed. - if (newDisplayType === undefined) { - return false; - } - - // Check if display type changed - if (currentDisplayType !== newDisplayType) { - return true; - } - - // Check if position changed within the same display type - if (newDisplayType === "modal") { - const currentPosition = currentMessage.position || "center"; - const newPosition = displaySettings.modalPosition || "center"; - if (currentPosition !== newPosition) { - return true; - } - } else if (newDisplayType === "overlay") { - const newElementId = mapOverlayPositionToElementId(displaySettings.overlayPosition); - if (currentMessage.elementId !== newElementId) { - return true; - } - } else if (newDisplayType === "inline") { - if (currentMessage.elementId !== displaySettings.elementSelector) { - return true; - } - } - - return false; -} - // Visual-only hide without side effects async function hideMessageVisually(message) { if (message.overlay) { @@ -437,43 +374,6 @@ async function hideMessageVisually(message) { // to keep the message in memory for re-rendering } -// Apply display settings to message -function applyDisplaySettings(message, displaySettings) { - // Ensure message.properties.gist exists - if (!message.properties) { - message.properties = {}; - } - if (!message.properties.gist) { - message.properties.gist = {}; - } - - // Apply display type specific settings - if (displaySettings.displayType === "modal") { - message.overlay = true; // Note: overlay property = true for modals - message.elementId = null; - message.position = displaySettings.modalPosition || "center"; - } else if (displaySettings.displayType === "overlay") { - message.overlay = false; - message.elementId = mapOverlayPositionToElementId(displaySettings.overlayPosition); - message.position = null; - } else if (displaySettings.displayType === "inline") { - message.overlay = false; - message.elementId = displaySettings.elementSelector; - message.position = null; - } - - // Apply other settings - if (displaySettings.maxWidth !== undefined) { - message.properties.gist.messageWidth = displaySettings.maxWidth; - } - if (displaySettings.overlayColor !== undefined) { - message.properties.gist.overlayColor = displaySettings.overlayColor; - } - if (displaySettings.dismissOutsideClick !== undefined) { - message.properties.gist.exitClick = displaySettings.dismissOutsideClick; - } -} - async function logUserMessageViewLocally(message) { log(`Logging user message view locally for: ${message.queueId}`); if (isMessageBroadcast(message)) { @@ -487,5 +387,7 @@ export async function logBroadcastDismissedLocally(message) { if (isMessageBroadcast(message)) { log(`Logging broadcast dismissed locally for: ${message.queueId}`); await markBroadcastAsDismissed(message.queueId); + // Clear saved message state when broadcast is dismissed + await clearMessageState(message.queueId); } } diff --git a/src/managers/message-user-queue-manager.js b/src/managers/message-user-queue-manager.js index 180b711..854caab 100644 --- a/src/managers/message-user-queue-manager.js +++ b/src/managers/message-user-queue-manager.js @@ -1,5 +1,6 @@ import { getKeyFromLocalStore, setKeyToLocalStore, clearKeyFromLocalStore } from '../utilities/local-storage'; import { getHashedUserToken } from './user-manager'; +import { log } from '../utilities/log'; const messageQueueLocalStoreName = "gist.web.message.user"; const messagesLocalStoreCacheInMinutes = 60; @@ -77,4 +78,43 @@ async function getMessageLoadingStateLocalStoreName(queueId) { const userToken = await getHashedUserToken(); if (!userToken) return null; return `${messageQueueLocalStoreName}.${userToken}.message.${queueId}.loading` +} + +async function getMessageStateLocalStoreName(queueId) { + const userToken = await getHashedUserToken(); + if (!userToken) return null; + return `${messageQueueLocalStoreName}.${userToken}.message.${queueId}.state`; +} + +export async function saveMessageState(queueId, stepName, displaySettings) { + const messageStateLocalStoreName = await getMessageStateLocalStoreName(queueId); + if (!messageStateLocalStoreName) return; + + // Get existing state to merge with new values + const existingState = getKeyFromLocalStore(messageStateLocalStoreName) || {}; + + const state = { + stepName: stepName !== undefined ? stepName : existingState.stepName, + displaySettings: displaySettings !== undefined ? displaySettings : existingState.displaySettings + }; + + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + 30); // 30-day TTL + setKeyToLocalStore(messageStateLocalStoreName, state, expiryDate); + log(`Saved message state for queueId: ${queueId}`, state); +} + +export async function getSavedMessageState(queueId) { + const messageStateLocalStoreName = await getMessageStateLocalStoreName(queueId); + if (!messageStateLocalStoreName) return null; + + return getKeyFromLocalStore(messageStateLocalStoreName); +} + +export async function clearMessageState(queueId) { + const messageStateLocalStoreName = await getMessageStateLocalStoreName(queueId); + if (!messageStateLocalStoreName) return; + + clearKeyFromLocalStore(messageStateLocalStoreName); + log(`Cleared message state for queueId: ${queueId}`); } \ No newline at end of file diff --git a/src/managers/queue-manager.js b/src/managers/queue-manager.js index 42b4ece..fb9ca91 100644 --- a/src/managers/queue-manager.js +++ b/src/managers/queue-manager.js @@ -6,8 +6,9 @@ import { showMessage, embedMessage } from "./message-manager"; import { resolveMessageProperties } from "./gist-properties-manager"; import { clearKeyFromLocalStore, getKeyFromLocalStore } from '../utilities/local-storage'; import { updateBroadcastsLocalStore, getEligibleBroadcasts, isShowAlwaysBroadcast } from './message-broadcast-manager'; -import { updateQueueLocalStore, getMessagesFromLocalStore, isMessageLoading, setMessageLoading } from './message-user-queue-manager'; +import { updateQueueLocalStore, getMessagesFromLocalStore, isMessageLoading, setMessageLoading, getSavedMessageState } from './message-user-queue-manager'; import { settings } from '../services/settings'; +import { applyDisplaySettings } from '../utilities/message-utils'; var sleep = time => new Promise(resolve => setTimeout(resolve, time)) var poll = (promiseFn, time) => promiseFn().then(sleep(time).then(() => poll(promiseFn, time))); @@ -60,6 +61,23 @@ async function handleMessage(message) { message.position = messageProperties.position; } + // Restore saved state for persistent messages or show-always broadcasts + if (messageProperties.persistent || isShowAlwaysBroadcast(message)) { + const savedState = await getSavedMessageState(message.queueId); + if (savedState) { + log(`Restoring saved state for queueId ${message.queueId}`); + + // Apply saved display settings if they exist + if (savedState.displaySettings) { + applyDisplaySettings(message, savedState.displaySettings); + messageProperties = resolveMessageProperties(message); // Re-resolve after applying + } + + // Store saved step for later use + message.savedStepName = savedState.stepName; + } + } + // If the message is not persistant, is not a show always broadcast, and is already loading, we skip it. if (!messageProperties.persistent && !isShowAlwaysBroadcast(message) && await isMessageLoading(message.queueId)) { log(`Not showing message with queueId ${message.queueId} because its already loading.`); diff --git a/src/utilities/local-storage.js b/src/utilities/local-storage.js index 9aa4d83..ce3de61 100644 --- a/src/utilities/local-storage.js +++ b/src/utilities/local-storage.js @@ -65,7 +65,7 @@ function checkKeyForExpiry(key) { const expiryTime = new Date(item.expiry); // remove old cache entries with long expiry times - const isBroadcastOrUserKey = (key.startsWith("gist.web.message.broadcasts") && !key.endsWith("shouldShow") && !key.endsWith("numberOfTimesShown")) || (key.startsWith("gist.web.message.user") && !key.endsWith("seen")); + const isBroadcastOrUserKey = (key.startsWith("gist.web.message.broadcasts") && !key.endsWith("shouldShow") && !key.endsWith("numberOfTimesShown")) || (key.startsWith("gist.web.message.user") && !key.endsWith("seen") && !key.endsWith("state")); const sixtyMinutesFromNow = new Date(now.getTime() + 61 * 60 * 1000); if (isBroadcastOrUserKey && expiryTime.getTime() > sixtyMinutesFromNow.getTime()) { clearKeyFromLocalStore(key); diff --git a/src/utilities/message-utils.js b/src/utilities/message-utils.js new file mode 100644 index 0000000..b6206eb --- /dev/null +++ b/src/utilities/message-utils.js @@ -0,0 +1,127 @@ +import Gist from '../gist'; +import { positions } from '../managers/page-component-manager'; + +export function fetchMessageByInstanceId(instanceId) { + return Gist.currentMessages.find(message => message.instanceId === instanceId); +} + +export function isQueueIdAlreadyShowing(queueId) { + if (!queueId) { + return false; + } + return Gist.currentMessages.some(message => message.queueId === queueId); +} + +export function removeMessageByInstanceId(instanceId) { + Gist.currentMessages = Gist.currentMessages.filter(message => message.instanceId !== instanceId) +} + +export function updateMessageByInstanceId(instanceId, message) { + removeMessageByInstanceId(instanceId); + Gist.currentMessages.push(message); +} + +export function mapOverlayPositionToElementId(overlayPosition) { + const positionMap = { + "topLeft": "x-gist-floating-top-left", + "topCenter": "x-gist-floating-top", + "topRight": "x-gist-floating-top-right", + "bottomLeft": "x-gist-floating-bottom-left", + "bottomCenter": "x-gist-floating-bottom", + "bottomRight": "x-gist-floating-bottom-right" + }; + return positionMap[overlayPosition] || "x-gist-floating-bottom"; +} + +// Helper function to determine current display type +export function getCurrentDisplayType(message) { + if (message.overlay) { + return "modal"; + } else if (message.elementId && positions.includes(message.elementId)) { + return "overlay"; + } else if (message.elementId) { + return "inline"; + } + return "modal"; // default +} + +// Helper function to check if display settings have changed +export function hasDisplayChanged(currentMessage, displaySettings) { + const currentDisplayType = getCurrentDisplayType(currentMessage); + const newDisplayType = displaySettings.displayType; + + // If the new display type is undefined, we don't need to check if it has changed. + if (newDisplayType === undefined) { + return false; + } + + // Check if display type changed + if (currentDisplayType !== newDisplayType) { + return true; + } + + // Check if position changed within the same display type + if (newDisplayType === "modal") { + const currentPosition = currentMessage.position || "center"; + const newPosition = displaySettings.modalPosition || "center"; + if (currentPosition !== newPosition) { + return true; + } + } else if (newDisplayType === "overlay") { + const newElementId = mapOverlayPositionToElementId(displaySettings.overlayPosition); + if (currentMessage.elementId !== newElementId) { + return true; + } + } else if (newDisplayType === "inline") { + if (currentMessage.elementId !== displaySettings.elementSelector) { + return true; + } + } + + return false; +} + +// Apply display settings to message +export function applyDisplaySettings(message, displaySettings) { + // Ensure message.properties.gist exists + if (!message.properties) { + message.properties = {}; + } + if (!message.properties.gist) { + message.properties.gist = {}; + } + + // Apply display type specific settings + if (displaySettings.displayType === "modal") { + message.overlay = true; // Note: overlay property = true for modals + message.elementId = null; + message.properties.gist.elementId = null; // Also update in gist properties + message.position = displaySettings.modalPosition || "center"; + message.properties.gist.position = displaySettings.modalPosition || "center"; + } else if (displaySettings.displayType === "overlay") { + message.overlay = false; + const elementId = mapOverlayPositionToElementId(displaySettings.overlayPosition); + message.elementId = elementId; + message.properties.gist.elementId = elementId; // Also update in gist properties + message.position = null; + message.properties.gist.position = null; + } else if (displaySettings.displayType === "inline") { + message.overlay = false; + message.elementId = displaySettings.elementSelector; + message.properties.gist.elementId = displaySettings.elementSelector; // Also update in gist properties + message.position = null; + message.properties.gist.position = null; + } + + // Apply other settings + if (displaySettings.maxWidth !== undefined) { + message.properties.gist.messageWidth = displaySettings.maxWidth; + } + if (displaySettings.overlayColor !== undefined) { + message.properties.gist.overlayColor = displaySettings.overlayColor; + } + if (displaySettings.dismissOutsideClick !== undefined) { + message.properties.gist.exitClick = displaySettings.dismissOutsideClick; + } +} + From 05f9c516ddabbd6a27419054e8d0905b79e7525b Mon Sep 17 00:00:00 2001 From: Bernard Gatt Date: Mon, 1 Dec 2025 12:28:35 +0100 Subject: [PATCH 6/8] Updated demo with multi-step message and test renderer --- examples/index.html | 16 ++++++++++++++++ src/services/settings.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/examples/index.html b/examples/index.html index a58bd9b..6cdf5dc 100644 --- a/examples/index.html +++ b/examples/index.html @@ -13,6 +13,7 @@

Gist for Web

Show HTML Message + Show HTML Multistep Message Show Simple Message Show Message With Properties Show Complex Message @@ -29,6 +30,7 @@

Gist for Web