Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69457a01cf | |||
| 9b604040af | |||
| 51dfc46e15 | |||
| 7529126ca3 |
@@ -32,6 +32,7 @@ export const JoinMeetingDialog = () => {
|
||||
<Ul>
|
||||
<li>{window.location.origin}/uio-azer-jkl</li>
|
||||
<li>uio-azer-jkl</li>
|
||||
<li>teamalpha</li>
|
||||
</Ul>
|
||||
</>
|
||||
) : null
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { FormEvent, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Dialog, type DialogProps, Text } from '@/primitives'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { RiSpam2Fill } from '@remixicon/react'
|
||||
import { navigateTo } from '@/navigation/navigateTo.ts'
|
||||
import { useCreateRoom } from '@/features/rooms'
|
||||
import {
|
||||
FieldError,
|
||||
Form as RACForm,
|
||||
Input,
|
||||
Label,
|
||||
TextField,
|
||||
} from 'react-aria-components'
|
||||
import { css } from '@/styled-system/css'
|
||||
import {
|
||||
MIN_ROOM_LENGTH,
|
||||
ALPHANUMERIC_LOWERCASE,
|
||||
} from '@/features/rooms/utils/isRoomValid'
|
||||
|
||||
export const PersonalizeMeetingDialog = ({
|
||||
isOpen,
|
||||
...dialogProps
|
||||
}: Omit<DialogProps, 'title'>) => {
|
||||
const { t } = useTranslation('home', {
|
||||
keyPrefix: 'personalizeMeetingDialog',
|
||||
})
|
||||
const { mutateAsync: createRoom } = useCreateRoom()
|
||||
const [roomSlug, setRoomSlug] = useState('')
|
||||
const [serverErrors, setServerErrors] = useState<Array<string>>([])
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
createRoom({ slug: roomSlug })
|
||||
.then((data) =>
|
||||
navigateTo('room', data.slug, {
|
||||
state: { create: true, initialRoomData: data },
|
||||
})
|
||||
)
|
||||
.catch((e) => {
|
||||
const msg =
|
||||
e.statusCode === 400
|
||||
? t('errors.server.taken')
|
||||
: t('errors.server.unknown')
|
||||
setServerErrors([msg])
|
||||
})
|
||||
}
|
||||
|
||||
const validationErrors = []
|
||||
if (roomSlug.length < MIN_ROOM_LENGTH) {
|
||||
validationErrors.push(t('errors.validation.length'))
|
||||
}
|
||||
if (!new RegExp(`^${ALPHANUMERIC_LOWERCASE}+$`).test(roomSlug)) {
|
||||
validationErrors.push(t('errors.validation.spaceOrSpecialCharacter'))
|
||||
}
|
||||
|
||||
const errors = [...validationErrors, ...serverErrors]
|
||||
|
||||
return (
|
||||
<Dialog isOpen={!!isOpen} {...dialogProps} title={t('heading')}>
|
||||
<RACForm onSubmit={onSubmit}>
|
||||
<TextField
|
||||
isRequired
|
||||
isInvalid={errors.length > 0}
|
||||
value={roomSlug}
|
||||
onChange={(e) => {
|
||||
setServerErrors([])
|
||||
setRoomSlug(e.toLowerCase())
|
||||
}}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
})}
|
||||
>
|
||||
<Label>{t('label')}</Label>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
})}
|
||||
>
|
||||
<Input
|
||||
className={css({
|
||||
height: '46px',
|
||||
border: '1px solid black',
|
||||
padding: '4px 8px',
|
||||
width: '80%',
|
||||
borderRadius: '0.25rem',
|
||||
})}
|
||||
placeholder={t('placeholder')}
|
||||
/>
|
||||
<Button type="submit">{t('submit')}</Button>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
minHeight: '72px',
|
||||
})}
|
||||
>
|
||||
<FieldError>
|
||||
<ul
|
||||
className={css({
|
||||
listStyle: 'square inside',
|
||||
color: 'red',
|
||||
marginLeft: '10px',
|
||||
marginTop: '10px',
|
||||
})}
|
||||
>
|
||||
{errors.map((error, i) => (
|
||||
<li key={i}>
|
||||
<Text as="span" variant={'sm'}>
|
||||
{error}
|
||||
</Text>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</FieldError>
|
||||
</div>
|
||||
</TextField>
|
||||
</RACForm>
|
||||
|
||||
<HStack>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#d9e5ff',
|
||||
borderRadius: '50%',
|
||||
padding: '4px',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<RiSpam2Fill size={22} style={{ fill: '#4c84fc' }} />
|
||||
</div>
|
||||
<Text variant="sm" style={{ marginTop: '1rem', textWrap: 'balance' }}>
|
||||
{t('warning')}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -10,8 +10,9 @@ import { JoinMeetingDialog } from '../components/JoinMeetingDialog'
|
||||
import { ProConnectButton } from '@/components/ProConnectButton'
|
||||
import { useCreateRoom } from '@/features/rooms'
|
||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
||||
import { RiAddLine, RiLink } from '@remixicon/react'
|
||||
import { RiAddLine, RiLink, RiUserAddLine } from '@remixicon/react'
|
||||
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||
import { PersonalizeMeetingDialog } from '@/features/home/components/PersonalizeMeetingDialog'
|
||||
import { IntroSlider } from '@/features/home/components/IntroSlider'
|
||||
import { MoreLink } from '@/features/home/components/MoreLink'
|
||||
import { ReactNode, useState } from 'react'
|
||||
@@ -155,6 +156,7 @@ export const Home = () => {
|
||||
|
||||
const { mutateAsync: createRoom } = useCreateRoom()
|
||||
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
|
||||
const [isPersonalizeModalOpen, setIsPersonalizeModalOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<UserAware>
|
||||
@@ -209,6 +211,18 @@ export const Home = () => {
|
||||
<RiLink size={18} />
|
||||
{t('createMenu.laterOption')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className={
|
||||
menuRecipe({ icon: true, variant: 'light' }).item
|
||||
}
|
||||
onAction={() => {
|
||||
setIsPersonalizeModalOpen(true)
|
||||
}}
|
||||
data-attr="create-option-personalize"
|
||||
>
|
||||
<RiUserAddLine size={18} />
|
||||
{t('createMenu.personalizeOption')}
|
||||
</MenuItem>
|
||||
</RACMenu>
|
||||
</Menu>
|
||||
) : (
|
||||
@@ -250,6 +264,10 @@ export const Home = () => {
|
||||
roomId={laterRoomId || ''}
|
||||
onOpenChange={() => setLaterRoomId(null)}
|
||||
/>
|
||||
<PersonalizeMeetingDialog
|
||||
isOpen={isPersonalizeModalOpen}
|
||||
onOpenChange={() => setIsPersonalizeModalOpen(false)}
|
||||
/>
|
||||
</Screen>
|
||||
</UserAware>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
export const roomIdPattern = '[a-z]{3}-[a-z]{4}-[a-z]{3}'
|
||||
/**
|
||||
* Pattern for system-generated room IDs
|
||||
* Format: xxx-xxxx-xxx (e.g., abc-defg-hij)
|
||||
* This pattern is used when rooms are automatically created by the system,
|
||||
* ensuring consistent and predictable room IDs for randomly generated rooms.
|
||||
*/
|
||||
export const generatedRoomPattern = '[a-z]{3}-[a-z]{4}-[a-z]{3}'
|
||||
|
||||
export const ALPHANUMERIC_LOWERCASE = '[a-z0-9]'
|
||||
|
||||
// Minimum length requirement for personalized rooms
|
||||
export const MIN_ROOM_LENGTH = 5
|
||||
|
||||
/**
|
||||
* Pattern for user-defined custom room IDs
|
||||
* Format: Minimum 5 lowercase alphanumeric characters
|
||||
* This pattern allows users to create memorable, personalized room names
|
||||
* while maintaining basic validation rules (e.g., myroom123, teamspace, project2024)
|
||||
*/
|
||||
export const personalizedRoomPattern = `${ALPHANUMERIC_LOWERCASE}{${MIN_ROOM_LENGTH},}`
|
||||
|
||||
// Combined pattern that accepts both system-generated and personalized room IDs
|
||||
// This allows flexibility in room creation while maintaining consistent validation
|
||||
export const roomIdPattern = `(?:${generatedRoomPattern}|${personalizedRoomPattern})`
|
||||
|
||||
export const isRoomValid = (roomIdOrUrl: string) =>
|
||||
new RegExp(`^${roomIdPattern}$`).test(roomIdOrUrl) ||
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"moreAbout": "",
|
||||
"createMenu": {
|
||||
"laterOption": "",
|
||||
"instantOption": ""
|
||||
"instantOption": "",
|
||||
"personalizeOption": ""
|
||||
},
|
||||
"laterMeetingDialog": {
|
||||
"heading": "",
|
||||
@@ -24,6 +25,23 @@
|
||||
"copied": "",
|
||||
"permissions": ""
|
||||
},
|
||||
"personalizeMeetingDialog": {
|
||||
"heading": "",
|
||||
"label": "",
|
||||
"submit": "",
|
||||
"placeholder": "",
|
||||
"errors": {
|
||||
"validation": {
|
||||
"length": "",
|
||||
"spaceOrSpecialCharacter": ""
|
||||
},
|
||||
"server": {
|
||||
"taken": "",
|
||||
"unknown": ""
|
||||
}
|
||||
},
|
||||
"warning": ""
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"heading": "Simple and Secure Video Conferencing",
|
||||
"intro": "Communicate and work with ease, without compromising your sovereignty",
|
||||
"joinInputError": "Use a meeting link or code. Examples:",
|
||||
"joinInputExample": "URL or 10-letter code",
|
||||
"joinInputExample": "Example of meeting codes: abc-defg-hij ou teamalpha",
|
||||
"joinInputLabel": "Meeting link",
|
||||
"joinInputSubmit": "Join meeting",
|
||||
"joinMeeting": "Join a meeting",
|
||||
@@ -15,7 +15,8 @@
|
||||
"moreAbout": "about Visio",
|
||||
"createMenu": {
|
||||
"laterOption": "Create a meeting for a later date",
|
||||
"instantOption": "Start an instant meeting"
|
||||
"instantOption": "Start an instant meeting",
|
||||
"personalizeOption": "Create a custom link"
|
||||
},
|
||||
"laterMeetingDialog": {
|
||||
"heading": "Your connection details",
|
||||
@@ -24,6 +25,23 @@
|
||||
"copied": "Link copied to clipboard",
|
||||
"permissions": "People with this link do not need your permission to join this meeting."
|
||||
},
|
||||
"personalizeMeetingDialog": {
|
||||
"heading": "Your custom link",
|
||||
"label": "Your custom link",
|
||||
"submit": "Create",
|
||||
"placeholder": "standupbizdev",
|
||||
"errors": {
|
||||
"validation": {
|
||||
"length": "Must contain at least 5 characters.",
|
||||
"spaceOrSpecialCharacter": "Cannot contain spaces or special characters"
|
||||
},
|
||||
"server": {
|
||||
"taken": "Already in use, please try something else",
|
||||
"unknown": "An unknown error occurred, please try again"
|
||||
}
|
||||
},
|
||||
"warning": "This link provides direct access to the meeting. Choose a long and complex name to limit unauthorized access."
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "previous",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"heading": "Visioconférences simples et sécurisées",
|
||||
"intro": "Communiquez et travaillez en toute simplicité, sans compromis sur votre souveraineté",
|
||||
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
|
||||
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij",
|
||||
"joinInputExample": "Un code de réunion ressemble à ceci : abc-defg-hij ou teamalpha",
|
||||
"joinInputLabel": "Lien complet ou code de la réunion",
|
||||
"joinInputSubmit": "Rejoindre la réunion",
|
||||
"joinMeeting": "Rejoindre une réunion",
|
||||
@@ -15,7 +15,8 @@
|
||||
"moreAbout": "sur Visio",
|
||||
"createMenu": {
|
||||
"laterOption": "Créer une réunion pour une date ultérieure",
|
||||
"instantOption": "Démarrer une réunion instantanée"
|
||||
"instantOption": "Démarrer une réunion instantanée",
|
||||
"personalizeOption": "Créer un lien personnalisé"
|
||||
},
|
||||
"laterMeetingDialog": {
|
||||
"heading": "Vos informations de connexion",
|
||||
@@ -24,6 +25,23 @@
|
||||
"copied": "Lien copié dans le presse-papiers",
|
||||
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion."
|
||||
},
|
||||
"personalizeMeetingDialog": {
|
||||
"heading": "Votre lien personnalisé",
|
||||
"label": "Votre lien personnalisé",
|
||||
"submit": "Créer",
|
||||
"placeholder": "standupbizdev",
|
||||
"errors": {
|
||||
"validation": {
|
||||
"length": "Doit contenir au moins 5 caractères.",
|
||||
"spaceOrSpecialCharacter": "Ne peut pas contenir d'espaces ou de caractères spéciaux"
|
||||
},
|
||||
"server": {
|
||||
"taken": "Déjà utilisé, veuillez essayer autre chose",
|
||||
"unknown": "Une erreur inconnue s'est produite, veuillez réessayer"
|
||||
}
|
||||
},
|
||||
"warning": "Ce lien permet un accès direct à la réunion. Choisissez un nom long et complexe pour limiter les accès non autorisés."
|
||||
},
|
||||
"introSlider": {
|
||||
"previous": {
|
||||
"label": "précédent",
|
||||
|
||||
Reference in New Issue
Block a user