mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-13 12:17:24 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69457a01cf | |||
| 9b604040af | |||
| 51dfc46e15 | |||
| 7529126ca3 |
@@ -32,6 +32,7 @@ export const JoinMeetingDialog = () => {
|
|||||||
<Ul>
|
<Ul>
|
||||||
<li>{window.location.origin}/uio-azer-jkl</li>
|
<li>{window.location.origin}/uio-azer-jkl</li>
|
||||||
<li>uio-azer-jkl</li>
|
<li>uio-azer-jkl</li>
|
||||||
|
<li>teamalpha</li>
|
||||||
</Ul>
|
</Ul>
|
||||||
</>
|
</>
|
||||||
) : null
|
) : 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 { ProConnectButton } from '@/components/ProConnectButton'
|
||||||
import { useCreateRoom } from '@/features/rooms'
|
import { useCreateRoom } from '@/features/rooms'
|
||||||
import { usePersistentUserChoices } from '@livekit/components-react'
|
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 { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||||
|
import { PersonalizeMeetingDialog } from '@/features/home/components/PersonalizeMeetingDialog'
|
||||||
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, useState } from 'react'
|
import { ReactNode, useState } from 'react'
|
||||||
@@ -155,6 +156,7 @@ 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 [isPersonalizeModalOpen, setIsPersonalizeModalOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserAware>
|
<UserAware>
|
||||||
@@ -209,6 +211,18 @@ export const Home = () => {
|
|||||||
<RiLink size={18} />
|
<RiLink size={18} />
|
||||||
{t('createMenu.laterOption')}
|
{t('createMenu.laterOption')}
|
||||||
</MenuItem>
|
</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>
|
</RACMenu>
|
||||||
</Menu>
|
</Menu>
|
||||||
) : (
|
) : (
|
||||||
@@ -250,6 +264,10 @@ export const Home = () => {
|
|||||||
roomId={laterRoomId || ''}
|
roomId={laterRoomId || ''}
|
||||||
onOpenChange={() => setLaterRoomId(null)}
|
onOpenChange={() => setLaterRoomId(null)}
|
||||||
/>
|
/>
|
||||||
|
<PersonalizeMeetingDialog
|
||||||
|
isOpen={isPersonalizeModalOpen}
|
||||||
|
onOpenChange={() => setIsPersonalizeModalOpen(false)}
|
||||||
|
/>
|
||||||
</Screen>
|
</Screen>
|
||||||
</UserAware>
|
</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) =>
|
export const isRoomValid = (roomIdOrUrl: string) =>
|
||||||
new RegExp(`^${roomIdPattern}$`).test(roomIdOrUrl) ||
|
new RegExp(`^${roomIdPattern}$`).test(roomIdOrUrl) ||
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"moreAbout": "",
|
"moreAbout": "",
|
||||||
"createMenu": {
|
"createMenu": {
|
||||||
"laterOption": "",
|
"laterOption": "",
|
||||||
"instantOption": ""
|
"instantOption": "",
|
||||||
|
"personalizeOption": ""
|
||||||
},
|
},
|
||||||
"laterMeetingDialog": {
|
"laterMeetingDialog": {
|
||||||
"heading": "",
|
"heading": "",
|
||||||
@@ -24,6 +25,23 @@
|
|||||||
"copied": "",
|
"copied": "",
|
||||||
"permissions": ""
|
"permissions": ""
|
||||||
},
|
},
|
||||||
|
"personalizeMeetingDialog": {
|
||||||
|
"heading": "",
|
||||||
|
"label": "",
|
||||||
|
"submit": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"errors": {
|
||||||
|
"validation": {
|
||||||
|
"length": "",
|
||||||
|
"spaceOrSpecialCharacter": ""
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"taken": "",
|
||||||
|
"unknown": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warning": ""
|
||||||
|
},
|
||||||
"introSlider": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
"label": "",
|
"label": "",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"heading": "Simple and Secure Video Conferencing",
|
"heading": "Simple and Secure Video Conferencing",
|
||||||
"intro": "Communicate and work with ease, without compromising your sovereignty",
|
"intro": "Communicate and work with ease, without compromising your sovereignty",
|
||||||
"joinInputError": "Use a meeting link or code. Examples:",
|
"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",
|
"joinInputLabel": "Meeting link",
|
||||||
"joinInputSubmit": "Join meeting",
|
"joinInputSubmit": "Join meeting",
|
||||||
"joinMeeting": "Join a meeting",
|
"joinMeeting": "Join a meeting",
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"moreAbout": "about Visio",
|
"moreAbout": "about Visio",
|
||||||
"createMenu": {
|
"createMenu": {
|
||||||
"laterOption": "Create a meeting for a later date",
|
"laterOption": "Create a meeting for a later date",
|
||||||
"instantOption": "Start an instant meeting"
|
"instantOption": "Start an instant meeting",
|
||||||
|
"personalizeOption": "Create a custom link"
|
||||||
},
|
},
|
||||||
"laterMeetingDialog": {
|
"laterMeetingDialog": {
|
||||||
"heading": "Your connection details",
|
"heading": "Your connection details",
|
||||||
@@ -24,6 +25,23 @@
|
|||||||
"copied": "Link copied to clipboard",
|
"copied": "Link copied to clipboard",
|
||||||
"permissions": "People with this link do not need your permission to join this meeting."
|
"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": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
"label": "previous",
|
"label": "previous",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"heading": "Visioconférences simples et sécurisées",
|
"heading": "Visioconférences simples et sécurisées",
|
||||||
"intro": "Communiquez et travaillez en toute simplicité, sans compromis sur votre souveraineté",
|
"intro": "Communiquez et travaillez en toute simplicité, sans compromis sur votre souveraineté",
|
||||||
"joinInputError": "Saisissez un lien ou un code de réunion. Exemples :",
|
"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",
|
"joinInputLabel": "Lien complet ou code de la réunion",
|
||||||
"joinInputSubmit": "Rejoindre la réunion",
|
"joinInputSubmit": "Rejoindre la réunion",
|
||||||
"joinMeeting": "Rejoindre une réunion",
|
"joinMeeting": "Rejoindre une réunion",
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"moreAbout": "sur Visio",
|
"moreAbout": "sur Visio",
|
||||||
"createMenu": {
|
"createMenu": {
|
||||||
"laterOption": "Créer une réunion pour une date ultérieure",
|
"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": {
|
"laterMeetingDialog": {
|
||||||
"heading": "Vos informations de connexion",
|
"heading": "Vos informations de connexion",
|
||||||
@@ -24,6 +25,23 @@
|
|||||||
"copied": "Lien copié dans le presse-papiers",
|
"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."
|
"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": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
"label": "précédent",
|
"label": "précédent",
|
||||||
|
|||||||
Reference in New Issue
Block a user