mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-27 18:56:58 +00:00
✅ (ci) try to pass more checks with sonarqube
This commit is contained in:
@@ -88,17 +88,18 @@ def generate_token(
|
|||||||
str: The LiveKit JWT access token.
|
str: The LiveKit JWT access token.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if is_admin_or_owner:
|
# Local import: core.models loads core.utils mid-import (see models.py:28),
|
||||||
sources = settings.LIVEKIT_DEFAULT_SOURCES
|
# so importing EncryptionMode at module top would deadlock the bootstrap.
|
||||||
|
from core.models import EncryptionMode # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
if sources is None:
|
if is_admin_or_owner or sources is None:
|
||||||
sources = settings.LIVEKIT_DEFAULT_SOURCES
|
sources = settings.LIVEKIT_DEFAULT_SOURCES
|
||||||
|
|
||||||
# In encrypted rooms, no one can change their name/attributes after the
|
# In encrypted rooms, no one can change their name/attributes after the
|
||||||
# admin accepted them — otherwise a participant authenticated under one
|
# admin accepted them — otherwise a participant authenticated under one
|
||||||
# identity could rewrite their JWT-presented name to spoof someone else
|
# identity could rewrite their JWT-presented name to spoof someone else
|
||||||
# mid-meeting. In plain rooms, free naming is fine.
|
# mid-meeting. In plain rooms, free naming is fine.
|
||||||
can_update_own_metadata = encryption_mode == "none"
|
can_update_own_metadata = encryption_mode == EncryptionMode.NONE
|
||||||
|
|
||||||
video_grants = VideoGrants(
|
video_grants = VideoGrants(
|
||||||
room=room,
|
room=room,
|
||||||
@@ -136,7 +137,7 @@ def generate_token(
|
|||||||
# otherwise.
|
# otherwise.
|
||||||
if (
|
if (
|
||||||
not user.is_anonymous
|
not user.is_anonymous
|
||||||
and encryption_mode != "none"
|
and encryption_mode != EncryptionMode.NONE
|
||||||
and getattr(user, "email", None)
|
and getattr(user, "email", None)
|
||||||
):
|
):
|
||||||
attributes["email"] = user.email
|
attributes["email"] = user.email
|
||||||
@@ -159,7 +160,7 @@ def generate_token(
|
|||||||
# that moment — no extra round-trip, no race window where a SIP
|
# that moment — no extra round-trip, no race window where a SIP
|
||||||
# caller could read empty metadata before the `room_started` webhook
|
# caller could read empty metadata before the `room_started` webhook
|
||||||
# has time to push it.
|
# has time to push it.
|
||||||
if encryption_mode != "none":
|
if encryption_mode != EncryptionMode.NONE:
|
||||||
token = token.with_room_config(
|
token = token.with_room_config(
|
||||||
RoomConfiguration(
|
RoomConfiguration(
|
||||||
name=room,
|
name=room,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { css } from '@/styled-system/css'
|
|||||||
|
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder'
|
import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder'
|
||||||
|
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
participant: Participant
|
participant: Participant
|
||||||
@@ -28,7 +29,7 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
|
|||||||
})
|
})
|
||||||
const room = useRoomContext()
|
const room = useRoomContext()
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
|
|
||||||
const [failed, setFailed] = useState(false)
|
const [failed, setFailed] = useState(false)
|
||||||
|
|
||||||
@@ -56,8 +57,7 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
|
|||||||
if (!failed) return null
|
if (!failed) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<output
|
||||||
role="status"
|
|
||||||
aria-label={t('title')}
|
aria-label={t('title')}
|
||||||
className={css({
|
className={css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -108,6 +108,6 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
|
|||||||
{t('body')}
|
{t('body')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</output>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
import { RecordingMode, useRecordingStatuses } from '@/features/recording'
|
import { RecordingMode, useRecordingStatuses } from '@/features/recording'
|
||||||
|
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
|
|
||||||
const COLLAPSE_DELAY_MS = 4000
|
const COLLAPSE_DELAY_MS = 4000
|
||||||
|
|
||||||
@@ -39,10 +40,9 @@ function StatusPill({ icon, label, background, pulse }: PillProps) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<output
|
||||||
onMouseEnter={() => setCollapsed(false)}
|
onMouseEnter={() => setCollapsed(false)}
|
||||||
onMouseLeave={() => setCollapsed(true)}
|
onMouseLeave={() => setCollapsed(true)}
|
||||||
role="status"
|
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className={css({
|
className={css({
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
@@ -78,7 +78,7 @@ function StatusPill({ icon, label, background, pulse }: PillProps) {
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</output>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export function RoomStatusBanner() {
|
|||||||
const isRecording = screenRec.isStarted
|
const isRecording = screenRec.isStarted
|
||||||
const isTranscribing = transcript.isStarted
|
const isTranscribing = transcript.isStarted
|
||||||
|
|
||||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
|
|
||||||
if (!isEncrypted && !isRecording && !isTranscribing) {
|
if (!isEncrypted && !isRecording && !isTranscribing) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { isRoomValid } from '@/features/rooms'
|
|||||||
import { normalizeRoomId } from '@/features/rooms/utils/isRoomValid'
|
import { normalizeRoomId } from '@/features/rooms/utils/isRoomValid'
|
||||||
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
|
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
|
||||||
import { isValidPassphrase } from '@/features/encryption'
|
import { isValidPassphrase } from '@/features/encryption'
|
||||||
|
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
|
|
||||||
export const JoinMeetingDialog = () => {
|
export const JoinMeetingDialog = () => {
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
@@ -39,7 +40,7 @@ export const JoinMeetingDialog = () => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const room = await fetchRoom({ roomId: parsed.roomId })
|
const room = await fetchRoom({ roomId: parsed.roomId })
|
||||||
if (room.encryption_mode === 'basic') {
|
if (room.encryption_mode === ApiEncryptionMode.BASIC) {
|
||||||
setRoomId(parsed.roomId)
|
setRoomId(parsed.roomId)
|
||||||
setStep('passphrase')
|
setStep('passphrase')
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -103,17 +103,19 @@ export const Conference = ({
|
|||||||
const hasValidHash = isValidPassphrase(hashPassphrase)
|
const hasValidHash = isValidPassphrase(hashPassphrase)
|
||||||
const dbSaysEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC
|
const dbSaysEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
|
|
||||||
const encryptionMismatch:
|
type EncryptionMismatch =
|
||||||
| 'missingPassphrase'
|
| 'missingPassphrase'
|
||||||
| 'unexpectedPassphrase'
|
| 'unexpectedPassphrase'
|
||||||
| null =
|
| null
|
||||||
data === undefined
|
|
||||||
? null
|
let encryptionMismatch: EncryptionMismatch = null
|
||||||
: dbSaysEncrypted && !hasValidHash
|
if (data !== undefined) {
|
||||||
? 'missingPassphrase'
|
if (dbSaysEncrypted && !hasValidHash) {
|
||||||
: !dbSaysEncrypted && hashPassphrase.length > 0
|
encryptionMismatch = 'missingPassphrase'
|
||||||
? 'unexpectedPassphrase'
|
} else if (!dbSaysEncrypted && hashPassphrase.length > 0) {
|
||||||
: null
|
encryptionMismatch = 'unexpectedPassphrase'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We treat the room as encrypted purely because we have a valid hash.
|
// We treat the room as encrypted purely because we have a valid hash.
|
||||||
// No server condition. If the hash is valid we MUST run E2EE; if not,
|
// No server condition. If the hash is valid we MUST run E2EE; if not,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { useMemo } from 'react'
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
import { FeaturePill } from '@/features/encryption'
|
import { FeaturePill } from '@/features/encryption'
|
||||||
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
|
import { ApiAccessLevel, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony'
|
import { useTelephony } from '@/features/rooms/livekit/hooks/useTelephony'
|
||||||
import { formatPinCode } from '@/features/rooms/utils/telephony'
|
import { formatPinCode } from '@/features/rooms/utils/telephony'
|
||||||
import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard'
|
import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard'
|
||||||
@@ -53,7 +53,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
const isAdminOrOwner = !!roomData?.is_administrable
|
const isAdminOrOwner = !!roomData?.is_administrable
|
||||||
const baseRoomUrl = getRouteUrl('room', roomData?.slug)
|
const baseRoomUrl = getRouteUrl('room', roomData?.slug)
|
||||||
// Include the hash (passphrase) for basic encrypted rooms so the full link is visible
|
// Include the hash (passphrase) for basic encrypted rooms so the full link is visible
|
||||||
@@ -108,219 +108,230 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
|
|||||||
) : (
|
) : (
|
||||||
<P>{t('description')}</P>
|
<P>{t('description')}</P>
|
||||||
)}
|
)}
|
||||||
{isEncrypted && !isAdminOrOwner ? null : isEncrypted ? (
|
{(() => {
|
||||||
<div
|
if (isEncrypted && !isAdminOrOwner) return null
|
||||||
className={css({
|
if (isEncrypted) {
|
||||||
width: '100%',
|
return (
|
||||||
marginTop: '0.5rem',
|
<div
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '0.75rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
gap: '0.5rem',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '0.6rem 0.85rem',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
backgroundColor: '#fff7ed',
|
|
||||||
border: '1px solid #fed7aa',
|
|
||||||
color: '#7c2d12',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<RiAlertFill
|
|
||||||
size={18}
|
|
||||||
color="#b45309"
|
|
||||||
className={css({ flexShrink: 0 })}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
variant="sm"
|
|
||||||
margin={false}
|
|
||||||
className={css({
|
className={css({
|
||||||
color: '#7c2d12',
|
width: '100%',
|
||||||
fontSize: '0.85rem',
|
marginTop: '0.5rem',
|
||||||
lineHeight: 1.4,
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.75rem',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{tHome('warning')}
|
<div
|
||||||
</Text>
|
role="alert"
|
||||||
</div>
|
className={css({
|
||||||
<div
|
display: 'flex',
|
||||||
className={css({
|
gap: '0.5rem',
|
||||||
display: 'flex',
|
alignItems: 'center',
|
||||||
alignItems: 'center',
|
padding: '0.6rem 0.85rem',
|
||||||
gap: '0.5rem',
|
borderRadius: '0.5rem',
|
||||||
padding: '0.5rem 0.75rem',
|
backgroundColor: '#fff7ed',
|
||||||
borderRadius: '0.5rem',
|
border: '1px solid #fed7aa',
|
||||||
border: '1px solid',
|
color: '#7c2d12',
|
||||||
borderColor: 'greyscale.250',
|
})}
|
||||||
})}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={css({
|
|
||||||
flex: 1,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{roomUrl?.replace(/^https?:\/\//, '')}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
|
|
||||||
square
|
|
||||||
size="sm"
|
|
||||||
onPress={copyRoomUrlToClipboard}
|
|
||||||
aria-label={isRoomUrlCopied ? t('copied') : t('copyUrl')}
|
|
||||||
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
|
|
||||||
>
|
|
||||||
{isRoomUrlCopied ? (
|
|
||||||
<RiCheckLine size={16} />
|
|
||||||
) : (
|
|
||||||
<RiFileCopyLine size={16} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Text
|
|
||||||
margin={false}
|
|
||||||
className={css({
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 400,
|
|
||||||
color: 'greyscale.500',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t('encryptedDisabledHeading')}
|
|
||||||
</Text>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: '0.4rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<FeaturePill
|
|
||||||
size="sm"
|
|
||||||
icon={<RiPhoneLine size={13} />}
|
|
||||||
label={tFeatures('features.dialIn')}
|
|
||||||
/>
|
|
||||||
<FeaturePill
|
|
||||||
size="sm"
|
|
||||||
icon={<RiComputerLine size={13} />}
|
|
||||||
label={tFeatures('features.meetingRoom')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : isTelephonyReadyForUse ? (
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
marginTop: '0.5rem',
|
|
||||||
gap: '1rem',
|
|
||||||
overflow: 'visible',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Text as="p" wrap="pretty">
|
|
||||||
{roomUrl?.replace(/^https?:\/\//, '')}
|
|
||||||
</Text>
|
|
||||||
{isTelephonyReadyForUse && roomUrl && (
|
|
||||||
<Button
|
|
||||||
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
|
|
||||||
square
|
|
||||||
size={'sm'}
|
|
||||||
onPress={copyRoomUrlToClipboard}
|
|
||||||
aria-label={isRoomUrlCopied ? t('copied') : t('copyUrl')}
|
|
||||||
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
|
|
||||||
>
|
>
|
||||||
{isRoomUrlCopied ? (
|
<RiAlertFill
|
||||||
<RiCheckLine aria-hidden="true" />
|
size={18}
|
||||||
|
color="#b45309"
|
||||||
|
className={css({ flexShrink: 0 })}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
variant="sm"
|
||||||
|
margin={false}
|
||||||
|
className={css({
|
||||||
|
color: '#7c2d12',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{tHome('warning')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.5rem',
|
||||||
|
padding: '0.5rem 0.75rem',
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'greyscale.250',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={css({
|
||||||
|
flex: 1,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{roomUrl?.replace(/^https?:\/\//, '')}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
|
||||||
|
square
|
||||||
|
size="sm"
|
||||||
|
onPress={copyRoomUrlToClipboard}
|
||||||
|
aria-label={isRoomUrlCopied ? t('copied') : t('copyUrl')}
|
||||||
|
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
|
||||||
|
>
|
||||||
|
{isRoomUrlCopied ? (
|
||||||
|
<RiCheckLine size={16} />
|
||||||
|
) : (
|
||||||
|
<RiFileCopyLine size={16} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Text
|
||||||
|
margin={false}
|
||||||
|
className={css({
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 400,
|
||||||
|
color: 'greyscale.500',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('encryptedDisabledHeading')}
|
||||||
|
</Text>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '0.4rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FeaturePill
|
||||||
|
size="sm"
|
||||||
|
icon={<RiPhoneLine size={13} />}
|
||||||
|
label={tFeatures('features.dialIn')}
|
||||||
|
/>
|
||||||
|
<FeaturePill
|
||||||
|
size="sm"
|
||||||
|
icon={<RiComputerLine size={13} />}
|
||||||
|
label={tFeatures('features.meetingRoom')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isTelephonyReadyForUse) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
gap: '1rem',
|
||||||
|
overflow: 'visible',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Text as="p" wrap="pretty">
|
||||||
|
{roomUrl?.replace(/^https?:\/\//, '')}
|
||||||
|
</Text>
|
||||||
|
{isTelephonyReadyForUse && roomUrl && (
|
||||||
|
<Button
|
||||||
|
variant={isRoomUrlCopied ? 'success' : 'tertiaryText'}
|
||||||
|
square
|
||||||
|
size={'sm'}
|
||||||
|
onPress={copyRoomUrlToClipboard}
|
||||||
|
aria-label={
|
||||||
|
isRoomUrlCopied ? t('copied') : t('copyUrl')
|
||||||
|
}
|
||||||
|
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
|
||||||
|
>
|
||||||
|
{isRoomUrlCopied ? (
|
||||||
|
<RiCheckLine aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<RiFileCopyLine aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Text as="p" wrap="pretty">
|
||||||
|
<Bold>{t('phone.call')}</Bold> ({telephony?.country}){' '}
|
||||||
|
{telephony?.internationalPhoneNumber}
|
||||||
|
</Text>
|
||||||
|
<Text as="p" wrap="pretty">
|
||||||
|
<Bold>{t('phone.pinCode')}</Bold>{' '}
|
||||||
|
{formatPinCode(roomData?.pin_code)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={isCopied ? 'success' : 'secondaryText'}
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
|
aria-label={isCopied ? t('copied') : t('copy')}
|
||||||
|
style={{
|
||||||
|
justifyContent: 'start',
|
||||||
|
}}
|
||||||
|
onPress={copyRoomToClipboard}
|
||||||
|
data-attr="share-dialog-copy"
|
||||||
|
>
|
||||||
|
{isCopied ? (
|
||||||
|
<>
|
||||||
|
<RiCheckLine
|
||||||
|
size={18}
|
||||||
|
style={{ marginRight: '8px' }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{t('copied')}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<RiFileCopyLine aria-hidden="true" />
|
<>
|
||||||
|
<RiFileCopyLine
|
||||||
|
style={{ marginRight: '6px', minWidth: '18px' }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{t('copy')}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)
|
||||||
<div
|
}
|
||||||
className={css({
|
return (
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Text as="p" wrap="pretty">
|
|
||||||
<Bold>{t('phone.call')}</Bold> ({telephony?.country}){' '}
|
|
||||||
{telephony?.internationalPhoneNumber}
|
|
||||||
</Text>
|
|
||||||
<Text as="p" wrap="pretty">
|
|
||||||
<Bold>{t('phone.pinCode')}</Bold>{' '}
|
|
||||||
{formatPinCode(roomData?.pin_code)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant={isCopied ? 'success' : 'secondaryText'}
|
variant={isCopied ? 'success' : 'tertiary'}
|
||||||
size="sm"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
aria-label={isCopied ? t('copied') : t('copy')}
|
aria-label={isCopied ? t('copied') : t('copy')}
|
||||||
style={{
|
|
||||||
justifyContent: 'start',
|
|
||||||
}}
|
|
||||||
onPress={copyRoomToClipboard}
|
onPress={copyRoomToClipboard}
|
||||||
data-attr="share-dialog-copy"
|
data-attr="share-dialog-copy"
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<>
|
<>
|
||||||
<RiCheckLine
|
<RiCheckLine size={24} style={{ marginRight: '8px' }} />
|
||||||
size={18}
|
|
||||||
style={{ marginRight: '8px' }}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{t('copied')}
|
{t('copied')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<RiFileCopyLine
|
<RiFileCopyLine size={24} style={{ marginRight: '8px' }} />
|
||||||
style={{ marginRight: '6px', minWidth: '18px' }}
|
{t('copyUrl')}
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{t('copy')}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)
|
||||||
) : (
|
})()}
|
||||||
<Button
|
|
||||||
variant={isCopied ? 'success' : 'tertiary'}
|
|
||||||
fullWidth
|
|
||||||
aria-label={isCopied ? t('copied') : t('copy')}
|
|
||||||
onPress={copyRoomToClipboard}
|
|
||||||
data-attr="share-dialog-copy"
|
|
||||||
>
|
|
||||||
{isCopied ? (
|
|
||||||
<>
|
|
||||||
<RiCheckLine size={24} style={{ marginRight: '8px' }} />
|
|
||||||
{t('copied')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<RiFileCopyLine size={24} style={{ marginRight: '8px' }} />
|
|
||||||
{t('copyUrl')}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{roomData?.access_level === ApiAccessLevel.PUBLIC && (
|
{roomData?.access_level === ApiAccessLevel.PUBLIC && (
|
||||||
<HStack>
|
<HStack>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { useQuery } from '@tanstack/react-query'
|
|||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
import { ApiLobbyStatus, ApiRequestEntry } from '../api/requestEntry'
|
import { ApiLobbyStatus, ApiRequestEntry } from '../api/requestEntry'
|
||||||
import { Spinner } from '@/primitives/Spinner'
|
import { Spinner } from '@/primitives/Spinner'
|
||||||
import { ApiAccessLevel } from '../api/ApiRoom'
|
import { ApiAccessLevel, ApiEncryptionMode } from '../api/ApiRoom'
|
||||||
import {
|
import {
|
||||||
isValidPassphrase,
|
isValidPassphrase,
|
||||||
getPassphraseFromHash,
|
getPassphraseFromHash,
|
||||||
@@ -118,7 +118,7 @@ export const Join = ({
|
|||||||
retry: false,
|
retry: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const isEncryptedRoom = roomInfo?.encryption_mode === 'basic'
|
const isEncryptedRoom = roomInfo?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
const { user, isLoggedIn } = useUser()
|
const { user, isLoggedIn } = useUser()
|
||||||
// Authenticated joiners of an encrypted room can't pick an arbitrary
|
// Authenticated joiners of an encrypted room can't pick an arbitrary
|
||||||
// display name — it must match the OIDC profile, and the backend
|
// display name — it must match the OIDC profile, and the backend
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RiAlertFill } from '@remixicon/react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
||||||
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
|
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
|
||||||
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
|
import { ApiAccessLevel, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
@@ -168,7 +168,8 @@ export const Admin = () => {
|
|||||||
{t('access.description')}
|
{t('access.description')}
|
||||||
</Text>
|
</Text>
|
||||||
{(() => {
|
{(() => {
|
||||||
const isEncrypted = readOnlyData?.encryption_mode === 'basic'
|
const isEncrypted =
|
||||||
|
readOnlyData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isEncrypted && (
|
{isEncrypted && (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { formatPinCode } from '../../utils/telephony'
|
|||||||
import { useTelephony } from '../hooks/useTelephony'
|
import { useTelephony } from '../hooks/useTelephony'
|
||||||
import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard'
|
import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard'
|
||||||
import { FeaturePill } from '@/features/encryption'
|
import { FeaturePill } from '@/features/encryption'
|
||||||
|
import { ApiEncryptionMode } from '../../api/ApiRoom'
|
||||||
|
|
||||||
export const Info = () => {
|
export const Info = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'info' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'info' })
|
||||||
@@ -30,7 +31,7 @@ export const Info = () => {
|
|||||||
: baseRoomUrl
|
: baseRoomUrl
|
||||||
|
|
||||||
const telephony = useTelephony()
|
const telephony = useTelephony()
|
||||||
const isEncrypted = data?.encryption_mode === 'basic'
|
const isEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
const isAdminOrOwner = !!data?.is_administrable
|
const isAdminOrOwner = !!data?.is_administrable
|
||||||
|
|
||||||
const isTelephonyReadyForUse = useMemo(() => {
|
const isTelephonyReadyForUse = useMemo(() => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '@/features/recording'
|
} from '@/features/recording'
|
||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
|
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
|
|
||||||
export interface ToolsButtonProps {
|
export interface ToolsButtonProps {
|
||||||
icon: ReactNode
|
icon: ReactNode
|
||||||
@@ -108,7 +109,7 @@ export const Tools = () => {
|
|||||||
useSidePanel()
|
useSidePanel()
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
|
|
||||||
// Restore focus to the element that opened the Tools panel
|
// Restore focus to the element that opened the Tools panel
|
||||||
useRestoreFocus(isToolsOpen, {
|
useRestoreFocus(isToolsOpen, {
|
||||||
|
|||||||
+2
-1
@@ -6,13 +6,14 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
|||||||
import { RecordingMode, useHasRecordingAccess } from '@/features/recording'
|
import { RecordingMode, useHasRecordingAccess } from '@/features/recording'
|
||||||
import { FeatureFlags } from '@/features/analytics/enums'
|
import { FeatureFlags } from '@/features/analytics/enums'
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
|
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
|
|
||||||
export const ScreenRecordingMenuItem = () => {
|
export const ScreenRecordingMenuItem = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||||
const { isScreenRecordingOpen, openScreenRecording, toggleTools } =
|
const { isScreenRecordingOpen, openScreenRecording, toggleTools } =
|
||||||
useSidePanel()
|
useSidePanel()
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
|
|
||||||
const hasScreenRecordingAccess = useHasRecordingAccess(
|
const hasScreenRecordingAccess = useHasRecordingAccess(
|
||||||
RecordingMode.ScreenRecording,
|
RecordingMode.ScreenRecording,
|
||||||
|
|||||||
+2
-1
@@ -6,12 +6,13 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
|||||||
import { RecordingMode, useHasRecordingAccess } from '@/features/recording'
|
import { RecordingMode, useHasRecordingAccess } from '@/features/recording'
|
||||||
import { FeatureFlags } from '@/features/analytics/enums'
|
import { FeatureFlags } from '@/features/analytics/enums'
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
|
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
|
|
||||||
export const TranscriptMenuItem = () => {
|
export const TranscriptMenuItem = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||||
const { isTranscriptOpen, openTranscript, toggleTools } = useSidePanel()
|
const { isTranscriptOpen, openTranscript, toggleTools } = useSidePanel()
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||||
|
|
||||||
const hasTranscriptAccess = useHasRecordingAccess(
|
const hasTranscriptAccess = useHasRecordingAccess(
|
||||||
RecordingMode.Transcript,
|
RecordingMode.Transcript,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useTelephony } from './useTelephony'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { formatPinCode } from '@/features/rooms/utils/telephony'
|
import { formatPinCode } from '@/features/rooms/utils/telephony'
|
||||||
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
import { ApiRoom, ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||||
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
||||||
|
|
||||||
const COPY_SUCCESS_TIMEOUT = 3000
|
const COPY_SUCCESS_TIMEOUT = 3000
|
||||||
@@ -47,7 +47,9 @@ export const useCopyRoomToClipboard = (
|
|||||||
// so make sure we don't paste a non-functional phone+PIN snippet either.
|
// so make sure we don't paste a non-functional phone+PIN snippet either.
|
||||||
const hasTelephonyInfo = useMemo(() => {
|
const hasTelephonyInfo = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
telephony.enabled && room?.pin_code && room?.encryption_mode !== 'basic'
|
telephony.enabled &&
|
||||||
|
room?.pin_code &&
|
||||||
|
room?.encryption_mode !== ApiEncryptionMode.BASIC
|
||||||
)
|
)
|
||||||
}, [telephony.enabled, room?.pin_code, room?.encryption_mode])
|
}, [telephony.enabled, room?.pin_code, room?.encryption_mode])
|
||||||
|
|
||||||
|
|||||||
@@ -77,10 +77,17 @@ export const CreateMeetingButton = () => {
|
|||||||
|
|
||||||
// Communicate iframe height to parent for proper sizing
|
// Communicate iframe height to parent for proper sizing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const targetOrigin = (() => {
|
||||||
|
try {
|
||||||
|
return document.referrer ? new URL(document.referrer).origin : '/'
|
||||||
|
} catch {
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
})()
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
{ type: 'RESIZE', data: { height: document.body.scrollHeight } },
|
{ type: 'RESIZE', data: { height: document.body.scrollHeight } },
|
||||||
'*'
|
targetOrigin
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
observer.observe(document.body)
|
observer.observe(document.body)
|
||||||
|
|||||||
@@ -30,16 +30,21 @@ export const EncryptionDefaultField = () => {
|
|||||||
|
|
||||||
const isOn = user?.default_encryption_mode === ApiEncryptionMode.BASIC
|
const isOn = user?.default_encryption_mode === ApiEncryptionMode.BASIC
|
||||||
|
|
||||||
const handleToggle = (next: boolean) => {
|
const handleToggle = async (next: boolean) => {
|
||||||
if (!user) return
|
if (!user) return
|
||||||
void mutateAsync({
|
try {
|
||||||
user: {
|
await mutateAsync({
|
||||||
id: user.id,
|
user: {
|
||||||
default_encryption_mode: next
|
id: user.id,
|
||||||
? ApiEncryptionMode.BASIC
|
default_encryption_mode: next
|
||||||
: ApiEncryptionMode.NONE,
|
? ApiEncryptionMode.BASIC
|
||||||
},
|
: ApiEncryptionMode.NONE,
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// react-query stores the error on the mutation; the UI shows
|
||||||
|
// isPending while in flight and reverts the toggle on failure.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ export const navigateTo = <S = unknown>(
|
|||||||
// Including the hash in the URL passed to `navigate` lets us avoid a
|
// Including the hash in the URL passed to `navigate` lets us avoid a
|
||||||
// brittle pushState + replaceState dance at the call site: the URL the
|
// brittle pushState + replaceState dance at the call site: the URL the
|
||||||
// app first renders already carries the fragment.
|
// app first renders already carries the fragment.
|
||||||
const target = options?.hash ? `${to}#${options.hash}` : to
|
const { hash, ...navigateOptions } = options ?? {}
|
||||||
const { hash: _hash, ...navigateOptions } = options ?? {}
|
const target = hash ? `${to}#${hash}` : to
|
||||||
void _hash
|
|
||||||
return navigate(target, navigateOptions)
|
return navigate(target, navigateOptions)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user