crisp-im / crisp-sdk-web

:package: Include the Crisp chat widget from using frameworks such as React, VueJS, Angular...
https://www.npmjs.com/package/crisp-sdk-web
MIT License
40 stars 15 forks source link

Crisp Web Client not compatible with Ruby on Rails Turbo (and possibly other frameworks) #39

Open ishields opened 5 days ago

ishields commented 5 days ago

I'm migrating to Crisp and have been implementing the Chat bot on our website. I thought this would be a quick but I ran into a pretty big incompatibility issue with the popular web frame Ruby on Rails and it's Turbo functionality. The issue I observed was that when clicking around on my website, the Chatbot would disappear for a second and then re-appear.. This occurred when the chatbot was open or closed and didn't look great. The behavior I expected was that the chatbot would stay consistently in view like it does on crisp's main website. I did notice that on Crisps documentation page the same behavior exists (though in this case it's expected because the documentation does full page loads). Below video shows the expected vs the unexpected behavior. Notice on the documentation each click results in the widget disappearing and re-rendering. On the main crisp site this doesn't happen. (video below)

Nov-07-2024 09-01-47

Problem Summary:

Desired long term solution

This should be a simple fix. All we need to be able to do is be able to configure the element that the Crisp chatbot will be placed into. I imagine this will look something like this

Crisp.configure('secret key', { container: '#my-crisp-wrapper' })

We would then add the special ovrride option on that wrapper so that Turbo does not update the element.

<div id="my-crisp-wrapper" data-turbo-permanent></div>

My Hacky Solution

  1. Persistent Container (data-turbo-permanent): Wrapping Crisp in a data-turbo-permanent container prevents Turbo from replacing it. However, this doesn’t work directly because Crisp appends itself to , outside of any data-turbo-permanent wrapper. Disabling Crisp’s MutationObserver:

  2. We intercept Crisp's MutationObserver to prevent it from triggering a reload. By temporarily disconnecting the observer, we can move the Crisp widget to our container without triggering a re-render.

  3. When Crisp reloads, move it to a data-turbo-permanent positon Listening for Crisp's session:loaded event allows us to detect when Crisp reloads itself. Each time Crisp reloads, we move it to a data-turbo-permanent container to persist through Turbo navigations.

I will attach the actual source of this solution soon. It works however I'd like to clean it up before sharing.

baptistejamin commented 5 days ago

Hello, and thank you for the suggestion.

So the Crisp docs are not really affected with this because it's not a SPA, but regular HTML pages.

Do you have any link to your site so we can check this with turbolinks?

ishields commented 5 days ago

Thanks for the quick response. Yes that's totally fair - I was sort of just using it as an example of the behavior. Totally makes sense the doc page is just reloading so of course it's going to behave this way. I haven't deployed the chat widget to my site because of this problem. And since I have a fix I was going to deploy it with it working. Would you like me to temporarily deploy the chat widget with the flashing or would a video suffice? Alternatively I can make a feature toggle that lets you enable the problem on my domain but by default it will work as designed with my hack.

baptistejamin commented 5 days ago

Alternately just create a dummy rails project with turbo so we can fully reproduce

ishields commented 5 days ago

Happy to do that. Would you want me to just send you the zip of the dummy project and you can test locally or you need it deployed somewhere?

baptistejamin commented 5 days ago

Sure. Feel free to send to it baptiste@crisp.chat

ishields commented 5 days ago

Just emailed you the sample project with instructions. Also below is my fix for this issue. As mentioned it's a bit hacky and not something I really want to leave in long term. It messes with the MutationObserver instructor core js constructor in order to gain access to the crisps mutation observer so it can be disabled.

const { Crisp } = require('crisp-sdk-web')

let originalMutationObserver = window.MutationObserver
let listOfObservers = []

// Override Mutation Observer such that whenever a new one is created it checks to see if it's a Crisp one.
window.MutationObserver = (aFunction) => {
  let observer = new originalMutationObserver(aFunction)
  const stack = new Error().stack
  // Only add Crisp observers to the list we care about.
  if (stack?.includes('crisp')) {
    //console.log('Tracking this crisp observer')
    listOfObservers.push(observer)
  } else {
    //console.log('Not tracking this observer')
  }
  return observer
}

window.overrideCrispObserver = () => {
  const originalMO = window.MutationObserver

  // Backup the MutationObserver constructor
  window.MutationObserver = function (callback) {
    // Create an observer but do not attach it
    const observer = new originalMO(callback)

    // Save the observer to disable later
    originalMutationObserver = observer

    return observer
  }
}

const disconnectAllObservers = () => {
  listOfObservers.forEach((observer) => {
    observer.disconnect()
  })
}

const reconnectAllObservers = () => {
  listOfObservers.forEach((observer) => {
    observer.disconnect()
  })
}

const onCrispReady = () => {
  console.log('Crisp chat widget is rendered and ready!')
  // You can add other actions here
  moveCrispToPermanentContainer()
  preserveScrollOnChatBot()
}
// // This method is fired by crisp when it is initialized and ready
window.CRISP_READY_TRIGGER = onCrispReady

const moveCrispToPermanentContainer = () => {
  const crispWidget = document.querySelector('.crisp-client')
  const crispWrapper = document.getElementById('crisp-wrapper')

  if (crispWidget && crispWrapper && !crispWrapper.contains(crispWidget)) {
    // Temporarily disable the MutationObserver
    disconnectAllObservers()

    // Move the widget to a persistent container
    crispWrapper.appendChild(crispWidget)

    // Re-enable the MutationObserver
    reconnectAllObservers()

    // console.log('Crisp widget moved to permanent container and observers reconnected)
  }
}

const preserveScrollOnChatBot = () => {
 // not including this here. It fixes a scrolling issue also caused by Turbo. Turbo scrolls pages to the top when new pages 
 //load. Without a fix here it also scrolls the chat window to the beginning of the conversation
}

Crisp.configure('...TODO: Add your token... ')