mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-11 13:39:03 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9635fb972e |
@@ -13,6 +13,7 @@ and this project adheres to
|
|||||||
- 📈(frontend) include LiveKit SIDs in the connection analytics event
|
- 📈(frontend) include LiveKit SIDs in the connection analytics event
|
||||||
- 🔇(backend) silence expected 401 warnings on /me
|
- 🔇(backend) silence expected 401 warnings on /me
|
||||||
- 🔇(backend) silence noisy request summary info logs
|
- 🔇(backend) silence noisy request summary info logs
|
||||||
|
- ⚡️(frontend) defer loading the Crisp script until idle
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -2,23 +2,23 @@ import { RiQuestionLine } from '@remixicon/react'
|
|||||||
import { MenuItem } from 'react-aria-components'
|
import { MenuItem } from 'react-aria-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||||
import { Crisp } from 'crisp-sdk-web'
|
import {
|
||||||
import { useIsSupportEnabled } from '@/features/support/hooks/useSupport'
|
useIsSupportEnabled,
|
||||||
|
openSupportChat,
|
||||||
|
} from '@/features/support/hooks/useSupport'
|
||||||
|
|
||||||
export const SupportMenuItem = () => {
|
export const SupportMenuItem = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||||
const isSupportEnabled = useIsSupportEnabled()
|
const isSupportEnabled = useIsSupportEnabled()
|
||||||
|
|
||||||
if (!isSupportEnabled || !Crisp) {
|
if (!isSupportEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||||
onAction={() => {
|
onAction={openSupportChat}
|
||||||
Crisp?.chat.open()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<RiQuestionLine size={20} />
|
<RiQuestionLine size={20} />
|
||||||
{t('support')}
|
{t('support')}
|
||||||
|
|||||||
@@ -1,20 +1,45 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Crisp } from 'crisp-sdk-web'
|
|
||||||
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
||||||
import { useUser } from '@/features/auth/api/useUser'
|
import { useUser } from '@/features/auth/api/useUser'
|
||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
|
|
||||||
|
type CrispSdk = (typeof import('crisp-sdk-web'))['Crisp']
|
||||||
|
|
||||||
|
let crisp: CrispSdk | undefined
|
||||||
|
let crispPromise: Promise<CrispSdk> | undefined
|
||||||
|
|
||||||
|
const loadCrisp = (): Promise<CrispSdk> => {
|
||||||
|
crispPromise ??= import('crisp-sdk-web')
|
||||||
|
.then((module) => {
|
||||||
|
crisp = module.Crisp
|
||||||
|
return module.Crisp
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
crispPromise = undefined
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
return crispPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openSupportChat = () => {
|
||||||
|
if (!crisp?.isCrispInjected()) return
|
||||||
|
crisp.chat.open()
|
||||||
|
}
|
||||||
|
|
||||||
export const initializeSupportSession = (user: ApiUser) => {
|
export const initializeSupportSession = (user: ApiUser) => {
|
||||||
if (!Crisp.isCrispInjected()) return
|
if (!crisp?.isCrispInjected()) return
|
||||||
|
|
||||||
const { id, email } = user
|
const { id, email } = user
|
||||||
Crisp.setTokenId(`meet-${id}`)
|
crisp.setTokenId(`meet-${id}`)
|
||||||
if (email) Crisp.user.setEmail(email)
|
if (email) crisp.user.setEmail(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const terminateSupportSession = () => {
|
export const terminateSupportSession = () => {
|
||||||
if (!Crisp.isCrispInjected()) return
|
if (!crisp?.isCrispInjected()) return
|
||||||
Crisp.setTokenId()
|
|
||||||
Crisp.session.reset()
|
crisp.setTokenId()
|
||||||
|
crisp.session.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
export type useSupportProps = {
|
export type useSupportProps = {
|
||||||
@@ -22,26 +47,70 @@ export type useSupportProps = {
|
|||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure Crisp chat for real-time support across all pages.
|
const IDLE_TIMEOUT_MS = 10_000
|
||||||
|
|
||||||
|
const scheduleWhenIdle = (callback: () => void): (() => void) => {
|
||||||
|
if (typeof window.requestIdleCallback === 'function') {
|
||||||
|
const handle = window.requestIdleCallback(callback, {
|
||||||
|
timeout: IDLE_TIMEOUT_MS,
|
||||||
|
})
|
||||||
|
return () => window.cancelIdleCallback(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = window.setTimeout(callback, 1)
|
||||||
|
return () => window.clearTimeout(handle)
|
||||||
|
}
|
||||||
|
|
||||||
export const useSupport = ({ id, isDisabled }: useSupportProps) => {
|
export const useSupport = ({ id, isDisabled }: useSupportProps) => {
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
|
const [isInjected, setIsInjected] = useState(
|
||||||
|
() => crisp?.isCrispInjected() ?? false
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id || Crisp.isCrispInjected() || isDisabled) return
|
if (!id || isDisabled) return
|
||||||
Crisp.configure(id)
|
|
||||||
Crisp.setHideOnMobile(true)
|
if (crisp?.isCrispInjected()) {
|
||||||
|
setIsInjected(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
const cancelIdle = scheduleWhenIdle(() => {
|
||||||
|
void loadCrisp()
|
||||||
|
.then((sdk) => {
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
|
if (!sdk.isCrispInjected()) {
|
||||||
|
sdk.configure(id)
|
||||||
|
sdk.setHideOnMobile(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsInjected(true)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
console.error('Failed to initialize support chat', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
cancelIdle()
|
||||||
|
}
|
||||||
}, [id, isDisabled])
|
}, [id, isDisabled])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return
|
if (!user || !isInjected || isDisabled) return
|
||||||
initializeSupportSession(user)
|
initializeSupportSession(user)
|
||||||
}, [user])
|
}, [user, isInjected, isDisabled])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some users may block Crisp chat widget with browser ad blockers or anti-tracking plugins
|
// Some users block the chat widget, so check its availability safely.
|
||||||
// So we need to safely check if Crisp is available and not blocked
|
|
||||||
const isCrispAvailable = () => {
|
const isCrispAvailable = () => {
|
||||||
try {
|
try {
|
||||||
return !!window?.$crisp?.is
|
return !!window?.$crisp?.is
|
||||||
|
|||||||
Reference in New Issue
Block a user