(rooms) show an invite dialog when creating a room

for now the dialog appears as a regular dialog with an overlay and all,
would be better as less intrusive. but good as is as a first step
This commit is contained in:
Emmanuel Pelletier
2024-07-26 11:02:32 +02:00
parent c3617fc005
commit 8587574fcd
12 changed files with 164 additions and 11 deletions

View File

@@ -1,22 +1,25 @@
import { useMemo } from "react"; import { useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { import {
LiveKitRoom, LiveKitRoom,
VideoConference, VideoConference,
type LocalUserChoices, type LocalUserChoices,
} from '@livekit/components-react' } from '@livekit/components-react'
import { Room, RoomOptions } from "livekit-client"; import { Room, RoomOptions } from 'livekit-client'
import { keys } from '@/api/queryKeys' import { keys } from '@/api/queryKeys'
import { navigateTo } from '@/navigation/navigateTo' import { navigateTo } from '@/navigation/navigateTo'
import { QueryAware } from '@/layout/QueryAware' import { QueryAware } from '@/layout/QueryAware'
import { fetchRoom } from '../api/fetchRoom' import { fetchRoom } from '../api/fetchRoom'
import { InviteDialog } from './InviteDialog'
export const Conference = ({ export const Conference = ({
roomId, roomId,
userConfig, userConfig,
mode = 'join',
}: { }: {
roomId: string roomId: string
userConfig: LocalUserChoices userConfig: LocalUserChoices
mode?: 'join' | 'create'
}) => { }) => {
const { status, data } = useQuery({ const { status, data } = useQuery({
queryKey: [keys.room, roomId, userConfig.username], queryKey: [keys.room, roomId, userConfig.username],
@@ -39,7 +42,9 @@ export const Conference = ({
// do not rely on the userConfig object directly as its reference may change on every render // do not rely on the userConfig object directly as its reference may change on every render
}, [userConfig.videoDeviceId, userConfig.audioDeviceId]) }, [userConfig.videoDeviceId, userConfig.audioDeviceId])
const room = useMemo(() => new Room(roomOptions), [roomOptions]); const room = useMemo(() => new Room(roomOptions), [roomOptions])
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
return ( return (
<QueryAware status={status}> <QueryAware status={status}>
@@ -55,6 +60,14 @@ export const Conference = ({
}} }}
> >
<VideoConference /> <VideoConference />
{showInviteDialog && (
<InviteDialog
isOpen={showInviteDialog}
onOpenChange={setShowInviteDialog}
roomId={roomId}
onClose={() => setShowInviteDialog(false)}
/>
)}
</LiveKitRoom> </LiveKitRoom>
</QueryAware> </QueryAware>
) )

View File

@@ -0,0 +1,58 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getRouteUrl } from '@/navigation/getRouteUrl'
import { Div, Button, Dialog, Input, type DialogProps } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
export const InviteDialog = ({
roomId,
...dialogProps
}: { roomId: string } & Omit<DialogProps, 'title'>) => {
const { t } = useTranslation('rooms')
const roomUrl = getRouteUrl('room', roomId)
const copyLabel = t('shareDialog.copy')
const copiedLabel = t('shareDialog.copied')
const [copyLinkLabel, setCopyLinkLabel] = useState(copyLabel)
useEffect(() => {
if (copyLinkLabel == copiedLabel) {
const timeout = setTimeout(() => {
setCopyLinkLabel(copyLabel)
}, 5000)
return () => {
clearTimeout(timeout)
}
}
}, [copyLinkLabel, copyLabel, copiedLabel])
return (
<Dialog {...dialogProps} title={t('shareDialog.heading')}>
<HStack alignItems="stretch" gap="gutter">
<Div flex="1">
<Input
type="text"
aria-label={t('shareDialog.inputLabel')}
value={roomUrl}
readOnly
onClick={(e) => {
e.currentTarget.select()
}}
/>
</Div>
<Div minWidth="8rem">
<Button
variant="primary"
size="sm"
fullWidth
onPress={() => {
navigator.clipboard.writeText(roomUrl)
setCopyLinkLabel(copiedLabel)
}}
>
{copyLinkLabel}
</Button>
</Div>
</HStack>
</Dialog>
)
}

View File

@@ -35,6 +35,7 @@ export const Room = () => {
<Screen> <Screen>
<Conference <Conference
roomId={roomId} roomId={roomId}
mode={mode}
userConfig={{ userConfig={{
...existingUserChoices, ...existingUserChoices,
...(skipJoinScreen ? { username: user?.email as string } : {}), ...(skipJoinScreen ? { username: user?.email as string } : {}),

View File

@@ -6,5 +6,10 @@
"micLabel": "", "micLabel": "",
"userLabel": "" "userLabel": ""
}, },
"leaveRoomPrompt": "" "leaveRoomPrompt": "",
"shareDialog": {
"copy": "",
"heading": "",
"inputLabel": ""
}
} }

View File

@@ -6,5 +6,11 @@
"micLabel": "Microphone", "micLabel": "Microphone",
"userLabel": "Your name" "userLabel": "Your name"
}, },
"leaveRoomPrompt": "This will make you leave the meeting." "leaveRoomPrompt": "This will make you leave the meeting.",
"shareDialog": {
"copy": "Copy",
"copied": "Copied",
"heading": "Share the meeting link",
"inputLabel": "Meeting link"
}
} }

View File

@@ -6,5 +6,11 @@
"micLabel": "Micro", "micLabel": "Micro",
"userLabel": "Votre nom" "userLabel": "Votre nom"
}, },
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion." "leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
"shareDialog": {
"copy": "Copier le lien",
"copied": "Lien copié",
"heading": "Partager le lien vers la réunion",
"inputLabel": "Lien vers la réunion"
}
} }

View File

@@ -0,0 +1,19 @@
import { RouteName } from '@/routes'
import { getRouteByName } from './getRouteByName'
export const getRoutePath = (
routeName: RouteName,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params?: any
) => {
const route = getRouteByName(routeName)
const to = route.to
? route.to(params)
: typeof route.path === 'string'
? route.path
: null
if (!to) {
throw new Error(`Can't find path to navigate to for ${routeName}`)
}
return to
}

View File

@@ -0,0 +1,11 @@
import { RouteName } from '@/routes'
import { getRoutePath } from './getRoutePath'
export const getRouteUrl = (
routeName: RouteName,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params?: any
) => {
const to = getRoutePath(routeName, params)
return `${window.location.origin}${to}`
}

View File

@@ -97,6 +97,11 @@ const button = cva({
}, },
}, },
}, },
fullWidth: {
true: {
width: 'full',
},
},
}, },
defaultVariants: { defaultVariants: {
size: 'default', size: 'default',

View File

@@ -5,7 +5,7 @@ import {
Dialog as RACDialog, Dialog as RACDialog,
ModalOverlay, ModalOverlay,
Modal, Modal,
type DialogProps, type DialogProps as RACDialogProps,
Heading, Heading,
} from 'react-aria-components' } from 'react-aria-components'
import { Div, Button, Box, VerticallyOffCenter } from '@/primitives' import { Div, Button, Box, VerticallyOffCenter } from '@/primitives'
@@ -46,16 +46,44 @@ const StyledRACDialog = styled(RACDialog, {
pointerEvents: 'none', pointerEvents: 'none',
}, },
}) })
export type DialogProps = RACDialogProps & {
title: string
onClose?: () => void
/**
* use the Dialog as a controlled component
*/
isOpen?: boolean
/**
* use the Dialog as a controlled component:
* this is called when isOpen should be updated
* after user interaction
*/
onOpenChange?: (isOpen: boolean) => void
}
export const Dialog = ({ export const Dialog = ({
title, title,
children, children,
onClose,
isOpen,
onOpenChange,
...dialogProps ...dialogProps
}: DialogProps & { title: string }) => { }: DialogProps) => {
const isAlert = dialogProps['role'] === 'alertdialog' const isAlert = dialogProps['role'] === 'alertdialog'
return ( return (
<StyledModalOverlay <StyledModalOverlay
isKeyboardDismissDisabled={isAlert} isKeyboardDismissDisabled={isAlert}
isDismissable={!isAlert} isDismissable={!isAlert}
isOpen={isOpen}
onOpenChange={(isOpen) => {
if (onOpenChange) {
onOpenChange(isOpen)
}
if (!isOpen && onClose) {
onClose()
}
}}
> >
<StyledModal> <StyledModal>
<StyledRACDialog {...dialogProps}> <StyledRACDialog {...dialogProps}>

View File

@@ -9,8 +9,8 @@ import { Input as RACInput } from 'react-aria-components'
export const Input = styled(RACInput, { export const Input = styled(RACInput, {
base: { base: {
width: 'full', width: 'full',
paddingY: 0.125, paddingY: 0.25,
paddingX: 0.25, paddingX: 0.5,
border: '1px solid', border: '1px solid',
borderColor: 'control.border', borderColor: 'control.border',
color: 'control.text', color: 'control.text',

View File

@@ -4,13 +4,14 @@ export { Bold } from './Bold'
export { Box } from './Box' export { Box } from './Box'
export { Button } from './Button' export { Button } from './Button'
export { useCloseDialog } from './useCloseDialog' export { useCloseDialog } from './useCloseDialog'
export { Dialog } from './Dialog' export { Dialog, type DialogProps } from './Dialog'
export { Div } from './Div' export { Div } from './Div'
export { Field } from './Field' export { Field } from './Field'
export { Form } from './Form' export { Form } from './Form'
export { H } from './H' export { H } from './H'
export { Hr } from './Hr' export { Hr } from './Hr'
export { Italic } from './Italic' export { Italic } from './Italic'
export { Input } from './Input'
export { Link } from './Link' export { Link } from './Link'
export { P } from './P' export { P } from './P'
export { Popover } from './Popover' export { Popover } from './Popover'