mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 20:08:24 +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.
|
||||
"""
|
||||
|
||||
if is_admin_or_owner:
|
||||
sources = settings.LIVEKIT_DEFAULT_SOURCES
|
||||
# Local import: core.models loads core.utils mid-import (see models.py:28),
|
||||
# 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
|
||||
|
||||
# In encrypted rooms, no one can change their name/attributes after the
|
||||
# admin accepted them — otherwise a participant authenticated under one
|
||||
# identity could rewrite their JWT-presented name to spoof someone else
|
||||
# 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(
|
||||
room=room,
|
||||
@@ -136,7 +137,7 @@ def generate_token(
|
||||
# otherwise.
|
||||
if (
|
||||
not user.is_anonymous
|
||||
and encryption_mode != "none"
|
||||
and encryption_mode != EncryptionMode.NONE
|
||||
and getattr(user, "email", None)
|
||||
):
|
||||
attributes["email"] = user.email
|
||||
@@ -159,7 +160,7 @@ def generate_token(
|
||||
# that moment — no extra round-trip, no race window where a SIP
|
||||
# caller could read empty metadata before the `room_started` webhook
|
||||
# has time to push it.
|
||||
if encryption_mode != "none":
|
||||
if encryption_mode != EncryptionMode.NONE:
|
||||
token = token.with_room_config(
|
||||
RoomConfiguration(
|
||||
name=room,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { css } from '@/styled-system/css'
|
||||
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder'
|
||||
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||
|
||||
interface Props {
|
||||
participant: Participant
|
||||
@@ -28,7 +29,7 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
|
||||
})
|
||||
const room = useRoomContext()
|
||||
const roomData = useRoomData()
|
||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
||||
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
@@ -56,8 +57,7 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
|
||||
if (!failed) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
<output
|
||||
aria-label={t('title')}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
@@ -108,6 +108,6 @@ export function DecryptionFailedTileOverlay({ participant }: Props) {
|
||||
{t('body')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</output>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { RecordingMode, useRecordingStatuses } from '@/features/recording'
|
||||
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||
|
||||
const COLLAPSE_DELAY_MS = 4000
|
||||
|
||||
@@ -39,10 +40,9 @@ function StatusPill({ icon, label, background, pulse }: PillProps) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
<output
|
||||
onMouseEnter={() => setCollapsed(false)}
|
||||
onMouseLeave={() => setCollapsed(true)}
|
||||
role="status"
|
||||
aria-label={label}
|
||||
className={css({
|
||||
display: 'inline-flex',
|
||||
@@ -78,7 +78,7 @@ function StatusPill({ icon, label, background, pulse }: PillProps) {
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</output>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function RoomStatusBanner() {
|
||||
const isRecording = screenRec.isStarted
|
||||
const isTranscribing = transcript.isStarted
|
||||
|
||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
||||
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
|
||||
if (!isEncrypted && !isRecording && !isTranscribing) {
|
||||
return null
|
||||
|
||||
@@ -7,6 +7,7 @@ import { isRoomValid } from '@/features/rooms'
|
||||
import { normalizeRoomId } from '@/features/rooms/utils/isRoomValid'
|
||||
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
|
||||
import { isValidPassphrase } from '@/features/encryption'
|
||||
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||
|
||||
export const JoinMeetingDialog = () => {
|
||||
const { t } = useTranslation('home')
|
||||
@@ -39,7 +40,7 @@ export const JoinMeetingDialog = () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const room = await fetchRoom({ roomId: parsed.roomId })
|
||||
if (room.encryption_mode === 'basic') {
|
||||
if (room.encryption_mode === ApiEncryptionMode.BASIC) {
|
||||
setRoomId(parsed.roomId)
|
||||
setStep('passphrase')
|
||||
return
|
||||
|
||||
@@ -103,17 +103,19 @@ export const Conference = ({
|
||||
const hasValidHash = isValidPassphrase(hashPassphrase)
|
||||
const dbSaysEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
|
||||
const encryptionMismatch:
|
||||
type EncryptionMismatch =
|
||||
| 'missingPassphrase'
|
||||
| 'unexpectedPassphrase'
|
||||
| null =
|
||||
data === undefined
|
||||
? null
|
||||
: dbSaysEncrypted && !hasValidHash
|
||||
? 'missingPassphrase'
|
||||
: !dbSaysEncrypted && hashPassphrase.length > 0
|
||||
? 'unexpectedPassphrase'
|
||||
: null
|
||||
| null
|
||||
|
||||
let encryptionMismatch: EncryptionMismatch = null
|
||||
if (data !== undefined) {
|
||||
if (dbSaysEncrypted && !hasValidHash) {
|
||||
encryptionMismatch = 'missingPassphrase'
|
||||
} else if (!dbSaysEncrypted && hashPassphrase.length > 0) {
|
||||
encryptionMismatch = 'unexpectedPassphrase'
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useMemo } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
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 { formatPinCode } from '@/features/rooms/utils/telephony'
|
||||
import { useCopyRoomToClipboard } from '@/features/rooms/livekit/hooks/useCopyRoomToClipboard'
|
||||
@@ -53,7 +53,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
|
||||
})
|
||||
|
||||
const roomData = useRoomData()
|
||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
||||
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
const isAdminOrOwner = !!roomData?.is_administrable
|
||||
const baseRoomUrl = getRouteUrl('room', roomData?.slug)
|
||||
// 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>
|
||||
)}
|
||||
{isEncrypted && !isAdminOrOwner ? null : isEncrypted ? (
|
||||
<div
|
||||
className={css({
|
||||
width: '100%',
|
||||
marginTop: '0.5rem',
|
||||
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}
|
||||
{(() => {
|
||||
if (isEncrypted && !isAdminOrOwner) return null
|
||||
if (isEncrypted) {
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
color: '#7c2d12',
|
||||
fontSize: '0.85rem',
|
||||
lineHeight: 1.4,
|
||||
width: '100%',
|
||||
marginTop: '0.5rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.75rem',
|
||||
})}
|
||||
>
|
||||
{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>
|
||||
) : 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')}
|
||||
<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',
|
||||
})}
|
||||
>
|
||||
{isRoomUrlCopied ? (
|
||||
<RiCheckLine aria-hidden="true" />
|
||||
<RiAlertFill
|
||||
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>
|
||||
)}
|
||||
</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>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
variant={isCopied ? 'success' : 'secondaryText'}
|
||||
size="sm"
|
||||
variant={isCopied ? 'success' : 'tertiary'}
|
||||
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"
|
||||
/>
|
||||
<RiCheckLine size={24} style={{ marginRight: '8px' }} />
|
||||
{t('copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiFileCopyLine
|
||||
style={{ marginRight: '6px', minWidth: '18px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('copy')}
|
||||
<RiFileCopyLine size={24} style={{ marginRight: '8px' }} />
|
||||
{t('copyUrl')}
|
||||
</>
|
||||
)}
|
||||
</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 && (
|
||||
<HStack>
|
||||
<div
|
||||
|
||||
@@ -32,7 +32,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { queryClient } from '@/api/queryClient'
|
||||
import { ApiLobbyStatus, ApiRequestEntry } from '../api/requestEntry'
|
||||
import { Spinner } from '@/primitives/Spinner'
|
||||
import { ApiAccessLevel } from '../api/ApiRoom'
|
||||
import { ApiAccessLevel, ApiEncryptionMode } from '../api/ApiRoom'
|
||||
import {
|
||||
isValidPassphrase,
|
||||
getPassphraseFromHash,
|
||||
@@ -118,7 +118,7 @@ export const Join = ({
|
||||
retry: false,
|
||||
})
|
||||
|
||||
const isEncryptedRoom = roomInfo?.encryption_mode === 'basic'
|
||||
const isEncryptedRoom = roomInfo?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
const { user, isLoggedIn } = useUser()
|
||||
// Authenticated joiners of an encrypted room can't pick an arbitrary
|
||||
// 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 { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
||||
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 { keys } from '@/api/queryKeys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
@@ -168,7 +168,8 @@ export const Admin = () => {
|
||||
{t('access.description')}
|
||||
</Text>
|
||||
{(() => {
|
||||
const isEncrypted = readOnlyData?.encryption_mode === 'basic'
|
||||
const isEncrypted =
|
||||
readOnlyData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
return (
|
||||
<>
|
||||
{isEncrypted && (
|
||||
|
||||
@@ -16,6 +16,7 @@ import { formatPinCode } from '../../utils/telephony'
|
||||
import { useTelephony } from '../hooks/useTelephony'
|
||||
import { useCopyRoomToClipboard } from '../hooks/useCopyRoomToClipboard'
|
||||
import { FeaturePill } from '@/features/encryption'
|
||||
import { ApiEncryptionMode } from '../../api/ApiRoom'
|
||||
|
||||
export const Info = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'info' })
|
||||
@@ -30,7 +31,7 @@ export const Info = () => {
|
||||
: baseRoomUrl
|
||||
|
||||
const telephony = useTelephony()
|
||||
const isEncrypted = data?.encryption_mode === 'basic'
|
||||
const isEncrypted = data?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
const isAdminOrOwner = !!data?.is_administrable
|
||||
|
||||
const isTelephonyReadyForUse = useMemo(() => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@/features/recording'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||
|
||||
export interface ToolsButtonProps {
|
||||
icon: ReactNode
|
||||
@@ -108,7 +109,7 @@ export const Tools = () => {
|
||||
useSidePanel()
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
|
||||
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
|
||||
useRestoreFocus(isToolsOpen, {
|
||||
|
||||
+2
-1
@@ -6,13 +6,14 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||
import { RecordingMode, useHasRecordingAccess } from '@/features/recording'
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||
|
||||
export const ScreenRecordingMenuItem = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const { isScreenRecordingOpen, openScreenRecording, toggleTools } =
|
||||
useSidePanel()
|
||||
const roomData = useRoomData()
|
||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
||||
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
|
||||
const hasScreenRecordingAccess = useHasRecordingAccess(
|
||||
RecordingMode.ScreenRecording,
|
||||
|
||||
+2
-1
@@ -6,12 +6,13 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||
import { RecordingMode, useHasRecordingAccess } from '@/features/recording'
|
||||
import { FeatureFlags } from '@/features/analytics/enums'
|
||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||
import { ApiEncryptionMode } from '@/features/rooms/api/ApiRoom'
|
||||
|
||||
export const TranscriptMenuItem = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const { isTranscriptOpen, openTranscript, toggleTools } = useSidePanel()
|
||||
const roomData = useRoomData()
|
||||
const isEncrypted = roomData?.encryption_mode === 'basic'
|
||||
const isEncrypted = roomData?.encryption_mode === ApiEncryptionMode.BASIC
|
||||
|
||||
const hasTranscriptAccess = useHasRecordingAccess(
|
||||
RecordingMode.Transcript,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useTelephony } from './useTelephony'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
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'
|
||||
|
||||
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.
|
||||
const hasTelephonyInfo = useMemo(() => {
|
||||
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])
|
||||
|
||||
|
||||
@@ -77,10 +77,17 @@ export const CreateMeetingButton = () => {
|
||||
|
||||
// Communicate iframe height to parent for proper sizing
|
||||
useEffect(() => {
|
||||
const targetOrigin = (() => {
|
||||
try {
|
||||
return document.referrer ? new URL(document.referrer).origin : '/'
|
||||
} catch {
|
||||
return '/'
|
||||
}
|
||||
})()
|
||||
const observer = new ResizeObserver(() => {
|
||||
window.parent.postMessage(
|
||||
{ type: 'RESIZE', data: { height: document.body.scrollHeight } },
|
||||
'*'
|
||||
targetOrigin
|
||||
)
|
||||
})
|
||||
observer.observe(document.body)
|
||||
|
||||
@@ -30,16 +30,21 @@ export const EncryptionDefaultField = () => {
|
||||
|
||||
const isOn = user?.default_encryption_mode === ApiEncryptionMode.BASIC
|
||||
|
||||
const handleToggle = (next: boolean) => {
|
||||
const handleToggle = async (next: boolean) => {
|
||||
if (!user) return
|
||||
void mutateAsync({
|
||||
user: {
|
||||
id: user.id,
|
||||
default_encryption_mode: next
|
||||
? ApiEncryptionMode.BASIC
|
||||
: ApiEncryptionMode.NONE,
|
||||
},
|
||||
})
|
||||
try {
|
||||
await mutateAsync({
|
||||
user: {
|
||||
id: user.id,
|
||||
default_encryption_mode: next
|
||||
? 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) {
|
||||
|
||||
@@ -20,8 +20,7 @@ export const navigateTo = <S = unknown>(
|
||||
// Including the hash in the URL passed to `navigate` lets us avoid a
|
||||
// brittle pushState + replaceState dance at the call site: the URL the
|
||||
// app first renders already carries the fragment.
|
||||
const target = options?.hash ? `${to}#${options.hash}` : to
|
||||
const { hash: _hash, ...navigateOptions } = options ?? {}
|
||||
void _hash
|
||||
const { hash, ...navigateOptions } = options ?? {}
|
||||
const target = hash ? `${to}#${hash}` : to
|
||||
return navigate(target, navigateOptions)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user