✨(frontend) add telephony info to meeting dialog with layout stability
Add telephony information display to the later meeting dialog while preserving existing layout when telephony is disabled. Prevent layout shift on modal close by collapsing all modal content immediately when room becomes undefined. Critical enhancement for users creating meeting links to have complete connection information available.
This commit is contained in:
committed by
aleb_the_flash
parent
7c67bacd94
commit
b54445739a
@@ -1,20 +1,26 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
||||||
import { Button, Dialog, type DialogProps, P, Text } from '@/primitives'
|
import { Bold, Button, Dialog, type DialogProps, P, Text } from '@/primitives'
|
||||||
import { HStack } from '@/styled-system/jsx'
|
import { HStack } from '@/styled-system/jsx'
|
||||||
import { RiCheckLine, RiFileCopyLine, RiSpam2Fill } from '@remixicon/react'
|
import { RiCheckLine, RiFileCopyLine, RiSpam2Fill } from '@remixicon/react'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
|
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
||||||
|
import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony'
|
||||||
|
import { formatPinCode } from '@/features/rooms/utils/telephony'
|
||||||
|
|
||||||
// fixme - duplication with the InviteDialog
|
// fixme - duplication with the InviteDialog
|
||||||
export const LaterMeetingDialog = ({
|
export const LaterMeetingDialog = ({
|
||||||
roomId,
|
room,
|
||||||
...dialogProps
|
...dialogProps
|
||||||
}: { roomId: string } & Omit<DialogProps, 'title'>) => {
|
}: { room: null | ApiRoom } & Omit<DialogProps, 'title'>) => {
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home', { keyPrefix: 'laterMeetingDialog' })
|
||||||
const roomUrl = getRouteUrl('room', roomId)
|
|
||||||
|
const roomUrl = room && getRouteUrl('room', room?.slug)
|
||||||
|
const telephony = useTelephony()
|
||||||
|
|
||||||
const [isCopied, setIsCopied] = useState(false)
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
|
const [isRoomUrlCopied, setIsRoomUrlCopied] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCopied) {
|
if (isCopied) {
|
||||||
@@ -25,76 +31,194 @@ export const LaterMeetingDialog = ({
|
|||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRoomUrlCopied) {
|
||||||
|
const timeout = setTimeout(() => setIsRoomUrlCopied(false), 3000)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [isRoomUrlCopied])
|
||||||
|
|
||||||
|
const isTelephonyReadyForUse = useMemo(() => {
|
||||||
|
return telephony?.enabled && room?.pin_code
|
||||||
|
}, [telephony?.enabled, room?.pin_code])
|
||||||
|
|
||||||
|
const clipboardContent = useMemo(() => {
|
||||||
|
if (isTelephonyReadyForUse) {
|
||||||
|
return [
|
||||||
|
t('clipboard.url', { roomUrl }),
|
||||||
|
t('clipboard.numberAndPin', {
|
||||||
|
phoneNumber: telephony?.internationalPhoneNumber,
|
||||||
|
pinCode: formatPinCode(room?.pin_code),
|
||||||
|
}),
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
return roomUrl
|
||||||
|
}, [
|
||||||
|
isTelephonyReadyForUse,
|
||||||
|
roomUrl,
|
||||||
|
telephony?.internationalPhoneNumber,
|
||||||
|
room?.pin_code,
|
||||||
|
t,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog isOpen={!!room} {...dialogProps} title={t('heading')}>
|
||||||
isOpen={!!roomId}
|
<P>{t('description')}</P>
|
||||||
{...dialogProps}
|
{!!roomUrl && (
|
||||||
title={t('laterMeetingDialog.heading')}
|
<>
|
||||||
>
|
{isTelephonyReadyForUse ? (
|
||||||
<P>{t('laterMeetingDialog.description')}</P>
|
<div
|
||||||
<Button
|
className={css({
|
||||||
variant={isCopied ? 'success' : 'primary'}
|
width: '100%',
|
||||||
size="sm"
|
backgroundColor: 'gray.50',
|
||||||
fullWidth
|
borderRadius: '0.75rem',
|
||||||
aria-label={t('laterMeetingDialog.copy')}
|
display: 'flex',
|
||||||
style={{
|
flexDirection: 'column',
|
||||||
justifyContent: 'start',
|
padding: '1.75rem 1.5rem',
|
||||||
}}
|
marginTop: '0.5rem',
|
||||||
onPress={() => {
|
gap: '1rem',
|
||||||
navigator.clipboard.writeText(roomUrl)
|
overflow: 'hidden',
|
||||||
setIsCopied(true)
|
})}
|
||||||
}}
|
>
|
||||||
onHoverChange={setIsHovered}
|
|
||||||
data-attr="later-dialog-copy"
|
|
||||||
>
|
|
||||||
{isCopied ? (
|
|
||||||
<>
|
|
||||||
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
|
|
||||||
{t('laterMeetingDialog.copied')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<RiFileCopyLine
|
|
||||||
size={18}
|
|
||||||
style={{ marginRight: '8px', minWidth: '18px' }}
|
|
||||||
/>
|
|
||||||
{isHovered ? (
|
|
||||||
t('laterMeetingDialog.copy')
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
className={css({
|
||||||
textOverflow: 'ellipsis',
|
display: 'flex',
|
||||||
overflow: 'hidden',
|
alignItems: 'center',
|
||||||
userSelect: 'none',
|
justifyContent: 'space-between',
|
||||||
textWrap: 'nowrap',
|
})}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{roomUrl.replace(/^https?:\/\//, '')}
|
<Text as="p" wrap="pretty">
|
||||||
|
{roomUrl?.replace(/^https?:\/\//, '')}
|
||||||
|
</Text>
|
||||||
|
{isTelephonyReadyForUse && (
|
||||||
|
<Button
|
||||||
|
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
|
||||||
|
square
|
||||||
|
size={'sm'}
|
||||||
|
onPress={() => {
|
||||||
|
navigator.clipboard.writeText(roomUrl)
|
||||||
|
setIsRoomUrlCopied(true)
|
||||||
|
}}
|
||||||
|
aria-label={t('copyUrl')}
|
||||||
|
tooltip={t('copyUrl')}
|
||||||
|
>
|
||||||
|
{isRoomUrlCopied ? <RiCheckLine /> : <RiFileCopyLine />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div
|
||||||
</>
|
className={css({
|
||||||
)}
|
display: 'flex',
|
||||||
</Button>
|
flexDirection: 'column',
|
||||||
<HStack>
|
})}
|
||||||
<div
|
>
|
||||||
className={css({
|
<Text as="p" wrap="pretty">
|
||||||
backgroundColor: 'primary.200',
|
<Bold>{t('phone.call')}</Bold> ({telephony?.country}){' '}
|
||||||
borderRadius: '50%',
|
{telephony?.internationalPhoneNumber}
|
||||||
padding: '4px',
|
</Text>
|
||||||
marginTop: '1rem',
|
<Text as="p" wrap="pretty">
|
||||||
})}
|
<Bold>{t('phone.pinCode')}</Bold>{' '}
|
||||||
>
|
{formatPinCode(room?.pin_code)}
|
||||||
<RiSpam2Fill
|
</Text>
|
||||||
size={22}
|
</div>
|
||||||
className={css({
|
{clipboardContent && (
|
||||||
fill: 'primary.500',
|
<Button
|
||||||
})}
|
variant={isCopied ? 'success' : 'tertiaryText'}
|
||||||
/>
|
size="sm"
|
||||||
</div>
|
fullWidth
|
||||||
<Text variant="sm" style={{ marginTop: '1rem' }}>
|
aria-label={t('copy')}
|
||||||
{t('laterMeetingDialog.permissions')}
|
style={{
|
||||||
</Text>
|
justifyContent: 'start',
|
||||||
</HStack>
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
navigator.clipboard.writeText(clipboardContent)
|
||||||
|
setIsCopied(true)
|
||||||
|
}}
|
||||||
|
data-attr="later-dialog-copy"
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<>
|
||||||
|
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
|
||||||
|
{t('copied')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RiFileCopyLine
|
||||||
|
style={{ marginRight: '6px', minWidth: '18px' }}
|
||||||
|
/>
|
||||||
|
{t('copy')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant={isCopied ? 'success' : 'primary'}
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
|
aria-label={t('copy')}
|
||||||
|
style={{
|
||||||
|
justifyContent: 'start',
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
navigator.clipboard.writeText(roomUrl)
|
||||||
|
setIsCopied(true)
|
||||||
|
}}
|
||||||
|
onHoverChange={setIsHovered}
|
||||||
|
data-attr="later-dialog-copy"
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<>
|
||||||
|
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
|
||||||
|
{t('copied')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RiFileCopyLine
|
||||||
|
size={18}
|
||||||
|
style={{ marginRight: '8px', minWidth: '18px' }}
|
||||||
|
/>
|
||||||
|
{isHovered ? (
|
||||||
|
t('copy')
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
userSelect: 'none',
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{roomUrl?.replace(/^https?:\/\//, '')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<HStack>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
backgroundColor: 'primary.200',
|
||||||
|
borderRadius: '50%',
|
||||||
|
padding: '4px',
|
||||||
|
marginTop: '1rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<RiSpam2Fill
|
||||||
|
size={22}
|
||||||
|
className={css({
|
||||||
|
fill: 'primary.500',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Text variant="sm" style={{ marginTop: '1rem' }}>
|
||||||
|
{t('permissions')}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
|||||||
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
|
||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { LoginButton } from '@/components/LoginButton'
|
import { LoginButton } from '@/components/LoginButton'
|
||||||
|
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
||||||
|
|
||||||
const Columns = ({ children }: { children?: ReactNode }) => {
|
const Columns = ({ children }: { children?: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
@@ -153,7 +154,7 @@ export const Home = () => {
|
|||||||
} = usePersistentUserChoices()
|
} = usePersistentUserChoices()
|
||||||
|
|
||||||
const { mutateAsync: createRoom } = useCreateRoom()
|
const { mutateAsync: createRoom } = useCreateRoom()
|
||||||
const [laterRoomId, setLaterRoomId] = useState<null | string>(null)
|
const [laterRoom, setLaterRoom] = useState<null | ApiRoom>(null)
|
||||||
|
|
||||||
const { data } = useConfig()
|
const { data } = useConfig()
|
||||||
|
|
||||||
@@ -202,7 +203,7 @@ export const Home = () => {
|
|||||||
onAction={() => {
|
onAction={() => {
|
||||||
const slug = generateRoomId()
|
const slug = generateRoomId()
|
||||||
createRoom({ slug, username }).then((data) =>
|
createRoom({ slug, username }).then((data) =>
|
||||||
setLaterRoomId(data.slug)
|
setLaterRoom(data)
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
data-attr="create-option-later"
|
data-attr="create-option-later"
|
||||||
@@ -251,8 +252,8 @@ export const Home = () => {
|
|||||||
</RightColumn>
|
</RightColumn>
|
||||||
</Columns>
|
</Columns>
|
||||||
<LaterMeetingDialog
|
<LaterMeetingDialog
|
||||||
roomId={laterRoomId ?? ''}
|
room={laterRoom}
|
||||||
onOpenChange={() => setLaterRoomId(null)}
|
onOpenChange={() => setLaterRoom(null)}
|
||||||
/>
|
/>
|
||||||
</Screen>
|
</Screen>
|
||||||
</UserAware>
|
</UserAware>
|
||||||
|
|||||||
@@ -20,9 +20,18 @@
|
|||||||
"laterMeetingDialog": {
|
"laterMeetingDialog": {
|
||||||
"heading": "Ihre Zugangsdaten",
|
"heading": "Ihre Zugangsdaten",
|
||||||
"description": "Teilen Sie diese Informationen mit den Gästen. Sie können dem Meeting beitreten, ohne sich anmelden zu müssen. Dieses Meeting ist dauerhaft und kann wiederverwendet werden.",
|
"description": "Teilen Sie diese Informationen mit den Gästen. Sie können dem Meeting beitreten, ohne sich anmelden zu müssen. Dieses Meeting ist dauerhaft und kann wiederverwendet werden.",
|
||||||
"copy": "Meeting-Link kopieren",
|
"permissions": "Personen mit diesem Link benötigen keine Genehmigung, um diesem Meeting beizutreten.",
|
||||||
"copied": "Link in die Zwischenablage kopiert",
|
"copy": "Informationen kopieren",
|
||||||
"permissions": "Personen mit diesem Link benötigen keine Genehmigung, um diesem Meeting beizutreten."
|
"copied": "Informationen kopiert",
|
||||||
|
"copyUrl": "Meeting-Link kopieren",
|
||||||
|
"phone": {
|
||||||
|
"call": "Rufen Sie an:",
|
||||||
|
"pinCode": "Code:"
|
||||||
|
},
|
||||||
|
"clipboard": {
|
||||||
|
"url": "Um an der Videokonferenz teilzunehmen, klicken Sie auf diesen Link: {{roomUrl}}",
|
||||||
|
"numberAndPin": "Um telefonisch teilzunehmen, wählen Sie {{phoneNumber}} und geben Sie diesen Code ein: {{pinCode}}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"introSlider": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
|
|||||||
@@ -20,9 +20,18 @@
|
|||||||
"laterMeetingDialog": {
|
"laterMeetingDialog": {
|
||||||
"heading": "Your connection details",
|
"heading": "Your connection details",
|
||||||
"description": "Share this information with the guests. They will be able to join the meeting without needing to sign in. This meeting is permanent and can be reused.",
|
"description": "Share this information with the guests. They will be able to join the meeting without needing to sign in. This meeting is permanent and can be reused.",
|
||||||
"copy": "Copy the meeting link",
|
"permissions": "People with this link do not need your permission to join this meeting.",
|
||||||
"copied": "Link copied to clipboard",
|
"copy": "Copy information",
|
||||||
"permissions": "People with this link do not need your permission to join this meeting."
|
"copied": "Information copied to clipboard",
|
||||||
|
"copyUrl": "Copy the meeting link",
|
||||||
|
"phone": {
|
||||||
|
"call": "Call:",
|
||||||
|
"pinCode": "Code:"
|
||||||
|
},
|
||||||
|
"clipboard": {
|
||||||
|
"url": "To join the video conference, click on this link: {{roomUrl}}",
|
||||||
|
"numberAndPin": "To join by phone, dial {{phoneNumber}} and enter this code: {{pinCode}}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"introSlider": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
|
|||||||
@@ -19,10 +19,19 @@
|
|||||||
},
|
},
|
||||||
"laterMeetingDialog": {
|
"laterMeetingDialog": {
|
||||||
"heading": "Vos informations de connexion",
|
"heading": "Vos informations de connexion",
|
||||||
"copy": "Copier le lien de la réunion",
|
|
||||||
"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."
|
|
||||||
"description": "Partagez ces informations avec les invités. Ils pourront rejoindre la réunion sans avoir besoin de se connecter. Cette réunion est permanente et peut être réutilisée.",
|
"description": "Partagez ces informations avec les invités. Ils pourront rejoindre la réunion sans avoir besoin de se connecter. Cette réunion est permanente et peut être réutilisée.",
|
||||||
|
"permissions": "Les personnes disposant de ce lien n'ont pas besoin de votre autorisation pour rejoindre cette réunion.",
|
||||||
|
"copy": "Copier les informations",
|
||||||
|
"copied": "Copiées dans le presse-papiers",
|
||||||
|
"copyUrl": "Copier le lien de la réunion",
|
||||||
|
"phone": {
|
||||||
|
"call": "Appelez le :",
|
||||||
|
"pinCode": "Code :"
|
||||||
|
},
|
||||||
|
"clipboard": {
|
||||||
|
"url": "Pour participer à la visioconférence, cliquez sur ce lien : {{roomUrl}}",
|
||||||
|
"numberAndPin": "Pour participer par téléphone, composez le {{phoneNumber}} et saisissez ce code : {{pinCode}}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"introSlider": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
|
|||||||
@@ -20,9 +20,18 @@
|
|||||||
"laterMeetingDialog": {
|
"laterMeetingDialog": {
|
||||||
"heading": "Uw verbindingsgegevens",
|
"heading": "Uw verbindingsgegevens",
|
||||||
"description": "Deel deze informatie met de genodigden. Zij kunnen deelnemen aan de vergadering zonder zich aan te melden. Deze vergadering is permanent en kan hergebruikt worden.",
|
"description": "Deel deze informatie met de genodigden. Zij kunnen deelnemen aan de vergadering zonder zich aan te melden. Deze vergadering is permanent en kan hergebruikt worden.",
|
||||||
"copy": "Kopieer de vergaderlink",
|
"permissions": "Mensen met deze link hebben uw toestemming niet nodig om deel te nemen aan deze vergadering.",
|
||||||
"copied": "Link gekopieerd naar klembord",
|
"copy": "Informatie kopiëren",
|
||||||
"permissions": "Mensen met deze link hebben uw toestemming niet nodig om deel te nemen aan deze vergadering."
|
"copied": "Informatie gekopieerd",
|
||||||
|
"copyUrl": "Kopieer de vergaderlink",
|
||||||
|
"phone": {
|
||||||
|
"call": "Bel:",
|
||||||
|
"pinCode": "Code:"
|
||||||
|
},
|
||||||
|
"clipboard": {
|
||||||
|
"url": "Klik op deze link om deel te nemen aan de videoconferentie: {{roomUrl}}",
|
||||||
|
"numberAndPin": "Bel {{phoneNumber}} en voer deze code in om telefonisch deel te nemen: {{pinCode}}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"introSlider": {
|
"introSlider": {
|
||||||
"previous": {
|
"previous": {
|
||||||
|
|||||||
Reference in New Issue
Block a user