(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 {
LiveKitRoom,
VideoConference,
type LocalUserChoices,
} from '@livekit/components-react'
import { Room, RoomOptions } from "livekit-client";
import { Room, RoomOptions } from 'livekit-client'
import { keys } from '@/api/queryKeys'
import { navigateTo } from '@/navigation/navigateTo'
import { QueryAware } from '@/layout/QueryAware'
import { fetchRoom } from '../api/fetchRoom'
import { InviteDialog } from './InviteDialog'
export const Conference = ({
roomId,
userConfig,
mode = 'join',
}: {
roomId: string
userConfig: LocalUserChoices
mode?: 'join' | 'create'
}) => {
const { status, data } = useQuery({
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
}, [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 (
<QueryAware status={status}>
@@ -55,6 +60,14 @@ export const Conference = ({
}}
>
<VideoConference />
{showInviteDialog && (
<InviteDialog
isOpen={showInviteDialog}
onOpenChange={setShowInviteDialog}
roomId={roomId}
onClose={() => setShowInviteDialog(false)}
/>
)}
</LiveKitRoom>
</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>
<Conference
roomId={roomId}
mode={mode}
userConfig={{
...existingUserChoices,
...(skipJoinScreen ? { username: user?.email as string } : {}),

View File

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

View File

@@ -6,5 +6,11 @@
"micLabel": "Microphone",
"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",
"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: {
size: 'default',

View File

@@ -5,7 +5,7 @@ import {
Dialog as RACDialog,
ModalOverlay,
Modal,
type DialogProps,
type DialogProps as RACDialogProps,
Heading,
} from 'react-aria-components'
import { Div, Button, Box, VerticallyOffCenter } from '@/primitives'
@@ -46,16 +46,44 @@ const StyledRACDialog = styled(RACDialog, {
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 = ({
title,
children,
onClose,
isOpen,
onOpenChange,
...dialogProps
}: DialogProps & { title: string }) => {
}: DialogProps) => {
const isAlert = dialogProps['role'] === 'alertdialog'
return (
<StyledModalOverlay
isKeyboardDismissDisabled={isAlert}
isDismissable={!isAlert}
isOpen={isOpen}
onOpenChange={(isOpen) => {
if (onOpenChange) {
onOpenChange(isOpen)
}
if (!isOpen && onClose) {
onClose()
}
}}
>
<StyledModal>
<StyledRACDialog {...dialogProps}>

View File

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

View File

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