(ci) try to pass more checks with sonarqube

This commit is contained in:
Thomas Ramé
2026-05-18 10:59:25 +02:00
parent 668f093063
commit e2a3b286ca
16 changed files with 273 additions and 240 deletions
+7 -6
View File
@@ -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, {
@@ -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,
@@ -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) {
+2 -3
View File
@@ -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)
} }