🐛(frontend) rework the calendar integration approach
Use a totally different strategy, which hopefully works in prod env. Actually broadcast channel cannot be shared between two different browsing context, even on a same domain, an iFrame and a pop up cannot communicate. Moreover, in an iFrame session cookie are unavailable. Rely on a newly created backend endpoint to pass room data.
This commit is contained in:
committed by
aleb_the_flash
parent
506b3978e1
commit
66f307b7e8
@@ -5,7 +5,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|||||||
import { QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLang } from 'hoofd'
|
import { useLang } from 'hoofd'
|
||||||
import { Switch, Route, useLocation } from 'wouter'
|
import { Switch, Route } from 'wouter'
|
||||||
import { I18nProvider } from 'react-aria-components'
|
import { I18nProvider } from 'react-aria-components'
|
||||||
import { Layout } from './layout/Layout'
|
import { Layout } from './layout/Layout'
|
||||||
import { NotFoundScreen } from './components/NotFoundScreen'
|
import { NotFoundScreen } from './components/NotFoundScreen'
|
||||||
@@ -13,35 +13,11 @@ import { routes } from './routes'
|
|||||||
import './i18n/init'
|
import './i18n/init'
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
import { AppInitialization } from '@/components/AppInitialization'
|
import { AppInitialization } from '@/components/AppInitialization'
|
||||||
import { SdkCreateButton } from './features/sdk/routes/CreateButton'
|
|
||||||
|
|
||||||
const SDK_BASE_ROUTE = '/sdk'
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
useLang(i18n.language)
|
useLang(i18n.language)
|
||||||
|
|
||||||
const [location] = useLocation()
|
|
||||||
const isSDKRoute = location.startsWith(SDK_BASE_ROUTE)
|
|
||||||
|
|
||||||
if (isSDKRoute) {
|
|
||||||
return (
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<Suspense fallback={null}>
|
|
||||||
<I18nProvider locale={i18n.language}>
|
|
||||||
<Switch>
|
|
||||||
<Route path={SDK_BASE_ROUTE} nest>
|
|
||||||
<Route path="/create-button">
|
|
||||||
<SdkCreateButton />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
</I18nProvider>
|
|
||||||
</Suspense>
|
|
||||||
</QueryClientProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AppInitialization />
|
<AppInitialization />
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export const keys = {
|
|||||||
config: 'config',
|
config: 'config',
|
||||||
requestEntry: 'requestEntry',
|
requestEntry: 'requestEntry',
|
||||||
waitingParticipants: 'waitingParticipants',
|
waitingParticipants: 'waitingParticipants',
|
||||||
|
roomCreationCallback: 'roomCreationCallback',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import { silenceLiveKitLogs } from '@/utils/livekit'
|
|||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
|
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
|
||||||
import { useSupport } from '@/features/support/hooks/useSupport'
|
import { useSupport } from '@/features/support/hooks/useSupport'
|
||||||
|
import { useLocation } from 'wouter'
|
||||||
|
|
||||||
|
const SDK_BASE_ROUTE = '/sdk'
|
||||||
|
|
||||||
export const AppInitialization = () => {
|
export const AppInitialization = () => {
|
||||||
const { data } = useConfig()
|
const { data } = useConfig()
|
||||||
|
const [location] = useLocation()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
analytics = {},
|
analytics = {},
|
||||||
@@ -12,8 +16,11 @@ export const AppInitialization = () => {
|
|||||||
silence_livekit_debug_logs = false,
|
silence_livekit_debug_logs = false,
|
||||||
} = data || {}
|
} = data || {}
|
||||||
|
|
||||||
useAnalytics(analytics)
|
const isSDKContext = location.includes(SDK_BASE_ROUTE)
|
||||||
useSupport(support)
|
|
||||||
|
useAnalytics({ ...analytics, isDisabled: isSDKContext })
|
||||||
|
useSupport({ ...support, isDisabled: isSDKContext })
|
||||||
|
|
||||||
silenceLiveKitLogs(silence_livekit_debug_logs)
|
silenceLiveKitLogs(silence_livekit_debug_logs)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -17,18 +17,19 @@ export const terminateAnalyticsSession = () => {
|
|||||||
export type useAnalyticsProps = {
|
export type useAnalyticsProps = {
|
||||||
id?: string
|
id?: string
|
||||||
host?: string
|
host?: string
|
||||||
|
isDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAnalytics = ({ id, host }: useAnalyticsProps) => {
|
export const useAnalytics = ({ id, host, isDisabled }: useAnalyticsProps) => {
|
||||||
const [location] = useLocation()
|
const [location] = useLocation()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id || !host) return
|
if (!id || !host || isDisabled) return
|
||||||
if (posthog.__loaded) return
|
if (posthog.__loaded) return
|
||||||
posthog.init(id, {
|
posthog.init(id, {
|
||||||
api_host: host,
|
api_host: host,
|
||||||
person_profiles: 'always',
|
person_profiles: 'always',
|
||||||
})
|
})
|
||||||
}, [id, host])
|
}, [id, host, isDisabled])
|
||||||
|
|
||||||
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
|
// From PostHog tutorial on PageView tracking in a Single Page Application (SPA) context.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import { RiAddLine, RiLink } from '@remixicon/react'
|
|||||||
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||||
import { IntroSlider } from '@/features/home/components/IntroSlider'
|
import { IntroSlider } from '@/features/home/components/IntroSlider'
|
||||||
import { MoreLink } from '@/features/home/components/MoreLink'
|
import { MoreLink } from '@/features/home/components/MoreLink'
|
||||||
import { ReactNode, useEffect, useState } from 'react'
|
import { ReactNode, useState } from 'react'
|
||||||
|
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
||||||
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
|
||||||
import { SdkReverseClient } from '@/features/sdk/SdkReverseClient'
|
|
||||||
|
|
||||||
const Columns = ({ children }: { children?: ReactNode }) => {
|
const Columns = ({ children }: { children?: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
@@ -156,18 +155,6 @@ export const Home = () => {
|
|||||||
const { mutateAsync: createRoom } = useCreateRoom()
|
const { mutateAsync: createRoom } = useCreateRoom()
|
||||||
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
|
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
|
||||||
|
|
||||||
const { user } = useUser()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used for SDK popup to close automatically.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!user) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
SdkReverseClient.broadcastAuthentication()
|
|
||||||
}, [user])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserAware>
|
<UserAware>
|
||||||
<Screen>
|
<Screen>
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ import { ApiRoom } from './ApiRoom'
|
|||||||
|
|
||||||
export interface CreateRoomParams {
|
export interface CreateRoomParams {
|
||||||
slug: string
|
slug: string
|
||||||
|
callbackId?: string
|
||||||
username?: string
|
username?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRoom = ({
|
const createRoom = ({
|
||||||
slug,
|
slug,
|
||||||
|
callbackId,
|
||||||
username = '',
|
username = '',
|
||||||
}: CreateRoomParams): Promise<ApiRoom> => {
|
}: CreateRoomParams): Promise<ApiRoom> => {
|
||||||
return fetchApi(`rooms/?username=${encodeURIComponent(username)}`, {
|
return fetchApi(`rooms/?username=${encodeURIComponent(username)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: slug,
|
name: slug,
|
||||||
|
callback_id: callbackId,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import { authUrl, useUser } from '../auth'
|
|
||||||
|
|
||||||
export enum ClientMessageType {
|
|
||||||
ROOM_CREATED = 'ROOM_CREATED',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SdkReverseClient {
|
|
||||||
/**
|
|
||||||
* IDEA: Use API Key. Must be based on some sort of credentials? No needs for now as there are no security
|
|
||||||
* plausible at the moment.
|
|
||||||
*/
|
|
||||||
static getAllowTargetOrigin() {
|
|
||||||
return '*'
|
|
||||||
}
|
|
||||||
|
|
||||||
static post(type: ClientMessageType, data: unknown = {}) {
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
},
|
|
||||||
SdkReverseClient.getAllowTargetOrigin()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static broadcastAuthentication() {
|
|
||||||
const bc = new BroadcastChannel('APP_CHANNEL')
|
|
||||||
bc.postMessage({ type: 'AUTHENTICATED' })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This means the parent window has authenticated has successfully refetched user, then we can close the popup.
|
|
||||||
*/
|
|
||||||
bc.onmessage = (event) => {
|
|
||||||
if (event.data.type === 'AUTHENTICATED_ACK') {
|
|
||||||
window.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static waitForAuthenticationAck() {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
const bc = new BroadcastChannel('APP_CHANNEL')
|
|
||||||
bc.onmessage = async (event) => {
|
|
||||||
if (event.data.type === 'AUTHENTICATED') {
|
|
||||||
resolve()
|
|
||||||
bc.postMessage({ type: 'AUTHENTICATED_ACK' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function to be awaited in order to make sure the user is logged in.
|
|
||||||
* If not logged-in it opens a popup with the connection flow, the promise returned is resolved
|
|
||||||
* once logged-in.
|
|
||||||
*
|
|
||||||
* To be used in SDK scope.
|
|
||||||
*/
|
|
||||||
export function useEnsureAuth() {
|
|
||||||
const { isLoggedIn, refetch } = useUser({
|
|
||||||
fetchUserOptions: { attemptSilent: false },
|
|
||||||
})
|
|
||||||
|
|
||||||
const startSSO = () => {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
SdkReverseClient.waitForAuthenticationAck().then(async () => {
|
|
||||||
await refetch()
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
const params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
|
|
||||||
width=400,height=900,left=100,top=100`
|
|
||||||
window.open(new URL('authenticate/', authUrl()).href, '', params)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureAuth = async () => {
|
|
||||||
if (!isLoggedIn) {
|
|
||||||
await startSSO()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ensureAuth }
|
|
||||||
}
|
|
||||||
35
src/frontend/src/features/sdk/api/useRoomCreationCallback.ts
Normal file
35
src/frontend/src/features/sdk/api/useRoomCreationCallback.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { fetchApi } from '@/api/fetchApi'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { keys } from '@/api/queryKeys'
|
||||||
|
import { CallbackCreationRoomData } from '../utils/types'
|
||||||
|
|
||||||
|
export type CallbackResponse = {
|
||||||
|
status: string
|
||||||
|
room: CallbackCreationRoomData
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchRoomGenerationState = async ({
|
||||||
|
callbackId,
|
||||||
|
}: {
|
||||||
|
callbackId: string
|
||||||
|
}) => {
|
||||||
|
return fetchApi<CallbackResponse>(`/rooms/creation-callback/`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
callback_id: callbackId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRoomCreationCallback = ({
|
||||||
|
callbackId = '',
|
||||||
|
}: {
|
||||||
|
callbackId?: string
|
||||||
|
}) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [keys.roomCreationCallback, callbackId],
|
||||||
|
queryFn: () => fetchRoomGenerationState({ callbackId }),
|
||||||
|
enabled: !!callbackId,
|
||||||
|
refetchInterval: 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import { Button } from '@/primitives/Button'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react'
|
|
||||||
import { VisioIcon } from '@/assets/VisioIcon'
|
|
||||||
import { generateRoomId, useCreateRoom } from '../../rooms'
|
|
||||||
import {
|
|
||||||
ClientMessageType,
|
|
||||||
SdkReverseClient,
|
|
||||||
useEnsureAuth,
|
|
||||||
} from '../SdkReverseClient'
|
|
||||||
|
|
||||||
export const SdkCreateButton = () => {
|
|
||||||
const { t } = useTranslation('sdk', { keyPrefix: 'createButton' })
|
|
||||||
const [roomUrl, setRoomUrl] = useState<string>()
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const {
|
|
||||||
userChoices: { username },
|
|
||||||
} = usePersistentUserChoices()
|
|
||||||
|
|
||||||
const { mutateAsync: createRoom } = useCreateRoom()
|
|
||||||
const { ensureAuth } = useEnsureAuth()
|
|
||||||
|
|
||||||
const submitCreateRoom = async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
const slug = generateRoomId()
|
|
||||||
const data = await createRoom({ slug, username })
|
|
||||||
const roomUrlTmp = getRouteUrl('room', data.slug)
|
|
||||||
setRoomUrl(roomUrlTmp)
|
|
||||||
setIsLoading(false)
|
|
||||||
SdkReverseClient.post(ClientMessageType.ROOM_CREATED, {
|
|
||||||
url: roomUrlTmp,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
await ensureAuth()
|
|
||||||
submitCreateRoom()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
paddingTop: '3px',
|
|
||||||
paddingLeft: '3px',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{roomUrl ? (
|
|
||||||
<RoomUrl roomUrl={roomUrl} />
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="primaryDark"
|
|
||||||
aria-label={t('label')}
|
|
||||||
onPress={submit}
|
|
||||||
data-attr="sdk-create"
|
|
||||||
loading={isLoading}
|
|
||||||
icon={<VisioIcon />}
|
|
||||||
>
|
|
||||||
{t('label')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const RoomUrl = ({ roomUrl }: { roomUrl: string }) => {
|
|
||||||
const [isCopied, setIsCopied] = useState(false)
|
|
||||||
|
|
||||||
const copy = () => {
|
|
||||||
navigator.clipboard.writeText(roomUrl!)
|
|
||||||
setIsCopied(true)
|
|
||||||
setTimeout(() => setIsCopied(false), 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={css({
|
|
||||||
color: 'greyscale.600',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{roomUrl}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant={isCopied ? 'success' : 'quaternaryText'}
|
|
||||||
data-attr="sdk-create-copy"
|
|
||||||
onPress={copy}
|
|
||||||
square
|
|
||||||
>
|
|
||||||
{isCopied ? <RiCheckLine /> : <RiFileCopyLine />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
164
src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx
Normal file
164
src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { Button } from '@/primitives/Button'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { Link } from 'react-aria-components'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { HStack, VStack } from '@/styled-system/jsx'
|
||||||
|
import { css } from '@/styled-system/css'
|
||||||
|
import { RiCloseLine, RiFileCopyLine } from '@remixicon/react'
|
||||||
|
import { Text } from '@/primitives'
|
||||||
|
import { Spinner } from '@/primitives/Spinner'
|
||||||
|
import { buttonRecipe } from '@/primitives/buttonRecipe'
|
||||||
|
import { VisioIcon } from '@/assets/VisioIcon'
|
||||||
|
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
||||||
|
import { useRoomCreationCallback } from '../api/useRoomCreationCallback'
|
||||||
|
import { PopupManager } from '../utils/PopupManager'
|
||||||
|
import { CallbackCreationRoomData } from '../utils/types'
|
||||||
|
import { useSearchParams } from 'wouter'
|
||||||
|
|
||||||
|
const popupManager = new PopupManager()
|
||||||
|
|
||||||
|
export const CreateMeetingButton = () => {
|
||||||
|
const { t } = useTranslation('sdk', { keyPrefix: 'createMeeting' })
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const [callbackId, setCallbackId] = useState<string | undefined>(undefined)
|
||||||
|
const [isPending, setIsPending] = useState(false)
|
||||||
|
|
||||||
|
const initialRoom = useMemo(() => {
|
||||||
|
const roomSlug = searchParams.get('slug')
|
||||||
|
if (!roomSlug) return undefined
|
||||||
|
return {
|
||||||
|
slug: roomSlug.trim(), // Trim whitespace for safety
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const [room, setRoom] = useState<CallbackCreationRoomData | undefined>(
|
||||||
|
initialRoom
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data } = useRoomCreationCallback({ callbackId })
|
||||||
|
|
||||||
|
const roomUrl = useMemo(() => {
|
||||||
|
if (room?.slug) return getRouteUrl('room', room.slug)
|
||||||
|
}, [room])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data?.room?.slug) return
|
||||||
|
setRoom(data.room)
|
||||||
|
setCallbackId(undefined)
|
||||||
|
setIsPending(false)
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
popupManager.setupMessageListener(
|
||||||
|
(id) => setCallbackId(id),
|
||||||
|
(data) => {
|
||||||
|
setRoom(data)
|
||||||
|
setIsPending(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => popupManager.cleanup()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
setRoom(undefined)
|
||||||
|
setCallbackId(undefined)
|
||||||
|
setIsPending(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="p-6"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'start',
|
||||||
|
alignItems: 'start',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{roomUrl && room?.slug ? (
|
||||||
|
<VStack justify={'start'} alignItems={'start'} gap={0.25}>
|
||||||
|
<HStack>
|
||||||
|
<Link
|
||||||
|
className={buttonRecipe({ size: 'sm' })}
|
||||||
|
href={roomUrl}
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VisioIcon />
|
||||||
|
{t('joinButton')}
|
||||||
|
</Link>
|
||||||
|
<HStack gap={0}>
|
||||||
|
<Button
|
||||||
|
variant="quaternaryText"
|
||||||
|
square
|
||||||
|
icon={<RiFileCopyLine />}
|
||||||
|
tooltip={t('copyLinkTooltip')}
|
||||||
|
onPress={() => {
|
||||||
|
navigator.clipboard.writeText(roomUrl)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{searchParams.get('readOnly') === 'false' && (
|
||||||
|
<Button
|
||||||
|
variant="quaternaryText"
|
||||||
|
square
|
||||||
|
icon={<RiCloseLine />}
|
||||||
|
onPress={resetState}
|
||||||
|
aria-label={t('resetLabel')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
<VStack justify={'start'} alignItems="start" gap={0.25}>
|
||||||
|
<Text variant={'smNote'} margin={false} centered={false}>
|
||||||
|
{roomUrl.replace('https://', '')}
|
||||||
|
</Text>
|
||||||
|
<Text variant={'smNote'} margin={false} centered={false}>
|
||||||
|
{t('participantLimit')}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
minHeight: '46px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/*
|
||||||
|
* Using popup for Visio to access session cookies (blocked in iframes).
|
||||||
|
* If authenticated: Popup creates room and returns data directly.
|
||||||
|
* If not: Popup sends callbackId, redirects to login, then backend
|
||||||
|
* associates new room with callbackId after authentication.
|
||||||
|
*/}
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
setIsPending(true)
|
||||||
|
popupManager.createPopupWindow(() => {
|
||||||
|
setIsPending(false)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<VisioIcon />
|
||||||
|
{t('createButton')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
76
src/frontend/src/features/sdk/routes/CreatePopup.tsx
Normal file
76
src/frontend/src/features/sdk/routes/CreatePopup.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect, useMemo } from 'react'
|
||||||
|
import { css } from '@/styled-system/css'
|
||||||
|
import { generateRoomId, useCreateRoom } from '../../rooms'
|
||||||
|
import { useUser } from '@/features/auth'
|
||||||
|
import { Spinner } from '@/primitives/Spinner'
|
||||||
|
import { CallbackIdHandler } from '../utils/CallbackIdHandler'
|
||||||
|
import { PopupWindow } from '../utils/PopupWindow'
|
||||||
|
|
||||||
|
const callbackIdHandler = new CallbackIdHandler()
|
||||||
|
const popupWindow = new PopupWindow()
|
||||||
|
|
||||||
|
export const CreatePopup = () => {
|
||||||
|
const { isLoggedIn } = useUser({ fetchUserOptions: { attemptSilent: false } })
|
||||||
|
const { mutateAsync: createRoom } = useCreateRoom()
|
||||||
|
|
||||||
|
const callbackId = useMemo(() => callbackIdHandler.getOrCreate(), [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle unauthenticated users by redirecting to login
|
||||||
|
*
|
||||||
|
* When redirecting to authentication, the window.location change breaks the connection
|
||||||
|
* between this popup and its parent window. We need to send the callbackId to the parent
|
||||||
|
* before redirecting so it can re-establish connection after authentication completes.
|
||||||
|
* This prevents the popup from becoming orphaned and ensures state consistency.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn === false) {
|
||||||
|
// redirection loses the connection to the manager
|
||||||
|
// prevent it passing an async callback id
|
||||||
|
popupWindow.sendCallbackId(callbackId, () => {
|
||||||
|
popupWindow.navigateToAuthentication()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [isLoggedIn, callbackId])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically create meeting room once user is authenticated
|
||||||
|
* This effect will trigger either immediately if the user is already logged in,
|
||||||
|
* or after successful authentication and return to this popup
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const createMeetingRoom = async () => {
|
||||||
|
try {
|
||||||
|
const slug = generateRoomId()
|
||||||
|
const roomData = await createRoom({
|
||||||
|
slug,
|
||||||
|
callbackId,
|
||||||
|
})
|
||||||
|
// Send room data back to parent window and clean up resources
|
||||||
|
popupWindow.sendRoomData(roomData, () => {
|
||||||
|
callbackIdHandler.clear()
|
||||||
|
popupWindow.close()
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create meeting room:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isLoggedIn && callbackId) {
|
||||||
|
createMeetingRoom()
|
||||||
|
}
|
||||||
|
}, [isLoggedIn, callbackId, createRoom])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
45
src/frontend/src/features/sdk/utils/CallbackIdHandler.ts
Normal file
45
src/frontend/src/features/sdk/utils/CallbackIdHandler.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export class CallbackIdHandler {
|
||||||
|
private storageKey = 'popup_callback_id'
|
||||||
|
|
||||||
|
private generateId(): string {
|
||||||
|
return (
|
||||||
|
Math.random().toString(36).substring(2, 15) +
|
||||||
|
Math.random().toString(36).substring(2, 15)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an existing callback ID or creates a new one
|
||||||
|
*/
|
||||||
|
public getOrCreate(): string {
|
||||||
|
const existingId = this.get()
|
||||||
|
if (existingId) {
|
||||||
|
return existingId
|
||||||
|
}
|
||||||
|
|
||||||
|
const newId = this.generateId()
|
||||||
|
this.set(newId)
|
||||||
|
return newId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current callback ID if one exists
|
||||||
|
*/
|
||||||
|
public get(): string | null {
|
||||||
|
return sessionStorage.getItem(this.storageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a callback ID
|
||||||
|
*/
|
||||||
|
private set(id: string): void {
|
||||||
|
sessionStorage.setItem(this.storageKey, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the current callback ID
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
sessionStorage.removeItem(this.storageKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/frontend/src/features/sdk/utils/PopupManager.ts
Normal file
63
src/frontend/src/features/sdk/utils/PopupManager.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
||||||
|
import {
|
||||||
|
CallbackCreationRoomData,
|
||||||
|
ClientMessageType,
|
||||||
|
PopupMessageData,
|
||||||
|
PopupMessageType,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
export class PopupManager {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
private messageHandler: (event: MessageEvent<any>) => void = () => {}
|
||||||
|
|
||||||
|
public createPopupWindow(onFailure: () => void) {
|
||||||
|
const popupWindow = window.open(
|
||||||
|
`${window.location.origin}/sdk/create-popup`,
|
||||||
|
'CreatePopupWindow',
|
||||||
|
`status=no,location=no,toolbar=no,menubar=no,width=600,height=800,left=100,top=100, resizable=yes,scrollbars=yes`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (popupWindow) {
|
||||||
|
popupWindow.focus()
|
||||||
|
} else {
|
||||||
|
onFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupMessageListener(
|
||||||
|
onCallbackId: (id: string) => void,
|
||||||
|
onRoomData: (data: CallbackCreationRoomData) => void
|
||||||
|
) {
|
||||||
|
this.messageHandler = (event) => {
|
||||||
|
const data = event.data as PopupMessageData
|
||||||
|
// Skip messages from untrusted sources
|
||||||
|
if (data.source !== window.location.origin) return
|
||||||
|
switch (data.type) {
|
||||||
|
case PopupMessageType.CALLBACK_ID:
|
||||||
|
onCallbackId(data.callbackId as string)
|
||||||
|
return
|
||||||
|
case PopupMessageType.ROOM_DATA:
|
||||||
|
if (!data?.room) return
|
||||||
|
onRoomData(data.room)
|
||||||
|
window?.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: ClientMessageType.ROOM_CREATED,
|
||||||
|
data: {
|
||||||
|
room: {
|
||||||
|
url: getRouteUrl('room', data.room.slug),
|
||||||
|
...data.room,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('message', this.messageHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
public cleanup() {
|
||||||
|
window.removeEventListener('message', this.messageHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/frontend/src/features/sdk/utils/PopupWindow.ts
Normal file
50
src/frontend/src/features/sdk/utils/PopupWindow.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { authUrl } from '@/features/auth'
|
||||||
|
import { PopupMessageType, CallbackCreationRoomData } from './types'
|
||||||
|
|
||||||
|
export class PopupWindow {
|
||||||
|
private sendMessageToManager(
|
||||||
|
type: PopupMessageType,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
data: any,
|
||||||
|
callback?: () => void
|
||||||
|
) {
|
||||||
|
if (!window.opener) {
|
||||||
|
console.error('No manager window found')
|
||||||
|
window.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.opener.postMessage(
|
||||||
|
{
|
||||||
|
source: window.location.origin,
|
||||||
|
type,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
window.location.origin
|
||||||
|
)
|
||||||
|
callback?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendRoomData(data: CallbackCreationRoomData, callback?: () => void) {
|
||||||
|
this.sendMessageToManager(
|
||||||
|
PopupMessageType.ROOM_DATA,
|
||||||
|
{ room: { slug: data.slug } },
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendCallbackId(callbackId: string, callback?: () => void) {
|
||||||
|
this.sendMessageToManager(
|
||||||
|
PopupMessageType.CALLBACK_ID,
|
||||||
|
{ callbackId },
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
window.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
public navigateToAuthentication() {
|
||||||
|
window.location.href = authUrl({})
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/frontend/src/features/sdk/utils/types.ts
Normal file
19
src/frontend/src/features/sdk/utils/types.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export type CallbackCreationRoomData = {
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ClientMessageType {
|
||||||
|
ROOM_CREATED = 'ROOM_CREATED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopupMessageData {
|
||||||
|
type: PopupMessageType
|
||||||
|
source: string
|
||||||
|
callbackId?: string
|
||||||
|
room?: CallbackCreationRoomData
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PopupMessageType {
|
||||||
|
CALLBACK_ID,
|
||||||
|
ROOM_DATA,
|
||||||
|
}
|
||||||
@@ -18,15 +18,16 @@ export const terminateSupportSession = () => {
|
|||||||
|
|
||||||
export type useSupportProps = {
|
export type useSupportProps = {
|
||||||
id?: string
|
id?: string
|
||||||
|
isDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure Crisp chat for real-time support across all pages.
|
// Configure Crisp chat for real-time support across all pages.
|
||||||
export const useSupport = ({ id }: useSupportProps) => {
|
export const useSupport = ({ id, isDisabled }: useSupportProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id || Crisp.isCrispInjected()) return
|
if (!id || Crisp.isCrispInjected() || isDisabled) return
|
||||||
Crisp.configure(id)
|
Crisp.configure(id)
|
||||||
Crisp.setHideOnMobile(true)
|
Crisp.setHideOnMobile(true)
|
||||||
}, [id])
|
}, [id, isDisabled])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"createButton": {
|
"createMeeting": {
|
||||||
"label": ""
|
"createButton": "",
|
||||||
|
"joinButton": "",
|
||||||
|
"copyLinkTooltip": "",
|
||||||
|
"resetLabel": "",
|
||||||
|
"participantLimit": "",
|
||||||
|
"popupBlocked": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"createButton": {
|
"createMeeting": {
|
||||||
"label": "Create a Visio link"
|
"createButton": "Create",
|
||||||
|
"joinButton": "Join with Visio",
|
||||||
|
"copyLinkTooltip": "Copy link",
|
||||||
|
"resetLabel": "Reset",
|
||||||
|
"participantLimit": "Up to 150 participants.",
|
||||||
|
"popupBlocked": "Popup was blocked. Please allow popups for this site."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"createButton": {
|
"createMeeting": {
|
||||||
"label": "Créer un lien Visio"
|
"createButton": "Créer un lien",
|
||||||
|
"joinButton": "Participer avec Visio",
|
||||||
|
"copyLinkTooltip": "Copier le lien",
|
||||||
|
"resetLabel": "Réinitialiser",
|
||||||
|
"participantLimit": "Jusqu'à 150 participants.",
|
||||||
|
"popupBlocked": "La fenêtre pop-up a été bloquée. Veuillez autoriser les pop-ups pour ce site."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"createButton": {
|
"createMeeting": {
|
||||||
"label": "Maak een Visio link"
|
"createButton": "Aanmaken",
|
||||||
|
"joinButton": "Deelnemen met Visio",
|
||||||
|
"copyLinkTooltip": "Link kopiëren",
|
||||||
|
"resetLabel": "Resetten",
|
||||||
|
"participantLimit": "Tot 150 deelnemers.",
|
||||||
|
"popupBlocked": "Pop-up werd geblokkeerd. Sta pop-ups toe voor deze site."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { HomeRoute } from '@/features/home'
|
|||||||
import { LegalTermsRoute } from '@/features/legalsTerms/LegalTermsRoute'
|
import { LegalTermsRoute } from '@/features/legalsTerms/LegalTermsRoute'
|
||||||
import { AccessibilityRoute } from '@/features/legalsTerms/Accessibility'
|
import { AccessibilityRoute } from '@/features/legalsTerms/Accessibility'
|
||||||
import { TermsOfServiceRoute } from '@/features/legalsTerms/TermsOfService'
|
import { TermsOfServiceRoute } from '@/features/legalsTerms/TermsOfService'
|
||||||
|
import { CreatePopup } from '@/features/sdk/routes/CreatePopup'
|
||||||
|
import { CreateMeetingButton } from '@/features/sdk/routes/CreateMeetingButton'
|
||||||
|
|
||||||
export const routes: Record<
|
export const routes: Record<
|
||||||
| 'home'
|
| 'home'
|
||||||
@@ -10,7 +12,9 @@ export const routes: Record<
|
|||||||
| 'feedback'
|
| 'feedback'
|
||||||
| 'legalTerms'
|
| 'legalTerms'
|
||||||
| 'accessibility'
|
| 'accessibility'
|
||||||
| 'termsOfService',
|
| 'termsOfService'
|
||||||
|
| 'sdkCreatePopup'
|
||||||
|
| 'sdkCreateButton',
|
||||||
{
|
{
|
||||||
name: RouteName
|
name: RouteName
|
||||||
path: RegExp | string
|
path: RegExp | string
|
||||||
@@ -50,6 +54,16 @@ export const routes: Record<
|
|||||||
path: '/conditions-utilisation',
|
path: '/conditions-utilisation',
|
||||||
Component: TermsOfServiceRoute,
|
Component: TermsOfServiceRoute,
|
||||||
},
|
},
|
||||||
|
sdkCreatePopup: {
|
||||||
|
name: 'sdkCreatePopup',
|
||||||
|
path: '/sdk/create-popup',
|
||||||
|
Component: CreatePopup,
|
||||||
|
},
|
||||||
|
sdkCreateButton: {
|
||||||
|
name: 'sdkCreateButton',
|
||||||
|
path: '/sdk/create-button',
|
||||||
|
Component: CreateMeetingButton,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouteName = keyof typeof routes
|
export type RouteName = keyof typeof routes
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<label>Visioconference</label>
|
<label>Visioconference</label>
|
||||||
<VisioCreateButton onRoomCreated={setRoomUrl} />
|
<VisioCreateButton onRoomCreated={(data) => setRoomUrl(data.url)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
|
|||||||
@@ -72,3 +72,56 @@ button {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-meeting {
|
||||||
|
background-color: rgb(45, 45, 70);
|
||||||
|
color: white;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 300;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem 0.75rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-meeting svg path {
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-button {
|
||||||
|
background-color: rgb(45, 45, 70);
|
||||||
|
color: white;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 300;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-button svg path {
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-link {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-link svg path {
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
|
|||||||
1
src/sdk/library/.env.development
Normal file
1
src/sdk/library/.env.development
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_VISIO_SDK_URL=https://meet.127.0.0.1.nip.io/sdk
|
||||||
@@ -5,3 +5,10 @@ export type ConfigType = typeof DEFAULT_CONFIG
|
|||||||
export enum ClientMessageType {
|
export enum ClientMessageType {
|
||||||
ROOM_CREATED = 'ROOM_CREATED',
|
ROOM_CREATED = 'ROOM_CREATED',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RoomData = {
|
||||||
|
slug: string
|
||||||
|
url: string
|
||||||
|
phone?: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { DEFAULT_CONFIG } from '@/Config'
|
|
||||||
import { ClientMessageType } from '@/Types'
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { ClientMessageType, RoomData } from '@/Types'
|
||||||
|
import { DEFAULT_CONFIG } from '@/Config'
|
||||||
|
|
||||||
export const VisioCreateButton = ({
|
export const VisioCreateButton = ({
|
||||||
onRoomCreated,
|
onRoomCreated,
|
||||||
|
readOnly = false,
|
||||||
|
slug,
|
||||||
}: {
|
}: {
|
||||||
onRoomCreated: (roomUrl: string) => void
|
onRoomCreated: (roomData: RoomData) => void
|
||||||
|
readOnly?: boolean
|
||||||
|
slug?: string
|
||||||
}) => {
|
}) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onMessage = (event: MessageEvent) => {
|
const onMessage = (event: MessageEvent) => {
|
||||||
@@ -13,13 +17,11 @@ export const VisioCreateButton = ({
|
|||||||
if (event.origin !== new URL(DEFAULT_CONFIG.url).origin) {
|
if (event.origin !== new URL(DEFAULT_CONFIG.url).origin) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.data.type === ClientMessageType.ROOM_CREATED) {
|
const { type, data } = event.data
|
||||||
const data = event.data.data
|
if (type == ClientMessageType.ROOM_CREATED && data?.room) {
|
||||||
const roomUrl = data.url
|
onRoomCreated(data.room)
|
||||||
onRoomCreated(roomUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('message', onMessage)
|
window.addEventListener('message', onMessage)
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('message', onMessage)
|
window.removeEventListener('message', onMessage)
|
||||||
@@ -30,10 +32,13 @@ export const VisioCreateButton = ({
|
|||||||
// eslint-disable-next-line jsx-a11y/iframe-has-title
|
// eslint-disable-next-line jsx-a11y/iframe-has-title
|
||||||
<iframe
|
<iframe
|
||||||
allow="clipboard-read; clipboard-write"
|
allow="clipboard-read; clipboard-write"
|
||||||
src={DEFAULT_CONFIG.url + '/create-button'}
|
src={
|
||||||
|
DEFAULT_CONFIG.url +
|
||||||
|
`/create-button?readOnly=${readOnly}${slug ? '&slug=' + slug : ''}`
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '52px',
|
height: '100px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|||||||
Reference in New Issue
Block a user