Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ishields opened this issue Nov 7, 2024 · 7 comments

Comments

@ishields
Copy link

ishields commented Nov 7, 2024

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:

  • Turbo Drive (a feature of Hotwire in Rails) that improves the speed of websites by rendering the page inteligently, only updating parts that have changed. This prevents flickering and reloading entire pages when it's not necessary. It does so by replaces elements in the body that have changed, but keeping the others in place.
  • Crisp automatically appends its widget to the and uses an internal MutationObserver to monitor DOM changes, re-rendering itself whenever it detects changes.
  • Because Turbo replaces the , Crisp's MutationObserver detects this as a change, which causes the Crisp widget to reload or reinitialize each time Turbo navigates to a new page. This leads to the widget "resetting" or duplicating, impacting user experience.
  • Turbo allows you to put attributes on elements you don't want to be replaced. Crisp js on the other hand does not allow setting a container element, class, or id for the widget to use. As a result, we can't tell Turbo to not manipulate the crisps chat.

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
Copy link
Member

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
Copy link
Author

ishields commented Nov 7, 2024

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
Copy link
Member

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

@ishields
Copy link
Author

ishields commented Nov 7, 2024

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
Copy link
Member

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

@ishields
Copy link
Author

ishields commented Nov 7, 2024

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... ')

@ishields
Copy link
Author

@baptistejamin Hello is there any update on this issue? I'm concerned about leaving this hack in my code long term and would really appreciate an update. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants