Plugin Directory


Ignore:
Timestamp:
10/24/2026 11:12:46 AM (5 days ago)
Author:
mxchat
Message:

Release 3.2.27 - security hardening, lead auto-delete, xAI image generation, Complianz consent for embedded chats, transcript, model, sync and layout fixes

File:
1 edited

Legend:

Unmodified
Added
Removed
  • mxchat-basic/trunk/js/chat-script.js

    r3751971 r3763174  
    6060            mxchatChat.nonce = cachedFreshNonce;
    6161            if (callback) callback();
     62            return;
     63        }
     64        // The page's first nonce rides on the dynamic-settings request (plan
     65        // feeb79): one /nonce?with_settings=1 call instead of two ~1 ms apart,
     66        // the second of which the endpoint's 2-second per-IP limit answered 429.
     67        // When that request fails this falls through to its own fetch below.
     68        if (dynamicSettingsState !== 'done') {
     69            dynamicSettingsWaiters.push(function () { withFreshNonce(callback); });
     70            if (dynamicSettingsState === 'idle') fetchDynamicSettings();
    6271            return;
    6372        }
    … …  
    119128    // posture). At most one request per page load, only if a widget opens.
    120129    var dynamicSettingsState = 'idle'; // 'idle' | 'pending' | 'done'
     130    var dynamicSettingsWaiters = []; // withFreshNonce() calls waiting on it (plan feeb79)
    121131
    122132    function mxchatRefreshDynamicSettings() {
    123133        if (dynamicSettingsState !== 'idle') return;
    124134        if (typeof mxchatChat === 'undefined') return;
     135        fetchDynamicSettings();
     136    }
     137
     138    // The one ?with_settings=1 request per page load. Also issues the page's
     139    // first nonce, so withFreshNonce() waits on it instead of racing it.
     140    function fetchDynamicSettings() {
    125141        dynamicSettingsState = 'pending';
    126142
    127143        var applied = function (data) {
    128144            dynamicSettingsState = 'done';
    129             if (!data) return; // endpoint unavailable — inline values stand.
    130             if (data.nonce) {
     145            if (data && data.nonce) {
    131146                // Seed the nonce cache too: saves the first send's REST
    132147                // round-trip and keeps us under the endpoint's rate limit.
    … …  
    135150                mxchatChat.nonce = data.nonce;
    136151            }
    137             if (data.settings && typeof data.settings === 'object') {
     152            if (data && data.settings && typeof data.settings === 'object') {
    138153                $.extend(mxchatChat, data.settings);
    139154                mxchatRebuildHeaderMenus();
    140155            }
     156            // No data = endpoint unavailable: inline values stand, and each
     157            // waiting withFreshNonce() call runs its own fetch and fallback.
     158            var waiting = dynamicSettingsWaiters;
     159            dynamicSettingsWaiters = [];
     160            waiting.forEach(function (cb) { try { cb(); } catch (e) {} });
    141161        };
    142162
    … …  
    253273            if (instance.sessionId) {
    254274                return instance.sessionId;
     275            }
     276
     277            // An embedded chat waiting for cookie consent starts no session:
     278            // no session cookie, nonce request or page tracking (plan 2b137b).
     279            if (mxchatEmbeddedConsentPending(botId)) {
     280                return null;
    255281            }
    256282
    … …  
    840866function sendMessage(botId) {
    841867    botId = botId || 'default';
     868    if (mxchatEmbeddedConsentBlocksSend(botId)) {
     869        return;
     870    }
    842871    MxChatInstances.ensureSession(botId);
    843872    var $chatInput = getElement(botId, 'chat-input');
    … …  
    891920function sendMessageToChatbot(message, botId) {
    892921    botId = botId || 'default';
     922    if (mxchatEmbeddedConsentBlocksSend(botId)) {
     923        return;
     924    }
    893925    MxChatInstances.ensureSession(botId);
    894926
    … …  
    9901022function callMxChat(message, callback, botId) {
    9911023    botId = botId || getMxChatBotId();
     1024    if (mxchatEmbeddedConsentBlocksSend(botId)) {
     1025        return;
     1026    }
    9921027
    9931028    // Streaming fallbacks land here: drop any leftover stream handle and
    … …  
    12561291function callMxChatStream(message, callback, botId) {
    12571292    botId = botId || getMxChatBotId();
     1293    if (mxchatEmbeddedConsentBlocksSend(botId)) {
     1294        return;
     1295    }
    12581296
    12591297    // Store the message in case we need to retry after session reset
    … …  
    17671805    const modelPrefix = model.split('-')[0].toLowerCase();
    17681806
    1769     // Support streaming for OpenAI, Claude, Grok, DeepSeek, and OpenRouter models
    1770     const isSupported = modelPrefix === 'gpt' ||
    1771                         modelPrefix === 'o1' ||
    1772                         modelPrefix === 'claude' ||
    1773                         modelPrefix === 'grok' ||
    1774                         modelPrefix === 'deepseek' ||
    1775                         model === 'openrouter';  // Add this line - check full model name for OpenRouter
     1807    // Support streaming for OpenAI, Claude, Grok, DeepSeek, OpenRouter and the
     1808    // Custom (OpenAI-compatible) provider: every provider the server can stream
     1809    // (plan d6c505). Gemini has no streaming route on the server, so it stays on
     1810    // the regular request.
     1811    const isSupported = modelPrefix === 'gpt' ||
     1812                        modelPrefix === 'o1' ||
     1813                        modelPrefix === 'claude' ||
     1814                        modelPrefix === 'grok' ||
     1815                        modelPrefix === 'deepseek' ||
     1816                        model === 'openrouter' ||  // check full model name for OpenRouter
     1817                        model === 'custom-provider';
    17761818   
    17771819    return isSupported;
    … …  
    21612203    // renders — refresh dynamic settings at init (plan-32db95). Floating
    21622204    // widgets refresh on first launcher open instead.
     2205    // An embedded chat waiting for cookie consent refreshes when it is
     2206    // revealed instead (plan 2b137b).
    21632207    var hasEmbeddedWidget = $('.mxchat-chatbot-wrapper').filter(function() {
    2164         return !$(this).closest('.floating-chatbot').length;
     2208        return !$(this).closest('.floating-chatbot').length && !$(this).hasClass('no-consent');
    21652209    }).length > 0;
    21662210    if (hasEmbeddedWidget) {
    … …  
    21682212    }
    21692213});
     2214
     2215// Whether a reply's html part puts anything on screen. A part made only of
     2216// markup that renders nothing (a script, a style block, comments, whitespace),
     2217// such as the Add to Cart reply's script, or an element carrying the hidden
     2218// attribute (the Add to Cart reply's cart signal since mxchat-woo 1.7.15),
     2219// or one whose inline style says display none (plan 0634d7), gets no line
     2220// breaks in front of it, so it leaves no empty lines in the bubble (plan
     2221// 4bca7e). Any other element counts as visible, even an empty one a script
     2222// fills later. Parsed in an inert template, so nothing in it runs or loads
     2223// here. Same rule as MxChat_Admin::mxchat_transcript_part_kind() in PHP.
     2224function mxchatHtmlHasVisibleContent(html) {
     2225    var inlineDisplayNone = /(?:^|;)\s*display\s*:\s*none\s*(?:!\s*important\s*)?(?:;|$)/i;
     2226    if (!html) {
     2227        return false;
     2228    }
     2229    var tpl = document.createElement('template');
     2230    if (!('content' in tpl)) {
     2231        return true;
     2232    }
     2233    tpl.innerHTML = html;
     2234    var nodes = tpl.content.childNodes;
     2235    for (var i = 0; i < nodes.length; i++) {
     2236        var node = nodes[i];
     2237        if (node.nodeType === 3 && node.textContent.trim() !== '') {
     2238            return true;
     2239        }
     2240        if (node.nodeType === 1 && !/^(SCRIPT|STYLE|LINK|META|TEMPLATE|NOSCRIPT)$/.test(node.tagName) && !node.hasAttribute('hidden')
     2241            && !inlineDisplayNone.test(node.getAttribute('style') || '')) {
     2242            return true;
     2243        }
     2244    }
     2245    return false;
     2246}
     2247
     2248// What goes between a reply's text and its html part (plans 4bca7e, 45d9b5).
     2249// Nothing in front of a part that renders nothing. Reply text that already
     2250// ends in a paragraph (linkify() wraps reply text in paragraphs, and so does
     2251// the replay) is followed by that paragraph's own bottom margin, one normal
     2252// paragraph gap; the two line breaks used to add two blank lines on top of
     2253// it. Text that does not end in a paragraph keeps the two line breaks.
     2254function mxchatHtmlPartSeparator(textHtml, html) {
     2255    if (!mxchatHtmlHasVisibleContent(html)) {
     2256        return '';
     2257    }
     2258    return /<\/p>\s*$/i.test(String(textHtml || '')) ? '' : '<br><br>';
     2259}
    21702260
    21712261function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false, botId = 'default') {
    … …  
    22342324        // Append HTML content if provided
    22352325        if (messageHtml && sender !== "user") {
    2236             // Only add line breaks if there's actual text content before the HTML
     2326            // Only separate the HTML from actual text content before it, and
     2327            // only when the HTML puts something on screen (plans 4bca7e, 45d9b5)
    22372328            if (fullMessage && fullMessage.trim()) {
    2238                 fullMessage += '<br><br>' + messageHtml;
     2329                fullMessage += mxchatHtmlPartSeparator(fullMessage, messageHtml) + messageHtml;
    22392330            } else {
    22402331                fullMessage = messageHtml;
    … …  
    23902481
    23912482    if (responseHtml) {
    2392         // Only add line breaks if there's actual text content before the HTML
     2483        // Only separate the HTML from actual text content before it, and
     2484        // only when the HTML puts something on screen (plans 4bca7e, 45d9b5)
    23932485        if (fullMessage && fullMessage.trim()) {
    2394             fullMessage += '<br><br>' + responseHtml;
     2486            fullMessage += mxchatHtmlPartSeparator(fullMessage, responseHtml) + responseHtml;
    23952487        } else {
    23962488            fullMessage = responseHtml;
    … …  
    30893181    // ====================================
    30903182   
    3091     function createNotificationBadge() {
    3092         const chatButton = document.getElementById('floating-chatbot-button');
    3093 
    3094         if (!chatButton) return;
    3095 
    3096         // Remove any existing badge first
    3097         const existingBadge = chatButton.querySelector('.chat-notification-badge');
    3098         if (existingBadge) {
    3099             existingBadge.remove();
    3100         }
    3101    
    3102         notificationBadge = document.createElement('div');
    3103         notificationBadge.className = 'chat-notification-badge';
    3104         notificationBadge.style.cssText = `
    3105             display: none;
    3106             position: absolute;
    3107             top: -5px;
    3108             right: -5px;
    3109             background-color: red;
    3110             color: white;
    3111             border-radius: 50%;
    3112             padding: 4px 8px;
    3113             font-size: 12px;
    3114             font-weight: bold;
    3115             z-index: 10001;
    3116         `;
    3117         chatButton.style.position = 'relative';
    3118         chatButton.appendChild(notificationBadge);
    3119        
    3120     }
    3121    
    31223183    function showNotification(botId) {
    31233184        botId = botId || 'default';
    … …  
    31383199    }
    31393200
    3140     function startNotificationChecking(botId) {
    3141         botId = botId || 'default';
    3142         const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
    3143         if (!chatPersistenceEnabled) return;
    3144 
    3145         createNotificationBadge(botId);
    3146         var instance = MxChatInstances.get(botId);
    3147         instance.notificationCheckInterval = setInterval(function() {
    3148             checkForNewMessages(botId);
    3149         }, 30000); // Check every 30 seconds
    3150     }
    3151 
    3152     function stopNotificationChecking(botId) {
    3153         botId = botId || 'default';
    3154         var instance = MxChatInstances.get(botId);
    3155         if (instance.notificationCheckInterval) {
    3156             clearInterval(instance.notificationCheckInterval);
    3157         }
    3158     }
    3159    
    3160     function checkForNewMessages() {
    3161         const sessionId = getChatSession();
    3162         const chatPersistenceEnabled = mxchatChat.chat_persistence_toggle === 'on';
    3163        
    3164         if (!chatPersistenceEnabled) return;
    3165    
    3166         $.ajax({
    3167             url: mxchatChat.ajax_url,
    3168             type: 'POST',
    3169             data: {
    3170                 action: 'mxchat_check_new_messages',
    3171                 session_id: sessionId,
    3172                 last_seen_id: lastSeenMessageId,
    3173                 nonce: mxchatChat.nonce
    3174             },
    3175             success: function(response) {
    3176                 if (response.success && response.data.hasNewMessages) {
    3177                     showNotification();
    3178                 }
    3179             }
    3180         });
    3181     }
     3201    // Plan 0c480a: the old 30-second unread-badge poll helpers were removed. Nothing ever started them and no
     3202    // server handler existed for their AJAX action. Live-agent replies reach the widget through
     3203    // checkForAgentMessages() below (mxchat_fetch_new_messages every 5 s in agent mode), which also shows the
     3204    // badge via showNotification().
    31823205
    31833206
    … …  
    32603283    // ====================================
    32613284
     3285// A stored bot row that is MxChat-generated markup (forms, product cards,
     3286// galleries, generated images, a script) rather than reply text. The server
     3287// stores a reply's text and its html part as two rows; replay joins them
     3288// back into one bubble (plan e19ea8). Same family check as the replay's
     3289// raw-html branch below, plus markup that renders nothing (a script part).
     3290function mxchatIsStoredHtmlPart(content) {
     3291    if (/<[a-z][^>]*class\s*=\s*["'][^"']*\bmxchat-/i.test(content) ||
     3292        content.includes("<form") ||
     3293        content.includes("<input") ||
     3294        content.includes("<select") ||
     3295        content.includes("<textarea")) {
     3296        return true;
     3297    }
     3298    return /^\s*</.test(content) && !mxchatHtmlHasVisibleContent(content);
     3299}
     3300
    32623301function loadChatHistory(botId, onComplete) {
    32633302    botId = botId || 'default';
    … …  
    33393378                            $chatBox.empty();
    33403379
     3380                            // The bot bubble the next html part of the same
     3381                            // turn joins (plan e19ea8), and its row's time.
     3382                            var $mxTurnBubble = null;
     3383                            var mxTurnBubbleTs = 0;
     3384                            var mxTurnBubbleHasHtml = false;
     3385
    33413386                            $.each(response.data.conversation, function(index, message) {
    33423387                                // Skip agent messages if persistence is off
    33433388                                if (!chatPersistenceEnabled && message.role === 'agent') {
    33443389                                    return;
     3390                                }
     3391
     3392                                // A reply's html part is stored as its own row
     3393                                // after the reply's text. Live, both were one
     3394                                // bubble: join the part to the bot bubble just
     3395                                // before it when no other row came between and
     3396                                // it was written in the same request (plan
     3397                                // e19ea8). A part that renders nothing (the Add
     3398                                // to Cart or checkout script) already ran live
     3399                                // and is not replayed. A YouTube embed stays its
     3400                                // own bubble, as a streamed reply shows it.
     3401                                if (message.role === 'bot' && typeof message.content === 'string') {
     3402                                    var mxPart = decodeHTMLEntities(message.content.replace(/\\'/g, "'").replace(/\\"/g, '"'));
     3403                                    if (mxPart.trim() !== '' && mxchatIsStoredHtmlPart(mxPart)) {
     3404                                        var mxPartTs = parseInt(message.timestamp, 10) || 0;
     3405                                        var mxIsEmbed = /\bmxchat-youtube-embed\b/.test(mxPart);
     3406                                        var mxRendersNothing = !mxchatHtmlHasVisibleContent(mxPart);
     3407                                        var mxSameTurn = $mxTurnBubble && !mxIsEmbed &&
     3408                                            (!mxPartTs || !mxTurnBubbleTs || Math.abs(mxPartTs - mxTurnBubbleTs) <= 10000);
     3409                                        if (mxRendersNothing || mxSameTurn) {
     3410                                            if (!mxRendersNothing) {
     3411                                                var mxSep = (!mxTurnBubbleHasHtml && $.trim($mxTurnBubble.text()) !== '') ? mxchatHtmlPartSeparator($mxTurnBubble.html(), mxPart) : '';
     3412                                                $mxTurnBubble.append(mxSep + mxPart);
     3413                                                mxTurnBubbleHasHtml = true;
     3414                                            }
     3415                                            if (message.id) {
     3416                                                highestMessageId = Math.max(highestMessageId, message.id);
     3417                                                instance.processedMessageIds.add(message.id);
     3418                                            }
     3419                                            return;
     3420                                        }
     3421                                    }
    33453422                                }
    33463423
    … …  
    34073484                                $fragment.append(messageElement);
    34083485
     3486                                // Only a bot bubble from this turn takes the
     3487                                // turn's html parts; any other row ends the turn.
     3488                                if (message.role === 'bot' && !/\bmxchat-youtube-embed\b/.test(content)) {
     3489                                    $mxTurnBubble = messageElement;
     3490                                    mxTurnBubbleTs = parseInt(message.timestamp, 10) || 0;
     3491                                    mxTurnBubbleHasHtml = mxchatIsStoredHtmlPart(content);
     3492                                } else {
     3493                                    $mxTurnBubble = null;
     3494                                }
     3495
    34093496                                // Track message IDs
    34103497                                if (message.id) {
    … …  
    35773664    // ====================================
    35783665
     3666    // A setting arrives as "1" from the normal loader (wp_localize_script), as
     3667    // a JSON true from the delayed and On User Interaction loader, or as "on"
     3668    // as the settings store it: every loading mode reads it the same way (M7g).
     3669    function isSettingOn(value) {
     3670        return value === true || value === 'on' || value === '1' || value === 1;
     3671    }
     3672
     3673    // MxChat's Complianz integration is switched on.
     3674    function isComplianzEnabled() {
     3675        return isSettingOn(mxchatChat.complianz_toggle);
     3676    }
     3677
     3678    // Complianz's browser API is on the page.
     3679    function isComplianzReady() {
     3680        return typeof cmplz_has_consent === 'function' && typeof complianz !== 'undefined';
     3681    }
     3682
     3683    // The server found Complianz active on the site (plan 352c55), so its
     3684    // script is coming even when a Delay JavaScript setup holds it back.
     3685    function isComplianzExpected() {
     3686        return isSettingOn(mxchatChat.complianz_active);
     3687    }
     3688
     3689    // The consent check every opener goes through (plan 993fbf): the
     3690    // visitor's click, the pre-chat bubble, Trigger, the Multi-Bot handoff and
     3691    // any custom code clicking the launcher. With the integration on, the chat
     3692    // opens only with Complianz's marketing consent; while Complianz's script
     3693    // is expected but not loaded yet, it stays closed.
     3694    function consentAllowsOpen() {
     3695        if (!isComplianzEnabled()) {
     3696            return true;
     3697        }
     3698        if (isComplianzReady()) {
     3699            return cmplz_has_consent('marketing') === true;
     3700        }
     3701        return !isComplianzExpected();
     3702    }
     3703
     3704    // ---- Embedded chats (shortcode floating="no", the inline block) -------
     3705    // With the integration on, the server renders an embedded chat hidden and
     3706    // marked no-consent, with a one-line placeholder in its place (plan
     3707    // 2b137b). Until consent, nothing of it runs: no settings, nonce, session,
     3708    // email check or history request, no mxchat:opened, no send. That work is
     3709    // queued here and runs once, when the chat is revealed in place.
     3710    var mxchatEmbeddedConsentWork = {};
     3711
     3712    function mxchatConsentPlaceholder(botId) {
     3713        return document.getElementById('mxchat-consent-placeholder-' + (botId || 'default'));
     3714    }
     3715
     3716    // The bot is an embedded chat the server gated, still waiting for consent.
     3717    function mxchatEmbeddedConsentPending(botId) {
     3718        botId = botId || 'default';
     3719        var wrapper = getElementDOM(botId, 'mxchat-chatbot-wrapper');
     3720        return !!(wrapper && wrapper.classList.contains('no-consent') && isEmbeddedBot(botId));
     3721    }
     3722
     3723    // A send to a gated embedded chat (a script, an add-on) goes nowhere and
     3724    // leaves no thinking bubble behind.
     3725    function mxchatEmbeddedConsentBlocksSend(botId) {
     3726        if (!mxchatEmbeddedConsentPending(botId)) {
     3727            return false;
     3728        }
     3729        getElement(botId, 'chat-box').find('.temporary-message').remove();
     3730        return true;
     3731    }
     3732
     3733    // Run now, or once the embedded chat is revealed.
     3734    function mxchatWhenEmbeddedConsented(botId, work) {
     3735        botId = botId || 'default';
     3736        if (!mxchatEmbeddedConsentPending(botId)) {
     3737            work();
     3738            return;
     3739        }
     3740        if (!mxchatEmbeddedConsentWork) {
     3741            mxchatEmbeddedConsentWork = {};
     3742        }
     3743        (mxchatEmbeddedConsentWork[botId] = mxchatEmbeddedConsentWork[botId] || []).push(work);
     3744    }
     3745
     3746    // Consent given (or nothing to ask): the chat replaces its placeholder.
     3747    function mxchatShowEmbeddedChat(botId) {
     3748        botId = botId || 'default';
     3749        var placeholder = mxchatConsentPlaceholder(botId);
     3750        var wrapper = getElementDOM(botId, 'mxchat-chatbot-wrapper');
     3751        if (!placeholder || !wrapper || !isEmbeddedBot(botId)) {
     3752            return;
     3753        }
     3754        placeholder.hidden = true;
     3755        wrapper.classList.remove('no-consent');
     3756        var work = (mxchatEmbeddedConsentWork && mxchatEmbeddedConsentWork[botId]) || [];
     3757        if (mxchatEmbeddedConsentWork) {
     3758            delete mxchatEmbeddedConsentWork[botId];
     3759        }
     3760        work.forEach(function(fn) {
     3761            try { fn(); } catch (e) { /* one failing step never blocks the rest */ }
     3762        });
     3763    }
     3764
     3765    // Consent withdrawn: the placeholder comes back, a reply still streaming
     3766    // ends and keyboard focus leaves the chat, as the floating window does
     3767    // (plan 8df5b3).
     3768    function mxchatHideEmbeddedChat(botId) {
     3769        botId = botId || 'default';
     3770        var placeholder = mxchatConsentPlaceholder(botId);
     3771        var wrapper = getElementDOM(botId, 'mxchat-chatbot-wrapper');
     3772        if (!placeholder || !wrapper || !isEmbeddedBot(botId)) {
     3773            return;
     3774        }
     3775        if (!wrapper.classList.contains('no-consent')) {
     3776            mxchatStopStreaming(botId);
     3777            wrapper.classList.add('no-consent');
     3778            if (wrapper.contains(document.activeElement)) {
     3779                document.activeElement.blur();
     3780            }
     3781        }
     3782        placeholder.hidden = false;
     3783    }
     3784
     3785    function watchComplianzConsent(botId) {
     3786        // Initial check
     3787        checkConsentAndShowChat(botId);
     3788
     3789        // Listen for consent changes
     3790        $(document).on('cmplz_status_change', function(event) {
     3791            checkConsentAndShowChat(botId);
     3792        });
     3793    }
     3794
    35793795    function initializeChatVisibility(botId) {
    35803796        botId = botId || 'default';
    3581         // The Complianz setting arrives as "1" from the normal loader
    3582         // (wp_localize_script), as a JSON true from the delayed and On User
    3583         // Interaction loader, or as "on" as the settings store it: every
    3584         // loading mode asks for consent the same way (M7g).
    3585         const complianzToggle = mxchatChat.complianz_toggle;
    3586         const complianzEnabled = complianzToggle === true ||
    3587                                 complianzToggle === 'on' ||
    3588                                 complianzToggle === '1' ||
    3589                                 complianzToggle === 1;
    3590 
    3591         if (complianzEnabled && typeof cmplz_has_consent === "function" && typeof complianz !== 'undefined') {
    3592             // Initial check
    3593             checkConsentAndShowChat(botId);
    3594 
    3595             // Listen for consent changes
    3596             $(document).on('cmplz_status_change', function(event) {
    3597                 checkConsentAndShowChat(botId);
    3598             });
     3797        const complianzEnabled = isComplianzEnabled();
     3798
     3799        if (complianzEnabled && isComplianzReady()) {
     3800            watchComplianzConsent(botId);
     3801        } else if (complianzEnabled && isComplianzExpected()) {
     3802            // Complianz is active but its script is not on the page yet: a
     3803            // Delay JavaScript setup can hold it back while this script runs
     3804            // (plan 352c55). The launcher stays as the server rendered it,
     3805            // hidden and marked no-consent, until Complianz loads and answers.
     3806            var complianzWait = setInterval(function() {
     3807                if (isComplianzReady()) {
     3808                    clearInterval(complianzWait);
     3809                    watchComplianzConsent(botId);
     3810                }
     3811            }, 250);
    35993812        } else {
    3600             // If Complianz is not enabled (or its script is not on the page),
    3601             // always show. The window's no-consent mark is cleared with the
     3813            // If Complianz is not enabled (or not installed while the setting
     3814            // is on), always show. The window's no-consent mark is cleared with the
    36023815            // launcher's, so once this script has run both marks mean the same
    36033816            // thing in every loading mode: consent is needed and not given.
    … …  
    36073820                .fadeTo(500, 1);
    36083821            getElement(botId, 'floating-chatbot').removeClass('no-consent');
     3822            mxchatShowEmbeddedChat(botId);
    36093823
    36103824            // Also check pre-chat message when Complianz is not enabled
    … …  
    36303844                .fadeTo(500, 1);
    36313845            $chatbot.removeClass('no-consent');
     3846            mxchatShowEmbeddedChat(botId);
    36323847
    36333848            // Show pre-chat message if not dismissed
    … …  
    36423857                });
    36433858            $chatbot.addClass('no-consent');
     3859
     3860            // Consent withdrawn while the chat is open (plan 8df5b3): close the
     3861            // window too, as the title bar does, end a reply still streaming,
     3862            // and take keyboard focus out of it so nothing more can be typed
     3863            // or sent. The launcher stays hidden until consent is given again.
     3864            if ($chatbot.hasClass('visible')) {
     3865                mxchatStopStreaming(botId);
     3866                $chatbot.removeClass('visible').addClass('hidden').removeAttr('aria-modal');
     3867                $widget.attr('aria-expanded', 'false');
     3868                enableScroll();
     3869                if ($chatbot[0] && $chatbot[0].contains(document.activeElement)) {
     3870                    document.activeElement.blur();
     3871                }
     3872            }
     3873
     3874            mxchatHideEmbeddedChat(botId);
    36443875
    36453876            // Hide pre-chat message when no consent
    … …  
    37724003
    37734004        if ($chatbot.hasClass('hidden')) {
     4005            // Every launcher click lands here, scripted ones included: no
     4006            // opening without the cookie consent Complianz requires (993fbf).
     4007            if (!consentAllowsOpen()) {
     4008                return;
     4009            }
    37744010            $chatbot.removeClass('hidden').addClass('visible')
    37754011                    .attr('aria-modal', 'true').attr('role', 'dialog');
    … …  
    45384774        if (emailBlocker) {
    45394775            if (isEmbeddedBot(botId)) {
    4540                 // Embedded bots are always visible — check now
    4541                 resolveEmailState(botId);
     4776                // Embedded bots are always visible — check now (once
     4777                // revealed, when waiting for cookie consent: plan 2b137b)
     4778                mxchatWhenEmbeddedConsented(botId, function() {
     4779                    resolveEmailState(botId);
     4780                });
    45424781            }
    45434782            // Floating bots: handled in the widget open handler
    … …  
    45464785            var chatPersistenceEnabled = typeof mxchatChat !== 'undefined' && mxchatChat.chat_persistence_toggle === 'on';
    45474786            if (chatPersistenceEnabled) {
    4548                 MxChatInstances.ensureSession(botId);
    4549                 showChatContainerForBot(botId);
     4787                mxchatWhenEmbeddedConsented(botId, function() {
     4788                    MxChatInstances.ensureSession(botId);
     4789                    showChatContainerForBot(botId);
     4790                });
    45504791            }
    45514792        }
    … …  
    45574798        var botId = getBotIdFromElement(this);
    45584799        var $chatbot = getElement(botId, 'floating-chatbot');
    4559         if ($chatbot.hasClass('hidden')) {
     4800        if ($chatbot.hasClass('hidden') && consentAllowsOpen()) {
    45604801            $chatbot.removeClass('hidden').addClass('visible');
    45614802            getElement(botId, 'floating-chatbot-button').addClass('hidden');
    … …  
    47124953        // An embedded widget is on screen from the first paint; a floating one
    47134954        // reports mxchat:opened when its window is shown (plan 65c9b6).
     4955        // An embedded chat waiting for cookie consent reports it (and
     4956        // refreshes its settings) when revealed (plan 2b137b).
    47144957        if (isEmbeddedBot(botId)) {
    4715             mxchatFireOpened(botId);
     4958            if (mxchatEmbeddedConsentPending(botId)) {
     4959                mxchatWhenEmbeddedConsented(botId, function() {
     4960                    mxchatRefreshDynamicSettings();
     4961                    mxchatFireOpened(botId);
     4962                });
     4963            } else {
     4964                mxchatFireOpened(botId);
     4965            }
    47164966        } else {
    47174967            mxchatWatchForOpen(botId);
    … …  
    47344984    window.updateStreamingMessage = updateStreamingMessage;
    47354985    window.MxChatInstances = MxChatInstances;
     4986    // Add-ons wait on core's own nonce refresh (cache, in-flight request and
     4987    // admin-ajax fallback shared) instead of guessing its timing (plan 3301ab).
     4988    // Feature-detect it: core 3.2.26 and older do not define it.
     4989    window.mxchatWithFreshNonce = withFreshNonce;
    47364990    window.getElement = getElement;
    47374991    window.getElementDOM = getElementDOM;
Note: See TracChangeset for help on using the changeset viewer.