Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine cffd658825 🩹(sdk) notify iframe parent when room is gathered through callback
On the first room creation, when the authentication redirection takes
place, the iframe wasn't properly messaging the parent.
Fixed it. Send the room data in any of the two methods to acquire it.

Actually, the code when gathering data through the callback endpoint is
a bit dirty.
2025-04-04 13:18:17 +02:00
56 changed files with 166 additions and 1654 deletions
+1 -1
View File
@@ -156,7 +156,7 @@ class RecordingSerializer(serializers.ModelSerializer):
class Meta:
model = models.Recording
fields = ["id", "room", "created_at", "updated_at", "status", "mode"]
fields = ["id", "room", "created_at", "updated_at", "status"]
read_only_fields = fields
+2 -85
View File
@@ -2,8 +2,6 @@
import uuid
from logging import getLogger
from urllib.parse import urlparse
from django.conf import settings
from django.db.models import Q
@@ -22,7 +20,7 @@ from rest_framework import (
status as drf_status,
)
from core import models, utils, enums
from core import models, utils
from core.recording.event.authentication import StorageEventAuthentication
from core.recording.event.exceptions import (
InvalidBucketError,
@@ -546,7 +544,6 @@ class ResourceAccessViewSet(
class RecordingViewSet(
mixins.DestroyModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
):
"""
@@ -594,7 +591,7 @@ class RecordingViewSet(
)
try:
recording = models.Recording.objects.select_related('room').get(id=recording_id)
recording = models.Recording.objects.get(id=recording_id)
except models.Recording.DoesNotExist as e:
raise drf_exceptions.NotFound("No recording found for this event.") from e
@@ -620,83 +617,3 @@ class RecordingViewSet(
return drf_response.Response(
{"message": "Event processed."},
)
def _auth_get_original_url(self, request):
"""
Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
Raises PermissionDenied if the header is missing.
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
See corresponding ingress configuration in Helm chart and read about the
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
is configured to do this.
Based on the original url and the logged-in user, we must decide if we authorize Nginx
to let this request go through (by returning a 200 code) or if we block it (by returning
a 403 error). Note that we return 403 errors without any further details for security
reasons.
"""
# Extract the original URL from the request header
original_url = request.META.get("HTTP_X_ORIGINAL_URL")
if not original_url:
logger.debug("Missing HTTP_X_ORIGINAL_URL header in subrequest")
raise drf_exceptions.PermissionDenied()
logger.debug("Original url: '%s'", original_url)
return urlparse(original_url)
def _auth_get_url_params(self, pattern, fragment):
"""
Extracts URL parameters from the given fragment using the specified regex pattern.
Raises PermissionDenied if parameters cannot be extracted.
"""
match = pattern.search(fragment)
try:
return match.groupdict()
except (ValueError, AttributeError) as exc:
logger.debug("Failed to extract parameters from subrequest URL: %s", exc)
raise drf_exceptions.PermissionDenied() from exc
@decorators.action(detail=False, methods=["get"], url_path="media-auth")
def media_auth(self, request, *args, **kwargs):
"""
This view is used by an Nginx subrequest to control access to a recording's
media file.
When we let the request go through, we compute authorization headers that will be added to
the request going through thanks to the nginx.ingress.kubernetes.io/auth-response-headers
annotation. The request will then be proxied to the object storage backend who will
respond with the file after checking the signature included in headers.
"""
parsed_url = self._auth_get_original_url(request)
url_params = self._auth_get_url_params(
enums.RECORDING_STORAGE_URL_PATTERN, parsed_url.path
)
user = request.user
recording_id = url_params['recording']
try:
recording = models.Recording.objects.get(id=recording_id)
except models.Recording.DoesNotExist as e:
raise drf_exceptions.NotFound("No recording found for this event.") from e
abilities = recording.get_abilities(user)
if not abilities["retrieve"]:
logger.debug("User '%s' lacks permission for attachment", user)
raise drf_exceptions.PermissionDenied()
if not recording.is_saved:
logger.debug("Recording '%s' has not been saved", recording)
raise drf_exceptions.PermissionDenied()
# Generate S3 authorization headers using the extracted URL parameters
request = utils.generate_s3_authorization_headers(recording.key)
return drf_response.Response("authorized", headers=request.headers, status=200)
-12
View File
@@ -1,22 +1,10 @@
"""
Core application enums declaration
"""
import re
from django.conf import global_settings, settings
from django.utils.translation import gettext_lazy as _
UUID_REGEX = (
r"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
)
FILE_EXT_REGEX = r"\.[a-zA-Z0-9]{1,10}"
RECORDING_FOLDER = 'recordings'
RECORDING_STORAGE_URL_PATTERN = re.compile(
f"/media/recordings/(?P<recording>{UUID_REGEX:s})(?P<extension>{FILE_EXT_REGEX:s})"
)
# Django sets `LANGUAGES` by default with all supported languages. We can use it for
# the choice of languages which should not be limited to the few languages active in
# the app.
-12
View File
@@ -576,18 +576,6 @@ class Recording(BaseModel):
RecordingStatusChoices.STOPPED,
}
@property
def is_saved(self) -> bool:
"""Wip"""
return self.status in {
RecordingStatusChoices.SAVED,
}
@property
def key(self):
"""Wip."""
extension = "ogg" if self.mode == RecordingModeChoices.TRANSCRIPT else "mp4"
return f"{settings.RECORDING_OUTPUT_FOLDER}/{self.id}.{extension}"
class RecordingAccess(BaseAccess):
"""Relation model to give access to a recording for a user or a team with a role."""
@@ -1,14 +1,8 @@
"""Service to notify external services when a new recording is ready."""
import logging
import smtplib
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.translation import get_language, override
from django.utils.translation import gettext_lazy as _
from django.contrib.sites.models import Site
from django.core.mail import send_mail
import requests
@@ -27,7 +21,10 @@ class NotificationService:
return self._notify_summary_service(recording)
if recording.mode == models.RecordingModeChoices.SCREEN_RECORDING:
return self._notify_user_by_email(recording)
logger.warning(
"Screen recording mode not implemented for recording %s", recording.id
)
return False
logger.error(
"Unknown recording mode %s for recording %s",
@@ -36,65 +33,6 @@ class NotificationService:
)
return False
@staticmethod
def _notify_user_by_email(recording) -> bool:
"""Wip."""
# subject
# emails
domain = Site.objects.get_current().domain
owner_accesses = (
models.RecordingAccess.objects.select_related("user")
.filter(
role=models.RoleChoices.OWNER,
recording_id=recording.id,
)
.all()
)
if not owner_accesses:
logger.error("No owner found for recording %s", recording.id)
return False
language = get_language()
context = (
{
"brandname": settings.EMAIL_BRAND_NAME,
"support_email": settings.EMAIL_SUPPORT_EMAIL,
"logo_img": settings.EMAIL_LOGO_IMG,
"domain": domain,
"room_name": recording.room.name,
"recording_date": recording.created_at.strftime("%A %d %B %Y"),
"recording_time": recording.created_at.strftime("%H:%M"),
"link": f"{domain}/recordings/{recording.id}",
}
)
emails = [access.user.email for access in owner_accesses]
with override(language):
msg_html = render_to_string("mail/html/screen_recording.html", context)
msg_plain = render_to_string("mail/text/screen_recording.txt", context)
subject = str(_("Your recording is ready")) # Force translation
try:
send_mail(
subject.capitalize(),
msg_plain,
settings.EMAIL_FROM,
emails,
html_message=msg_html,
fail_silently=False,
)
except smtplib.SMTPException as exception:
logger.error("notification to %s was not sent: %s", emails, exception)
return False
return True
@staticmethod
def _notify_summary_service(recording):
"""Notify summary service about a new recording."""
@@ -119,8 +57,10 @@ class NotificationService:
logger.error("No owner found for recording %s", recording.id)
return False
key = f"{settings.RECORDING_OUTPUT_FOLDER}/{recording.id}.ogg"
payload = {
"filename": recording.key,
"filename": key,
"email": owner_access.user.email,
"sub": owner_access.user.sub,
}
-35
View File
@@ -14,10 +14,6 @@ from django.conf import settings
from livekit.api import AccessToken, VideoGrants
from django.core.files.storage import default_storage
import botocore
def generate_color(identity: str) -> str:
"""Generates a consistent HSL color based on a given identity string.
@@ -114,34 +110,3 @@ def generate_livekit_config(
room=room_id, user=user, username=username, color=color
),
}
def generate_s3_authorization_headers(key):
"""
Generate authorization headers for an s3 object.
These headers can be used as an alternative to signed urls with many benefits:
- the urls of our files never expire and can be stored in our recording' metadata
- we don't leak authorized urls that could be shared (file access can only be done
with cookies)
- access control is truly realtime
- the object storage service does not need to be exposed on internet
"""
url = default_storage.unsigned_connection.meta.client.generate_presigned_url(
"get_object",
ExpiresIn=0,
Params={"Bucket": default_storage.bucket_name, "Key": key},
)
request = botocore.awsrequest.AWSRequest(method="get", url=url)
s3_client = default_storage.connection.meta.client
# pylint: disable=protected-access
credentials = s3_client._request_signer._credentials # noqa: SLF001
frozen_credentials = credentials.get_frozen_credentials()
region = s3_client.meta.region_name
auth = botocore.auth.S3SigV4Auth(frozen_credentials, "s3", region)
auth.add_auth(request)
return request
-3
View File
@@ -318,9 +318,6 @@ class Base(Configuration):
EMAIL_USE_TLS = values.BooleanValue(False)
EMAIL_USE_SSL = values.BooleanValue(False)
EMAIL_FROM = values.Value("from@example.com")
EMAIL_BRAND_NAME = values.Value(None)
EMAIL_SUPPORT_EMAIL = values.Value(None)
EMAIL_LOGO_IMG = values.Value(None)
AUTH_USER_MODEL = "core.User"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 KiB

@@ -2,7 +2,7 @@ import { css } from '@/styled-system/css'
import { RiErrorWarningLine, RiExternalLinkLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { Text, A } from '@/primitives'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
import { GRIST_FORM } from '@/utils/constants'
export const FeedbackBanner = () => {
const { t } = useTranslation()
@@ -35,7 +35,7 @@ export const FeedbackBanner = () => {
gap: 0.25,
})}
>
<A href={GRIST_FEEDBACKS_FORM} target="_blank" size="sm">
<A href={GRIST_FORM} target="_blank" size="sm">
{t('feedback.cta')}
</A>
<RiExternalLinkLine size={16} aria-hidden="true" />
@@ -8,7 +8,10 @@ import { Button, LinkButton } from '@/primitives'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BETA_USERS_FORM_URL } from '@/utils/constants'
// todo - extract in a proper env variable
const BETA_USERS_FORM_URL =
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17'
const Heading = styled('h2', {
base: {
@@ -204,7 +207,6 @@ export const IntroSlider = () => {
{slide.isAvailableInBeta && (
<LinkButton
href={BETA_USERS_FORM_URL}
target="_blank"
tooltip={t('beta.tooltip')}
variant={'primary'}
size={'sm'}
@@ -88,18 +88,6 @@ export const MainNotificationToast = () => {
if (notification.data?.emoji)
handleEmoji(notification.data.emoji, participant)
break
case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped:
case NotificationType.ScreenRecordingStarted:
case NotificationType.ScreenRecordingStopped:
toastQueue.add(
{
participant,
type: notification.type,
},
{ timeout: NotificationDuration.ALERT }
)
break
default:
return
}
@@ -6,8 +6,4 @@ export enum NotificationType {
LowerHand = 'lowerHand',
ReactionReceived = 'reactionReceived',
ParticipantWaiting = 'participantWaiting',
TranscriptionStarted = 'transcriptionStarted',
TranscriptionStopped = 'transcriptionStopped',
ScreenRecordingStarted = 'screenRecordingStarted',
ScreenRecordingStopped = 'screenRecordingStopped',
}
@@ -48,7 +48,7 @@ export function Toast({ state, ...props }: ToastProps) {
return (
<StyledToastContainer {...toastProps} ref={ref}>
<StyledToast>
<div {...contentProps}>{props.toast.content?.message}</div>
<div {...contentProps}>{props.toast.content?.message} machine a</div>
<Button square size="sm" invisible {...closeButtonProps}>
<RiCloseLine color="white" />
</Button>
@@ -1,48 +0,0 @@
import { useToast } from '@react-aria/toast'
import { useMemo, useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { NotificationType } from '../NotificationType'
export function ToastAnyRecording({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref)
const participant = props.toast.content.participant
const type = props.toast.content.type
const key = useMemo(() => {
switch (type) {
case NotificationType.TranscriptionStarted:
return 'transcript.started'
case NotificationType.TranscriptionStopped:
return 'transcript.stopped'
case NotificationType.ScreenRecordingStarted:
return 'screenRecording.started'
case NotificationType.ScreenRecordingStopped:
return 'screenRecording.stopped'
default:
return
}
}, [type])
if (!key) return
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={0}
>
{t(key, {
name: participant.name,
})}
</HStack>
</StyledToastContainer>
)
}
@@ -9,7 +9,6 @@ import { ToastRaised } from './ToastRaised'
import { ToastMuted } from './ToastMuted'
import { ToastMessageReceived } from './ToastMessageReceived'
import { ToastLowerHand } from './ToastLowerHand'
import { ToastAnyRecording } from './ToastAnyRecording'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -37,12 +36,6 @@ const renderToast = (
case NotificationType.LowerHand:
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped:
case NotificationType.ScreenRecordingStarted:
case NotificationType.ScreenRecordingStopped:
return <ToastAnyRecording key={toast.key} toast={toast} state={state} />
default:
return <Toast key={toast.key} toast={toast} state={state} />
}
@@ -1,19 +0,0 @@
import { fetchApi } from '@/api/fetchApi'
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
import { RecordingMode } from '@/features/rooms/api/startRecording'
export type RecordingApi = {
id: string
room: Pick<ApiRoom, 'id' | 'name' | 'slug' | 'access_level'>
created_at: string
mode: RecordingMode
}
export const fetchRecording = ({
recordingId,
}: {
roomId: string
username?: string
}) => {
return fetchApi<RecordingApi>(`/recordings/${recordingId}/`)
}
@@ -1 +0,0 @@
export { Recording as RecordingRoute } from './routes/Recording'
@@ -1,107 +0,0 @@
import { Center, VStack } from '@/styled-system/jsx'
import { Bold, Button, Field, H, Input, LinkButton, Text } from '@/primitives'
import { Screen } from '@/layout/Screen.tsx'
import { useParams } from 'wouter'
import { fetchRecording } from '../api/fetchRecording'
import { useQuery } from '@tanstack/react-query'
import { Spinner } from '@/primitives/Spinner'
import { UserAware, useUser } from '@/features/auth'
import { CenteredContent } from '@/layout/CenteredContent.tsx'
import { useState } from 'react'
import fourthSlide from '@/assets/intro-slider/4_record.png'
import { css } from '@/styled-system/css'
import { RecordingMode } from '@/features/rooms/api/startRecording.ts'
function formatDate(date, format = 'YYYY-MM-DD') {
// Accept Date object or create one from string
const dateObj = date instanceof Date ? date : new Date(date)
// Check if the date is valid
if (isNaN(dateObj.getTime())) {
return 'Invalid Date'
}
const year = dateObj.getFullYear()
const month = dateObj.getMonth() + 1 // getMonth() returns 0-11
const day = dateObj.getDate()
const hours = dateObj.getHours()
const minutes = dateObj.getMinutes()
const seconds = dateObj.getSeconds()
// Pad numbers with leading zeros
const pad = (num) => String(num).padStart(2, '0')
// Replace format tokens with actual values
return format
.replace('YYYY', year)
.replace('MM', pad(month))
.replace('DD', pad(day))
.replace('HH', pad(hours))
.replace('mm', pad(minutes))
.replace('ss', pad(seconds))
}
export const Recording = () => {
const { recordingId } = useParams()
const { isLoggedIn } = useUser()
const { data, isLoading, isError } = useQuery({
queryKey: ['recording', recordingId],
queryFn: () => fetchRecording({ recordingId }),
retry: false,
})
if (isLoading) {
return (
<Screen layout="centered" footer={false}>
<CenteredContent>
<Spinner />
</CenteredContent>
</Screen>
)
}
if (isError || !isLoggedIn) {
return (
<Screen layout="centered" footer={false}>
<CenteredContent title={'lien invalide'} withBackButton>
<Text centered>
Vous ne pouvez pas accéder à ce lien d'enregistrement
</Text>
</CenteredContent>
</Screen>
)
}
return (
<UserAware>
<Screen layout="centered" footer={false}>
<Center>
<VStack>
<img
src={fourthSlide}
alt={''}
className={css({
maxHeight: '309px',
})}
/>
<H lvl={1} centered>
Votre enregistrement est prêt !
</H>
<Text centered margin="md">
Enregistrement de la réunion <b>{data.room.name}</b> du{' '}
{formatDate(data.created_at, 'YYYY-MM-DD HH:mm')}. <br /> Cet
enregistrement est disponible <u>pendant 7 jours</u>.
</Text>
<LinkButton
href={`https://meet.127.0.0.1.nip.io/media/recordings/${data.id}.${data.mode == RecordingMode.ScreenRecording ? 'mp4' : 'ogg'}`}
download={`${data.room.name}-${formatDate(data.created_at)}`}
>
Télécharger
</LinkButton>
</VStack>
</Center>
</Screen>
</UserAware>
)
}
@@ -1,102 +1,15 @@
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio/index'
import { useRoomContext } from '@livekit/components-react'
import { Spinner } from '@/primitives/Spinner'
import { useEffect, useMemo } from 'react'
import { RiRecordCircleLine } from '@remixicon/react'
import { Text } from '@/primitives'
import { RemoteParticipant, RoomEvent } from 'livekit-client'
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
import { NotificationType } from '@/features/notifications/NotificationType'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import { useTranslation } from 'react-i18next'
import { useRoomContext } from '@livekit/components-react'
export const RecordingStateToast = () => {
const { t } = useTranslation('rooms', {
keyPrefix: 'recordingToast',
})
const { t } = useTranslation('rooms', { keyPrefix: 'recording' })
const room = useRoomContext()
const recordingSnap = useSnapshot(recordingStore)
useEffect(() => {
if (room.isRecording && recordingSnap.status == RecordingStatus.STOPPED) {
recordingStore.status = RecordingStatus.ANY_STARTED
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [room.isRecording])
useEffect(() => {
const handleDataReceived = (
payload: Uint8Array,
participant?: RemoteParticipant
) => {
const notification = decodeNotificationDataReceived(payload)
if (!participant || !notification) return
switch (notification.type) {
case NotificationType.TranscriptionStarted:
recordingStore.status = RecordingStatus.TRANSCRIPT_STARTING
break
case NotificationType.TranscriptionStopped:
recordingStore.status = RecordingStatus.TRANSCRIPT_STOPPING
break
case NotificationType.ScreenRecordingStarted:
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTING
break
case NotificationType.ScreenRecordingStopped:
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
break
default:
return
}
}
const handleRecordingStatusChanged = (status: boolean) => {
if (!status) {
recordingStore.status = RecordingStatus.STOPPED
} else if (recordingSnap.status == RecordingStatus.TRANSCRIPT_STARTING) {
recordingStore.status = RecordingStatus.TRANSCRIPT_STARTED
} else if (
recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STARTING
) {
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTED
} else {
recordingStore.status = RecordingStatus.ANY_STARTED
}
}
room.on(RoomEvent.DataReceived, handleDataReceived)
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
return () => {
room.off(RoomEvent.DataReceived, handleDataReceived)
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
}
}, [room, recordingSnap])
const key = useMemo(() => {
switch (recordingSnap.status) {
case RecordingStatus.TRANSCRIPT_STARTED:
return 'transcript.started'
case RecordingStatus.TRANSCRIPT_STOPPING:
return 'transcript.stopping'
case RecordingStatus.TRANSCRIPT_STARTING:
return 'transcript.starting'
case RecordingStatus.SCREEN_RECORDING_STARTED:
return 'screenRecording.started'
case RecordingStatus.SCREEN_RECORDING_STOPPING:
return 'screenRecording.stopping'
case RecordingStatus.SCREEN_RECORDING_STARTING:
return 'screenRecording.starting'
case RecordingStatus.ANY_STARTED:
return 'any.started'
default:
return
}
}, [recordingSnap])
if (!key) return
if (!room?.isRecording) return
return (
<div
@@ -106,24 +19,25 @@ export const RecordingStateToast = () => {
top: '10px',
left: '10px',
paddingY: '0.25rem',
paddingX: '0.75rem 0.75rem',
backgroundColor: 'primaryDark.100',
borderColor: 'white',
paddingX: '0.25rem 0.35rem',
backgroundColor: 'primaryDark.200',
borderColor: 'primaryDark.400',
border: '1px solid',
color: 'white',
borderRadius: '4px',
gap: '0.5rem',
})}
>
<Spinner size={20} variant="dark" />
<Text
variant={'sm'}
<RiRecordCircleLine
size={20}
className={css({
fontWeight: '500 !important',
color: 'white',
backgroundColor: 'danger.700',
padding: '3px',
borderRadius: '3px',
})}
>
{t(key)}
</Text>
/>
<Text variant={'sm'}>{t('label')}</Text>
</div>
)
}
@@ -1,166 +0,0 @@
import { A, Button, Div, Text } from '@/primitives'
import fourthSlide from '@/assets/intro-slider/4_record.png'
import { css } from '@/styled-system/css'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
import { useRoomContext } from '@livekit/components-react'
import {
RecordingMode,
useStartRecording,
} from '@/features/rooms/api/startRecording'
import { useStopRecording } from '@/features/rooms/api/stopRecording'
import { useEffect, useMemo, useState } from 'react'
import { RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { NotificationPayload } from '@/features/notifications/NotificationPayload'
import { NotificationType } from '@/features/notifications/NotificationType'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import { CRISP_HELP_ARTICLE_RECORDING } from '@/utils/constants'
import { useIsRecordingTransitioning } from '../hooks/useIsRecordingTransitioning'
import {
useIsScreenRecordingStarted,
useIsTranscriptStarted,
} from '../hooks/useIsRecordingStarted'
export const ScreenRecording = () => {
const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'screenRecording' })
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom } = useStartRecording()
const { mutateAsync: stopRecordingRoom } = useStopRecording()
const isScreenRecordingStarted = useIsScreenRecordingStarted()
const isTranscriptStarted = useIsTranscriptStarted()
const room = useRoomContext()
const isRecordingTransitioning = useIsRecordingTransitioning()
useEffect(() => {
const handleRecordingStatusChanged = () => {
setIsLoading(false)
}
room.on(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
return () => {
room.off(RoomEvent.RecordingStatusChanged, handleRecordingStatusChanged)
}
}, [room])
const notifyParticipant = async (status: NotificationType) => {
const encoder = new TextEncoder()
const payload: NotificationPayload = {
type: status,
}
const data = encoder.encode(JSON.stringify(payload))
await room.localParticipant.publishData(data, {
reliable: true,
})
}
const handleScreenRecording = async () => {
if (!roomId) {
console.warn('No room ID found')
return
}
try {
setIsLoading(true)
if (room.isRecording) {
await stopRecordingRoom({ id: roomId })
await notifyParticipant(NotificationType.ScreenRecordingStopped)
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STOPPING
} else {
await startRecordingRoom({
id: roomId,
mode: RecordingMode.ScreenRecording,
})
await notifyParticipant(NotificationType.ScreenRecordingStarted)
recordingStore.status = RecordingStatus.SCREEN_RECORDING_STARTING
}
} catch (error) {
console.error('Failed to handle transcript:', error)
setIsLoading(false)
}
}
const isDisabled = useMemo(
() => isLoading || isRecordingTransitioning || isTranscriptStarted,
[isLoading, isRecordingTransitioning, isTranscriptStarted]
)
return (
<Div
display="flex"
overflowY="scroll"
padding="0 1.5rem"
flexGrow={1}
flexDirection="column"
alignItems="center"
>
<img
src={fourthSlide}
alt={''}
className={css({
minHeight: '309px',
marginBottom: '1rem',
})}
/>
{isScreenRecordingStarted ? (
<>
<Text>{t('stop.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('stop.body')}
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleScreenRecording()}
data-attr="stop-transcript"
size="sm"
variant="tertiary"
>
{t('stop.button')}
</Button>
</>
) : (
<>
<Text>{t('start.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
maxWidth: '90%',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('start.body')} <br />{' '}
<A href={CRISP_HELP_ARTICLE_RECORDING} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleScreenRecording()}
data-attr="start-transcript"
size="sm"
variant="tertiary"
>
{t('start.button')}
</Button>
</>
)}
</Div>
)
}
@@ -3,15 +3,15 @@ import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text'
import { Button, Div } from '@/primitives'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react'
import { Chat } from '../prefabs/Chat'
import { Transcript } from './Transcript'
import { Effects } from './effects/Effects'
import { Admin } from './Admin'
import { Tools } from './Tools'
type StyledSidePanelProps = {
title: string
@@ -19,8 +19,6 @@ type StyledSidePanelProps = {
onClose: () => void
isClosed: boolean
closeButtonTooltip: string
isSubmenu: boolean
onBack: () => void
}
const StyledSidePanel = ({
@@ -29,8 +27,6 @@ const StyledSidePanel = ({
onClose,
isClosed,
closeButtonTooltip,
isSubmenu = false,
onBack,
}: StyledSidePanelProps) => (
<div
className={css({
@@ -65,22 +61,9 @@ const StyledSidePanel = ({
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
display: isClosed ? 'none' : undefined,
}}
>
{isSubmenu && (
<Button
variant="secondaryText"
size={'sm'}
square
className={css({ marginRight: '0.5rem' })}
onPress={onBack}
>
<RiArrowLeftLine size={20} />
</Button>
)}
{title}
</Heading>
<Div
@@ -131,26 +114,19 @@ export const SidePanel = () => {
isEffectsOpen,
isChatOpen,
isSidePanelOpen,
isToolsOpen,
isTranscriptOpen,
isAdminOpen,
isSubPanelOpen,
activeSubPanelId,
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
return (
<StyledSidePanel
title={t(`heading.${activeSubPanelId || activePanelId}`)}
onClose={() => {
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
}}
title={t(`heading.${activePanelId}`)}
onClose={() => (layoutStore.activePanelId = null)}
closeButtonTooltip={t('closeButton', {
content: t(`content.${activeSubPanelId || activePanelId}`),
content: t(`content.${activePanelId}`),
})}
isClosed={!isSidePanelOpen}
isSubmenu={isSubPanelOpen}
onBack={() => (layoutStore.activeSubPanelId = null)}
>
<Panel isOpen={isParticipantsOpen}>
<ParticipantsList />
@@ -161,8 +137,8 @@ export const SidePanel = () => {
<Panel isOpen={isChatOpen}>
<Chat />
</Panel>
<Panel isOpen={isToolsOpen}>
<Tools />
<Panel isOpen={isTranscriptOpen}>
<Transcript />
</Panel>
<Panel isOpen={isAdminOpen}>
<Admin />
@@ -1,133 +0,0 @@
import { A, Div, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { Button as RACButton } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { CRISP_HELP_ARTICLE_MORE_TOOLS } from '@/utils/constants'
import { ReactNode } from 'react'
import { Transcript } from './Transcript'
import { RiFileTextFill, RiLiveFill } from '@remixicon/react'
import { SubPanelId, useSidePanel } from '../hooks/useSidePanel'
import { ScreenRecording } from '@/features/rooms/livekit/components/ScreenRecording'
import { useIsRecordingEnabled } from '../hooks/useIsRecordingEnabled'
import { RecordingMode } from '@/features/rooms/api/startRecording'
export interface ToolsButtonProps {
icon: ReactNode
title: string
description: string
onPress: () => void
}
const ToolButton = ({
icon,
title,
description,
onPress,
}: ToolsButtonProps) => {
return (
<RACButton
className={css({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'start',
paddingY: '0.5rem',
paddingX: '0.75rem 1.5rem',
borderRadius: '5px',
gap: '1.25rem',
width: 'full',
textAlign: 'start',
'&[data-hovered]': {
backgroundColor: 'primary.50',
cursor: 'pointer',
},
})}
onPress={onPress}
>
<div
className={css({
height: '50px',
minWidth: '50px',
borderRadius: '25px',
backgroundColor: 'primary.800',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})}
>
{icon}
</div>
<div>
<Text margin={false} as="h3">
{title}
</Text>
<Text as="p" variant="smNote" wrap="pretty">
{description}
</Text>
</div>
</RACButton>
)
}
export const Tools = () => {
const { openTranscript, openScreenRecording, activeSubPanelId } =
useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
const isTranscriptEnabled = useIsRecordingEnabled(RecordingMode.Transcript)
// FIXME - use settings returned by backend
const isRecordingEnabled = true
switch (activeSubPanelId) {
case SubPanelId.TRANSCRIPT:
return <Transcript />
case SubPanelId.SCREEN_RECORDING:
return <ScreenRecording />
default:
break
}
return (
<Div
display="flex"
overflowY="scroll"
padding="0 0.75rem"
flexGrow={1}
flexDirection="column"
alignItems="start"
>
<Text
variant="note"
wrap="balance"
className={css({
textStyle: 'sm',
paddingX: '0.75rem',
})}
margin="md"
>
{t('body')}{' '}
<A href={CRISP_HELP_ARTICLE_MORE_TOOLS} target="_blank">
{t('moreLink')}
</A>
.
</Text>
{isTranscriptEnabled && (
<ToolButton
icon={<RiFileTextFill size={24} color="white" />}
title={t('tools.transcript.title')}
description={t('tools.transcript.body')}
onPress={() => openTranscript()}
/>
)}
{isRecordingEnabled && (
<ToolButton
icon={<RiLiveFill size={24} color="white" />}
title={t('tools.screenRecording.title')}
description={t('tools.screenRecording.body')}
onPress={() => openScreenRecording()}
/>
)}
</Div>
)
}
@@ -1,7 +1,10 @@
import { A, Button, Div, LinkButton, Text } from '@/primitives'
import { Button, Div, H, Text } from '@/primitives'
import thirdSlide from '@/assets/intro-slider/3_resume.png'
import { css } from '@/styled-system/css'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess'
import { RiRecordCircleLine, RiStopCircleLine } from '@remixicon/react'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
import { useRoomContext } from '@livekit/components-react'
import {
@@ -9,37 +12,22 @@ import {
useStartRecording,
} from '@/features/rooms/api/startRecording'
import { useStopRecording } from '@/features/rooms/api/stopRecording'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useState } from 'react'
import { RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { NotificationPayload } from '@/features/notifications/NotificationPayload'
import { NotificationType } from '@/features/notifications/NotificationType'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import { useHasTranscriptAccess } from '../hooks/useHasTranscriptAccess'
import {
BETA_USERS_FORM_URL,
CRISP_HELP_ARTICLE_TRANSCRIPT,
} from '@/utils/constants'
import { useIsRecordingTransitioning } from '../hooks/useIsRecordingTransitioning'
import {
useIsScreenRecordingStarted,
useIsTranscriptStarted,
} from '../hooks/useIsRecordingStarted'
export const Transcript = () => {
const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
const hasTranscriptAccess = useHasTranscriptAccess()
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom } = useStartRecording()
const { mutateAsync: stopRecordingRoom } = useStopRecording()
const isScreenRecordingStarted = useIsScreenRecordingStarted()
const isTranscriptStarted = useIsTranscriptStarted()
const isRecordingTransitioning = useIsRecordingTransitioning()
const room = useRoomContext()
useEffect(() => {
@@ -52,17 +40,6 @@ export const Transcript = () => {
}
}, [room])
const notifyParticipant = async (status: NotificationType) => {
const encoder = new TextEncoder()
const payload: NotificationPayload = {
type: status,
}
const data = encoder.encode(JSON.stringify(payload))
await room.localParticipant.publishData(data, {
reliable: true,
})
}
const handleTranscript = async () => {
if (!roomId) {
console.warn('No room ID found')
@@ -72,12 +49,8 @@ export const Transcript = () => {
setIsLoading(true)
if (room.isRecording) {
await stopRecordingRoom({ id: roomId })
await notifyParticipant(NotificationType.TranscriptionStopped)
recordingStore.status = RecordingStatus.TRANSCRIPT_STOPPING
} else {
await startRecordingRoom({ id: roomId, mode: RecordingMode.Transcript })
await notifyParticipant(NotificationType.TranscriptionStarted)
recordingStore.status = RecordingStatus.TRANSCRIPT_STARTING
}
} catch (error) {
console.error('Failed to handle transcript:', error)
@@ -85,10 +58,7 @@ export const Transcript = () => {
}
}
const isDisabled = useMemo(
() => isLoading || isRecordingTransitioning || isScreenRecordingStarted,
[isLoading, isRecordingTransitioning, isScreenRecordingStarted]
)
if (!hasTranscriptAccess) return
return (
<Div
@@ -99,98 +69,38 @@ export const Transcript = () => {
flexDirection="column"
alignItems="center"
>
<img
src={thirdSlide}
alt={''}
className={css({
minHeight: '309px',
marginBottom: '1rem',
})}
/>
{!hasTranscriptAccess ? (
<img src={thirdSlide} alt={'wip'} />
{room.isRecording ? (
<>
<Text>{t('beta.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('beta.body')}{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
<H lvl={2}>{t('stop.heading')}</H>
<Text variant="sm" centered wrap="balance">
{t('stop.body')}
</Text>
<LinkButton
size="sm"
variant="tertiary"
href={BETA_USERS_FORM_URL}
target="_blank"
<div className={css({ height: '2rem' })} />
<Button
isDisabled={isLoading}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
>
{t('beta.button')}
</LinkButton>
<RiStopCircleLine style={{ marginRight: '0.5rem' }} />{' '}
{t('stop.button')}
</Button>
</>
) : (
<>
{isTranscriptStarted ? (
<>
<Text>{t('stop.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('stop.body')}
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="stop-transcript"
size="sm"
variant="tertiary"
>
{t('stop.button')}
</Button>
</>
) : (
<>
<Text>{t('start.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
maxWidth: '90%',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('start.body')} <br />{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isDisabled}
onPress={() => handleTranscript()}
data-attr="start-transcript"
size="sm"
variant="tertiary"
>
{t('start.button')}
</Button>
</>
)}
<H lvl={2}>{t('start.heading')}</H>
<Text variant="sm" centered wrap="balance">
{t('start.body')}
</Text>
<div className={css({ height: '2rem' })} />
<Button
isDisabled={isLoading}
onPress={() => handleTranscript()}
data-attr="start-transcript"
>
<RiRecordCircleLine style={{ marginRight: '0.5rem' }} />{' '}
{t('start.button')}
</Button>
</>
)}
</Div>
@@ -2,14 +2,14 @@ import { RiMegaphoneLine } from '@remixicon/react'
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
import { GRIST_FORM } from '@/utils/constants'
export const FeedbackMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
return (
<MenuItem
href={GRIST_FEEDBACKS_FORM}
href={GRIST_FORM}
target="_blank"
className={menuRecipe({ icon: true, variant: 'dark' }).item}
>
@@ -1,19 +1,24 @@
import { ToggleButton } from '@/primitives'
import { RiShapesLine } from '@remixicon/react'
import { RiBardLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useSidePanel } from '../../hooks/useSidePanel'
import { useHasTranscriptAccess } from '../../hooks/useHasTranscriptAccess'
import { css } from '@/styled-system/css'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
export const ToolsToggle = ({
export const TranscriptToggle = ({
variant = 'primaryTextDark',
onPress,
...props
}: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.tools' })
const { t } = useTranslation('rooms', { keyPrefix: 'controls.transcript' })
const { isToolsOpen, toggleTools } = useSidePanel()
const tooltipLabel = isToolsOpen ? 'open' : 'closed'
const { isTranscriptOpen, toggleTranscript } = useSidePanel()
const tooltipLabel = isTranscriptOpen ? 'open' : 'closed'
const hasTranscriptAccess = useHasTranscriptAccess()
if (!hasTranscriptAccess) return
return (
<div
@@ -27,15 +32,15 @@ export const ToolsToggle = ({
variant={variant}
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isToolsOpen}
isSelected={isTranscriptOpen}
onPress={(e) => {
toggleTools()
toggleTranscript()
onPress?.(e)
}}
{...props}
data-attr="toggle-tools"
data-attr="toggle-transcript"
>
<RiShapesLine />
<RiBardLine />
</ToggleButton>
</div>
)
@@ -1,20 +0,0 @@
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import { RecordingMode } from '@/features/rooms/api/startRecording'
import { useIsRecordingEnabled } from './useIsRecordingEnabled'
import { useIsAdminOrOwner } from './useIsAdminOrOwner'
export const useHasScreenRecordingAccess = () => {
const featureEnabled = useFeatureFlagEnabled('screen-recording')
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isScreenRecordingEnabled = useIsRecordingEnabled(
RecordingMode.ScreenRecording
)
const isAdminOrOwner = useIsAdminOrOwner()
return (
(featureEnabled || !isAnalyticsEnabled) &&
isAdminOrOwner &&
isScreenRecordingEnabled
)
}
@@ -1,13 +1,12 @@
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import { RecordingMode } from '@/features/rooms/api/startRecording'
import { useIsTranscriptEnabled } from './useIsTranscriptEnabled'
import { useIsAdminOrOwner } from './useIsAdminOrOwner'
import { useIsRecordingEnabled } from './useIsRecordingEnabled'
export const useHasTranscriptAccess = () => {
const featureEnabled = useFeatureFlagEnabled('transcription-summary')
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isTranscriptEnabled = useIsRecordingEnabled(RecordingMode.Transcript)
const isTranscriptEnabled = useIsTranscriptEnabled()
const isAdminOrOwner = useIsAdminOrOwner()
return (
@@ -1,12 +0,0 @@
import { useSnapshot } from 'valtio'
import { RecordingStatus, recordingStore } from '@/stores/recording'
export const useIsScreenRecordingStarted = () => {
const recordingSnap = useSnapshot(recordingStore)
return recordingSnap.status == RecordingStatus.SCREEN_RECORDING_STARTED
}
export const useIsTranscriptStarted = () => {
const recordingSnap = useSnapshot(recordingStore)
return recordingSnap.status == RecordingStatus.TRANSCRIPT_STARTED
}
@@ -1,15 +0,0 @@
import { useSnapshot } from 'valtio'
import { RecordingStatus, recordingStore } from '@/stores/recording'
export const useIsRecordingTransitioning = () => {
const recordingSnap = useSnapshot(recordingStore)
const transitionalStates = [
RecordingStatus.TRANSCRIPT_STARTING,
RecordingStatus.TRANSCRIPT_STOPPING,
RecordingStatus.SCREEN_RECORDING_STARTING,
RecordingStatus.SCREEN_RECORDING_STOPPING,
]
return transitionalStates.includes(recordingSnap.status)
}
@@ -1,11 +1,11 @@
import { RecordingMode } from '@/features/rooms/api/startRecording'
import { useConfig } from '@/api/useConfig'
export const useIsRecordingEnabled = (mode: RecordingMode) => {
export const useIsTranscriptEnabled = () => {
const { data } = useConfig()
return (
data?.recording?.is_enabled &&
data?.recording?.available_modes?.includes(mode)
data?.recording?.available_modes?.includes(RecordingMode.Transcript)
)
}
@@ -5,83 +5,53 @@ export enum PanelId {
PARTICIPANTS = 'participants',
EFFECTS = 'effects',
CHAT = 'chat',
TOOLS = 'tools',
ADMIN = 'admin',
}
export enum SubPanelId {
TRANSCRIPT = 'transcript',
SCREEN_RECORDING = 'screenRecording',
ADMIN = 'admin',
}
export const useSidePanel = () => {
const layoutSnap = useSnapshot(layoutStore)
const activePanelId = layoutSnap.activePanelId
const activeSubPanelId = layoutSnap.activeSubPanelId
const isParticipantsOpen = activePanelId == PanelId.PARTICIPANTS
const isEffectsOpen = activePanelId == PanelId.EFFECTS
const isChatOpen = activePanelId == PanelId.CHAT
const isToolsOpen = activePanelId == PanelId.TOOLS
const isTranscriptOpen = activePanelId == PanelId.TRANSCRIPT
const isAdminOpen = activePanelId == PanelId.ADMIN
const isTranscriptOpen = activeSubPanelId == SubPanelId.TRANSCRIPT
const isScreenRecordingOpen = activeSubPanelId == SubPanelId.SCREEN_RECORDING
const isSidePanelOpen = !!activePanelId
const isSubPanelOpen = !!activeSubPanelId
const toggleAdmin = () => {
layoutStore.activePanelId = isAdminOpen ? null : PanelId.ADMIN
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleParticipants = () => {
layoutStore.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleChat = () => {
layoutStore.activePanelId = isChatOpen ? null : PanelId.CHAT
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleEffects = () => {
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const toggleTools = () => {
layoutStore.activePanelId = isToolsOpen ? null : PanelId.TOOLS
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
}
const openTranscript = () => {
layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT
layoutStore.activePanelId = PanelId.TOOLS
}
const openScreenRecording = () => {
layoutStore.activeSubPanelId = SubPanelId.SCREEN_RECORDING
layoutStore.activePanelId = PanelId.TOOLS
const toggleTranscript = () => {
layoutStore.activePanelId = isTranscriptOpen ? null : PanelId.TRANSCRIPT
}
return {
activePanelId,
activeSubPanelId,
toggleParticipants,
toggleChat,
toggleEffects,
toggleTools,
toggleTranscript,
toggleAdmin,
openTranscript,
openScreenRecording,
isSubPanelOpen,
isChatOpen,
isParticipantsOpen,
isEffectsOpen,
isSidePanelOpen,
isToolsOpen,
isAdminOpen,
isTranscriptOpen,
isScreenRecordingOpen,
isAdminOpen,
}
}
@@ -14,7 +14,6 @@ import {
RiMore2Line,
RiSettings3Line,
} from '@remixicon/react'
import { GRIST_FEEDBACKS_FORM } from '@/utils/constants'
import { ScreenShareToggle } from '../../components/controls/ScreenShareToggle'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
@@ -22,7 +21,7 @@ import { useSidePanel } from '../../hooks/useSidePanel'
import { LinkButton } from '@/primitives'
import { useSettingsDialog } from '../../components/controls/SettingsDialogContext'
import { ResponsiveMenu } from './ResponsiveMenu'
import { ToolsToggle } from '../../components/controls/ToolsToggle'
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
import { CameraSwitchButton } from '../../components/controls/CameraSwitchButton'
export function MobileControlBar({
@@ -134,7 +133,7 @@ export function MobileControlBar({
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<ToolsToggle
<TranscriptToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
@@ -151,7 +150,7 @@ export function MobileControlBar({
<RiAccountBoxLine size={20} />
</Button>
<LinkButton
href={GRIST_FEEDBACKS_FORM}
href="https://grist.incubateur.net/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4"
variant="primaryTextDark"
tooltip={t('options.items.feedback')}
aria-label={t('options.items.feedback')}
@@ -1,7 +1,7 @@
import { css } from '@/styled-system/css'
import { ChatToggle } from '../../components/controls/ChatToggle'
import { ParticipantsToggle } from '../../components/controls/Participants/ParticipantsToggle'
import { ToolsToggle } from '../../components/controls/ToolsToggle'
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
import { AdminToggle } from '../../components/AdminToggle'
import { useSize } from '../../hooks/useResizeObserver'
import { useState, RefObject } from 'react'
@@ -20,7 +20,7 @@ const NavigationControls = ({
<>
<ChatToggle onPress={onPress} tooltipType={tooltipType} />
<ParticipantsToggle onPress={onPress} tooltipType={tooltipType} />
<ToolsToggle onPress={onPress} tooltipType={tooltipType} />
<TranscriptToggle onPress={onPress} tooltipType={tooltipType} />
<AdminToggle onPress={onPress} tooltipType={tooltipType} />
</>
)
@@ -78,7 +78,7 @@ export const CreateMeetingButton = () => {
if (isPending) {
return (
<div>
<Spinner size={34} />
<Spinner />
</div>
)
}
@@ -21,13 +21,5 @@
"several": "",
"open": "",
"accept": ""
},
"transcript": {
"started": "",
"stopped": ""
},
"screenRecording": {
"started": "",
"stopped": ""
}
}
+6 -36
View File
@@ -104,7 +104,7 @@
"open": "",
"closed": ""
},
"tools": {
"transcript": {
"open": "",
"closed": ""
},
@@ -169,45 +169,27 @@
"effects": "",
"chat": "",
"transcript": "",
"admin": "",
"tools": ""
"admin": ""
},
"content": {
"participants": "",
"effects": "",
"chat": "",
"transcript": "",
"admin": "",
"tools": ""
"admin": ""
},
"closeButton": ""
},
"chat": {
"disclaimer": ""
},
"moreTools": {
"body": "",
"moreLink": "",
"tools": {
"transcript": {
"title": "",
"body": ""
}
}
},
"transcript": {
"start": {
"heading": "",
"body": "",
"button": "",
"linkMore": ""
},
"stop": {
"heading": "",
"body": "",
"button": ""
},
"beta": {
"stop": {
"heading": "",
"body": "",
"button": ""
@@ -292,20 +274,8 @@
}
}
},
"recordingToast": {
"transcript": {
"started": "",
"starting": "",
"stopping": ""
},
"screenRecording": {
"started": "",
"starting": "",
"stopping": ""
},
"any": {
"started": ""
}
"recording": {
"label": ""
},
"participantTileFocus": {
"pin": {
@@ -21,13 +21,5 @@
"several": "Several people want to join this call.",
"open": "Open",
"accept": "Accept"
},
"transcript": {
"started": "{{name}} started the meeting transcription.",
"stopped": "{{name}} stopped the meeting transcription."
},
"screenRecording": {
"started": "{{name}} started the meeting recording.",
"stopped": "{{name}} stopped the meeting recording."
}
}
+15 -64
View File
@@ -103,9 +103,9 @@
"open": "Hide everyone",
"closed": "See everyone"
},
"tools": {
"open": "Hide more tools",
"closed": "Show more tools"
"transcript": {
"open": "Hide AI assistant",
"closed": "Show AI assistant"
},
"admin": {
"open": "Hide admin",
@@ -167,68 +167,31 @@
"participants": "Participants",
"effects": "Effects",
"chat": "Messages in the chat",
"transcript": "Transcription",
"screenRecording": "Recording",
"admin": "Admin settings",
"tools": "More tools"
"transcript": "AI Assistant",
"admin": "Admin settings"
},
"content": {
"participants": "participants",
"effects": "effects",
"chat": "messages",
"transcript": "transcription",
"screenRecording": "recording",
"admin": "admin settings",
"tools": "more tools"
"transcript": "AI assistant",
"admin": "Admin settings"
},
"closeButton": "Hide {{content}}"
},
"chat": {
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
},
"moreTools": {
"body": "Access more tools in Visio to enhance your meetings,",
"moreLink": "learn more",
"tools": {
"transcript": {
"title": "Transcription",
"body": "Keep a written record of your meeting."
},
"screenRecording": {
"title": "Recording",
"body": "Record your meeting to watch it again whenever you like."
}
}
},
"transcript": {
"start": {
"heading": "Transcribe this call",
"body": "Automatically transcribe this call and receive the summary in Docs.",
"button": "Start transcription",
"linkMore": "Learn more"
"heading": "Start the Assistant!",
"body": "The assistant automatically starts recording your meeting audio (limited to 1 hour). At the end, you'll receive a clear and concise summary of the discussion directly via email.",
"button": "Start"
},
"stop": {
"heading": "Transcription in progress...",
"body": "The transcription of your meeting is in progress. You will receive the result by email once the meeting is finished.",
"button": "Stop transcription"
},
"beta": {
"heading": "Become a beta tester",
"body": "Record your meeting for later. You will receive a summary by email once the meeting is finished.",
"button": "Sign up"
}
},
"screenRecording": {
"start": {
"heading": "Record this call",
"body": "Record this call to watch it later and receive the video recording by email.",
"button": "Start recording",
"linkMore": "Learn more"
},
"stop": {
"heading": "Recording in progress…",
"body": "You will receive the result by email once the recording is complete.",
"button": "Stop recording"
"heading": "Recording in Progress...",
"body": "Your meeting is currently being recorded. You will receive a summary via email once the meeting ends.",
"button": "Stop Recording"
}
},
"admin": {
@@ -310,20 +273,8 @@
}
}
},
"recordingToast": {
"transcript": {
"started": "Transcribing",
"starting": "Transcription starting",
"stopping": "Transcription stopping"
},
"screenRecording": {
"started": "Recording in progress",
"starting": "Starting recording",
"stopping": "Stopping recording"
},
"any": {
"started": "Recording in progress"
}
"recording": {
"label": "Recording"
},
"participantTileFocus": {
"pin": {
@@ -21,13 +21,5 @@
"several": "Plusieurs personnes souhaitent participer à cet appel.",
"open": "Afficher",
"accept": "Accepter"
},
"transcript": {
"started": "{{name}} a démarré la transcription de la réunion.",
"stopped": "{{name}} a arrêté la transcription de la réunion."
},
"screenRecording": {
"started": "{{name}} a démarré l'enregistrement de la réunion.",
"stopped": "{{name}} a arrêté l'enregistrement de la réunion."
}
}
+13 -62
View File
@@ -103,9 +103,9 @@
"open": "Masquer les participants",
"closed": "Afficher les participants"
},
"tools": {
"open": "Masquer plus d'outils",
"closed": "Afficher plus d'outils"
"transcript": {
"open": "Masquer l'assistant IA",
"closed": "Afficher l'assistant IA"
},
"admin": {
"open": "Masquer l'admin",
@@ -167,67 +167,30 @@
"participants": "Participants",
"effects": "Effets",
"chat": "Messages dans l'appel",
"transcript": "Transcription",
"screenRecording": "Enregistrement",
"admin": "Commandes de l'organisateur",
"tools": "Plus d'outils"
"transcript": "Assistant IA",
"admin": "Commandes de l'organisateur"
},
"content": {
"participants": "les participants",
"effects": "les effets",
"chat": "les messages",
"transcript": "transcription",
"screenRecording": "enregistrement",
"admin": "commandes de l'organisateur",
"tools": "plus d'outils"
"transcript": "l'assistant IA",
"admin": "Commandes de l'organisateur"
},
"closeButton": "Masquer {{content}}"
},
"chat": {
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
},
"moreTools": {
"body": "Accèder à d'avantage d'outils dans Visio pour améliorer vos réunions,",
"moreLink": "en savoir plus",
"tools": {
"transcript": {
"title": "Transcription",
"body": "Conservez une trace écrite de votre réunion."
},
"screenRecording": {
"title": "Enregistrement",
"body": "Enregistrez votre réunion pour la revoir quand vous le souhaitez."
}
}
},
"transcript": {
"start": {
"heading": "Transcrire cet appel",
"body": "Transcrivez cet appel automatiquement et recevez le compte rendu dans Docs.",
"button": "Démarrer la transcription",
"linkMore": "En savoir plus"
},
"stop": {
"heading": "Transcription en cours …",
"body": "La transcription de votre réunion est en cours. Vous recevrez le resultat par email une fois la réunion terminée.",
"button": "Arrêter la transcription"
},
"beta": {
"heading": "Devenez beta testeur",
"body": "Enregistrer votre réunion pour plus tard. Vous recevrez un compte-rendu par email une fois la réunion terminée.",
"button": "Inscrivez-vous"
}
},
"screenRecording": {
"start": {
"heading": "Enregistrer cet appel",
"body": "Enregistrez cet appel pour plus tard et recevez l'enregistrement vidéo par mail.",
"button": "Démarrer l'enregistrement",
"linkMore": "En savoir plus"
"heading": "Démarrer l'assistant !",
"body": "L'assistant démarre automatiquement l'enregistrement sonore de votre réunion (limité à 1h). À la fin, vous recevrez un résumé clair et concis des échanges directement par e-mail.",
"button": "Démarrer"
},
"stop": {
"heading": "Enregistrement en cours …",
"body": "Vous recevrez le resultat par email une fois l'enregistrement terminé.",
"body": "L'enregistrement de votre réunion est en cours. Vous recevrez un compte-rendu par email une fois la réunion terminée.",
"button": "Arrêter l'enregistrement"
}
},
@@ -310,20 +273,8 @@
}
}
},
"recordingToast": {
"transcript": {
"started": "Transcription en cours",
"starting": "Démarrage de la transcription",
"stopping": "Arrêt de la transcription"
},
"screenRecording": {
"started": "Enregistrement en cours",
"starting": "Démarrage de l'enregistrement",
"stopping": "Arrêt de l'enregistrement"
},
"any": {
"started": "Enregistrement en cours"
}
"recording": {
"label": "Enregistrement"
},
"participantTileFocus": {
"pin": {
@@ -21,13 +21,5 @@
"several": "Meerdere mensen willen deelnemen aan dit gesprek.",
"open": "Openen",
"accept": "Accepteren"
},
"transcript": {
"started": "{{name}} is de transcriptie van de vergadering gestart.",
"stopped": "{{name}} heeft de transcriptie van de vergadering gestopt."
},
"screenRecording": {
"started": "{{name}} is begonnen met het opnemen van de vergadering.",
"stopped": "{{name}} is gestopt met het opnemen van de vergadering."
}
}
+15 -64
View File
@@ -103,9 +103,9 @@
"open": "Verberg iedereen",
"closed": "Toon iedereen"
},
"tools": {
"open": "Meer tools verbergen",
"closed": "Meer tools weergeven"
"transcript": {
"open": "Verberg AI-assistent",
"closed": "Toon AI-assistant"
},
"admin": {
"open": "Verberg beheerder",
@@ -167,68 +167,31 @@
"participants": "Deelnemers",
"effects": "Effecten",
"chat": "Berichten in de chat",
"transcript": "Transcriptie",
"screenRecording": "Schermopname",
"admin": "Beheerdersbediening",
"tools": "Meer tools"
"transcript": "AI-assistent",
"admin": "Beheerdersbediening"
},
"content": {
"participants": "deelnemers",
"effects": "effecten",
"chat": "berichten",
"screenRecording": "transcriptie",
"transcript": "schermopname",
"admin": "beheerdersbediening",
"tools": "meer tools"
"transcript": "AI-assistent",
"admin": "Beheerdersbediening"
},
"closeButton": "Verberg {{content}}"
},
"chat": {
"disclaimer": "De berichten zijn alleen voor de deelnemers zichtbaar op het moment dat ze worden verzonden. Alle berichten worden verwijderd aan het einde van het gesprek."
},
"moreTools": {
"body": "Toegang tot meer tools in Visio om je vergaderingen te verbeteren,",
"moreLink": "lees meer",
"tools": {
"transcript": {
"title": "Transcriptie",
"body": "Bewaar een schriftelijk verslag van je vergadering."
},
"screenRecording": {
"title": "Opname",
"body": "Neem je vergadering op om die later opnieuw te bekijken."
}
}
},
"transcript": {
"start": {
"heading": "Transcribeer dit gesprek",
"body": "Transcribeer dit gesprek automatisch en ontvang het verslag in Docs.",
"button": "Transcriptie starten",
"linkMore": "Meer informatie"
"heading": "Start de assistent!",
"body": "De assistent begint automatisch de audio van uw vergadering op te nemen (beperkt tot 1 uur). Na afloop krijgt u direct een heldere en beknopte samenvatting van de discussies in uw e-mail.",
"button": "Start"
},
"stop": {
"heading": "Transcriptie bezig...",
"body": "De transcriptie van uw vergadering is bezig. U ontvangt het resultaat per e-mail zodra de vergadering is afgelopen.",
"button": "Transcriptie stoppen"
},
"beta": {
"heading": "Word betatester",
"body": "Neem uw vergadering op voor later. U ontvangt een samenvatting per e-mail zodra de vergadering is afgelopen.",
"button": "Aanmelden"
}
},
"screenRecording": {
"start": {
"heading": "Dit gesprek opnemen",
"body": "Neem dit gesprek op om het later terug te kijken. Je ontvangt de video-opname per e-mail.",
"button": "Opname starten",
"linkMore": "Meer informatie"
},
"stop": {
"heading": "Opname bezig …",
"body": "Je ontvangt het resultaat per e-mail zodra de opname is voltooid.",
"button": "Opname stoppen"
"heading": "Opname loopt ...",
"body": "Uw vergadering wordt momenteel opgenomen. U ontvangt een samenvatting via e-mail, zo gauw de vergarding gesloten wordt.",
"button": "Stop met opname"
}
},
"admin": {
@@ -310,20 +273,8 @@
}
}
},
"recordingToast": {
"transcript": {
"started": "Transcriptie bezig",
"starting": "Transcriptie begint",
"stopping": "Transcriptie stopt"
},
"screenRecording": {
"started": "Opname bezig",
"starting": "Opname starten",
"stopping": "Opname stoppen"
},
"any": {
"started": "Opname bezig"
}
"recording": {
"label": "Opnemen"
},
"participantTileFocus": {
"pin": {
@@ -9,7 +9,6 @@ type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
TooltipWrapperProps & {
// Use tooltip as description below the button.
description?: boolean
target?: string
}
export const LinkButton = ({
+5 -11
View File
@@ -1,13 +1,7 @@
import { ProgressBar } from 'react-aria-components'
import { css } from '@/styled-system/css'
export const Spinner = ({
size = 56,
variant = 'light',
}: {
size?: number
variant?: 'light' | 'dark'
}) => {
export const Spinner = () => {
const center = 14
const strokeWidth = 3
const r = 14 - strokeWidth
@@ -17,8 +11,8 @@ export const Spinner = ({
{({ percentage }) => (
<>
<svg
width={size}
height={size}
width={56}
height={56}
viewBox="0 0 28 28"
fill="none"
strokeWidth={strokeWidth}
@@ -31,7 +25,7 @@ export const Spinner = ({
strokeDashoffset={0}
strokeLinecap="round"
className={css({
stroke: variant == 'light' ? 'primary.100' : 'primaryDark.100',
stroke: 'primary.100',
})}
style={{}}
/>
@@ -43,7 +37,7 @@ export const Spinner = ({
strokeDashoffset={percentage && c - (percentage / 100) * c}
strokeLinecap="round"
className={css({
stroke: variant == 'light' ? 'primary.800' : 'white',
stroke: 'primary.800',
})}
style={{
animation: `rotate 1s ease-in-out infinite`,
@@ -42,7 +42,6 @@ export const buttonRecipe = cva({
primary: {
backgroundColor: 'primary.800',
color: 'white',
fontWeight: 'medium !important',
'&[data-hovered]': {
backgroundColor: 'primary.action',
},
@@ -57,7 +56,6 @@ export const buttonRecipe = cva({
secondary: {
backgroundColor: 'white',
color: 'primary.800',
fontWeight: 'medium !important',
borderColor: 'primary.800',
'&[data-hovered]': {
backgroundColor: 'greyscale.100',
@@ -115,7 +113,6 @@ export const buttonRecipe = cva({
},
tertiary: {
backgroundColor: 'primary.100',
fontWeight: 'medium !important',
color: 'primary.800',
'&[data-hovered]': {
backgroundColor: 'primary.300',
@@ -123,14 +120,9 @@ export const buttonRecipe = cva({
'&[data-pressed]': {
backgroundColor: 'primary.300',
},
'&[data-disabled]': {
backgroundColor: 'transparent',
color: 'primary.400',
},
},
tertiaryText: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'primary.900',
'&[data-hovered]': {
backgroundColor: 'primary.300',
@@ -141,7 +133,6 @@ export const buttonRecipe = cva({
},
primaryDark: {
backgroundColor: 'primaryDark.100',
fontWeight: 'medium !important',
color: 'white',
'&[data-pressed]': {
backgroundColor: 'primaryDark.900',
@@ -158,7 +149,6 @@ export const buttonRecipe = cva({
},
secondaryDark: {
backgroundColor: 'primaryDark.50',
fontWeight: 'medium !important',
color: 'white',
'&[data-pressed]': {
backgroundColor: 'primaryDark.200',
@@ -174,7 +164,6 @@ export const buttonRecipe = cva({
},
primaryTextDark: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'white',
'&[data-hovered]': {
backgroundColor: 'primaryDark.100',
@@ -190,7 +179,6 @@ export const buttonRecipe = cva({
},
quaternaryText: {
backgroundColor: 'transparent',
fontWeight: 'medium !important',
color: 'greyscale.600',
'&[data-hovered]': {
backgroundColor: 'greyscale.100',
-9
View File
@@ -5,7 +5,6 @@ import { AccessibilityRoute } from '@/features/legalsTerms/Accessibility'
import { TermsOfServiceRoute } from '@/features/legalsTerms/TermsOfService'
import { CreatePopup } from '@/features/sdk/routes/CreatePopup'
import { CreateMeetingButton } from '@/features/sdk/routes/CreateMeetingButton'
import { DownloadRoute, RecordingRoute } from '@/features/recording'
export const routes: Record<
| 'home'
@@ -35,14 +34,6 @@ export const routes: Record<
to: (roomId: string) => `/${roomId.trim()}`,
Component: RoomRoute,
},
recording: {
name: 'recording',
path: new RegExp(
`^[/]recording[/](?<recordingId>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$`
),
to: (recordingId: string) => `/recording/${recordingId.trim()}`,
Component: RecordingRoute,
},
feedback: {
name: 'feedback',
path: '/feedback',
+1 -6
View File
@@ -1,19 +1,14 @@
import { proxy } from 'valtio'
import {
PanelId,
SubPanelId,
} from '@/features/rooms/livekit/hooks/useSidePanel'
import { PanelId } from '@/features/rooms/livekit/hooks/useSidePanel'
type State = {
showHeader: boolean
showFooter: boolean
activePanelId: PanelId | null
activeSubPanelId: SubPanelId | null
}
export const layoutStore = proxy<State>({
showHeader: false,
showFooter: false,
activePanelId: null,
activeSubPanelId: null,
})
-20
View File
@@ -1,20 +0,0 @@
import { proxy } from 'valtio'
export enum RecordingStatus {
TRANSCRIPT_STARTING,
TRANSCRIPT_STARTED,
TRANSCRIPT_STOPPING,
STOPPED,
SCREEN_RECORDING_STARTING,
SCREEN_RECORDING_STARTED,
SCREEN_RECORDING_STOPPING,
ANY_STARTED,
}
type State = {
status: RecordingStatus
}
export const recordingStore = proxy<State>({
status: RecordingStatus.STOPPED,
})
+2 -14
View File
@@ -1,14 +1,2 @@
export const GRIST_FEEDBACKS_FORM =
'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26' as const
export const BETA_USERS_FORM_URL =
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17' as const
export const CRISP_HELP_ARTICLE_MORE_TOOLS =
'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23' as const
export const CRISP_HELP_ARTICLE_TRANSCRIPT =
'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x' as const
export const CRISP_HELP_ARTICLE_RECORDING =
'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0' as const
export const GRIST_FORM =
'https://grist.numerique.gouv.fr/o/docs/forms/1YrfNP1QSSy8p2gCxMFnSf/4' as const
@@ -16,9 +16,6 @@ backend:
DJANGO_EMAIL_HOST: "mailcatcher"
DJANGO_EMAIL_PORT: 1025
DJANGO_EMAIL_USE_SSL: False
DJANGO_EMAIL_BRAND_NAME: "La Suite Numérique"
DJANGO_EMAIL_SUPPORT_EMAIL: "test@yopmail.com"
DJANGO_EMAIL_LOGO_IMG: https://meet.127.0.0.1.nip.io/assets/logo-suite-numerique.png
OIDC_OP_JWKS_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://keycloak.127.0.0.1.nip.io/realms/meet/protocol/openid-connect/token
@@ -198,17 +195,3 @@ celery:
- "worker"
- "--pool=solo"
- "--loglevel=info"
ingressMedia:
enabled: true
host: meet.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/recordings/media-auth/
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: minio.meet.svc.cluster.local:9000
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1
serviceMedia:
host: minio.meet.svc.cluster.local
port: 9000
@@ -32,8 +32,6 @@ backend:
DJANGO_EMAIL_HOST: "mailcatcher"
DJANGO_EMAIL_PORT: 1025
DJANGO_EMAIL_USE_SSL: False
DJANGO_EMAIL_BRAND_NAME: "La Suite Numérique"
DJANGO_EMAIL_SUPPORT_EMAIL: "test@yopmail.com"
OIDC_OP_JWKS_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
OIDC_OP_AUTHORIZATION_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
OIDC_OP_TOKEN_ENDPOINT: https://fca.integ01.dev-agentconnect.fr/api/v2/token
@@ -225,17 +223,3 @@ celery:
- "worker"
- "--pool=solo"
- "--loglevel=info"
ingressMedia:
enabled: true
host: meet.127.0.0.1.nip.io
annotations:
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/recordings/media-auth/
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: minio.meet.svc.cluster.local:9000
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1
serviceMedia:
host: minio.meet.svc.cluster.local
port: 9000
@@ -1,83 +0,0 @@
{{- if .Values.ingressMedia.enabled -}}
{{- $fullName := include "meet.fullname" . -}}
{{- if and .Values.ingressMedia.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingressMedia.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingressMedia.annotations "kubernetes.io/ingress.class" .Values.ingressMedia.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}-media
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "meet.labels" . | nindent 4 }}
{{- with .Values.ingressMedia.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingressMedia.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingressMedia.className }}
{{- end }}
{{- if .Values.ingressMedia.tls.enabled }}
tls:
{{- if .Values.ingressMedia.host }}
- secretName: {{ .Values.ingressMedia.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
hosts:
- {{ .Values.ingressMedia.host | quote }}
{{- end }}
{{- range .Values.ingressMedia.tls.additional }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- if .Values.ingressMedia.host }}
- host: {{ .Values.ingressMedia.host | quote }}
http:
paths:
- path: {{ .Values.ingressMedia.path | quote }}
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: ImplementationSpecific
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}-media
port:
number: {{ .Values.serviceMedia.port }}
{{- else }}
serviceName: {{ $fullName }}-media
servicePort: {{ .Values.serviceMedia.port }}
{{- end }}
{{- end }}
{{- range .Values.ingressMedia.hosts }}
- host: {{ . | quote }}
http:
paths:
- path: {{ $.Values.ingressMedia.path | quote }}
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: ImplementationSpecific
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}-media
port:
number: {{ .Values.serviceMedia.port }}
{{- else }}
serviceName: {{ $fullName }}-media
servicePort: {{ .Values.serviceMedia.port }}
{{- end }}
{{- end }}
{{- end }}
-14
View File
@@ -1,14 +0,0 @@
{{- $fullName := include "meet.fullname" . -}}
{{- $component := "media" -}}
apiVersion: v1
kind: Service
metadata:
name: {{ $fullName }}-media
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
annotations:
{{- toYaml $.Values.serviceMedia.annotations | nindent 4 }}
spec:
type: ExternalName
externalName: {{ $.Values.serviceMedia.host }}
-43
View File
@@ -70,47 +70,6 @@ ingressAdmin:
additional: []
## @param ingressMedia.enabled whether to enable the Ingress or not
## @param ingressMedia.className IngressClass to use for the Ingress
## @param ingressMedia.host Host for the Ingress
## @param ingressMedia.path Path to use for the Ingress
ingressMedia:
enabled: false
className: null
host: meet.example.com
path: /media/(.*)
## @param ingressMedia.hosts Additional host to configure for the Ingress
hosts: [ ]
# - chart-example.local
## @param ingressMedia.tls.enabled Weather to enable TLS for the Ingress
## @param ingressMedia.tls.secretName Secret name for TLS config
## @skip ingressMedia.tls.additional
## @extra ingressMedia.tls.additional[].secretName Secret name for additional TLS config
## @extra ingressMedia.tls.additional[].hosts[] Hosts for additional TLS config
tls:
enabled: true
secretName: null
additional: []
## @param ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-url
## @param ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-response-headers
## @param ingressMedia.annotations.nginx.ingress.kubernetes.io/upstream-vhost
annotations:
nginx.ingress.kubernetes.io/auth-url: https://meet.example.com/api/v1.0/recordings/media-auth/
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
nginx.ingress.kubernetes.io/upstream-vhost: minio.meet.svc.cluster.local:9000
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header Content-Security-Policy "default-src 'none'" always;
## @param serviceMedia.host
## @param serviceMedia.port
## @param serviceMedia.annotations
serviceMedia:
host: minio.meet.svc.cluster.local
port: 9000
annotations: {}
## @section backend
backend:
@@ -587,5 +546,3 @@ celery:
## @param celery.pdb.enabled Enable pdb on celery
pdb:
enabled: false
-67
View File
@@ -1,67 +0,0 @@
<mjml>
<mj-include path="./partial/header.mjml" />
<mj-body mj-class="bg--blue-100">
<mj-wrapper css-class="wrapper" padding="5px 25px 0px 25px">
<mj-section css-class="wrapper-logo">
<mj-column>
<mj-image
align="center"
src="{{logo_img}}"
width="320px"
alt="{%trans 'Logo email' %}"
/>
</mj-column>
</mj-section>
<mj-section mj-class="bg--white-100" padding="0px 20px 60px 20px">
<mj-column>
<mj-text align="center">
<h1>{% trans "Your recording is ready!"%}</h1>
</mj-text>
<!-- Main Message -->
<mj-text>
{% blocktrans %}
Your recording of "{{room_name}}" on {{recording_date}} at {{recording_time}} is now ready to view.
{% endblocktrans %}
</mj-text>
<mj-text>
<p>{% trans "To keep this recording permanently:" %}</p>
<ol>
<li>{% trans "Click the \"Open\" button below" %}</li>
<li>{% trans "Use the \"Download\" button in the interface" %}</li>
<li>{% trans "Save the file to your preferred location" %}</li>
</ol>
</mj-text>
<mj-button
href="{{link}}"
background-color="#000091"
color="white"
padding-bottom="30px"
>
{% trans "Open"%}
</mj-button>
<mj-text>
{% blocktrans %}
If you have any questions or need assistance, please contact our support team at {{support_email}}.
{% endblocktrans %}
</mj-text>
<mj-divider
border-width="1px"
border-style="solid"
border-color="#DDDDDD"
width="30%"
align="center"
/>
<!-- Signature -->
<mj-text>
<p>
{% blocktrans %}
Thank you for using {{brandname}}.
{% endblocktrans %}
</p>
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
</mj-body>
</mjml>