✨(frontend) new join screen with homemade buttons
- do not touch current Join screen as we might need it still for now - add a new HomemadeJoin, that is meant to be renamed simply "Join" when ready. It contains basically the same stuff as the livekit join but with homemade react aria buttons and a different layout. This will allow us to precisely customize how we want this screen later - store user device selections and name in a valtio store, synced with localstorage. This should end up in the same UX as before with livekit, but now we can store more things (like audio output) in the same place
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { usePersistedMediaDeviceSelect } from './utils/usePersistedMediaDeviceSelect'
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||
import { settingsStore } from '@/features/settings'
|
||||
|
||||
/**
|
||||
* wrap livekit's useMediaDeviceSelect to automatically save in our devices state user selection
|
||||
*
|
||||
* note: audiooutput devices are not handled here as we dont use useMediaDeviceSelect for them
|
||||
*/
|
||||
export const usePersistedMediaDeviceSelect = (
|
||||
...args: Parameters<typeof useMediaDeviceSelect>
|
||||
): ReturnType<typeof useMediaDeviceSelect> => {
|
||||
const results = useMediaDeviceSelect(...args)
|
||||
const originalSetter = results.setActiveMediaDevice
|
||||
results.setActiveMediaDevice = (
|
||||
...activeMediaDeviceArgs: Parameters<typeof results.setActiveMediaDevice>
|
||||
) => {
|
||||
const id = activeMediaDeviceArgs[0]
|
||||
if (args[0].kind === 'audioinput') {
|
||||
settingsStore.devices.micDeviceId = id
|
||||
}
|
||||
if (args[0].kind === 'videoinput') {
|
||||
settingsStore.devices.cameraDeviceId = id
|
||||
}
|
||||
return originalSetter(...activeMediaDeviceArgs)
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
formatChatMessageLinks,
|
||||
LiveKitRoom,
|
||||
VideoConference,
|
||||
type LocalUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import { Room, RoomOptions } from 'livekit-client'
|
||||
import { keys } from '@/api/queryKeys'
|
||||
@@ -18,6 +17,7 @@ import { fetchRoom } from '../api/fetchRoom'
|
||||
import { ApiRoom } from '../api/ApiRoom'
|
||||
import { useCreateRoom } from '../api/createRoom'
|
||||
import { InviteDialog } from './InviteDialog'
|
||||
import { type SettingsState } from '@/features/settings'
|
||||
|
||||
export const Conference = ({
|
||||
roomId,
|
||||
@@ -26,7 +26,10 @@ export const Conference = ({
|
||||
mode = 'join',
|
||||
}: {
|
||||
roomId: string
|
||||
userConfig: LocalUserChoices
|
||||
userConfig: {
|
||||
devices: SettingsState['devices']
|
||||
username: SettingsState['username']
|
||||
}
|
||||
mode?: 'join' | 'create'
|
||||
initialRoomData?: ApiRoom
|
||||
}) => {
|
||||
@@ -65,14 +68,14 @@ export const Conference = ({
|
||||
const roomOptions = useMemo((): RoomOptions => {
|
||||
return {
|
||||
videoCaptureDefaults: {
|
||||
deviceId: userConfig.videoDeviceId ?? undefined,
|
||||
deviceId: userConfig.devices.cameraDeviceId ?? undefined,
|
||||
},
|
||||
audioCaptureDefaults: {
|
||||
deviceId: userConfig.audioDeviceId ?? undefined,
|
||||
deviceId: userConfig.devices.micDeviceId ?? undefined,
|
||||
},
|
||||
}
|
||||
// do not rely on the userConfig object directly as its reference may change on every render
|
||||
}, [userConfig.videoDeviceId, userConfig.audioDeviceId])
|
||||
}, [userConfig.devices.cameraDeviceId, userConfig.devices.micDeviceId])
|
||||
|
||||
const room = useMemo(() => new Room(roomOptions), [roomOptions])
|
||||
|
||||
@@ -116,8 +119,8 @@ export const Conference = ({
|
||||
serverUrl={data?.livekit?.url}
|
||||
token={data?.livekit?.token}
|
||||
connect={true}
|
||||
audio={userConfig.audioEnabled}
|
||||
video={userConfig.videoEnabled}
|
||||
audio={userConfig.devices.enableMic}
|
||||
video={userConfig.devices.enableCamera}
|
||||
>
|
||||
<VideoConference chatMessageFormatter={formatChatMessageLinks} />
|
||||
{showInviteDialog && (
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
import { Screen } from '@/layout/Screen'
|
||||
import {
|
||||
Button,
|
||||
Div,
|
||||
Field,
|
||||
Form,
|
||||
H,
|
||||
Menu,
|
||||
MenuList,
|
||||
ToggleButton,
|
||||
VerticallyOffCenter,
|
||||
} from '@/primitives'
|
||||
import { Center, HStack, VStack } from '@/styled-system/jsx'
|
||||
import {
|
||||
RiArrowDropDownLine,
|
||||
RiMicLine,
|
||||
RiMicOffLine,
|
||||
RiVideoOffLine,
|
||||
RiVideoOnLine,
|
||||
RiVolumeUpLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useMaybeRoomContext,
|
||||
usePreviewTracks,
|
||||
} from '@livekit/components-react'
|
||||
import { Track, LocalVideoTrack, LocalAudioTrack } from 'livekit-client'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { usePersistedMediaDeviceSelect } from '@/features/devices'
|
||||
import { settingsStore, type SettingsState } from '@/features/settings'
|
||||
import { css } from '@/styled-system/css'
|
||||
|
||||
export const HomemadeJoin = ({
|
||||
onSubmit,
|
||||
}: {
|
||||
onSubmit: (choices: {
|
||||
devices: SettingsState['devices']
|
||||
username: SettingsState['username']
|
||||
}) => void
|
||||
}) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||
|
||||
const settingsSnap = useSnapshot(settingsStore)
|
||||
const [initialUserChoices] = useState({ ...settingsSnap.devices })
|
||||
|
||||
const tracks = usePreviewTracks({
|
||||
audio: settingsSnap.devices.enableMic
|
||||
? { deviceId: initialUserChoices.micDeviceId }
|
||||
: false,
|
||||
video: settingsSnap.devices.enableCamera
|
||||
? { deviceId: initialUserChoices.cameraDeviceId }
|
||||
: false,
|
||||
})
|
||||
|
||||
const videoEl = useRef(null)
|
||||
const videoTrack = useMemo(
|
||||
() =>
|
||||
tracks?.filter(
|
||||
(track) => track.kind === Track.Kind.Video
|
||||
)[0] as LocalVideoTrack,
|
||||
[tracks]
|
||||
)
|
||||
|
||||
const audioTrack = useMemo(
|
||||
() =>
|
||||
tracks?.filter(
|
||||
(track) => track.kind === Track.Kind.Audio
|
||||
)[0] as LocalAudioTrack,
|
||||
[tracks]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (videoEl.current && videoTrack) {
|
||||
videoTrack.unmute()
|
||||
videoTrack.attach(videoEl.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
videoTrack?.detach()
|
||||
}
|
||||
}, [videoTrack])
|
||||
const room = useMaybeRoomContext()
|
||||
|
||||
const {
|
||||
devices: micDevices,
|
||||
activeDeviceId: activeMicDeviceId,
|
||||
setActiveMediaDevice: setActiveMicDevice,
|
||||
} = usePersistedMediaDeviceSelect({
|
||||
kind: 'audioinput',
|
||||
room,
|
||||
track: audioTrack,
|
||||
requestPermissions: true,
|
||||
})
|
||||
const {
|
||||
devices: cameraDevices,
|
||||
activeDeviceId: activeCameraDeviceId,
|
||||
setActiveMediaDevice: setActiveCameraDevice,
|
||||
} = usePersistedMediaDeviceSelect({
|
||||
kind: 'videoinput',
|
||||
room,
|
||||
track: videoTrack,
|
||||
requestPermissions: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (settingsStore.devices.micDeviceId) {
|
||||
setActiveMicDevice(settingsStore.devices.micDeviceId)
|
||||
}
|
||||
if (settingsStore.devices.cameraDeviceId) {
|
||||
setActiveCameraDevice(settingsStore.devices.cameraDeviceId)
|
||||
}
|
||||
}, [setActiveCameraDevice, setActiveMicDevice])
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<VerticallyOffCenter>
|
||||
<Div
|
||||
className={css({
|
||||
margin: 'auto',
|
||||
flexWrap: 'wrap',
|
||||
width: 'fit-content',
|
||||
maxWidth: 'full',
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
paddingX: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
lg: {
|
||||
alignItems: 'stretch',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<VStack
|
||||
className={css({
|
||||
width: 'full',
|
||||
maxWidth: '38rem',
|
||||
margin: '0 auto',
|
||||
alignItems: 'center',
|
||||
flexShrink: '1',
|
||||
})}
|
||||
>
|
||||
<Center
|
||||
className={css({
|
||||
width: '38rem',
|
||||
maxWidth: 'full',
|
||||
height: 'auto',
|
||||
aspectRatio: '16 / 9',
|
||||
background: 'gray.900',
|
||||
color: 'white',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
{videoTrack && settingsSnap.devices.enableCamera ? (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<video
|
||||
ref={videoEl}
|
||||
width="608"
|
||||
height="342"
|
||||
className={css({
|
||||
width: 'full',
|
||||
height: 'auto',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
settingsSnap.devices.enableCamera === false && (
|
||||
<p>{t('cameraPlaceholder')}</p>
|
||||
)
|
||||
)}
|
||||
</Center>
|
||||
<HStack gap={1} justify="center" flexWrap={'wrap'}>
|
||||
{/* audio input toggle + dropdown */}
|
||||
<HStack gap={0}>
|
||||
<ToggleButton
|
||||
isSelected={settingsSnap.devices.enableMic}
|
||||
variant={
|
||||
settingsSnap.devices.enableMic ? undefined : 'danger'
|
||||
}
|
||||
toggledStyles={false}
|
||||
onChange={(enabled) =>
|
||||
(settingsStore.devices.enableMic = enabled)
|
||||
}
|
||||
aria-label={
|
||||
settingsSnap.devices.enableMic
|
||||
? `${t('micIsOn')} ${t('toggleOff')}`
|
||||
: `${t('micIsOff')} ${t('toggleOn')}`
|
||||
}
|
||||
tooltip={
|
||||
settingsSnap.devices.enableMic ? (
|
||||
<>
|
||||
{t('micIsOn')}
|
||||
<br />
|
||||
{t('toggleOff')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('micIsOff')}
|
||||
<br />
|
||||
{t('toggleOn')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
groupPosition="left"
|
||||
>
|
||||
{settingsSnap.devices.enableMic ? (
|
||||
<RiMicLine />
|
||||
) : (
|
||||
<RiMicOffLine />
|
||||
)}
|
||||
</ToggleButton>
|
||||
<Menu>
|
||||
<Button
|
||||
tooltip={t('chooseMic')}
|
||||
aria-label={t('chooseMic')}
|
||||
groupPosition="right"
|
||||
square
|
||||
>
|
||||
<RiArrowDropDownLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={micDevices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={activeMicDeviceId}
|
||||
onAction={(value) => {
|
||||
setActiveMicDevice(value as string)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</HStack>
|
||||
|
||||
{/* video toggle + dropdown */}
|
||||
<HStack gap={0}>
|
||||
<ToggleButton
|
||||
isSelected={settingsSnap.devices.enableCamera}
|
||||
variant={
|
||||
settingsSnap.devices.enableCamera ? undefined : 'danger'
|
||||
}
|
||||
toggledStyles={false}
|
||||
onChange={(enabled) =>
|
||||
(settingsStore.devices.enableCamera = enabled)
|
||||
}
|
||||
aria-label={
|
||||
settingsSnap.devices.enableMic
|
||||
? `${t('cameraIsOn')} ${t('toggleOff')}`
|
||||
: `${t('cameraIsOff')} ${t('toggleOn')}`
|
||||
}
|
||||
tooltip={
|
||||
settingsSnap.devices.enableMic ? (
|
||||
<>
|
||||
{t('cameraIsOn')}
|
||||
<br />
|
||||
{t('toggleOff')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('cameraIsOff')}
|
||||
<br />
|
||||
{t('toggleOn')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
groupPosition="left"
|
||||
>
|
||||
{settingsSnap.devices.enableCamera ? (
|
||||
<RiVideoOnLine />
|
||||
) : (
|
||||
<RiVideoOffLine />
|
||||
)}
|
||||
</ToggleButton>
|
||||
<Menu>
|
||||
<Button
|
||||
tooltip={t('chooseCamera')}
|
||||
aria-label={t('chooseCamera')}
|
||||
groupPosition="right"
|
||||
square
|
||||
>
|
||||
<RiArrowDropDownLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={cameraDevices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={activeCameraDeviceId}
|
||||
onAction={(value) => {
|
||||
setActiveCameraDevice(value as string)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<Div width="24rem" maxWidth="full" flexShrink="1">
|
||||
<VerticallyOffCenter>
|
||||
<Center>
|
||||
<H lvl={1}>{t('heading')}</H>
|
||||
</Center>
|
||||
<Form
|
||||
onSubmit={(data) => {
|
||||
settingsStore.username = (data.username as string).trim()
|
||||
onSubmit({
|
||||
devices: { ...settingsStore.devices },
|
||||
username: settingsStore.username,
|
||||
})
|
||||
}}
|
||||
submitLabel={t('joinMeeting')}
|
||||
withSubmitButton={false}
|
||||
>
|
||||
<Field
|
||||
type="text"
|
||||
name="username"
|
||||
defaultValue={settingsSnap.username}
|
||||
label={t('usernameLabel')}
|
||||
description={t('usernameHint')}
|
||||
isRequired
|
||||
/>
|
||||
<Center>
|
||||
<Button type="submit" variant="primary">
|
||||
{t('joinMeeting')}
|
||||
</Button>
|
||||
</Center>
|
||||
</Form>
|
||||
</VerticallyOffCenter>
|
||||
</Div>
|
||||
</Div>
|
||||
</VerticallyOffCenter>
|
||||
</Screen>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
usePersistentUserChoices,
|
||||
type LocalUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import { useParams } from 'wouter'
|
||||
import { ErrorScreen } from '@/components/ErrorScreen'
|
||||
import { useUser, UserAware } from '@/features/auth'
|
||||
import { Conference } from '../components/Conference'
|
||||
import { Join } from '../components/Join'
|
||||
import { HomemadeJoin } from '../components/HomemadeJoin'
|
||||
import { settingsStore, type SettingsState } from '@/features/settings'
|
||||
import { useSnapshot } from 'valtio'
|
||||
|
||||
export const Room = () => {
|
||||
const { isLoggedIn } = useUser()
|
||||
const { userChoices: existingUserChoices } = usePersistentUserChoices()
|
||||
const [userConfig, setUserConfig] = useState<LocalUserChoices | null>(null)
|
||||
const settingsSnap = useSnapshot(settingsStore)
|
||||
const existingUserConfig = {
|
||||
username: settingsSnap.username,
|
||||
devices: settingsSnap.devices,
|
||||
}
|
||||
const [userConfig, setUserConfig] = useState<null | {
|
||||
username: SettingsState['username']
|
||||
devices: SettingsState['devices']
|
||||
}>(null)
|
||||
|
||||
const { roomId } = useParams()
|
||||
const initialRoomData = history.state?.initialRoomData
|
||||
@@ -26,7 +31,7 @@ export const Room = () => {
|
||||
if (!userConfig && !skipJoinScreen) {
|
||||
return (
|
||||
<UserAware>
|
||||
<Join onSubmit={setUserConfig} />
|
||||
<HomemadeJoin onSubmit={setUserConfig} />
|
||||
</UserAware>
|
||||
)
|
||||
}
|
||||
@@ -38,7 +43,7 @@ export const Room = () => {
|
||||
roomId={roomId}
|
||||
mode={mode}
|
||||
userConfig={{
|
||||
...existingUserChoices,
|
||||
...existingUserConfig,
|
||||
...userConfig,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export { SettingsButton } from './components/SettingsButton'
|
||||
export { SettingsDialog } from './components/SettingsDialog'
|
||||
export { settingsStore } from './stores/settings'
|
||||
export { type SettingsState } from './stores/settings'
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
import { devtools } from 'valtio/utils'
|
||||
|
||||
export type SettingsState = {
|
||||
username: string | undefined
|
||||
devices: {
|
||||
/**
|
||||
* MediaDeviceInfo id
|
||||
*/
|
||||
micDeviceId: string | undefined
|
||||
/**
|
||||
* MediaDeviceInfo id
|
||||
*/
|
||||
cameraDeviceId: string | undefined
|
||||
enableMic: boolean
|
||||
enableCamera: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// sync the valtio store with localstorage data
|
||||
// @TODO: make it easier to have "persisted" stores as we will definitely use it quite often
|
||||
|
||||
const localData = localStorage.getItem('meet.settings')
|
||||
|
||||
export const settingsStore = proxy<SettingsState>(
|
||||
localData
|
||||
? JSON.parse(localData)
|
||||
: {
|
||||
username: undefined,
|
||||
devices: {
|
||||
micDeviceId: undefined,
|
||||
cameraDeviceId: undefined,
|
||||
enableMic: false,
|
||||
enableCamera: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
subscribe(settingsStore, () => {
|
||||
localStorage.setItem('meet.settings', JSON.stringify(settingsStore))
|
||||
})
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
devtools(settingsStore, { name: 'settings', enabled: true })
|
||||
}
|
||||
@@ -7,9 +7,6 @@
|
||||
"heading": ""
|
||||
},
|
||||
"feedbackAlert": "",
|
||||
"forbidden": {
|
||||
"heading": ""
|
||||
},
|
||||
"loading": "",
|
||||
"loggedInUserTooltip": "",
|
||||
"login": "Anmelden",
|
||||
|
||||
@@ -4,11 +4,23 @@
|
||||
"heading": ""
|
||||
},
|
||||
"join": {
|
||||
"cameraIsOff": "",
|
||||
"cameraIsOn": "",
|
||||
"cameraPlaceholder": "",
|
||||
"camlabel": "",
|
||||
"chooseCamera": "",
|
||||
"chooseMic": "",
|
||||
"heading": "",
|
||||
"joinLabel": "",
|
||||
"joinMeeting": "",
|
||||
"micIsOff": "",
|
||||
"micIsOn": "",
|
||||
"micLabel": "",
|
||||
"userLabel": ""
|
||||
"toggleOff": "",
|
||||
"toggleOn": "",
|
||||
"userLabel": "",
|
||||
"usernameHint": "",
|
||||
"usernameLabel": ""
|
||||
},
|
||||
"leaveRoomPrompt": "",
|
||||
"shareDialog": {
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"heading": "An error occured while loading the page"
|
||||
},
|
||||
"feedbackAlert": "Give us feedback",
|
||||
"forbidden": {
|
||||
"heading": "You don't have the permission to view this page"
|
||||
},
|
||||
"loading": "Loading…",
|
||||
"loggedInUserTooltip": "Logged in as…",
|
||||
"login": "Login",
|
||||
|
||||
@@ -4,11 +4,23 @@
|
||||
"heading": "Help us improve Meet"
|
||||
},
|
||||
"join": {
|
||||
"camlabel": "Camera",
|
||||
"heading": "Join the meeting",
|
||||
"joinLabel": "Join",
|
||||
"micLabel": "Microphone",
|
||||
"userLabel": "Your name"
|
||||
"cameraIsOff": "Camera is off.",
|
||||
"cameraIsOn": "Camera is on.",
|
||||
"cameraPlaceholder": "Turn on the camera to see the preview",
|
||||
"camlabel": "",
|
||||
"chooseCamera": "Select camera",
|
||||
"chooseMic": "Select microphone",
|
||||
"heading": "Verify your settings",
|
||||
"joinLabel": "",
|
||||
"joinMeeting": "Join meeting",
|
||||
"micIsOff": "Microphone is off.",
|
||||
"micIsOn": "Microphone is on.",
|
||||
"micLabel": "",
|
||||
"toggleOff": "Click to turn off",
|
||||
"toggleOn": "Click to turn on",
|
||||
"userLabel": "",
|
||||
"usernameHint": "Shown to other participants",
|
||||
"usernameLabel": "Your name"
|
||||
},
|
||||
"leaveRoomPrompt": "This will make you leave the meeting.",
|
||||
"shareDialog": {
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"heading": "Une erreur est survenue lors du chargement de la page"
|
||||
},
|
||||
"feedbackAlert": "Donnez-nous votre avis",
|
||||
"forbidden": {
|
||||
"heading": "Accès interdit"
|
||||
},
|
||||
"loading": "Chargement…",
|
||||
"loggedInUserTooltip": "Connecté en tant que…",
|
||||
"login": "Se connecter",
|
||||
|
||||
@@ -4,11 +4,23 @@
|
||||
"heading": "Aidez-nous à améliorer Meet"
|
||||
},
|
||||
"join": {
|
||||
"camlabel": "Webcam",
|
||||
"heading": "Rejoindre la réunion",
|
||||
"joinLabel": "Rejoindre",
|
||||
"micLabel": "Micro",
|
||||
"userLabel": "Votre nom"
|
||||
"cameraIsOff": "Webcam coupée.",
|
||||
"cameraIsOn": "Webcam activée.",
|
||||
"cameraPlaceholder": "Activez la webcam pour prévisualiser l'affichage",
|
||||
"camlabel": "",
|
||||
"chooseCamera": "Choisir la webcam",
|
||||
"chooseMic": "Choisir le micro",
|
||||
"heading": "Vérifiez vos paramètres",
|
||||
"joinLabel": "",
|
||||
"joinMeeting": "Rejoindre la réjoindre",
|
||||
"micIsOff": "Micro coupé.",
|
||||
"micIsOn": "Micro activé.",
|
||||
"micLabel": "",
|
||||
"toggleOff": "Cliquez pour désactiver",
|
||||
"toggleOn": "Cliquez pour activer",
|
||||
"userLabel": "",
|
||||
"usernameHint": "Affiché aux autres participants",
|
||||
"usernameLabel": "Votre nom"
|
||||
},
|
||||
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
|
||||
"shareDialog": {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Button, useCloseDialog } from '@/primitives'
|
||||
export const Form = ({
|
||||
onSubmit,
|
||||
submitLabel,
|
||||
withSubmitButton = true,
|
||||
withCancelButton = true,
|
||||
onCancelButtonPress,
|
||||
children,
|
||||
@@ -25,6 +26,7 @@ export const Form = ({
|
||||
event: FormEvent<HTMLFormElement>
|
||||
) => void
|
||||
submitLabel: string
|
||||
withSubmitButton?: boolean
|
||||
withCancelButton?: boolean
|
||||
onCancelButtonPress?: () => void
|
||||
}) => {
|
||||
@@ -46,16 +48,18 @@ export const Form = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<HStack gap="gutter">
|
||||
<Button type="submit" variant="primary">
|
||||
{submitLabel}
|
||||
</Button>
|
||||
{!!onCancel && (
|
||||
<Button variant="primary" outline onPress={() => onCancel()}>
|
||||
{t('cancel')}
|
||||
{withSubmitButton && (
|
||||
<HStack gap="gutter">
|
||||
<Button type="submit" variant="primary">
|
||||
{submitLabel}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
{!!onCancel && (
|
||||
<Button variant="primary" outline onPress={() => onCancel()}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
)}
|
||||
</RACForm>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
|
||||
export type TooltipWrapperProps = {
|
||||
tooltip?: string
|
||||
tooltip?: ReactNode
|
||||
tooltipType?: 'instant' | 'delayed'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user