Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom room names #315

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
138 changes: 138 additions & 0 deletions src/frontend/src/features/home/components/PersonalizeMeetingDialog.tsx
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may have overlooked something in the React Aria documentation, but this is the approach I found to prevent a HTML form from being submitted and navigating to a URL suffixed with a question mark.

createRoom({ slug: roomSlug })
.then((data) =>
navigateTo('room', data.slug, {
state: { create: true, initialRoomData: data },
})
)
.catch((e) => {
const msg =
e.statusCode === 400
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to my comment about error names: what if there are multiple possible errors with the same http code?

Not to fix now ofc

? 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')}>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broader subject (out of scope): on mobile I tend to think having a full screen modal with its content being aligned on top is more UX. Mobile user are more used to panes-like navigation.

Capture d’écran 2025-01-27 à 17 38 52

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we have a poor mobile UX.

<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>
)
}
20 changes: 19 additions & 1 deletion src/frontend/src/features/home/routes/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is it used for ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used by Posthog to scrap event data!

>
<RiUserAddLine size={18} />
{t('createMenu.personalizeOption')}
</MenuItem>
</RACMenu>
</Menu>
) : (
Expand Down Expand Up @@ -250,6 +264,10 @@ export const Home = () => {
roomId={laterRoomId || ''}
onOpenChange={() => setLaterRoomId(null)}
/>
<PersonalizeMeetingDialog
isOpen={isPersonalizeModalOpen}
onOpenChange={() => setIsPersonalizeModalOpen(false)}
/>
</Screen>
</UserAware>
)
Expand Down
25 changes: 24 additions & 1 deletion src/frontend/src/features/rooms/utils/isRoomValid.ts
Original file line number Diff line number Diff line change
@@ -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) ||
Expand Down
20 changes: 19 additions & 1 deletion src/frontend/src/locales/de/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"moreAbout": "",
"createMenu": {
"laterOption": "",
"instantOption": ""
"instantOption": "",
"personalizeOption": ""
},
"laterMeetingDialog": {
"heading": "",
Expand All @@ -24,6 +25,23 @@
"copied": "",
"permissions": ""
},
"personalizeMeetingDialog": {
"heading": "",
"label": "",
"submit": "",
"placeholder": "",
"errors": {
"validation": {
"length": "",
"spaceOrSpecialCharacter": ""
},
"server": {
"taken": "",
"unknown": ""
}
},
"warning": ""
},
"introSlider": {
"previous": {
"label": "",
Expand Down
22 changes: 20 additions & 2 deletions src/frontend/src/locales/en/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
22 changes: 20 additions & 2 deletions src/frontend/src/locales/fr/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about French placeholder?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt bizdev was kinda french, but would you prefer, "equipecom" or smth else?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe like "Réunion Hebdo" or smth?

"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",
Expand Down
Loading