Merge branch 'livekit' into toger5/track-processor-blur
This commit is contained in:
@@ -44,6 +44,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
// To encourage good usage of RxJS:
|
// To encourage good usage of RxJS:
|
||||||
"rxjs/no-exposed-subjects": "error",
|
"rxjs/no-exposed-subjects": "error",
|
||||||
|
"rxjs/finnish": "error",
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
"developer_tab_title": "Разработчик",
|
"developer_tab_title": "Разработчик",
|
||||||
"feedback_tab_h4": "Изпрати обратна връзка",
|
"feedback_tab_h4": "Изпрати обратна връзка",
|
||||||
"feedback_tab_send_logs_label": "Включи debug логове",
|
"feedback_tab_send_logs_label": "Включи debug логове",
|
||||||
"more_tab_title": "Още",
|
|
||||||
"speaker_device_selection_label": "Говорител"
|
"speaker_device_selection_label": "Говорител"
|
||||||
},
|
},
|
||||||
"unauthenticated_view_body": "Все още не сте регистрирани? <2>Създайте акаунт</2>",
|
"unauthenticated_view_body": "Все още не сте регистрирани? <2>Създайте акаунт</2>",
|
||||||
|
|||||||
@@ -60,12 +60,9 @@
|
|||||||
"return_home_button": "Vrátit se na domácí obrazovku",
|
"return_home_button": "Vrátit se na domácí obrazovku",
|
||||||
"screenshare_button_label": "Sdílet obrazovku",
|
"screenshare_button_label": "Sdílet obrazovku",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Vývojářské nastavení",
|
|
||||||
"developer_settings_label_description": "Zobrazit vývojářské nastavení.",
|
|
||||||
"developer_tab_title": "Vývojář",
|
"developer_tab_title": "Vývojář",
|
||||||
"feedback_tab_h4": "Dát feedback",
|
"feedback_tab_h4": "Dát feedback",
|
||||||
"feedback_tab_send_logs_label": "Zahrnout ladící záznamy",
|
"feedback_tab_send_logs_label": "Zahrnout ladící záznamy",
|
||||||
"more_tab_title": "Více",
|
|
||||||
"speaker_device_selection_label": "Reproduktor"
|
"speaker_device_selection_label": "Reproduktor"
|
||||||
},
|
},
|
||||||
"unauthenticated_view_body": "Nejste registrovaní? <2>Vytvořit účet</2>",
|
"unauthenticated_view_body": "Nejste registrovaní? <2>Vytvořit účet</2>",
|
||||||
|
|||||||
@@ -147,10 +147,9 @@
|
|||||||
"screenshare_button_label": "Bildschirm teilen",
|
"screenshare_button_label": "Bildschirm teilen",
|
||||||
"settings": {
|
"settings": {
|
||||||
"audio_tab": {
|
"audio_tab": {
|
||||||
|
"effect_volume_description": "Lautstärke anpassen, mit der Reaktionen und Handmeldungen abgespielt werden",
|
||||||
"effect_volume_label": "Lautstärke der Soundeffekte"
|
"effect_volume_label": "Lautstärke der Soundeffekte"
|
||||||
},
|
},
|
||||||
"developer_settings_label": "Entwicklereinstellungen",
|
|
||||||
"developer_settings_label_description": "Zeige die Entwicklereinstellungen im Einstellungsfenster.",
|
|
||||||
"developer_tab_title": "Entwickler",
|
"developer_tab_title": "Entwickler",
|
||||||
"feedback_tab_body": "Falls du auf Probleme stößt oder einfach nur eine Rückmeldung geben möchtest, sende uns bitte eine kurze Beschreibung.",
|
"feedback_tab_body": "Falls du auf Probleme stößt oder einfach nur eine Rückmeldung geben möchtest, sende uns bitte eine kurze Beschreibung.",
|
||||||
"feedback_tab_description_label": "Deine Rückmeldung",
|
"feedback_tab_description_label": "Deine Rückmeldung",
|
||||||
@@ -158,19 +157,13 @@
|
|||||||
"feedback_tab_send_logs_label": "Debug-Protokolle anhängen",
|
"feedback_tab_send_logs_label": "Debug-Protokolle anhängen",
|
||||||
"feedback_tab_thank_you": "Danke, wir haben deine Rückmeldung erhalten!",
|
"feedback_tab_thank_you": "Danke, wir haben deine Rückmeldung erhalten!",
|
||||||
"feedback_tab_title": "Rückmeldung",
|
"feedback_tab_title": "Rückmeldung",
|
||||||
"more_tab_title": "Mehr",
|
|
||||||
"opt_in_description": "<0></0><1></1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.",
|
"opt_in_description": "<0></0><1></1>Du kannst deine Zustimmung durch Abwählen dieses Kästchens zurückziehen. Falls du dich aktuell in einem Anruf befindest, wird diese Einstellung nach dem Ende des Anrufs wirksam.",
|
||||||
"preferences_tab": {
|
"preferences_tab": {
|
||||||
"reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet",
|
"reactions_play_sound_description": "Einen Soundeffekt abspielen, wenn jemand eine Reaktion sendet",
|
||||||
"reactions_play_sound_label": "Reaktionstöne abspielen",
|
"reactions_play_sound_label": "Reaktionstöne abspielen",
|
||||||
"reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.",
|
"reactions_show_description": "Zeige eine Animation, wenn jemand eine Reaktion sendet.",
|
||||||
"reactions_show_label": "Reaktionen anzeigen",
|
"reactions_show_label": "Reaktionen anzeigen"
|
||||||
"reactions_title": "Reaktionen"
|
|
||||||
},
|
},
|
||||||
"preferences_tab_body": "Hier können zusätzliche Optionen für individuelle Anforderungen eingestellt werden",
|
|
||||||
"preferences_tab_h4": "Einstellungen",
|
|
||||||
"preferences_tab_show_hand_raised_timer_description": "Einen Timer zur Handmeldung anzeigen",
|
|
||||||
"preferences_tab_show_hand_raised_timer_label": "Dauer der Handmeldung anzeigen",
|
|
||||||
"speaker_device_selection_label": "Lautsprecher"
|
"speaker_device_selection_label": "Lautsprecher"
|
||||||
},
|
},
|
||||||
"star_rating_input_label_one": "{{count}} Stern",
|
"star_rating_input_label_one": "{{count}} Stern",
|
||||||
|
|||||||
@@ -67,8 +67,6 @@
|
|||||||
"return_home_button": "Επιστροφή στην αρχική οθόνη",
|
"return_home_button": "Επιστροφή στην αρχική οθόνη",
|
||||||
"screenshare_button_label": "Κοινή χρήση οθόνης",
|
"screenshare_button_label": "Κοινή χρήση οθόνης",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Ρυθμίσεις προγραμματιστή",
|
|
||||||
"developer_settings_label_description": "Εμφάνιση ρυθμίσεων προγραμματιστή στο παράθυρο ρυθμίσεων.",
|
|
||||||
"developer_tab_title": "Προγραμματιστής",
|
"developer_tab_title": "Προγραμματιστής",
|
||||||
"feedback_tab_body": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.",
|
"feedback_tab_body": "Εάν αντιμετωπίζετε προβλήματα ή απλά θέλετε να μας δώσετε κάποια σχόλια, παρακαλούμε στείλτε μας μια σύντομη περιγραφή παρακάτω.",
|
||||||
"feedback_tab_description_label": "Τα σχόλιά σας",
|
"feedback_tab_description_label": "Τα σχόλιά σας",
|
||||||
@@ -76,7 +74,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Να συμπεριληφθούν αρχεία καταγραφής",
|
"feedback_tab_send_logs_label": "Να συμπεριληφθούν αρχεία καταγραφής",
|
||||||
"feedback_tab_thank_you": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!",
|
"feedback_tab_thank_you": "Ευχαριστούμε, λάβαμε τα σχόλιά σας!",
|
||||||
"feedback_tab_title": "Ανατροφοδότηση",
|
"feedback_tab_title": "Ανατροφοδότηση",
|
||||||
"more_tab_title": "Περισσότερα",
|
|
||||||
"opt_in_description": "<0></0><1></1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.",
|
"opt_in_description": "<0></0><1></1>Μπορείτε να ανακαλέσετε τη συγκατάθεσή σας αποεπιλέγοντας αυτό το πλαίσιο. Εάν βρίσκεστε σε κλήση, η ρύθμιση αυτή θα τεθεί σε ισχύ στο τέλος της.",
|
||||||
"speaker_device_selection_label": "Ηχείο"
|
"speaker_device_selection_label": "Ηχείο"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,13 +48,11 @@
|
|||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"camera": "Camera",
|
|
||||||
"display_name": "Display name",
|
"display_name": "Display name",
|
||||||
"encrypted": "Encrypted",
|
"encrypted": "Encrypted",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"microphone": "Microphone",
|
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
@@ -74,7 +72,8 @@
|
|||||||
"device_id": "Device ID: {{id}}",
|
"device_id": "Device ID: {{id}}",
|
||||||
"duplicate_tiles_label": "Number of additional tile copies per participant",
|
"duplicate_tiles_label": "Number of additional tile copies per participant",
|
||||||
"hostname": "Hostname: {{hostname}}",
|
"hostname": "Hostname: {{hostname}}",
|
||||||
"matrix_id": "Matrix ID: {{id}}"
|
"matrix_id": "Matrix ID: {{id}}",
|
||||||
|
"show_non_member_tiles": "Show tiles for non-member media"
|
||||||
},
|
},
|
||||||
"disconnected_banner": "Connectivity to the server has been lost.",
|
"disconnected_banner": "Connectivity to the server has been lost.",
|
||||||
"full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.</0>",
|
"full_screen_view_description": "<0>Submitting debug logs will help us track down the problem.</0>",
|
||||||
@@ -155,6 +154,16 @@
|
|||||||
"background_blur_label": "Blur the background of the video",
|
"background_blur_label": "Blur the background of the video",
|
||||||
"blur_not_supported_by_browser": "(Background blur is not supported by this device)",
|
"blur_not_supported_by_browser": "(Background blur is not supported by this device)",
|
||||||
"developer_tab_title": "Developer",
|
"developer_tab_title": "Developer",
|
||||||
|
"devices": {
|
||||||
|
"camera": "Camera",
|
||||||
|
"camera_numbered": "Camera {{n}}",
|
||||||
|
"default": "Default",
|
||||||
|
"default_named": "Default <2>({{name}})</2>",
|
||||||
|
"microphone": "Microphone",
|
||||||
|
"microphone_numbered": "Microphone {{n}}",
|
||||||
|
"speaker": "Speaker",
|
||||||
|
"speaker_numbered": "Speaker {{n}}"
|
||||||
|
},
|
||||||
"feedback_tab_body": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
"feedback_tab_body": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
|
||||||
"feedback_tab_description_label": "Your feedback",
|
"feedback_tab_description_label": "Your feedback",
|
||||||
"feedback_tab_h4": "Submit feedback",
|
"feedback_tab_h4": "Submit feedback",
|
||||||
@@ -165,15 +174,14 @@
|
|||||||
"preferences_tab": {
|
"preferences_tab": {
|
||||||
"developer_mode_label": "Developer mode",
|
"developer_mode_label": "Developer mode",
|
||||||
"developer_mode_label_description": "Enable developer mode and show developer settings tab.",
|
"developer_mode_label_description": "Enable developer mode and show developer settings tab.",
|
||||||
|
"introduction": "Here you can configure extra options for an improved experience.",
|
||||||
"reactions_play_sound_description": "Play a sound effect when anyone sends a reaction into a call.",
|
"reactions_play_sound_description": "Play a sound effect when anyone sends a reaction into a call.",
|
||||||
"reactions_play_sound_label": "Play reaction sounds",
|
"reactions_play_sound_label": "Play reaction sounds",
|
||||||
"reactions_show_description": "Show an animation when anyone sends a reaction.",
|
"reactions_show_description": "Show an animation when anyone sends a reaction.",
|
||||||
"reactions_show_label": "Show reactions"
|
"reactions_show_label": "Show reactions",
|
||||||
},
|
"show_hand_raised_timer_description": "Show a timer when a participant raises their hand",
|
||||||
"preferences_tab_body": "Here you can configure extra options for an improved experience.",
|
"show_hand_raised_timer_label": "Show hand raise duration"
|
||||||
"preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand",
|
}
|
||||||
"preferences_tab_show_hand_raised_timer_label": "Show hand raise duration",
|
|
||||||
"speaker_device_selection_label": "Speaker"
|
|
||||||
},
|
},
|
||||||
"star_rating_input_label_one": "{{count}} star",
|
"star_rating_input_label_one": "{{count}} star",
|
||||||
"star_rating_input_label_other": "{{count}} stars",
|
"star_rating_input_label_other": "{{count}} stars",
|
||||||
|
|||||||
@@ -67,8 +67,6 @@
|
|||||||
"room_auth_view_eula_caption": "Al hacer clic en \"Unirse a la llamada ahora\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
|
"room_auth_view_eula_caption": "Al hacer clic en \"Unirse a la llamada ahora\", aceptas nuestro <2>Contrato de Licencia de Usuario Final (CLUF)</2>",
|
||||||
"screenshare_button_label": "Compartir pantalla",
|
"screenshare_button_label": "Compartir pantalla",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Ajustes de desarrollador",
|
|
||||||
"developer_settings_label_description": "Muestra los ajustes de desarrollador en la ventana de ajustes.",
|
|
||||||
"developer_tab_title": "Desarrollador",
|
"developer_tab_title": "Desarrollador",
|
||||||
"feedback_tab_body": "Si tienes algún problema o simplemente quieres darnos tu opinión, por favor envíanos una breve descripción.",
|
"feedback_tab_body": "Si tienes algún problema o simplemente quieres darnos tu opinión, por favor envíanos una breve descripción.",
|
||||||
"feedback_tab_description_label": "Tus comentarios",
|
"feedback_tab_description_label": "Tus comentarios",
|
||||||
@@ -76,7 +74,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Incluir registros de depuración",
|
"feedback_tab_send_logs_label": "Incluir registros de depuración",
|
||||||
"feedback_tab_thank_you": "¡Gracias, hemos recibido tus comentarios!",
|
"feedback_tab_thank_you": "¡Gracias, hemos recibido tus comentarios!",
|
||||||
"feedback_tab_title": "Danos tu opinión",
|
"feedback_tab_title": "Danos tu opinión",
|
||||||
"more_tab_title": "Más",
|
|
||||||
"opt_in_description": "<0></0><1></1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta.",
|
"opt_in_description": "<0></0><1></1>Puedes retirar tu consentimiento desmarcando esta casilla. Si estás en una llamada, este ajuste se aplicará al final de esta.",
|
||||||
"speaker_device_selection_label": "Altavoz"
|
"speaker_device_selection_label": "Altavoz"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -97,8 +97,6 @@
|
|||||||
"room_auth_view_eula_caption": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
|
"room_auth_view_eula_caption": "Klõpsides „Liitu kõnega kohe“, nõustud sa meie <2>Lõppkasutaja litsentsilepinguga (EULA)</2>",
|
||||||
"screenshare_button_label": "Jaga ekraani",
|
"screenshare_button_label": "Jaga ekraani",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Arendaja seadistused",
|
|
||||||
"developer_settings_label_description": "Näita seadistuste aknas arendajale vajalikke seadeid.",
|
|
||||||
"developer_tab_title": "Arendaja",
|
"developer_tab_title": "Arendaja",
|
||||||
"feedback_tab_body": "Kui selle rakenduse kasutamisel tekib sul probleeme või lihtsalt soovid oma arvamust avaldada, siis palun täida alljärgnev lühike kirjeldus.",
|
"feedback_tab_body": "Kui selle rakenduse kasutamisel tekib sul probleeme või lihtsalt soovid oma arvamust avaldada, siis palun täida alljärgnev lühike kirjeldus.",
|
||||||
"feedback_tab_description_label": "Sinu tagasiside",
|
"feedback_tab_description_label": "Sinu tagasiside",
|
||||||
@@ -106,7 +104,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Lisa veatuvastuslogid",
|
"feedback_tab_send_logs_label": "Lisa veatuvastuslogid",
|
||||||
"feedback_tab_thank_you": "Tänud, me oleme sinu tagasiside kätte saanud!",
|
"feedback_tab_thank_you": "Tänud, me oleme sinu tagasiside kätte saanud!",
|
||||||
"feedback_tab_title": "Tagasiside",
|
"feedback_tab_title": "Tagasiside",
|
||||||
"more_tab_title": "Rohkem",
|
|
||||||
"opt_in_description": "<0></0><1></1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu.",
|
"opt_in_description": "<0></0><1></1>Sa võid selle valiku eelmaldamisega alati oma nõusoleku tagasi võtta. Kui sul parasjagu on kõne pooleli, siis seadistuste muudatus jõustub pärast kõne lõppu.",
|
||||||
"speaker_device_selection_label": "Kõlar"
|
"speaker_device_selection_label": "Kõlar"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,7 +64,6 @@
|
|||||||
"developer_tab_title": "توسعه دهنده",
|
"developer_tab_title": "توسعه دهنده",
|
||||||
"feedback_tab_h4": "بازخورد ارائه دهید",
|
"feedback_tab_h4": "بازخورد ارائه دهید",
|
||||||
"feedback_tab_send_logs_label": "شامل لاگهای عیبیابی",
|
"feedback_tab_send_logs_label": "شامل لاگهای عیبیابی",
|
||||||
"more_tab_title": "بیشتر",
|
|
||||||
"speaker_device_selection_label": "بلندگو"
|
"speaker_device_selection_label": "بلندگو"
|
||||||
},
|
},
|
||||||
"unauthenticated_view_body": "هنوز ثبتنام نکردهاید؟ <2>ساخت حساب کاربری</2>",
|
"unauthenticated_view_body": "هنوز ثبتنام نکردهاید؟ <2>ساخت حساب کاربری</2>",
|
||||||
|
|||||||
@@ -95,8 +95,6 @@
|
|||||||
"room_auth_view_eula_caption": "En cliquant sur « Rejoindre l’appel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
|
"room_auth_view_eula_caption": "En cliquant sur « Rejoindre l’appel maintenant », vous acceptez notre <2>Contrat de Licence Utilisateur Final (CLUF)</2>",
|
||||||
"screenshare_button_label": "Partage d’écran",
|
"screenshare_button_label": "Partage d’écran",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Paramètres développeurs",
|
|
||||||
"developer_settings_label_description": "Affiche les paramètres développeurs dans la fenêtre des paramètres.",
|
|
||||||
"developer_tab_title": "Développeur",
|
"developer_tab_title": "Développeur",
|
||||||
"feedback_tab_body": "Si vous rencontrez des problèmes, ou vous voulez simplement faire un commentaire, faites-en une courte description ci-dessous.",
|
"feedback_tab_body": "Si vous rencontrez des problèmes, ou vous voulez simplement faire un commentaire, faites-en une courte description ci-dessous.",
|
||||||
"feedback_tab_description_label": "Votre commentaire",
|
"feedback_tab_description_label": "Votre commentaire",
|
||||||
@@ -104,7 +102,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Inclure les journaux de débogage",
|
"feedback_tab_send_logs_label": "Inclure les journaux de débogage",
|
||||||
"feedback_tab_thank_you": "Merci, nous avons reçu vos commentaires !",
|
"feedback_tab_thank_you": "Merci, nous avons reçu vos commentaires !",
|
||||||
"feedback_tab_title": "Commentaires",
|
"feedback_tab_title": "Commentaires",
|
||||||
"more_tab_title": "Plus",
|
|
||||||
"opt_in_description": "<0></0><1></1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de l’appel.",
|
"opt_in_description": "<0></0><1></1>Vous pouvez retirer votre consentement en décochant cette case. Si vous êtes actuellement en communication, ce paramètre prendra effet à la fin de l’appel.",
|
||||||
"speaker_device_selection_label": "Intervenant"
|
"speaker_device_selection_label": "Intervenant"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -96,8 +96,6 @@
|
|||||||
"room_auth_view_eula_caption": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2> kami",
|
"room_auth_view_eula_caption": "Dengan mengeklik \"Bergabung ke panggilan sekarang\", Anda menyetujui <2>Perjanjian Lisensi Pengguna Akhir (EULA)</2> kami",
|
||||||
"screenshare_button_label": "Bagikan layar",
|
"screenshare_button_label": "Bagikan layar",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Pengaturan Pengembang",
|
|
||||||
"developer_settings_label_description": "Ekspos pengaturan pengembang dalam jendela pengaturan.",
|
|
||||||
"developer_tab_title": "Pengembang",
|
"developer_tab_title": "Pengembang",
|
||||||
"feedback_tab_body": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.",
|
"feedback_tab_body": "Jika Anda mengalami masalah atau hanya ingin memberikan masukan, silakan kirimkan kami deskripsi pendek di bawah.",
|
||||||
"feedback_tab_description_label": "Masukan Anda",
|
"feedback_tab_description_label": "Masukan Anda",
|
||||||
@@ -105,7 +103,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Termasuk catatan pengawakutuan",
|
"feedback_tab_send_logs_label": "Termasuk catatan pengawakutuan",
|
||||||
"feedback_tab_thank_you": "Terima kasih, kami telah menerima masukan Anda!",
|
"feedback_tab_thank_you": "Terima kasih, kami telah menerima masukan Anda!",
|
||||||
"feedback_tab_title": "Masukan",
|
"feedback_tab_title": "Masukan",
|
||||||
"more_tab_title": "Lainnya",
|
|
||||||
"opt_in_description": "<0></0><1></1>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan.",
|
"opt_in_description": "<0></0><1></1>Anda dapat mengurungkan kembali izin dengan mencentang kotak ini. Jika Anda saat ini dalam panggilan, pengaturan ini akan diterapkan di akhir panggilan.",
|
||||||
"speaker_device_selection_label": "Pembicara"
|
"speaker_device_selection_label": "Pembicara"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -94,15 +94,12 @@
|
|||||||
"room_auth_view_eula_caption": "Cliccando \"Entra in chiamata ora\", accetti il nostro <2>accordo di licenza con l'utente finale (EULA)</2>",
|
"room_auth_view_eula_caption": "Cliccando \"Entra in chiamata ora\", accetti il nostro <2>accordo di licenza con l'utente finale (EULA)</2>",
|
||||||
"screenshare_button_label": "Condividi schermo",
|
"screenshare_button_label": "Condividi schermo",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Impostazioni per sviluppatori",
|
|
||||||
"developer_settings_label_description": "Mostra le impostazioni per sviluppatori nella finestra delle impostazioni.",
|
|
||||||
"developer_tab_title": "Sviluppatore",
|
"developer_tab_title": "Sviluppatore",
|
||||||
"feedback_tab_body": "Se stai riscontrando problemi o semplicemente vuoi dare un'opinione, inviaci una breve descrizione qua sotto.",
|
"feedback_tab_body": "Se stai riscontrando problemi o semplicemente vuoi dare un'opinione, inviaci una breve descrizione qua sotto.",
|
||||||
"feedback_tab_description_label": "Il tuo commento",
|
"feedback_tab_description_label": "Il tuo commento",
|
||||||
"feedback_tab_h4": "Invia commento",
|
"feedback_tab_h4": "Invia commento",
|
||||||
"feedback_tab_send_logs_label": "Includi registri di debug",
|
"feedback_tab_send_logs_label": "Includi registri di debug",
|
||||||
"feedback_tab_thank_you": "Grazie, abbiamo ricevuto il tuo commento!",
|
"feedback_tab_thank_you": "Grazie, abbiamo ricevuto il tuo commento!",
|
||||||
"more_tab_title": "Altro",
|
|
||||||
"opt_in_description": "<0></0><1></1>Puoi revocare il consenso deselezionando questa casella. Se attualmente sei in una chiamata, avrà effetto al termine di essa.",
|
"opt_in_description": "<0></0><1></1>Puoi revocare il consenso deselezionando questa casella. Se attualmente sei in una chiamata, avrà effetto al termine di essa.",
|
||||||
"speaker_device_selection_label": "Altoparlante"
|
"speaker_device_selection_label": "Altoparlante"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -75,8 +75,6 @@
|
|||||||
"room_auth_view_eula_caption": "Klikšķināšana uz \"Pievienoties zvanam tagad\" apliecina piekrišanu mūsu <2>galalietotāja licencēšanas nolīgumam (GLLN)</2>",
|
"room_auth_view_eula_caption": "Klikšķināšana uz \"Pievienoties zvanam tagad\" apliecina piekrišanu mūsu <2>galalietotāja licencēšanas nolīgumam (GLLN)</2>",
|
||||||
"screenshare_button_label": "Kopīgot ekrānu",
|
"screenshare_button_label": "Kopīgot ekrānu",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Izstrādātāja iestatījumi",
|
|
||||||
"developer_settings_label_description": "Izstādīt izstrādātāja iestatījumus iestatījumu logā.",
|
|
||||||
"developer_tab_title": "Izstrādātājs",
|
"developer_tab_title": "Izstrādātājs",
|
||||||
"feedback_tab_body": "Ja tiek piedzīvoti sarežģījumi vai vienkārši ir vēlme sniegt kādu atsauksmi, lūgums zemāk nosūtīt mums īsu aprakstu.",
|
"feedback_tab_body": "Ja tiek piedzīvoti sarežģījumi vai vienkārši ir vēlme sniegt kādu atsauksmi, lūgums zemāk nosūtīt mums īsu aprakstu.",
|
||||||
"feedback_tab_description_label": "Tava atsauksme",
|
"feedback_tab_description_label": "Tava atsauksme",
|
||||||
@@ -84,7 +82,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Iekļaut atkļūdošanas žurnāla ierakstus",
|
"feedback_tab_send_logs_label": "Iekļaut atkļūdošanas žurnāla ierakstus",
|
||||||
"feedback_tab_thank_you": "Paldies, mēs saņēmām atsauksmi!",
|
"feedback_tab_thank_you": "Paldies, mēs saņēmām atsauksmi!",
|
||||||
"feedback_tab_title": "Atsauksmes",
|
"feedback_tab_title": "Atsauksmes",
|
||||||
"more_tab_title": "Vairāk",
|
|
||||||
"opt_in_description": "<0></0><1></1>Savu piekrišanu var atsaukt ar atzīmes noņemšanu no šīs rūtiņas. Ja pašreiz atrodies zvanā, šis iestatījums stāsies spēkā zvana beigās.",
|
"opt_in_description": "<0></0><1></1>Savu piekrišanu var atsaukt ar atzīmes noņemšanu no šīs rūtiņas. Ja pašreiz atrodies zvanā, šis iestatījums stāsies spēkā zvana beigās.",
|
||||||
"speaker_device_selection_label": "Runātājs"
|
"speaker_device_selection_label": "Runātājs"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -99,8 +99,6 @@
|
|||||||
"room_auth_view_eula_caption": "Klikając \"Dołącz teraz do rozmowy\", zgadzasz się na naszą <2>Umowę licencyjną (EULA)</2>",
|
"room_auth_view_eula_caption": "Klikając \"Dołącz teraz do rozmowy\", zgadzasz się na naszą <2>Umowę licencyjną (EULA)</2>",
|
||||||
"screenshare_button_label": "Udostępnij ekran",
|
"screenshare_button_label": "Udostępnij ekran",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Opcje programisty",
|
|
||||||
"developer_settings_label_description": "Wyświetl opcje programisty w oknie ustawień.",
|
|
||||||
"developer_tab_title": "Programista",
|
"developer_tab_title": "Programista",
|
||||||
"feedback_tab_body": "Jeśli posiadasz problemy lub chciałbyś zgłosić swoją opinię, wyślij nam krótki opis.",
|
"feedback_tab_body": "Jeśli posiadasz problemy lub chciałbyś zgłosić swoją opinię, wyślij nam krótki opis.",
|
||||||
"feedback_tab_description_label": "Twoje opinie",
|
"feedback_tab_description_label": "Twoje opinie",
|
||||||
@@ -108,7 +106,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Dołącz dzienniki debugowania",
|
"feedback_tab_send_logs_label": "Dołącz dzienniki debugowania",
|
||||||
"feedback_tab_thank_you": "Dziękujemy, otrzymaliśmy Twoją opinię!",
|
"feedback_tab_thank_you": "Dziękujemy, otrzymaliśmy Twoją opinię!",
|
||||||
"feedback_tab_title": "Opinia użytkownika",
|
"feedback_tab_title": "Opinia użytkownika",
|
||||||
"more_tab_title": "Więcej",
|
|
||||||
"opt_in_description": "<0></0><1></1>Możesz wycofać swoją zgodę poprzez odznaczenie tego pola. Jeśli już jesteś w trakcie rozmowy, opcja zostanie zastosowana po jej zakończeniu.",
|
"opt_in_description": "<0></0><1></1>Możesz wycofać swoją zgodę poprzez odznaczenie tego pola. Jeśli już jesteś w trakcie rozmowy, opcja zostanie zastosowana po jej zakończeniu.",
|
||||||
"speaker_device_selection_label": "Głośnik"
|
"speaker_device_selection_label": "Głośnik"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -148,8 +148,6 @@
|
|||||||
"effect_volume_description": "Reglați volumul la care reacționează reacțiile și efectele ridicate de mână",
|
"effect_volume_description": "Reglați volumul la care reacționează reacțiile și efectele ridicate de mână",
|
||||||
"effect_volume_label": "Volumul efectului sonor"
|
"effect_volume_label": "Volumul efectului sonor"
|
||||||
},
|
},
|
||||||
"developer_settings_label": "Setări pentru dezvoltatori",
|
|
||||||
"developer_settings_label_description": "Expuneți setările dezvoltatorului în fereastra de setări.",
|
|
||||||
"developer_tab_title": "dezvoltator",
|
"developer_tab_title": "dezvoltator",
|
||||||
"feedback_tab_body": "Dacă întâmpinați probleme sau pur și simplu doriți să oferiți feedback, vă rugăm să ne trimiteți o scurtă descriere mai jos.",
|
"feedback_tab_body": "Dacă întâmpinați probleme sau pur și simplu doriți să oferiți feedback, vă rugăm să ne trimiteți o scurtă descriere mai jos.",
|
||||||
"feedback_tab_description_label": "Feedback-ul tău",
|
"feedback_tab_description_label": "Feedback-ul tău",
|
||||||
@@ -157,19 +155,13 @@
|
|||||||
"feedback_tab_send_logs_label": "Includeți jurnale de depanare",
|
"feedback_tab_send_logs_label": "Includeți jurnale de depanare",
|
||||||
"feedback_tab_thank_you": "Vă mulțumim, am primit feedback-ul dvs.!",
|
"feedback_tab_thank_you": "Vă mulțumim, am primit feedback-ul dvs.!",
|
||||||
"feedback_tab_title": "Feedback",
|
"feedback_tab_title": "Feedback",
|
||||||
"more_tab_title": "Mai mult",
|
|
||||||
"opt_in_description": "<0></0><1></1>Puteți retrage consimțământul debifând această casetă. Dacă sunteți în prezent la un apel, această setare va intra în vigoare la sfârșitul apelului.",
|
"opt_in_description": "<0></0><1></1>Puteți retrage consimțământul debifând această casetă. Dacă sunteți în prezent la un apel, această setare va intra în vigoare la sfârșitul apelului.",
|
||||||
"preferences_tab": {
|
"preferences_tab": {
|
||||||
"reactions_play_sound_description": "Redați un efect sonor atunci când cineva trimite o reacție la un apel.",
|
"reactions_play_sound_description": "Redați un efect sonor atunci când cineva trimite o reacție la un apel.",
|
||||||
"reactions_play_sound_label": "Redați sunete de reacție",
|
"reactions_play_sound_label": "Redați sunete de reacție",
|
||||||
"reactions_show_description": "Afișați o animație atunci când cineva trimite o reacție.",
|
"reactions_show_description": "Afișați o animație atunci când cineva trimite o reacție.",
|
||||||
"reactions_show_label": "Afișați reacțiile",
|
"reactions_show_label": "Afișați reacțiile"
|
||||||
"reactions_title": "Reacții"
|
|
||||||
},
|
},
|
||||||
"preferences_tab_body": "Aici puteți configura opțiuni suplimentare pentru o experiență îmbunătățită",
|
|
||||||
"preferences_tab_h4": "preferinte",
|
|
||||||
"preferences_tab_show_hand_raised_timer_description": "Afișați un cronometru atunci când un participant ridică mâna",
|
|
||||||
"preferences_tab_show_hand_raised_timer_label": "Afișați durata ridicării mâinii",
|
|
||||||
"speaker_device_selection_label": "vorbitor"
|
"speaker_device_selection_label": "vorbitor"
|
||||||
},
|
},
|
||||||
"start_new_call": "Începe un nou apel",
|
"start_new_call": "Începe un nou apel",
|
||||||
|
|||||||
@@ -69,8 +69,6 @@
|
|||||||
"return_home_button": "Вернуться в Начало",
|
"return_home_button": "Вернуться в Начало",
|
||||||
"screenshare_button_label": "Поделиться экраном",
|
"screenshare_button_label": "Поделиться экраном",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Настройки Разработчика",
|
|
||||||
"developer_settings_label_description": "Раскрыть настройки разработчика в окне настроек.",
|
|
||||||
"developer_tab_title": "Разработчику",
|
"developer_tab_title": "Разработчику",
|
||||||
"feedback_tab_body": "Если у вас возникли проблемы или вы просто хотите оставить отзыв, отправьте нам краткое описание ниже.",
|
"feedback_tab_body": "Если у вас возникли проблемы или вы просто хотите оставить отзыв, отправьте нам краткое описание ниже.",
|
||||||
"feedback_tab_description_label": "Ваш отзыв",
|
"feedback_tab_description_label": "Ваш отзыв",
|
||||||
@@ -78,7 +76,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Приложить журнал отладки",
|
"feedback_tab_send_logs_label": "Приложить журнал отладки",
|
||||||
"feedback_tab_thank_you": "Спасибо. Мы получили ваш отзыв!",
|
"feedback_tab_thank_you": "Спасибо. Мы получили ваш отзыв!",
|
||||||
"feedback_tab_title": "Отзыв",
|
"feedback_tab_title": "Отзыв",
|
||||||
"more_tab_title": "Больше",
|
|
||||||
"opt_in_description": "<0></0><1></1>Вы можете отозвать согласие, сняв этот флажок. Если вы в данный момент находитесь в разговоре, эта настройка вступит в силу по окончании разговора.",
|
"opt_in_description": "<0></0><1></1>Вы можете отозвать согласие, сняв этот флажок. Если вы в данный момент находитесь в разговоре, эта настройка вступит в силу по окончании разговора.",
|
||||||
"speaker_device_selection_label": "Динамик"
|
"speaker_device_selection_label": "Динамик"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -97,8 +97,6 @@
|
|||||||
"room_auth_view_eula_caption": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
|
"room_auth_view_eula_caption": "Kliknutím na \"Pripojiť sa k hovoru teraz\" súhlasíte s našou <2>Licenčnou zmluvou s koncovým používateľom (EULA)</2>",
|
||||||
"screenshare_button_label": "Zdieľať obrazovku",
|
"screenshare_button_label": "Zdieľať obrazovku",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Nastavenia pre vývojárov",
|
|
||||||
"developer_settings_label_description": "Zobraziť nastavenia pre vývojárov v okne nastavení.",
|
|
||||||
"developer_tab_title": "Vývojár",
|
"developer_tab_title": "Vývojár",
|
||||||
"feedback_tab_body": "Ak máte problémy alebo jednoducho chcete poskytnúť spätnú väzbu, pošlite nám krátky popis nižšie.",
|
"feedback_tab_body": "Ak máte problémy alebo jednoducho chcete poskytnúť spätnú väzbu, pošlite nám krátky popis nižšie.",
|
||||||
"feedback_tab_description_label": "Vaša spätná väzba",
|
"feedback_tab_description_label": "Vaša spätná väzba",
|
||||||
@@ -106,7 +104,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Zahrnúť záznamy o ladení",
|
"feedback_tab_send_logs_label": "Zahrnúť záznamy o ladení",
|
||||||
"feedback_tab_thank_you": "Ďakujeme, dostali sme vašu spätnú väzbu!",
|
"feedback_tab_thank_you": "Ďakujeme, dostali sme vašu spätnú väzbu!",
|
||||||
"feedback_tab_title": "Spätná väzba",
|
"feedback_tab_title": "Spätná väzba",
|
||||||
"more_tab_title": "Viac",
|
|
||||||
"opt_in_description": "<0></0><1></1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru.",
|
"opt_in_description": "<0></0><1></1>Súhlas môžete odvolať zrušením označenia tohto políčka. Ak práve prebieha hovor, toto nastavenie nadobudne platnosť po skončení hovoru.",
|
||||||
"speaker_device_selection_label": "Reproduktor"
|
"speaker_device_selection_label": "Reproduktor"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,8 +52,7 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"developer_tab_title": "Geliştirici",
|
"developer_tab_title": "Geliştirici",
|
||||||
"feedback_tab_h4": "Geri bildirim ver",
|
"feedback_tab_h4": "Geri bildirim ver",
|
||||||
"feedback_tab_send_logs_label": "Hata ayıklama kütüğünü dahil et",
|
"feedback_tab_send_logs_label": "Hata ayıklama kütüğünü dahil et"
|
||||||
"more_tab_title": "Daha"
|
|
||||||
},
|
},
|
||||||
"unauthenticated_view_body": "Kaydolmadınız mı? <2>Hesap açın</2>",
|
"unauthenticated_view_body": "Kaydolmadınız mı? <2>Hesap açın</2>",
|
||||||
"unauthenticated_view_login_button": "Hesabınıza girin"
|
"unauthenticated_view_login_button": "Hesabınıza girin"
|
||||||
|
|||||||
@@ -99,8 +99,6 @@
|
|||||||
"room_auth_view_eula_caption": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
|
"room_auth_view_eula_caption": "Натискаючи \"Приєднатися до виклику зараз\", ви погоджуєтеся з нашою <2>Ліцензійною угодою з кінцевим користувачем (EULA)</2>",
|
||||||
"screenshare_button_label": "Поділитися екраном",
|
"screenshare_button_label": "Поділитися екраном",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Налаштування розробника",
|
|
||||||
"developer_settings_label_description": "Відкрийте налаштування розробника у вікні налаштувань.",
|
|
||||||
"developer_tab_title": "Розробнику",
|
"developer_tab_title": "Розробнику",
|
||||||
"feedback_tab_body": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.",
|
"feedback_tab_body": "Якщо у вас виникли проблеми або ви просто хочете залишити відгук, надішліть нам короткий опис нижче.",
|
||||||
"feedback_tab_description_label": "Ваш відгук",
|
"feedback_tab_description_label": "Ваш відгук",
|
||||||
@@ -108,7 +106,6 @@
|
|||||||
"feedback_tab_send_logs_label": "Долучити журнали налагодження",
|
"feedback_tab_send_logs_label": "Долучити журнали налагодження",
|
||||||
"feedback_tab_thank_you": "Дякуємо, ми отримали ваш відгук!",
|
"feedback_tab_thank_you": "Дякуємо, ми отримали ваш відгук!",
|
||||||
"feedback_tab_title": "Відгук",
|
"feedback_tab_title": "Відгук",
|
||||||
"more_tab_title": "Докладніше",
|
|
||||||
"opt_in_description": "<0></0><1></1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.",
|
"opt_in_description": "<0></0><1></1>Ви можете відкликати згоду, прибравши цей прапорець. Якщо ви зараз розмовляєте, це налаштування застосується після завершення виклику.",
|
||||||
"speaker_device_selection_label": "Динамік"
|
"speaker_device_selection_label": "Динамік"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
"register_confirm_password_label": "Xác nhận mật khẩu",
|
"register_confirm_password_label": "Xác nhận mật khẩu",
|
||||||
"screenshare_button_label": "Chia sẻ màn hình",
|
"screenshare_button_label": "Chia sẻ màn hình",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "Cài đặt phát triển",
|
|
||||||
"developer_tab_title": "Nhà phát triển",
|
"developer_tab_title": "Nhà phát triển",
|
||||||
"feedback_tab_description_label": "Phản hồi của bạn",
|
"feedback_tab_description_label": "Phản hồi của bạn",
|
||||||
"feedback_tab_h4": "Gửi phản hồi",
|
"feedback_tab_h4": "Gửi phản hồi",
|
||||||
|
|||||||
@@ -92,8 +92,6 @@
|
|||||||
"room_auth_view_eula_caption": "点击 \"加入通话\",即表示您同意我们的<2>最终用户许可协议 (EULA)</2>",
|
"room_auth_view_eula_caption": "点击 \"加入通话\",即表示您同意我们的<2>最终用户许可协议 (EULA)</2>",
|
||||||
"screenshare_button_label": "屏幕共享",
|
"screenshare_button_label": "屏幕共享",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "开发者设置",
|
|
||||||
"developer_settings_label_description": "在设置中显示开发者设置。",
|
|
||||||
"developer_tab_title": "开发者",
|
"developer_tab_title": "开发者",
|
||||||
"feedback_tab_body": "如果遇到问题或想提供一些反馈意见,请在下面向我们发送简短描述。",
|
"feedback_tab_body": "如果遇到问题或想提供一些反馈意见,请在下面向我们发送简短描述。",
|
||||||
"feedback_tab_description_label": "您的反馈",
|
"feedback_tab_description_label": "您的反馈",
|
||||||
@@ -101,7 +99,6 @@
|
|||||||
"feedback_tab_send_logs_label": "包含调试日志",
|
"feedback_tab_send_logs_label": "包含调试日志",
|
||||||
"feedback_tab_thank_you": "谢谢,我们收到了反馈!",
|
"feedback_tab_thank_you": "谢谢,我们收到了反馈!",
|
||||||
"feedback_tab_title": "反馈",
|
"feedback_tab_title": "反馈",
|
||||||
"more_tab_title": "更多",
|
|
||||||
"opt_in_description": "<0></0><1></1>您可以取消选中复选框来撤回同意。如果正在通话中,此设置将在通话结束时生效。",
|
"opt_in_description": "<0></0><1></1>您可以取消选中复选框来撤回同意。如果正在通话中,此设置将在通话结束时生效。",
|
||||||
"speaker_device_selection_label": "发言人"
|
"speaker_device_selection_label": "发言人"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -99,8 +99,6 @@
|
|||||||
"room_auth_view_eula_caption": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
|
"room_auth_view_eula_caption": "點擊「立刻加入通話」即表示您同意我們的<2>終端使用者授權協議 (EULA)</2>",
|
||||||
"screenshare_button_label": "分享畫面",
|
"screenshare_button_label": "分享畫面",
|
||||||
"settings": {
|
"settings": {
|
||||||
"developer_settings_label": "開發者設定",
|
|
||||||
"developer_settings_label_description": "在設定視窗中顯示開發者設定。",
|
|
||||||
"developer_tab_title": "開發者",
|
"developer_tab_title": "開發者",
|
||||||
"feedback_tab_body": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。",
|
"feedback_tab_body": "若您遇到問題或只是想提供一些回饋,請在下方傳送簡短說明給我們。",
|
||||||
"feedback_tab_description_label": "您的回饋",
|
"feedback_tab_description_label": "您的回饋",
|
||||||
@@ -108,7 +106,6 @@
|
|||||||
"feedback_tab_send_logs_label": "包含除錯紀錄",
|
"feedback_tab_send_logs_label": "包含除錯紀錄",
|
||||||
"feedback_tab_thank_you": "感謝,我們已經收到您的回饋了!",
|
"feedback_tab_thank_you": "感謝,我們已經收到您的回饋了!",
|
||||||
"feedback_tab_title": "回饋",
|
"feedback_tab_title": "回饋",
|
||||||
"more_tab_title": "更多",
|
|
||||||
"opt_in_description": "<0></0><1></1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。",
|
"opt_in_description": "<0></0><1></1>您可以透過取消核取此方塊來撤回同意。若您目前正在通話中,此設定將在通話結束時生效。",
|
||||||
"speaker_device_selection_label": "發言者"
|
"speaker_device_selection_label": "發言者"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test, afterEach } from "vitest";
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { type ReactNode, useState } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
import { afterEach } from "node:test";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { Modal } from "./Modal";
|
import { Modal } from "./Modal";
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ export class PosthogAnalytics {
|
|||||||
// * When the user changes their preferences on this device
|
// * When the user changes their preferences on this device
|
||||||
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
|
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
|
||||||
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
|
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
|
||||||
optInAnalytics.value.subscribe((optIn) => {
|
optInAnalytics.value$.subscribe((optIn) => {
|
||||||
this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled);
|
this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled);
|
||||||
this.maybeIdentifyUser().catch(() =>
|
this.maybeIdentifyUser().catch(() =>
|
||||||
logger.log("Could not identify user"),
|
logger.log("Could not identify user"),
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ export interface Controls {
|
|||||||
disablePip: () => void;
|
disablePip: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setPipEnabled = new Subject<boolean>();
|
export const setPipEnabled$ = new Subject<boolean>();
|
||||||
|
|
||||||
window.controls = {
|
window.controls = {
|
||||||
canEnterPip(): boolean {
|
canEnterPip(): boolean {
|
||||||
return setPipEnabled.observed;
|
return setPipEnabled$.observed;
|
||||||
},
|
},
|
||||||
enablePip(): void {
|
enablePip(): void {
|
||||||
if (!setPipEnabled.observed) throw new Error("No call is running");
|
if (!setPipEnabled$.observed) throw new Error("No call is running");
|
||||||
setPipEnabled.next(true);
|
setPipEnabled$.next(true);
|
||||||
},
|
},
|
||||||
disablePip(): void {
|
disablePip(): void {
|
||||||
if (!setPipEnabled.observed) throw new Error("No call is running");
|
if (!setPipEnabled$.observed) throw new Error("No call is running");
|
||||||
setPipEnabled.next(false);
|
setPipEnabled$.next(false);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,15 +31,15 @@ export interface CallLayoutInputs {
|
|||||||
/**
|
/**
|
||||||
* The minimum bounds of the layout area.
|
* The minimum bounds of the layout area.
|
||||||
*/
|
*/
|
||||||
minBounds: Observable<Bounds>;
|
minBounds$: Observable<Bounds>;
|
||||||
/**
|
/**
|
||||||
* The alignment of the floating spotlight tile, if present.
|
* The alignment of the floating spotlight tile, if present.
|
||||||
*/
|
*/
|
||||||
spotlightAlignment: BehaviorSubject<Alignment>;
|
spotlightAlignment$: BehaviorSubject<Alignment>;
|
||||||
/**
|
/**
|
||||||
* The alignment of the small picture-in-picture tile, if present.
|
* The alignment of the small picture-in-picture tile, if present.
|
||||||
*/
|
*/
|
||||||
pipAlignment: BehaviorSubject<Alignment>;
|
pipAlignment$: BehaviorSubject<Alignment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CallLayoutOutputs<Model> {
|
export interface CallLayoutOutputs<Model> {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
createContext,
|
createContext,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -54,7 +53,6 @@ interface Tile<Model> {
|
|||||||
id: string;
|
id: string;
|
||||||
model: Model;
|
model: Model;
|
||||||
onDrag: DragCallback | undefined;
|
onDrag: DragCallback | undefined;
|
||||||
setVisible: (visible: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlacedTile<Model> = Tile<Model> & Rect;
|
type PlacedTile<Model> = Tile<Model> & Rect;
|
||||||
@@ -88,7 +86,6 @@ interface SlotProps<Model> extends Omit<ComponentProps<"div">, "onDrag"> {
|
|||||||
id: string;
|
id: string;
|
||||||
model: Model;
|
model: Model;
|
||||||
onDrag?: DragCallback;
|
onDrag?: DragCallback;
|
||||||
onVisibilityChange?: (visible: boolean) => void;
|
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -115,28 +112,51 @@ function offset(element: HTMLElement, relativeTo: Element): Offset {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VisibleTilesCallback = (visibleTiles: number) => void;
|
||||||
|
|
||||||
interface LayoutContext {
|
interface LayoutContext {
|
||||||
setGeneration: Dispatch<SetStateAction<number | null>>;
|
setGeneration: Dispatch<SetStateAction<number | null>>;
|
||||||
|
setVisibleTilesCallback: Dispatch<
|
||||||
|
SetStateAction<VisibleTilesCallback | null>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LayoutContext = createContext<LayoutContext | null>(null);
|
const LayoutContext = createContext<LayoutContext | null>(null);
|
||||||
|
|
||||||
|
function useLayoutContext(): LayoutContext {
|
||||||
|
const context = useContext(LayoutContext);
|
||||||
|
if (context === null)
|
||||||
|
throw new Error("useUpdateLayout called outside a Grid layout context");
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables Grid to react to layout changes. You must call this in your Layout
|
* Enables Grid to react to layout changes. You must call this in your Layout
|
||||||
* component or else Grid will not be reactive.
|
* component or else Grid will not be reactive.
|
||||||
*/
|
*/
|
||||||
export function useUpdateLayout(): void {
|
export function useUpdateLayout(): void {
|
||||||
const context = useContext(LayoutContext);
|
const { setGeneration } = useLayoutContext();
|
||||||
if (context === null)
|
|
||||||
throw new Error("useUpdateLayout called outside a Grid layout context");
|
|
||||||
|
|
||||||
// On every render, tell Grid that the layout may have changed
|
// On every render, tell Grid that the layout may have changed
|
||||||
useEffect(() =>
|
useEffect(() => setGeneration((prev) => (prev === null ? 0 : prev + 1)));
|
||||||
context.setGeneration((prev) => (prev === null ? 0 : prev + 1)),
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks Grid to call a callback whenever the number of visible tiles may have
|
||||||
|
* changed.
|
||||||
|
*/
|
||||||
|
export function useVisibleTiles(callback: VisibleTilesCallback): void {
|
||||||
|
const { setVisibleTilesCallback } = useLayoutContext();
|
||||||
|
useEffect(
|
||||||
|
() => setVisibleTilesCallback(() => callback),
|
||||||
|
[callback, setVisibleTilesCallback],
|
||||||
|
);
|
||||||
|
useEffect(
|
||||||
|
() => (): void => setVisibleTilesCallback(null),
|
||||||
|
[setVisibleTilesCallback],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowHeightObservable = fromEvent(window, "resize").pipe(
|
const windowHeightObservable$ = fromEvent(window, "resize").pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
map(() => window.innerHeight),
|
map(() => window.innerHeight),
|
||||||
);
|
);
|
||||||
@@ -242,42 +262,23 @@ export function Grid<
|
|||||||
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
|
const [gridRoot, gridRef2] = useState<HTMLElement | null>(null);
|
||||||
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);
|
const gridRef = useMergedRefs<HTMLElement>(gridRef1, gridRef2);
|
||||||
|
|
||||||
const windowHeight = useObservableEagerState(windowHeightObservable);
|
const windowHeight = useObservableEagerState(windowHeightObservable$);
|
||||||
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
|
const [layoutRoot, setLayoutRoot] = useState<HTMLElement | null>(null);
|
||||||
const [generation, setGeneration] = useState<number | null>(null);
|
const [generation, setGeneration] = useState<number | null>(null);
|
||||||
|
const [visibleTilesCallback, setVisibleTilesCallback] =
|
||||||
|
useState<VisibleTilesCallback | null>(null);
|
||||||
const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
|
const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
|
||||||
const prefersReducedMotion = usePrefersReducedMotion();
|
const prefersReducedMotion = usePrefersReducedMotion();
|
||||||
|
|
||||||
const Slot: FC<SlotProps<TileModel>> = useMemo(
|
const Slot: FC<SlotProps<TileModel>> = useMemo(
|
||||||
() =>
|
() =>
|
||||||
function Slot({
|
function Slot({ id, model, onDrag, style, className, ...props }) {
|
||||||
id,
|
|
||||||
model,
|
|
||||||
onDrag,
|
|
||||||
onVisibilityChange,
|
|
||||||
style,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const prevVisible = useRef<boolean | null>(null);
|
|
||||||
const setVisible = useCallback(
|
|
||||||
(visible: boolean) => {
|
|
||||||
if (
|
|
||||||
onVisibilityChange !== undefined &&
|
|
||||||
visible !== prevVisible.current
|
|
||||||
) {
|
|
||||||
onVisibilityChange(visible);
|
|
||||||
prevVisible.current = visible;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onVisibilityChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
tiles.set(id, { id, model, onDrag, setVisible });
|
tiles.set(id, { id, model, onDrag });
|
||||||
return (): void => void tiles.delete(id);
|
return (): void => void tiles.delete(id);
|
||||||
}, [id, model, onDrag, setVisible]);
|
}, [id, model, onDrag]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -307,7 +308,10 @@ export function Grid<
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const context: LayoutContext = useMemo(() => ({ setGeneration }), []);
|
const context: LayoutContext = useMemo(
|
||||||
|
() => ({ setGeneration, setVisibleTilesCallback }),
|
||||||
|
[setVisibleTilesCallback],
|
||||||
|
);
|
||||||
|
|
||||||
// Combine the tile definitions and slots together to create placed tiles
|
// Combine the tile definitions and slots together to create placed tiles
|
||||||
const placedTiles = useMemo(() => {
|
const placedTiles = useMemo(() => {
|
||||||
@@ -342,9 +346,11 @@ export function Grid<
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const tile of placedTiles)
|
visibleTilesCallback?.(
|
||||||
tile.setVisible(tile.y + tile.height <= visibleHeight);
|
placedTiles.filter((tile) => tile.y + tile.height <= visibleHeight)
|
||||||
}, [placedTiles, visibleHeight]);
|
.length,
|
||||||
|
);
|
||||||
|
}, [placedTiles, visibleTilesCallback, visibleHeight]);
|
||||||
|
|
||||||
// Drag state is stored in a ref rather than component state, because we use
|
// Drag state is stored in a ref rather than component state, because we use
|
||||||
// react-spring's imperative API during gestures to improve responsiveness
|
// react-spring's imperative API during gestures to improve responsiveness
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { type GridLayout as GridLayoutModel } from "../state/CallViewModel";
|
|||||||
import styles from "./GridLayout.module.css";
|
import styles from "./GridLayout.module.css";
|
||||||
import { useInitial } from "../useInitial";
|
import { useInitial } from "../useInitial";
|
||||||
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
||||||
import { type DragCallback, useUpdateLayout } from "./Grid";
|
import { type DragCallback, useUpdateLayout, useVisibleTiles } from "./Grid";
|
||||||
|
|
||||||
interface GridCSSProperties extends CSSProperties {
|
interface GridCSSProperties extends CSSProperties {
|
||||||
"--gap": string;
|
"--gap": string;
|
||||||
@@ -26,8 +26,8 @@ interface GridCSSProperties extends CSSProperties {
|
|||||||
* together in a scrolling grid.
|
* together in a scrolling grid.
|
||||||
*/
|
*/
|
||||||
export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
||||||
minBounds,
|
minBounds$,
|
||||||
spotlightAlignment,
|
spotlightAlignment$,
|
||||||
}) => ({
|
}) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const alignment = useObservableEagerState(
|
const alignment = useObservableEagerState(
|
||||||
useInitial(() =>
|
useInitial(() =>
|
||||||
spotlightAlignment.pipe(
|
spotlightAlignment$.pipe(
|
||||||
distinctUntilChanged(
|
distinctUntilChanged(
|
||||||
(a1, a2) => a1.block === a2.block && a1.inline === a2.inline,
|
(a1, a2) => a1.block === a2.block && a1.inline === a2.inline,
|
||||||
),
|
),
|
||||||
@@ -47,7 +47,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
|
|
||||||
const onDragSpotlight: DragCallback = useCallback(
|
const onDragSpotlight: DragCallback = useCallback(
|
||||||
({ xRatio, yRatio }) =>
|
({ xRatio, yRatio }) =>
|
||||||
spotlightAlignment.next({
|
spotlightAlignment$.next({
|
||||||
block: yRatio < 0.5 ? "start" : "end",
|
block: yRatio < 0.5 ? "start" : "end",
|
||||||
inline: xRatio < 0.5 ? "start" : "end",
|
inline: xRatio < 0.5 ? "start" : "end",
|
||||||
}),
|
}),
|
||||||
@@ -73,7 +73,8 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
// The scrolling part of the layout is where all the grid tiles live
|
// The scrolling part of the layout is where all the grid tiles live
|
||||||
scrolling: forwardRef(function GridLayout({ model, Slot }, ref) {
|
scrolling: forwardRef(function GridLayout({ model, Slot }, ref) {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const { width, height: minHeight } = useObservableEagerState(minBounds);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
|
const { width, height: minHeight } = useObservableEagerState(minBounds$);
|
||||||
const { gap, tileWidth, tileHeight } = useMemo(
|
const { gap, tileWidth, tileHeight } = useMemo(
|
||||||
() => arrangeTiles(width, minHeight, model.grid.length),
|
() => arrangeTiles(width, minHeight, model.grid.length),
|
||||||
[width, minHeight, model.grid.length],
|
[width, minHeight, model.grid.length],
|
||||||
@@ -93,13 +94,7 @@ export const makeGridLayout: CallLayout<GridLayoutModel> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{model.grid.map((m) => (
|
{model.grid.map((m) => (
|
||||||
<Slot
|
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />
|
||||||
key={m.id}
|
|
||||||
className={styles.slot}
|
|
||||||
id={m.id}
|
|
||||||
model={m}
|
|
||||||
onVisibilityChange={m.setVisible}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import { type DragCallback, useUpdateLayout } from "./Grid";
|
|||||||
* is shown at maximum size, overlaid by a small view of the local participant.
|
* is shown at maximum size, overlaid by a small view of the local participant.
|
||||||
*/
|
*/
|
||||||
export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
||||||
minBounds,
|
minBounds$,
|
||||||
pipAlignment,
|
pipAlignment$,
|
||||||
}) => ({
|
}) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
|
|
||||||
scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) {
|
scrolling: forwardRef(function OneOnOneLayoutScrolling({ model, Slot }, ref) {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const { width, height } = useObservableEagerState(minBounds);
|
const { width, height } = useObservableEagerState(minBounds$);
|
||||||
const pipAlignmentValue = useObservableEagerState(pipAlignment);
|
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
|
||||||
const { tileWidth, tileHeight } = useMemo(
|
const { tileWidth, tileHeight } = useMemo(
|
||||||
() => arrangeTiles(width, height, 1),
|
() => arrangeTiles(width, height, 1),
|
||||||
[width, height],
|
[width, height],
|
||||||
@@ -40,7 +40,7 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
|
|
||||||
const onDragLocalTile: DragCallback = useCallback(
|
const onDragLocalTile: DragCallback = useCallback(
|
||||||
({ xRatio, yRatio }) =>
|
({ xRatio, yRatio }) =>
|
||||||
pipAlignment.next({
|
pipAlignment$.next({
|
||||||
block: yRatio < 0.5 ? "start" : "end",
|
block: yRatio < 0.5 ? "start" : "end",
|
||||||
inline: xRatio < 0.5 ? "start" : "end",
|
inline: xRatio < 0.5 ? "start" : "end",
|
||||||
}),
|
}),
|
||||||
@@ -52,7 +52,6 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
<Slot
|
<Slot
|
||||||
id={model.remote.id}
|
id={model.remote.id}
|
||||||
model={model.remote}
|
model={model.remote}
|
||||||
onVisibilityChange={model.remote.setVisible}
|
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
style={{ width: tileWidth, height: tileHeight }}
|
style={{ width: tileWidth, height: tileHeight }}
|
||||||
>
|
>
|
||||||
@@ -61,7 +60,6 @@ export const makeOneOnOneLayout: CallLayout<OneOnOneLayoutModel> = ({
|
|||||||
id={model.local.id}
|
id={model.local.id}
|
||||||
model={model.local}
|
model={model.local}
|
||||||
onDrag={onDragLocalTile}
|
onDrag={onDragLocalTile}
|
||||||
onVisibilityChange={model.local.setVisible}
|
|
||||||
data-block-alignment={pipAlignmentValue.block}
|
data-block-alignment={pipAlignmentValue.block}
|
||||||
data-inline-alignment={pipAlignmentValue.inline}
|
data-inline-alignment={pipAlignmentValue.inline}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import styles from "./SpotlightExpandedLayout.module.css";
|
|||||||
*/
|
*/
|
||||||
export const makeSpotlightExpandedLayout: CallLayout<
|
export const makeSpotlightExpandedLayout: CallLayout<
|
||||||
SpotlightExpandedLayoutModel
|
SpotlightExpandedLayoutModel
|
||||||
> = ({ pipAlignment }) => ({
|
> = ({ pipAlignment$ }) => ({
|
||||||
scrollingOnTop: true,
|
scrollingOnTop: true,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightExpandedLayoutFixed(
|
fixed: forwardRef(function SpotlightExpandedLayoutFixed(
|
||||||
@@ -44,11 +44,11 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const pipAlignmentValue = useObservableEagerState(pipAlignment);
|
const pipAlignmentValue = useObservableEagerState(pipAlignment$);
|
||||||
|
|
||||||
const onDragPip: DragCallback = useCallback(
|
const onDragPip: DragCallback = useCallback(
|
||||||
({ xRatio, yRatio }) =>
|
({ xRatio, yRatio }) =>
|
||||||
pipAlignment.next({
|
pipAlignment$.next({
|
||||||
block: yRatio < 0.5 ? "start" : "end",
|
block: yRatio < 0.5 ? "start" : "end",
|
||||||
inline: xRatio < 0.5 ? "start" : "end",
|
inline: xRatio < 0.5 ? "start" : "end",
|
||||||
}),
|
}),
|
||||||
@@ -63,7 +63,6 @@ export const makeSpotlightExpandedLayout: CallLayout<
|
|||||||
id={model.pip.id}
|
id={model.pip.id}
|
||||||
model={model.pip}
|
model={model.pip}
|
||||||
onDrag={onDragPip}
|
onDrag={onDragPip}
|
||||||
onVisibilityChange={model.pip.setVisible}
|
|
||||||
data-block-alignment={pipAlignmentValue.block}
|
data-block-alignment={pipAlignmentValue.block}
|
||||||
data-inline-alignment={pipAlignmentValue.inline}
|
data-inline-alignment={pipAlignmentValue.inline}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import classNames from "classnames";
|
|||||||
import { type CallLayout } from "./CallLayout";
|
import { type CallLayout } from "./CallLayout";
|
||||||
import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel";
|
import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/CallViewModel";
|
||||||
import styles from "./SpotlightLandscapeLayout.module.css";
|
import styles from "./SpotlightLandscapeLayout.module.css";
|
||||||
import { useUpdateLayout } from "./Grid";
|
import { useUpdateLayout, useVisibleTiles } from "./Grid";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of the "spotlight landscape" layout, in which the spotlight
|
* An implementation of the "spotlight landscape" layout, in which the spotlight
|
||||||
@@ -21,7 +21,7 @@ import { useUpdateLayout } from "./Grid";
|
|||||||
*/
|
*/
|
||||||
export const makeSpotlightLandscapeLayout: CallLayout<
|
export const makeSpotlightLandscapeLayout: CallLayout<
|
||||||
SpotlightLandscapeLayoutModel
|
SpotlightLandscapeLayoutModel
|
||||||
> = ({ minBounds }) => ({
|
> = ({ minBounds$ }) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightLandscapeLayoutFixed(
|
fixed: forwardRef(function SpotlightLandscapeLayoutFixed(
|
||||||
@@ -29,7 +29,7 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useObservableEagerState(minBounds);
|
useObservableEagerState(minBounds$);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={styles.layer}>
|
<div ref={ref} className={styles.layer}>
|
||||||
@@ -50,9 +50,10 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
useObservableEagerState(minBounds);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
|
useObservableEagerState(minBounds$);
|
||||||
const withIndicators =
|
const withIndicators =
|
||||||
useObservableEagerState(model.spotlight.media).length > 1;
|
useObservableEagerState(model.spotlight.media$).length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={styles.layer}>
|
<div ref={ref} className={styles.layer}>
|
||||||
@@ -63,13 +64,7 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
|||||||
/>
|
/>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{model.grid.map((m) => (
|
{model.grid.map((m) => (
|
||||||
<Slot
|
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />
|
||||||
key={m.id}
|
|
||||||
className={styles.slot}
|
|
||||||
id={m.id}
|
|
||||||
model={m}
|
|
||||||
onVisibilityChange={m.setVisible}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import classNames from "classnames";
|
|||||||
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
import { type CallLayout, arrangeTiles } from "./CallLayout";
|
||||||
import { type SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel";
|
import { type SpotlightPortraitLayout as SpotlightPortraitLayoutModel } from "../state/CallViewModel";
|
||||||
import styles from "./SpotlightPortraitLayout.module.css";
|
import styles from "./SpotlightPortraitLayout.module.css";
|
||||||
import { useUpdateLayout } from "./Grid";
|
import { useUpdateLayout, useVisibleTiles } from "./Grid";
|
||||||
|
|
||||||
interface GridCSSProperties extends CSSProperties {
|
interface GridCSSProperties extends CSSProperties {
|
||||||
"--grid-gap": string;
|
"--grid-gap": string;
|
||||||
@@ -27,7 +27,7 @@ interface GridCSSProperties extends CSSProperties {
|
|||||||
*/
|
*/
|
||||||
export const makeSpotlightPortraitLayout: CallLayout<
|
export const makeSpotlightPortraitLayout: CallLayout<
|
||||||
SpotlightPortraitLayoutModel
|
SpotlightPortraitLayoutModel
|
||||||
> = ({ minBounds }) => ({
|
> = ({ minBounds$ }) => ({
|
||||||
scrollingOnTop: false,
|
scrollingOnTop: false,
|
||||||
|
|
||||||
fixed: forwardRef(function SpotlightPortraitLayoutFixed(
|
fixed: forwardRef(function SpotlightPortraitLayoutFixed(
|
||||||
@@ -54,7 +54,8 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
useUpdateLayout();
|
useUpdateLayout();
|
||||||
const { width } = useObservableEagerState(minBounds);
|
useVisibleTiles(model.setVisibleTiles);
|
||||||
|
const { width } = useObservableEagerState(minBounds$);
|
||||||
const { gap, tileWidth, tileHeight } = arrangeTiles(
|
const { gap, tileWidth, tileHeight } = arrangeTiles(
|
||||||
width,
|
width,
|
||||||
// TODO: We pretend that the minimum height is the width, because the
|
// TODO: We pretend that the minimum height is the width, because the
|
||||||
@@ -63,7 +64,7 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
model.grid.length,
|
model.grid.length,
|
||||||
);
|
);
|
||||||
const withIndicators =
|
const withIndicators =
|
||||||
useObservableEagerState(model.spotlight.media).length > 1;
|
useObservableEagerState(model.spotlight.media$).length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -84,13 +85,7 @@ export const makeSpotlightPortraitLayout: CallLayout<
|
|||||||
/>
|
/>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{model.grid.map((m) => (
|
{model.grid.map((m) => (
|
||||||
<Slot
|
<Slot key={m.id} className={styles.slot} id={m.id} model={m} />
|
||||||
key={m.id}
|
|
||||||
className={styles.slot}
|
|
||||||
id={m.id}
|
|
||||||
model={m}
|
|
||||||
onVisibilityChange={m.setVisible}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { createMediaDeviceObserver } from "@livekit/components-core";
|
import { createMediaDeviceObserver } from "@livekit/components-core";
|
||||||
import { type Observable } from "rxjs";
|
import { map, startWith } from "rxjs";
|
||||||
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -26,11 +27,25 @@ import {
|
|||||||
videoInput as videoInputSetting,
|
videoInput as videoInputSetting,
|
||||||
type Setting,
|
type Setting,
|
||||||
} from "../settings/settings";
|
} from "../settings/settings";
|
||||||
import { isFirefox } from "../Platform";
|
|
||||||
|
export type DeviceLabel =
|
||||||
|
| { type: "name"; name: string }
|
||||||
|
| { type: "number"; number: number }
|
||||||
|
| { type: "default"; name: string | null };
|
||||||
|
|
||||||
export interface MediaDevice {
|
export interface MediaDevice {
|
||||||
available: MediaDeviceInfo[];
|
/**
|
||||||
|
* A map from available device IDs to labels.
|
||||||
|
*/
|
||||||
|
available: Map<string, DeviceLabel>;
|
||||||
selectedId: string | undefined;
|
selectedId: string | undefined;
|
||||||
|
/**
|
||||||
|
* The group ID of the selected device.
|
||||||
|
*/
|
||||||
|
// This is exposed sort of ad-hoc because it's only needed for knowing when to
|
||||||
|
// restart the tracks of default input devices, and ideally this behavior
|
||||||
|
// would be encapsulated somehow…
|
||||||
|
selectedGroupId: string | undefined;
|
||||||
select: (deviceId: string) => void;
|
select: (deviceId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,26 +57,10 @@ export interface MediaDevices {
|
|||||||
stopUsingDeviceNames: () => void;
|
stopUsingDeviceNames: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargo-culted from @livekit/components-react
|
|
||||||
function useObservableState<T>(
|
|
||||||
observable: Observable<T> | undefined,
|
|
||||||
startWith: T,
|
|
||||||
): T {
|
|
||||||
const [state, setState] = useState<T>(startWith);
|
|
||||||
useEffect(() => {
|
|
||||||
// observable state doesn't run in SSR
|
|
||||||
if (typeof window === "undefined" || !observable) return;
|
|
||||||
const subscription = observable.subscribe(setState);
|
|
||||||
return (): void => subscription.unsubscribe();
|
|
||||||
}, [observable]);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useMediaDevice(
|
function useMediaDevice(
|
||||||
kind: MediaDeviceKind,
|
kind: MediaDeviceKind,
|
||||||
setting: Setting<string | undefined>,
|
setting: Setting<string | undefined>,
|
||||||
usingNames: boolean,
|
usingNames: boolean,
|
||||||
alwaysDefault: boolean = false,
|
|
||||||
): MediaDevice {
|
): MediaDevice {
|
||||||
// Make sure we don't needlessly reset to a device observer without names,
|
// Make sure we don't needlessly reset to a device observer without names,
|
||||||
// once permissions are already given
|
// once permissions are already given
|
||||||
@@ -75,49 +74,97 @@ function useMediaDevice(
|
|||||||
// useMediaDevices provides no way to request device names.
|
// useMediaDevices provides no way to request device names.
|
||||||
// Tragically, the only way to get device names out of LiveKit is to specify a
|
// Tragically, the only way to get device names out of LiveKit is to specify a
|
||||||
// kind, which then results in multiple permissions requests.
|
// kind, which then results in multiple permissions requests.
|
||||||
const deviceObserver = useMemo(
|
const deviceObserver$ = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createMediaDeviceObserver(
|
createMediaDeviceObserver(
|
||||||
kind,
|
kind,
|
||||||
() => logger.error("Error creating MediaDeviceObserver"),
|
() => logger.error("Error creating MediaDeviceObserver"),
|
||||||
requestPermissions,
|
requestPermissions,
|
||||||
),
|
).pipe(startWith([])),
|
||||||
[kind, requestPermissions],
|
[kind, requestPermissions],
|
||||||
);
|
);
|
||||||
const available = useObservableState(deviceObserver, []);
|
const available = useObservableEagerState(
|
||||||
const [preferredId, select] = useSetting(setting);
|
useMemo(
|
||||||
|
() =>
|
||||||
|
deviceObserver$.pipe(
|
||||||
|
map((availableRaw) => {
|
||||||
|
// Sometimes browsers (particularly Firefox) can return multiple device
|
||||||
|
// entries for the exact same device ID; using a map deduplicates them
|
||||||
|
let available = new Map<string, DeviceLabel>(
|
||||||
|
availableRaw.map((d, i) => [
|
||||||
|
d.deviceId,
|
||||||
|
d.label
|
||||||
|
? { type: "name", name: d.label }
|
||||||
|
: { type: "number", number: i + 1 },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
// Create a virtual default audio output for browsers that don't have one.
|
||||||
|
// Its device ID must be the empty string because that's what setSinkId
|
||||||
|
// recognizes.
|
||||||
|
if (
|
||||||
|
kind === "audiooutput" &&
|
||||||
|
available.size &&
|
||||||
|
!available.has("") &&
|
||||||
|
!available.has("default")
|
||||||
|
)
|
||||||
|
available = new Map([
|
||||||
|
["", { type: "default", name: availableRaw[0]?.label || null }],
|
||||||
|
...available,
|
||||||
|
]);
|
||||||
|
// Note: creating virtual default input devices would be another problem
|
||||||
|
// entirely, because requesting a media stream from deviceId "" won't
|
||||||
|
// automatically track the default device.
|
||||||
|
return available;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[kind, deviceObserver$],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return useMemo(() => {
|
const [preferredId, select] = useSetting(setting);
|
||||||
let selectedId: string | undefined = undefined;
|
const selectedId = useMemo(() => {
|
||||||
if (!alwaysDefault && available) {
|
if (available.size) {
|
||||||
// If the preferred device is available, use it. Or if every available
|
// If the preferred device is available, use it. Or if every available
|
||||||
// device ID is falsy, the browser is probably just being paranoid about
|
// device ID is falsy, the browser is probably just being paranoid about
|
||||||
// fingerprinting and we should still try using the preferred device.
|
// fingerprinting and we should still try using the preferred device.
|
||||||
// Worst case it is not available and the browser will gracefully fall
|
// Worst case it is not available and the browser will gracefully fall
|
||||||
// back to some other device for us when requesting the media stream.
|
// back to some other device for us when requesting the media stream.
|
||||||
// Otherwise, select the first available device.
|
// Otherwise, select the first available device.
|
||||||
selectedId =
|
return (preferredId !== undefined && available.has(preferredId)) ||
|
||||||
available.some((d) => d.deviceId === preferredId) ||
|
(available.size === 1 && available.has(""))
|
||||||
available.every((d) => d.deviceId === "")
|
? preferredId
|
||||||
? preferredId
|
: available.keys().next().value;
|
||||||
: available.at(0)?.deviceId;
|
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [available, preferredId]);
|
||||||
|
const selectedGroupId = useObservableEagerState(
|
||||||
|
useMemo(
|
||||||
|
() =>
|
||||||
|
deviceObserver$.pipe(
|
||||||
|
map(
|
||||||
|
(availableRaw) =>
|
||||||
|
availableRaw.find((d) => d.deviceId === selectedId)?.groupId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[deviceObserver$, selectedId],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return useMemo(
|
||||||
available: available
|
() => ({
|
||||||
? // Sometimes browsers (particularly Firefox) can return multiple
|
available,
|
||||||
// device entries for the exact same device ID; deduplicate them
|
|
||||||
[...new Map(available.map((d) => [d.deviceId, d])).values()]
|
|
||||||
: [],
|
|
||||||
selectedId,
|
selectedId,
|
||||||
|
selectedGroupId,
|
||||||
select,
|
select,
|
||||||
};
|
}),
|
||||||
}, [available, preferredId, select, alwaysDefault]);
|
[available, selectedId, selectedGroupId, select],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deviceStub: MediaDevice = {
|
export const deviceStub: MediaDevice = {
|
||||||
available: [],
|
available: new Map(),
|
||||||
selectedId: undefined,
|
selectedId: undefined,
|
||||||
|
selectedGroupId: undefined,
|
||||||
select: () => {},
|
select: () => {},
|
||||||
};
|
};
|
||||||
export const devicesStub: MediaDevices = {
|
export const devicesStub: MediaDevices = {
|
||||||
@@ -139,15 +186,6 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
|||||||
const [numCallersUsingNames, setNumCallersUsingNames] = useState(0);
|
const [numCallersUsingNames, setNumCallersUsingNames] = useState(0);
|
||||||
const usingNames = numCallersUsingNames > 0;
|
const usingNames = numCallersUsingNames > 0;
|
||||||
|
|
||||||
// Setting the audio device to something other than 'undefined' breaks echo-cancellation
|
|
||||||
// and even can introduce multiple different output devices for one call.
|
|
||||||
const alwaysUseDefaultAudio = isFirefox();
|
|
||||||
|
|
||||||
// On FF we dont need to query the names
|
|
||||||
// (call enumerateDevices + create meadia stream to trigger permissions)
|
|
||||||
// for ouput devices because the selector wont be shown on FF.
|
|
||||||
const useOutputNames = usingNames && !isFirefox();
|
|
||||||
|
|
||||||
const audioInput = useMediaDevice(
|
const audioInput = useMediaDevice(
|
||||||
"audioinput",
|
"audioinput",
|
||||||
audioInputSetting,
|
audioInputSetting,
|
||||||
@@ -156,8 +194,7 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
|||||||
const audioOutput = useMediaDevice(
|
const audioOutput = useMediaDevice(
|
||||||
"audiooutput",
|
"audiooutput",
|
||||||
audioOutputSetting,
|
audioOutputSetting,
|
||||||
useOutputNames,
|
usingNames,
|
||||||
alwaysUseDefaultAudio,
|
|
||||||
);
|
);
|
||||||
const videoInput = useMediaDevice(
|
const videoInput = useMediaDevice(
|
||||||
"videoinput",
|
"videoinput",
|
||||||
|
|||||||
@@ -310,18 +310,14 @@ export function useLiveKit(
|
|||||||
room.localParticipant.audioTrackPublications.values(),
|
room.localParticipant.audioTrackPublications.values(),
|
||||||
).find((d) => d.source === Track.Source.Microphone)?.track;
|
).find((d) => d.source === Track.Source.Microphone)?.track;
|
||||||
|
|
||||||
const defaultDevice = device.available.find(
|
|
||||||
(d) => d.deviceId === "default",
|
|
||||||
);
|
|
||||||
if (
|
if (
|
||||||
defaultDevice &&
|
|
||||||
activeMicTrack &&
|
activeMicTrack &&
|
||||||
// only restart if the stream is still running: LiveKit will detect
|
// only restart if the stream is still running: LiveKit will detect
|
||||||
// when a track stops & restart appropriately, so this is not our job.
|
// when a track stops & restart appropriately, so this is not our job.
|
||||||
// Plus, we need to avoid restarting again if the track is already in
|
// Plus, we need to avoid restarting again if the track is already in
|
||||||
// the process of being restarted.
|
// the process of being restarted.
|
||||||
activeMicTrack.mediaStreamTrack.readyState !== "ended" &&
|
activeMicTrack.mediaStreamTrack.readyState !== "ended" &&
|
||||||
defaultDevice.groupId !==
|
device.selectedGroupId !==
|
||||||
activeMicTrack.mediaStreamTrack.getSettings().groupId
|
activeMicTrack.mediaStreamTrack.getSettings().groupId
|
||||||
) {
|
) {
|
||||||
// It's different, so restart the track, ie. cause Livekit to do another
|
// It's different, so restart the track, ie. cause Livekit to do another
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import {
|
|||||||
type MockedFunction,
|
type MockedFunction,
|
||||||
test,
|
test,
|
||||||
vitest,
|
vitest,
|
||||||
|
afterEach,
|
||||||
} from "vitest";
|
} from "vitest";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { ConnectionState } from "livekit-client";
|
import { ConnectionState } from "livekit-client";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
import { afterEach } from "node:test";
|
|
||||||
import { act, type ReactNode } from "react";
|
import { act, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
@@ -100,13 +100,13 @@ function getMockEnv(
|
|||||||
): {
|
): {
|
||||||
vm: CallViewModel;
|
vm: CallViewModel;
|
||||||
session: MockRTCSession;
|
session: MockRTCSession;
|
||||||
remoteRtcMemberships: BehaviorSubject<CallMembership[]>;
|
remoteRtcMemberships$: BehaviorSubject<CallMembership[]>;
|
||||||
} {
|
} {
|
||||||
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
|
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
|
||||||
const remoteParticipants = of([aliceParticipant]);
|
const remoteParticipants$ = of([aliceParticipant]);
|
||||||
const liveKitRoom = mockLivekitRoom(
|
const liveKitRoom = mockLivekitRoom(
|
||||||
{ localParticipant },
|
{ localParticipant },
|
||||||
{ remoteParticipants },
|
{ remoteParticipants$ },
|
||||||
);
|
);
|
||||||
const matrixRoom = mockMatrixRoom({
|
const matrixRoom = mockMatrixRoom({
|
||||||
client: {
|
client: {
|
||||||
@@ -118,14 +118,14 @@ function getMockEnv(
|
|||||||
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const remoteRtcMemberships = new BehaviorSubject<CallMembership[]>(
|
const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>(
|
||||||
initialRemoteRtcMemberships,
|
initialRemoteRtcMemberships,
|
||||||
);
|
);
|
||||||
|
|
||||||
const session = new MockRTCSession(
|
const session = new MockRTCSession(
|
||||||
matrixRoom,
|
matrixRoom,
|
||||||
localRtcMember,
|
localRtcMember,
|
||||||
).withMemberships(remoteRtcMemberships);
|
).withMemberships(remoteRtcMemberships$);
|
||||||
|
|
||||||
const vm = new CallViewModel(
|
const vm = new CallViewModel(
|
||||||
session as unknown as MatrixRTCSession,
|
session as unknown as MatrixRTCSession,
|
||||||
@@ -135,7 +135,7 @@ function getMockEnv(
|
|||||||
},
|
},
|
||||||
of(ConnectionState.Connected),
|
of(ConnectionState.Connected),
|
||||||
);
|
);
|
||||||
return { vm, session, remoteRtcMemberships };
|
return { vm, session, remoteRtcMemberships$ };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,33 +146,33 @@ function getMockEnv(
|
|||||||
* a noise every time.
|
* a noise every time.
|
||||||
*/
|
*/
|
||||||
test("plays one sound when entering a call", () => {
|
test("plays one sound when entering a call", () => {
|
||||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]);
|
||||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||||
// Joining a call usually means remote participants are added later.
|
// Joining a call usually means remote participants are added later.
|
||||||
act(() => {
|
act(() => {
|
||||||
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]);
|
remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]);
|
||||||
});
|
});
|
||||||
expect(playSound).toHaveBeenCalledOnce();
|
expect(playSound).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Same test?
|
// TODO: Same test?
|
||||||
test("plays a sound when a user joins", () => {
|
test("plays a sound when a user joins", () => {
|
||||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]);
|
||||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]);
|
remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]);
|
||||||
});
|
});
|
||||||
// Play a sound when joining a call.
|
// Play a sound when joining a call.
|
||||||
expect(playSound).toBeCalledWith("join");
|
expect(playSound).toBeCalledWith("join");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plays a sound when a user leaves", () => {
|
test("plays a sound when a user leaves", () => {
|
||||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]);
|
||||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
remoteRtcMemberships.next([]);
|
remoteRtcMemberships$.next([]);
|
||||||
});
|
});
|
||||||
expect(playSound).toBeCalledWith("left");
|
expect(playSound).toBeCalledWith("left");
|
||||||
});
|
});
|
||||||
@@ -185,7 +185,7 @@ test("plays no sound when the participant list is more than the maximum size", (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { session, vm, remoteRtcMemberships } = getMockEnv(
|
const { session, vm, remoteRtcMemberships$ } = getMockEnv(
|
||||||
[local, alice],
|
[local, alice],
|
||||||
mockRtcMemberships,
|
mockRtcMemberships,
|
||||||
);
|
);
|
||||||
@@ -193,7 +193,7 @@ test("plays no sound when the participant list is more than the maximum size", (
|
|||||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||||
expect(playSound).not.toBeCalled();
|
expect(playSound).not.toBeCalled();
|
||||||
act(() => {
|
act(() => {
|
||||||
remoteRtcMemberships.next(
|
remoteRtcMemberships$.next(
|
||||||
mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1),
|
mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function CallEventAudioRenderer({
|
|||||||
}, [audioEngineRef, previousRaisedHandCount, raisedHandCount]);
|
}, [audioEngineRef, previousRaisedHandCount, raisedHandCount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const joinSub = vm.memberChanges
|
const joinSub = vm.memberChanges$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(
|
filter(
|
||||||
({ joined, ids }) =>
|
({ joined, ids }) =>
|
||||||
@@ -77,7 +77,7 @@ export function CallEventAudioRenderer({
|
|||||||
void audioEngineRef.current?.playSound("join");
|
void audioEngineRef.current?.playSound("join");
|
||||||
});
|
});
|
||||||
|
|
||||||
const leftSub = vm.memberChanges
|
const leftSub = vm.memberChanges$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(
|
filter(
|
||||||
({ ids, left }) =>
|
({ ids, left }) =>
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
sfuConfig,
|
sfuConfig,
|
||||||
props.e2eeSystem,
|
props.e2eeSystem,
|
||||||
);
|
);
|
||||||
const connStateObservable = useObservable(
|
const connStateObservable$ = useObservable(
|
||||||
(inputs) => inputs.pipe(map(([connState]) => connState)),
|
(inputs$) => inputs$.pipe(map(([connState]) => connState)),
|
||||||
[connState],
|
[connState],
|
||||||
);
|
);
|
||||||
const [vm, setVm] = useState<CallViewModel | null>(null);
|
const [vm, setVm] = useState<CallViewModel | null>(null);
|
||||||
@@ -131,12 +131,12 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
props.rtcSession,
|
props.rtcSession,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
props.e2eeSystem,
|
props.e2eeSystem,
|
||||||
connStateObservable,
|
connStateObservable$,
|
||||||
);
|
);
|
||||||
setVm(vm);
|
setVm(vm);
|
||||||
return (): void => vm.destroy();
|
return (): void => vm.destroy();
|
||||||
}
|
}
|
||||||
}, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable]);
|
}, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable$]);
|
||||||
|
|
||||||
if (livekitRoom === undefined || vm === null) return null;
|
if (livekitRoom === undefined || vm === null) return null;
|
||||||
|
|
||||||
@@ -225,14 +225,14 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
() => void toggleRaisedHand(),
|
() => void toggleRaisedHand(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const windowMode = useObservableEagerState(vm.windowMode);
|
const windowMode = useObservableEagerState(vm.windowMode$);
|
||||||
const layout = useObservableEagerState(vm.layout);
|
const layout = useObservableEagerState(vm.layout$);
|
||||||
const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration);
|
const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration$);
|
||||||
const [debugTileLayout] = useSetting(debugTileLayoutSetting);
|
const [debugTileLayout] = useSetting(debugTileLayoutSetting);
|
||||||
const gridMode = useObservableEagerState(vm.gridMode);
|
const gridMode = useObservableEagerState(vm.gridMode$);
|
||||||
const showHeader = useObservableEagerState(vm.showHeader);
|
const showHeader = useObservableEagerState(vm.showHeader$);
|
||||||
const showFooter = useObservableEagerState(vm.showFooter);
|
const showFooter = useObservableEagerState(vm.showFooter$);
|
||||||
const switchCamera = useSwitchCamera(vm.localVideo);
|
const switchCamera = useSwitchCamera(vm.localVideo$);
|
||||||
|
|
||||||
// Ideally we could detect taps by listening for click events and checking
|
// Ideally we could detect taps by listening for click events and checking
|
||||||
// that the pointerType of the event is "touch", but this isn't yet supported
|
// that the pointerType of the event is "touch", but this isn't yet supported
|
||||||
@@ -317,15 +317,15 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
windowMode,
|
windowMode,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const gridBoundsObservable = useObservable(
|
const gridBoundsObservable$ = useObservable(
|
||||||
(inputs) => inputs.pipe(map(([gridBounds]) => gridBounds)),
|
(inputs$) => inputs$.pipe(map(([gridBounds]) => gridBounds)),
|
||||||
[gridBounds],
|
[gridBounds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const spotlightAlignment = useInitial(
|
const spotlightAlignment$ = useInitial(
|
||||||
() => new BehaviorSubject(defaultSpotlightAlignment),
|
() => new BehaviorSubject(defaultSpotlightAlignment),
|
||||||
);
|
);
|
||||||
const pipAlignment = useInitial(
|
const pipAlignment$ = useInitial(
|
||||||
() => new BehaviorSubject(defaultPipAlignment),
|
() => new BehaviorSubject(defaultPipAlignment),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -383,15 +383,17 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
{ className, style, targetWidth, targetHeight, model },
|
{ className, style, targetWidth, targetHeight, model },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const spotlightExpanded = useObservableEagerState(vm.spotlightExpanded);
|
const spotlightExpanded = useObservableEagerState(
|
||||||
|
vm.spotlightExpanded$,
|
||||||
|
);
|
||||||
const onToggleExpanded = useObservableEagerState(
|
const onToggleExpanded = useObservableEagerState(
|
||||||
vm.toggleSpotlightExpanded,
|
vm.toggleSpotlightExpanded$,
|
||||||
);
|
);
|
||||||
const showSpeakingIndicatorsValue = useObservableEagerState(
|
const showSpeakingIndicatorsValue = useObservableEagerState(
|
||||||
vm.showSpeakingIndicators,
|
vm.showSpeakingIndicators$,
|
||||||
);
|
);
|
||||||
const showSpotlightIndicatorsValue = useObservableEagerState(
|
const showSpotlightIndicatorsValue = useObservableEagerState(
|
||||||
vm.showSpotlightIndicators,
|
vm.showSpotlightIndicators$,
|
||||||
);
|
);
|
||||||
|
|
||||||
return model instanceof GridTileViewModel ? (
|
return model instanceof GridTileViewModel ? (
|
||||||
@@ -424,9 +426,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
|
|
||||||
const layouts = useMemo(() => {
|
const layouts = useMemo(() => {
|
||||||
const inputs = {
|
const inputs = {
|
||||||
minBounds: gridBoundsObservable,
|
minBounds$: gridBoundsObservable$,
|
||||||
spotlightAlignment,
|
spotlightAlignment$,
|
||||||
pipAlignment,
|
pipAlignment$,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
grid: makeGridLayout(inputs),
|
grid: makeGridLayout(inputs),
|
||||||
@@ -435,7 +437,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
"spotlight-expanded": makeSpotlightExpandedLayout(inputs),
|
"spotlight-expanded": makeSpotlightExpandedLayout(inputs),
|
||||||
"one-on-one": makeOneOnOneLayout(inputs),
|
"one-on-one": makeOneOnOneLayout(inputs),
|
||||||
};
|
};
|
||||||
}, [gridBoundsObservable, spotlightAlignment, pipAlignment]);
|
}, [gridBoundsObservable$, spotlightAlignment$, pipAlignment$]);
|
||||||
|
|
||||||
const renderContent = (): JSX.Element => {
|
const renderContent = (): JSX.Element => {
|
||||||
if (layout.type === "pip") {
|
if (layout.type === "pip") {
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export const LobbyView: FC<Props> = ({
|
|||||||
useTrackProcessorSync(videoTrack);
|
useTrackProcessorSync(videoTrack);
|
||||||
const showSwitchCamera = useShowSwitchCamera(
|
const showSwitchCamera = useShowSwitchCamera(
|
||||||
useObservable(
|
useObservable(
|
||||||
(inputs) => inputs.pipe(map(([video]) => video)),
|
(inputs$) => inputs$.pipe(map(([video]) => video)),
|
||||||
[videoTrack],
|
[videoTrack],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import React, { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { beforeEach } from "vitest";
|
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
|
||||||
import { useMuteStates } from "./MuteStates";
|
import { useMuteStates } from "./MuteStates";
|
||||||
import {
|
import {
|
||||||
|
type DeviceLabel,
|
||||||
type MediaDevice,
|
type MediaDevice,
|
||||||
type MediaDevices,
|
type MediaDevices,
|
||||||
MediaDevicesContext,
|
MediaDevicesContext,
|
||||||
@@ -63,10 +63,11 @@ const mockCamera: MediaDeviceInfo = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function mockDevices(available: MediaDeviceInfo[]): MediaDevice {
|
function mockDevices(available: Map<string, DeviceLabel>): MediaDevice {
|
||||||
return {
|
return {
|
||||||
available,
|
available,
|
||||||
selectedId: "",
|
selectedId: "",
|
||||||
|
selectedGroupId: "",
|
||||||
select: (): void => {},
|
select: (): void => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -83,25 +84,29 @@ function mockMediaDevices(
|
|||||||
} = { microphone: true, speaker: true, camera: true },
|
} = { microphone: true, speaker: true, camera: true },
|
||||||
): MediaDevices {
|
): MediaDevices {
|
||||||
return {
|
return {
|
||||||
audioInput: mockDevices(microphone ? [mockMicrophone] : []),
|
audioInput: mockDevices(
|
||||||
audioOutput: mockDevices(speaker ? [mockSpeaker] : []),
|
microphone
|
||||||
videoInput: mockDevices(camera ? [mockCamera] : []),
|
? new Map([[mockMicrophone.deviceId, mockMicrophone]])
|
||||||
|
: new Map(),
|
||||||
|
),
|
||||||
|
audioOutput: mockDevices(
|
||||||
|
speaker ? new Map([[mockSpeaker.deviceId, mockSpeaker]]) : new Map(),
|
||||||
|
),
|
||||||
|
videoInput: mockDevices(
|
||||||
|
camera ? new Map([[mockCamera.deviceId, mockCamera]]) : new Map(),
|
||||||
|
),
|
||||||
startUsingDeviceNames: (): void => {},
|
startUsingDeviceNames: (): void => {},
|
||||||
stopUsingDeviceNames: (): void => {},
|
stopUsingDeviceNames: (): void => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("useMuteStates", () => {
|
describe("useMuteStates", () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.spyOn(React, "useContext").mockReturnValue({});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
vi.clearAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disabled when no input devices", () => {
|
it("disabled when no input devices", () => {
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ function useMuteState(
|
|||||||
): MuteState {
|
): MuteState {
|
||||||
const [enabled, setEnabled] = useReactiveState<boolean | undefined>(
|
const [enabled, setEnabled] = useReactiveState<boolean | undefined>(
|
||||||
(prev) =>
|
(prev) =>
|
||||||
device.available.length > 0 ? (prev ?? enabledByDefault()) : undefined,
|
device.available.size > 0 ? (prev ?? enabledByDefault()) : undefined,
|
||||||
[device],
|
[device],
|
||||||
);
|
);
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
device.available.length === 0
|
device.available.size === 0
|
||||||
? deviceUnavailable
|
? deviceUnavailable
|
||||||
: {
|
: {
|
||||||
enabled: enabled ?? false,
|
enabled: enabled ?? false,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import {
|
import {
|
||||||
afterAll,
|
afterAll,
|
||||||
|
afterEach,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
expect,
|
expect,
|
||||||
test,
|
test,
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
} from "vitest";
|
} from "vitest";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { act, type ReactNode } from "react";
|
import { act, type ReactNode } from "react";
|
||||||
import { afterEach } from "node:test";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MockRoom,
|
MockRoom,
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test, afterEach } from "vitest";
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
import { act, type ReactNode } from "react";
|
import { act, type ReactNode } from "react";
|
||||||
import { afterEach } from "node:test";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MockRoom,
|
MockRoom,
|
||||||
|
|||||||
@@ -31,17 +31,17 @@ import { useLatest } from "../useLatest";
|
|||||||
* producing a callback if so.
|
* producing a callback if so.
|
||||||
*/
|
*/
|
||||||
export function useSwitchCamera(
|
export function useSwitchCamera(
|
||||||
video: Observable<LocalVideoTrack | null>,
|
video$: Observable<LocalVideoTrack | null>,
|
||||||
): (() => void) | null {
|
): (() => void) | null {
|
||||||
const mediaDevices = useMediaDevices();
|
const mediaDevices = useMediaDevices();
|
||||||
const setVideoInput = useLatest(mediaDevices.videoInput.select);
|
const setVideoInput = useLatest(mediaDevices.videoInput.select);
|
||||||
|
|
||||||
// Produce an observable like the input 'video' observable, except make it
|
// Produce an observable like the input 'video' observable, except make it
|
||||||
// emit whenever the track is muted or the device changes
|
// emit whenever the track is muted or the device changes
|
||||||
const videoTrack: Observable<LocalVideoTrack | null> = useObservable(
|
const videoTrack$: Observable<LocalVideoTrack | null> = useObservable(
|
||||||
(inputs) =>
|
(inputs$) =>
|
||||||
inputs.pipe(
|
inputs$.pipe(
|
||||||
switchMap(([video]) => video),
|
switchMap(([video$]) => video$),
|
||||||
switchMap((video) => {
|
switchMap((video) => {
|
||||||
if (video === null) return of(null);
|
if (video === null) return of(null);
|
||||||
return merge(
|
return merge(
|
||||||
@@ -53,15 +53,15 @@ export function useSwitchCamera(
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
[video],
|
[video$],
|
||||||
);
|
);
|
||||||
|
|
||||||
const switchCamera: Observable<(() => void) | null> = useObservable(
|
const switchCamera$: Observable<(() => void) | null> = useObservable(
|
||||||
(inputs) =>
|
(inputs$) =>
|
||||||
platform === "desktop"
|
platform === "desktop"
|
||||||
? of(null)
|
? of(null)
|
||||||
: inputs.pipe(
|
: inputs$.pipe(
|
||||||
switchMap(([track]) => track),
|
switchMap(([track$]) => track$),
|
||||||
map((track) => {
|
map((track) => {
|
||||||
if (track === null) return null;
|
if (track === null) return null;
|
||||||
const facingMode = facingModeFromLocalTrack(track).facingMode;
|
const facingMode = facingModeFromLocalTrack(track).facingMode;
|
||||||
@@ -86,8 +86,8 @@ export function useSwitchCamera(
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
[videoTrack],
|
[videoTrack$],
|
||||||
);
|
);
|
||||||
|
|
||||||
return useObservableEagerState(switchCamera);
|
return useObservableEagerState(switchCamera$);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
import { expect, test, vi } from "vitest";
|
import { expect, test, vi } from "vitest";
|
||||||
|
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
|
||||||
|
|
||||||
import { enterRTCSession } from "../src/rtcSessionHelpers";
|
import { enterRTCSession } from "../src/rtcSessionHelpers";
|
||||||
import { mockConfig } from "./utils/test";
|
import { mockConfig } from "./utils/test";
|
||||||
@@ -36,11 +37,21 @@ test("It joins the correct Session", async () => {
|
|||||||
mockConfig({
|
mockConfig({
|
||||||
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockImplementation(
|
||||||
|
async (domain) => {
|
||||||
|
if (domain === "example.org") {
|
||||||
|
return Promise.resolve(clientWellKnown);
|
||||||
|
}
|
||||||
|
return Promise.resolve({});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const mockedSession = vi.mocked({
|
const mockedSession = vi.mocked({
|
||||||
room: {
|
room: {
|
||||||
roomId: "roomId",
|
roomId: "roomId",
|
||||||
client: {
|
client: {
|
||||||
getClientWellKnown: vi.fn().mockReturnValue(clientWellKnown),
|
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
isLivekitFocus,
|
isLivekitFocus,
|
||||||
isLivekitFocusConfig,
|
isLivekitFocusConfig,
|
||||||
} from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
|
} from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
|
||||||
|
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
|
||||||
|
|
||||||
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
@@ -43,19 +44,28 @@ async function makePreferredLivekitFoci(
|
|||||||
preferredFoci.push(focusInUse);
|
preferredFoci.push(focusInUse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prioritize the client well known over the configured sfu.
|
// Prioritize the .well-known/matrix/client, if available, over the configured SFU
|
||||||
const wellKnownFoci =
|
const domain = rtcSession.room.client.getDomain();
|
||||||
rtcSession.room.client.getClientWellKnown()?.[FOCI_WK_KEY];
|
if (domain) {
|
||||||
if (Array.isArray(wellKnownFoci)) {
|
// we use AutoDiscovery instead of relying on the MatrixClient having already
|
||||||
preferredFoci.push(
|
// been fully configured and started
|
||||||
...wellKnownFoci
|
const wellKnownFoci = (await AutoDiscovery.getRawClientConfig(domain))?.[
|
||||||
.filter((f) => !!f)
|
FOCI_WK_KEY
|
||||||
.filter(isLivekitFocusConfig)
|
];
|
||||||
.map((wellKnownFocus) => {
|
if (Array.isArray(wellKnownFoci)) {
|
||||||
logger.log("Adding livekit focus from well known: ", wellKnownFocus);
|
preferredFoci.push(
|
||||||
return { ...wellKnownFocus, livekit_alias: livekitAlias };
|
...wellKnownFoci
|
||||||
}),
|
.filter((f) => !!f)
|
||||||
);
|
.filter(isLivekitFocusConfig)
|
||||||
|
.map((wellKnownFocus) => {
|
||||||
|
logger.log(
|
||||||
|
"Adding livekit focus from well known: ",
|
||||||
|
wellKnownFocus,
|
||||||
|
);
|
||||||
|
return { ...wellKnownFocus, livekit_alias: livekitAlias };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
useSetting,
|
useSetting,
|
||||||
duplicateTiles as duplicateTilesSetting,
|
duplicateTiles as duplicateTilesSetting,
|
||||||
debugTileLayout as debugTileLayoutSetting,
|
debugTileLayout as debugTileLayoutSetting,
|
||||||
|
showNonMemberTiles as showNonMemberTilesSetting,
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
@@ -26,6 +27,9 @@ export const DeveloperSettingsTab: FC<Props> = ({ client }) => {
|
|||||||
const [debugTileLayout, setDebugTileLayout] = useSetting(
|
const [debugTileLayout, setDebugTileLayout] = useSetting(
|
||||||
debugTileLayoutSetting,
|
debugTileLayoutSetting,
|
||||||
);
|
);
|
||||||
|
const [showNonMemberTiles, setShowNonMemberTiles] = useSetting(
|
||||||
|
showNonMemberTilesSetting,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -85,6 +89,20 @@ export const DeveloperSettingsTab: FC<Props> = ({ client }) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="showNonMemberTiles"
|
||||||
|
type="checkbox"
|
||||||
|
label={t("developer_mode.show_non_member_tiles")}
|
||||||
|
checked={!!showNonMemberTiles}
|
||||||
|
onChange={useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setShowNonMemberTiles(event.target.checked);
|
||||||
|
},
|
||||||
|
[setShowNonMemberTiles],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,3 +16,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--cpd-space-4x);
|
gap: var(--cpd-space-4x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type ChangeEvent, type FC, useCallback, useId } from "react";
|
import {
|
||||||
|
type ChangeEvent,
|
||||||
|
type FC,
|
||||||
|
type ReactElement,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useId,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Heading,
|
Heading,
|
||||||
InlineField,
|
InlineField,
|
||||||
@@ -13,16 +20,23 @@ import {
|
|||||||
RadioControl,
|
RadioControl,
|
||||||
Separator,
|
Separator,
|
||||||
} from "@vector-im/compound-web";
|
} from "@vector-im/compound-web";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { type MediaDevice } from "../livekit/MediaDevicesContext";
|
import { type MediaDevice } from "../livekit/MediaDevicesContext";
|
||||||
import styles from "./DeviceSelection.module.css";
|
import styles from "./DeviceSelection.module.css";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
devices: MediaDevice;
|
devices: MediaDevice;
|
||||||
caption: string;
|
title: string;
|
||||||
|
numberedLabel: (number: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeviceSelection: FC<Props> = ({ devices, caption }) => {
|
export const DeviceSelection: FC<Props> = ({
|
||||||
|
devices,
|
||||||
|
title,
|
||||||
|
numberedLabel,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const groupId = useId();
|
const groupId = useId();
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -31,7 +45,7 @@ export const DeviceSelection: FC<Props> = ({ devices, caption }) => {
|
|||||||
[devices],
|
[devices],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (devices.available.length == 0) return null;
|
if (devices.available.size == 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.selection}>
|
<div className={styles.selection}>
|
||||||
@@ -42,29 +56,53 @@ export const DeviceSelection: FC<Props> = ({ devices, caption }) => {
|
|||||||
as="h4"
|
as="h4"
|
||||||
className={styles.title}
|
className={styles.title}
|
||||||
>
|
>
|
||||||
{caption}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Separator className={styles.separator} />
|
<Separator className={styles.separator} />
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
{devices.available.map(({ deviceId, label }, index) => (
|
{[...devices.available].map(([id, label]) => {
|
||||||
<InlineField
|
let labelText: ReactNode;
|
||||||
key={deviceId}
|
switch (label.type) {
|
||||||
name={groupId}
|
case "name":
|
||||||
control={
|
labelText = label.name;
|
||||||
<RadioControl
|
break;
|
||||||
checked={deviceId === devices.selectedId}
|
case "number":
|
||||||
onChange={onChange}
|
labelText = numberedLabel(label.number);
|
||||||
value={deviceId}
|
break;
|
||||||
/>
|
case "default":
|
||||||
}
|
labelText =
|
||||||
>
|
label.name === null ? (
|
||||||
<Label>
|
t("settings.devices.default")
|
||||||
{!!label && label.trim().length > 0
|
) : (
|
||||||
? label
|
<Trans
|
||||||
: `${caption} ${index + 1}`}
|
i18nKey="settings.devices.default_named"
|
||||||
</Label>
|
name={label.name}
|
||||||
</InlineField>
|
>
|
||||||
))}
|
Default{" "}
|
||||||
|
<span className={styles.secondary}>
|
||||||
|
({{ name: label.name } as unknown as ReactElement})
|
||||||
|
</span>
|
||||||
|
</Trans>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InlineField
|
||||||
|
key={id}
|
||||||
|
name={groupId}
|
||||||
|
control={
|
||||||
|
<RadioControl
|
||||||
|
checked={id === devices.selectedId}
|
||||||
|
onChange={onChange}
|
||||||
|
value={id}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Label>{labelText}</Label>
|
||||||
|
</InlineField>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ export const PreferencesSettingsTab: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Text>{t("settings.preferences_tab_body")}</Text>
|
<Text>{t("settings.preferences_tab.introduction")}</Text>
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<InputField
|
<InputField
|
||||||
id="showHandRaisedTimer"
|
id="showHandRaisedTimer"
|
||||||
label={t("settings.preferences_tab_show_hand_raised_timer_label")}
|
label={t("settings.preferences_tab.show_hand_raised_timer_label")}
|
||||||
description={t(
|
description={t(
|
||||||
"settings.preferences_tab_show_hand_raised_timer_description",
|
"settings.preferences_tab.show_hand_raised_timer_description",
|
||||||
)}
|
)}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showHandRaisedTimer}
|
checked={showHandRaisedTimer}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
backgroundBlur as backgroundBlurSetting,
|
backgroundBlur as backgroundBlurSetting,
|
||||||
developerMode,
|
developerMode,
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
import { isFirefox } from "../Platform";
|
|
||||||
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
||||||
import { Slider } from "../Slider";
|
import { Slider } from "../Slider";
|
||||||
import { DeviceSelection } from "./DeviceSelection";
|
import { DeviceSelection } from "./DeviceSelection";
|
||||||
@@ -107,14 +106,16 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
<Form>
|
<Form>
|
||||||
<DeviceSelection
|
<DeviceSelection
|
||||||
devices={devices.audioInput}
|
devices={devices.audioInput}
|
||||||
caption={t("common.microphone")}
|
title={t("settings.devices.microphone")}
|
||||||
|
numberedLabel={(n) =>
|
||||||
|
t("settings.devices.microphone_numbered", { n })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DeviceSelection
|
||||||
|
devices={devices.audioOutput}
|
||||||
|
title={t("settings.devices.speaker")}
|
||||||
|
numberedLabel={(n) => t("settings.devices.speaker_numbered", { n })}
|
||||||
/>
|
/>
|
||||||
{!isFirefox() && (
|
|
||||||
<DeviceSelection
|
|
||||||
devices={devices.audioOutput}
|
|
||||||
caption={t("settings.speaker_device_selection_label")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={styles.volumeSlider}>
|
<div className={styles.volumeSlider}>
|
||||||
<label>{t("settings.audio_tab.effect_volume_label")}</label>
|
<label>{t("settings.audio_tab.effect_volume_label")}</label>
|
||||||
<p>{t("settings.audio_tab.effect_volume_description")}</p>
|
<p>{t("settings.audio_tab.effect_volume_description")}</p>
|
||||||
@@ -141,7 +142,8 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
<Form>
|
<Form>
|
||||||
<DeviceSelection
|
<DeviceSelection
|
||||||
devices={devices.videoInput}
|
devices={devices.videoInput}
|
||||||
caption={t("common.camera")}
|
title={t("settings.devices.camera")}
|
||||||
|
numberedLabel={(n) => t("settings.devices.camera_numbered", { n })}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
@@ -31,17 +31,17 @@ export class Setting<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._value = new BehaviorSubject(initialValue);
|
this._value$ = new BehaviorSubject(initialValue);
|
||||||
this.value = this._value;
|
this.value$ = this._value$;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly key: string;
|
private readonly key: string;
|
||||||
|
|
||||||
private readonly _value: BehaviorSubject<T>;
|
private readonly _value$: BehaviorSubject<T>;
|
||||||
public readonly value: Observable<T>;
|
public readonly value$: Observable<T>;
|
||||||
|
|
||||||
public readonly setValue = (value: T): void => {
|
public readonly setValue = (value: T): void => {
|
||||||
this._value.next(value);
|
this._value$.next(value);
|
||||||
localStorage.setItem(this.key, JSON.stringify(value));
|
localStorage.setItem(this.key, JSON.stringify(value));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ export class Setting<T> {
|
|||||||
* React hook that returns a settings's current value and a setter.
|
* React hook that returns a settings's current value and a setter.
|
||||||
*/
|
*/
|
||||||
export function useSetting<T>(setting: Setting<T>): [T, (value: T) => void] {
|
export function useSetting<T>(setting: Setting<T>): [T, (value: T) => void] {
|
||||||
return [useObservableEagerState(setting.value), setting.setValue];
|
return [useObservableEagerState(setting.value$), setting.setValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
// null = undecided
|
// null = undecided
|
||||||
@@ -72,6 +72,10 @@ export const developerMode = new Setting("developer-settings-tab", false);
|
|||||||
|
|
||||||
export const duplicateTiles = new Setting("duplicate-tiles", 0);
|
export const duplicateTiles = new Setting("duplicate-tiles", 0);
|
||||||
|
|
||||||
|
export const showNonMemberTiles = new Setting<boolean>(
|
||||||
|
"show-non-member-tiles",
|
||||||
|
false,
|
||||||
|
);
|
||||||
export const debugTileLayout = new Setting("debug-tile-layout", false);
|
export const debugTileLayout = new Setting("debug-tile-layout", false);
|
||||||
|
|
||||||
export const audioInput = new Setting<string | undefined>(
|
export const audioInput = new Setting<string | undefined>(
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
type ECConnectionState,
|
type ECConnectionState,
|
||||||
} from "../livekit/useECConnectionState";
|
} from "../livekit/useECConnectionState";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
|
import { showNonMemberTiles } from "../settings/settings";
|
||||||
|
|
||||||
vi.mock("@livekit/components-core");
|
vi.mock("@livekit/components-core");
|
||||||
|
|
||||||
@@ -123,15 +124,15 @@ export type LayoutSummary =
|
|||||||
| OneOnOneLayoutSummary
|
| OneOnOneLayoutSummary
|
||||||
| PipLayoutSummary;
|
| PipLayoutSummary;
|
||||||
|
|
||||||
function summarizeLayout(l: Observable<Layout>): Observable<LayoutSummary> {
|
function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
|
||||||
return l.pipe(
|
return l$.pipe(
|
||||||
switchMap((l) => {
|
switchMap((l) => {
|
||||||
switch (l.type) {
|
switch (l.type) {
|
||||||
case "grid":
|
case "grid":
|
||||||
return combineLatest(
|
return combineLatest(
|
||||||
[
|
[
|
||||||
l.spotlight?.media ?? of(undefined),
|
l.spotlight?.media$ ?? of(undefined),
|
||||||
...l.grid.map((vm) => vm.media),
|
...l.grid.map((vm) => vm.media$),
|
||||||
],
|
],
|
||||||
(spotlight, ...grid) => ({
|
(spotlight, ...grid) => ({
|
||||||
type: l.type,
|
type: l.type,
|
||||||
@@ -142,7 +143,7 @@ function summarizeLayout(l: Observable<Layout>): Observable<LayoutSummary> {
|
|||||||
case "spotlight-landscape":
|
case "spotlight-landscape":
|
||||||
case "spotlight-portrait":
|
case "spotlight-portrait":
|
||||||
return combineLatest(
|
return combineLatest(
|
||||||
[l.spotlight.media, ...l.grid.map((vm) => vm.media)],
|
[l.spotlight.media$, ...l.grid.map((vm) => vm.media$)],
|
||||||
(spotlight, ...grid) => ({
|
(spotlight, ...grid) => ({
|
||||||
type: l.type,
|
type: l.type,
|
||||||
spotlight: spotlight.map((vm) => vm.id),
|
spotlight: spotlight.map((vm) => vm.id),
|
||||||
@@ -151,7 +152,7 @@ function summarizeLayout(l: Observable<Layout>): Observable<LayoutSummary> {
|
|||||||
);
|
);
|
||||||
case "spotlight-expanded":
|
case "spotlight-expanded":
|
||||||
return combineLatest(
|
return combineLatest(
|
||||||
[l.spotlight.media, l.pip?.media ?? of(undefined)],
|
[l.spotlight.media$, l.pip?.media$ ?? of(undefined)],
|
||||||
(spotlight, pip) => ({
|
(spotlight, pip) => ({
|
||||||
type: l.type,
|
type: l.type,
|
||||||
spotlight: spotlight.map((vm) => vm.id),
|
spotlight: spotlight.map((vm) => vm.id),
|
||||||
@@ -160,7 +161,7 @@ function summarizeLayout(l: Observable<Layout>): Observable<LayoutSummary> {
|
|||||||
);
|
);
|
||||||
case "one-on-one":
|
case "one-on-one":
|
||||||
return combineLatest(
|
return combineLatest(
|
||||||
[l.local.media, l.remote.media],
|
[l.local.media$, l.remote.media$],
|
||||||
(local, remote) => ({
|
(local, remote) => ({
|
||||||
type: l.type,
|
type: l.type,
|
||||||
local: local.id,
|
local: local.id,
|
||||||
@@ -168,7 +169,7 @@ function summarizeLayout(l: Observable<Layout>): Observable<LayoutSummary> {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
case "pip":
|
case "pip":
|
||||||
return l.spotlight.media.pipe(
|
return l.spotlight.media$.pipe(
|
||||||
map((spotlight) => ({
|
map((spotlight) => ({
|
||||||
type: l.type,
|
type: l.type,
|
||||||
spotlight: spotlight.map((vm) => vm.id),
|
spotlight: spotlight.map((vm) => vm.id),
|
||||||
@@ -185,9 +186,9 @@ function summarizeLayout(l: Observable<Layout>): Observable<LayoutSummary> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function withCallViewModel(
|
function withCallViewModel(
|
||||||
remoteParticipants: Observable<RemoteParticipant[]>,
|
remoteParticipants$: Observable<RemoteParticipant[]>,
|
||||||
rtcMembers: Observable<Partial<CallMembership>[]>,
|
rtcMembers$: Observable<Partial<CallMembership>[]>,
|
||||||
connectionState: Observable<ECConnectionState>,
|
connectionState$: Observable<ECConnectionState>,
|
||||||
speaking: Map<Participant, Observable<boolean>>,
|
speaking: Map<Participant, Observable<boolean>>,
|
||||||
continuation: (vm: CallViewModel) => void,
|
continuation: (vm: CallViewModel) => void,
|
||||||
): void {
|
): void {
|
||||||
@@ -202,10 +203,10 @@ function withCallViewModel(
|
|||||||
room,
|
room,
|
||||||
localRtcMember,
|
localRtcMember,
|
||||||
[],
|
[],
|
||||||
).withMemberships(rtcMembers);
|
).withMemberships(rtcMembers$);
|
||||||
const participantsSpy = vi
|
const participantsSpy = vi
|
||||||
.spyOn(ComponentsCore, "connectedParticipantsObserver")
|
.spyOn(ComponentsCore, "connectedParticipantsObserver")
|
||||||
.mockReturnValue(remoteParticipants);
|
.mockReturnValue(remoteParticipants$);
|
||||||
const mediaSpy = vi
|
const mediaSpy = vi
|
||||||
.spyOn(ComponentsCore, "observeParticipantMedia")
|
.spyOn(ComponentsCore, "observeParticipantMedia")
|
||||||
.mockImplementation((p) =>
|
.mockImplementation((p) =>
|
||||||
@@ -231,7 +232,7 @@ function withCallViewModel(
|
|||||||
|
|
||||||
const liveKitRoom = mockLivekitRoom(
|
const liveKitRoom = mockLivekitRoom(
|
||||||
{ localParticipant },
|
{ localParticipant },
|
||||||
{ remoteParticipants },
|
{ remoteParticipants$ },
|
||||||
);
|
);
|
||||||
|
|
||||||
const vm = new CallViewModel(
|
const vm = new CallViewModel(
|
||||||
@@ -240,7 +241,7 @@ function withCallViewModel(
|
|||||||
{
|
{
|
||||||
kind: E2eeType.PER_PARTICIPANT,
|
kind: E2eeType.PER_PARTICIPANT,
|
||||||
},
|
},
|
||||||
connectionState,
|
connectionState$,
|
||||||
);
|
);
|
||||||
|
|
||||||
onTestFinished(() => {
|
onTestFinished(() => {
|
||||||
@@ -275,7 +276,7 @@ test("participants are retained during a focus switch", () => {
|
|||||||
}),
|
}),
|
||||||
new Map(),
|
new Map(),
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
a: {
|
a: {
|
||||||
@@ -319,7 +320,7 @@ test("screen sharing activates spotlight layout", () => {
|
|||||||
g: () => vm.setGridMode("grid"),
|
g: () => vm.setGridMode("grid"),
|
||||||
});
|
});
|
||||||
|
|
||||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
a: {
|
a: {
|
||||||
@@ -362,7 +363,7 @@ test("screen sharing activates spotlight layout", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expectObservable(vm.showSpeakingIndicators).toBe(
|
expectObservable(vm.showSpeakingIndicators$).toBe(
|
||||||
expectedShowSpeakingMarbles,
|
expectedShowSpeakingMarbles,
|
||||||
{
|
{
|
||||||
y: true,
|
y: true,
|
||||||
@@ -376,16 +377,16 @@ test("screen sharing activates spotlight layout", () => {
|
|||||||
|
|
||||||
test("participants stay in the same order unless to appear/disappear", () => {
|
test("participants stay in the same order unless to appear/disappear", () => {
|
||||||
withTestScheduler(({ hot, schedule, expectObservable }) => {
|
withTestScheduler(({ hot, schedule, expectObservable }) => {
|
||||||
const modeInputMarbles = " a";
|
const visibilityInputMarbles = "a";
|
||||||
// First Bob speaks, then Dave, then Alice
|
// First Bob speaks, then Dave, then Alice
|
||||||
const aSpeakingInputMarbles = "n- 1998ms - 1999ms y";
|
const aSpeakingInputMarbles = " n- 1998ms - 1999ms y";
|
||||||
const bSpeakingInputMarbles = "ny 1998ms n 1999ms -";
|
const bSpeakingInputMarbles = " ny 1998ms n 1999ms -";
|
||||||
const dSpeakingInputMarbles = "n- 1998ms y 1999ms n";
|
const dSpeakingInputMarbles = " n- 1998ms y 1999ms n";
|
||||||
// Nothing should change when Bob speaks, because Bob is already on screen.
|
// Nothing should change when Bob speaks, because Bob is already on screen.
|
||||||
// When Dave speaks he should switch with Alice because she's the one who
|
// When Dave speaks he should switch with Alice because she's the one who
|
||||||
// hasn't spoken at all. Then when Alice speaks, she should return to her
|
// hasn't spoken at all. Then when Alice speaks, she should return to her
|
||||||
// place at the top.
|
// place at the top.
|
||||||
const expectedLayoutMarbles = "a 1999ms b 1999ms a 57999ms c 1999ms a";
|
const expectedLayoutMarbles = " a 1999ms b 1999ms a 57999ms c 1999ms a";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
of([aliceParticipant, bobParticipant, daveParticipant]),
|
of([aliceParticipant, bobParticipant, daveParticipant]),
|
||||||
@@ -397,20 +398,17 @@ test("participants stay in the same order unless to appear/disappear", () => {
|
|||||||
[daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })],
|
[daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })],
|
||||||
]),
|
]),
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, {
|
schedule(visibilityInputMarbles, {
|
||||||
a: () => {
|
a: () => {
|
||||||
// We imagine that only three tiles (the first three) will be visible
|
// We imagine that only three tiles (the first three) will be visible
|
||||||
// on screen at a time
|
// on screen at a time
|
||||||
vm.layout.subscribe((layout) => {
|
vm.layout$.subscribe((layout) => {
|
||||||
if (layout.type === "grid") {
|
if (layout.type === "grid") layout.setVisibleTiles(3);
|
||||||
for (let i = 0; i < layout.grid.length; i++)
|
|
||||||
layout.grid[i].setVisible(i < 3);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
a: {
|
a: {
|
||||||
@@ -435,6 +433,56 @@ test("participants stay in the same order unless to appear/disappear", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("participants adjust order when space becomes constrained", () => {
|
||||||
|
withTestScheduler(({ hot, schedule, expectObservable }) => {
|
||||||
|
// Start with all tiles on screen then shrink to 3
|
||||||
|
const visibilityInputMarbles = "a-b";
|
||||||
|
// Bob and Dave speak
|
||||||
|
const bSpeakingInputMarbles = " ny";
|
||||||
|
const dSpeakingInputMarbles = " ny";
|
||||||
|
// Nothing should change when Bob or Dave initially speak, because they are
|
||||||
|
// on screen. When the screen becomes smaller Alice should move off screen
|
||||||
|
// to make way for the speakers (specifically, she should swap with Dave).
|
||||||
|
const expectedLayoutMarbles = " a-b";
|
||||||
|
|
||||||
|
withCallViewModel(
|
||||||
|
of([aliceParticipant, bobParticipant, daveParticipant]),
|
||||||
|
of([aliceRtcMember, bobRtcMember, daveRtcMember]),
|
||||||
|
of(ConnectionState.Connected),
|
||||||
|
new Map([
|
||||||
|
[bobParticipant, hot(bSpeakingInputMarbles, { y: true, n: false })],
|
||||||
|
[daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })],
|
||||||
|
]),
|
||||||
|
(vm) => {
|
||||||
|
let setVisibleTiles: ((value: number) => void) | null = null;
|
||||||
|
vm.layout$.subscribe((layout) => {
|
||||||
|
if (layout.type === "grid") setVisibleTiles = layout.setVisibleTiles;
|
||||||
|
});
|
||||||
|
schedule(visibilityInputMarbles, {
|
||||||
|
a: () => setVisibleTiles!(Infinity),
|
||||||
|
b: () => setVisibleTiles!(3),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
|
expectedLayoutMarbles,
|
||||||
|
{
|
||||||
|
a: {
|
||||||
|
type: "grid",
|
||||||
|
spotlight: undefined,
|
||||||
|
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`, `${daveId}:0`],
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
type: "grid",
|
||||||
|
spotlight: undefined,
|
||||||
|
grid: ["local:0", `${daveId}:0`, `${bobId}:0`, `${aliceId}:0`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("spotlight speakers swap places", () => {
|
test("spotlight speakers swap places", () => {
|
||||||
withTestScheduler(({ hot, schedule, expectObservable }) => {
|
withTestScheduler(({ hot, schedule, expectObservable }) => {
|
||||||
// Go immediately into spotlight mode for the test
|
// Go immediately into spotlight mode for the test
|
||||||
@@ -461,7 +509,7 @@ test("spotlight speakers swap places", () => {
|
|||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") });
|
schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") });
|
||||||
|
|
||||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
a: {
|
a: {
|
||||||
@@ -509,7 +557,7 @@ test("layout enters picture-in-picture mode when requested", () => {
|
|||||||
d: () => window.controls.disablePip(),
|
d: () => window.controls.disablePip(),
|
||||||
});
|
});
|
||||||
|
|
||||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
a: {
|
a: {
|
||||||
@@ -552,12 +600,12 @@ test("spotlight remembers whether it's expanded", () => {
|
|||||||
schedule(expandInputMarbles, {
|
schedule(expandInputMarbles, {
|
||||||
a: () => {
|
a: () => {
|
||||||
let toggle: () => void;
|
let toggle: () => void;
|
||||||
vm.toggleSpotlightExpanded.subscribe((val) => (toggle = val!));
|
vm.toggleSpotlightExpanded$.subscribe((val) => (toggle = val!));
|
||||||
toggle!();
|
toggle!();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
a: {
|
a: {
|
||||||
@@ -614,7 +662,7 @@ test("participants must have a MatrixRTCSession to be visible", () => {
|
|||||||
new Map(),
|
new Map(),
|
||||||
(vm) => {
|
(vm) => {
|
||||||
vm.setGridMode("grid");
|
vm.setGridMode("grid");
|
||||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
a: {
|
a: {
|
||||||
@@ -639,6 +687,53 @@ test("participants must have a MatrixRTCSession to be visible", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("shows participants without MatrixRTCSession when enabled in settings", () => {
|
||||||
|
try {
|
||||||
|
// enable the setting:
|
||||||
|
showNonMemberTiles.setValue(true);
|
||||||
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
|
const scenarioInputMarbles = " abc";
|
||||||
|
const expectedLayoutMarbles = "abc";
|
||||||
|
|
||||||
|
withCallViewModel(
|
||||||
|
hot(scenarioInputMarbles, {
|
||||||
|
a: [],
|
||||||
|
b: [aliceParticipant],
|
||||||
|
c: [aliceParticipant, bobParticipant],
|
||||||
|
}),
|
||||||
|
of([]), // No one joins the MatrixRTC session
|
||||||
|
of(ConnectionState.Connected),
|
||||||
|
new Map(),
|
||||||
|
(vm) => {
|
||||||
|
vm.setGridMode("grid");
|
||||||
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
|
expectedLayoutMarbles,
|
||||||
|
{
|
||||||
|
a: {
|
||||||
|
type: "grid",
|
||||||
|
spotlight: undefined,
|
||||||
|
grid: ["local:0"],
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
type: "one-on-one",
|
||||||
|
local: "local:0",
|
||||||
|
remote: `${aliceId}:0`,
|
||||||
|
},
|
||||||
|
c: {
|
||||||
|
type: "grid",
|
||||||
|
spotlight: undefined,
|
||||||
|
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
showNonMemberTiles.setValue(showNonMemberTiles.defaultValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("should show at least one tile per MatrixRTCSession", () => {
|
it("should show at least one tile per MatrixRTCSession", () => {
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
// iterate through some combinations of MatrixRTC memberships
|
// iterate through some combinations of MatrixRTC memberships
|
||||||
@@ -658,7 +753,7 @@ it("should show at least one tile per MatrixRTCSession", () => {
|
|||||||
new Map(),
|
new Map(),
|
||||||
(vm) => {
|
(vm) => {
|
||||||
vm.setGridMode("grid");
|
vm.setGridMode("grid");
|
||||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
{
|
{
|
||||||
a: {
|
a: {
|
||||||
|
|||||||
@@ -62,16 +62,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
LocalUserMediaViewModel,
|
LocalUserMediaViewModel,
|
||||||
type MediaViewModel,
|
type MediaViewModel,
|
||||||
observeTrackReference,
|
observeTrackReference$,
|
||||||
RemoteUserMediaViewModel,
|
RemoteUserMediaViewModel,
|
||||||
ScreenShareViewModel,
|
ScreenShareViewModel,
|
||||||
type UserMediaViewModel,
|
type UserMediaViewModel,
|
||||||
} from "./MediaViewModel";
|
} from "./MediaViewModel";
|
||||||
import { accumulate, finalizeValue } from "../utils/observable";
|
import { accumulate, finalizeValue } from "../utils/observable";
|
||||||
import { ObservableScope } from "./ObservableScope";
|
import { ObservableScope } from "./ObservableScope";
|
||||||
import { duplicateTiles } from "../settings/settings";
|
import { duplicateTiles, showNonMemberTiles } from "../settings/settings";
|
||||||
import { isFirefox } from "../Platform";
|
import { isFirefox } from "../Platform";
|
||||||
import { setPipEnabled } from "../controls";
|
import { setPipEnabled$ } from "../controls";
|
||||||
import {
|
import {
|
||||||
type GridTileViewModel,
|
type GridTileViewModel,
|
||||||
type SpotlightTileViewModel,
|
type SpotlightTileViewModel,
|
||||||
@@ -82,7 +82,8 @@ import { spotlightExpandedLayout } from "./SpotlightExpandedLayout";
|
|||||||
import { oneOnOneLayout } from "./OneOnOneLayout";
|
import { oneOnOneLayout } from "./OneOnOneLayout";
|
||||||
import { pipLayout } from "./PipLayout";
|
import { pipLayout } from "./PipLayout";
|
||||||
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
|
||||||
import { observeSpeaker } from "./observeSpeaker";
|
import { observeSpeaker$ } from "./observeSpeaker";
|
||||||
|
import { shallowEquals } from "../utils/array";
|
||||||
|
|
||||||
// How long we wait after a focus switch before showing the real participant
|
// How long we wait after a focus switch before showing the real participant
|
||||||
// list again
|
// list again
|
||||||
@@ -143,18 +144,21 @@ export interface GridLayout {
|
|||||||
type: "grid";
|
type: "grid";
|
||||||
spotlight?: SpotlightTileViewModel;
|
spotlight?: SpotlightTileViewModel;
|
||||||
grid: GridTileViewModel[];
|
grid: GridTileViewModel[];
|
||||||
|
setVisibleTiles: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpotlightLandscapeLayout {
|
export interface SpotlightLandscapeLayout {
|
||||||
type: "spotlight-landscape";
|
type: "spotlight-landscape";
|
||||||
spotlight: SpotlightTileViewModel;
|
spotlight: SpotlightTileViewModel;
|
||||||
grid: GridTileViewModel[];
|
grid: GridTileViewModel[];
|
||||||
|
setVisibleTiles: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpotlightPortraitLayout {
|
export interface SpotlightPortraitLayout {
|
||||||
type: "spotlight-portrait";
|
type: "spotlight-portrait";
|
||||||
spotlight: SpotlightTileViewModel;
|
spotlight: SpotlightTileViewModel;
|
||||||
grid: GridTileViewModel[];
|
grid: GridTileViewModel[];
|
||||||
|
setVisibleTiles: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpotlightExpandedLayout {
|
export interface SpotlightExpandedLayout {
|
||||||
@@ -223,18 +227,17 @@ enum SortingBin {
|
|||||||
interface LayoutScanState {
|
interface LayoutScanState {
|
||||||
layout: Layout | null;
|
layout: Layout | null;
|
||||||
tiles: TileStore;
|
tiles: TileStore;
|
||||||
visibleTiles: Set<GridTileViewModel>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserMedia {
|
class UserMedia {
|
||||||
private readonly scope = new ObservableScope();
|
private readonly scope = new ObservableScope();
|
||||||
public readonly vm: UserMediaViewModel;
|
public readonly vm: UserMediaViewModel;
|
||||||
private readonly participant: BehaviorSubject<
|
private readonly participant$: BehaviorSubject<
|
||||||
LocalParticipant | RemoteParticipant | undefined
|
LocalParticipant | RemoteParticipant | undefined
|
||||||
>;
|
>;
|
||||||
|
|
||||||
public readonly speaker: Observable<boolean>;
|
public readonly speaker$: Observable<boolean>;
|
||||||
public readonly presenter: Observable<boolean>;
|
public readonly presenter$: Observable<boolean>;
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly id: string,
|
public readonly id: string,
|
||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
@@ -242,13 +245,13 @@ class UserMedia {
|
|||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
this.participant = new BehaviorSubject(participant);
|
this.participant$ = new BehaviorSubject(participant);
|
||||||
|
|
||||||
if (participant?.isLocal) {
|
if (participant?.isLocal) {
|
||||||
this.vm = new LocalUserMediaViewModel(
|
this.vm = new LocalUserMediaViewModel(
|
||||||
this.id,
|
this.id,
|
||||||
member,
|
member,
|
||||||
this.participant.asObservable() as Observable<LocalParticipant>,
|
this.participant$.asObservable() as Observable<LocalParticipant>,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
);
|
);
|
||||||
@@ -256,7 +259,7 @@ class UserMedia {
|
|||||||
this.vm = new RemoteUserMediaViewModel(
|
this.vm = new RemoteUserMediaViewModel(
|
||||||
id,
|
id,
|
||||||
member,
|
member,
|
||||||
this.participant.asObservable() as Observable<
|
this.participant$.asObservable() as Observable<
|
||||||
RemoteParticipant | undefined
|
RemoteParticipant | undefined
|
||||||
>,
|
>,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
@@ -264,9 +267,9 @@ class UserMedia {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.speaker = observeSpeaker(this.vm.speaking).pipe(this.scope.state());
|
this.speaker$ = observeSpeaker$(this.vm.speaking$).pipe(this.scope.state());
|
||||||
|
|
||||||
this.presenter = this.participant.pipe(
|
this.presenter$ = this.participant$.pipe(
|
||||||
switchMap(
|
switchMap(
|
||||||
(p) =>
|
(p) =>
|
||||||
(p &&
|
(p &&
|
||||||
@@ -286,9 +289,9 @@ class UserMedia {
|
|||||||
public updateParticipant(
|
public updateParticipant(
|
||||||
newParticipant: LocalParticipant | RemoteParticipant | undefined,
|
newParticipant: LocalParticipant | RemoteParticipant | undefined,
|
||||||
): void {
|
): void {
|
||||||
if (this.participant.value !== newParticipant) {
|
if (this.participant$.value !== newParticipant) {
|
||||||
// Update the BehaviourSubject in the UserMedia.
|
// Update the BehaviourSubject in the UserMedia.
|
||||||
this.participant.next(newParticipant);
|
this.participant$.next(newParticipant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +303,7 @@ class UserMedia {
|
|||||||
|
|
||||||
class ScreenShare {
|
class ScreenShare {
|
||||||
public readonly vm: ScreenShareViewModel;
|
public readonly vm: ScreenShareViewModel;
|
||||||
private readonly participant: BehaviorSubject<
|
private readonly participant$: BehaviorSubject<
|
||||||
LocalParticipant | RemoteParticipant
|
LocalParticipant | RemoteParticipant
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -311,12 +314,12 @@ class ScreenShare {
|
|||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
liveKitRoom: LivekitRoom,
|
liveKitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
this.participant = new BehaviorSubject(participant);
|
this.participant$ = new BehaviorSubject(participant);
|
||||||
|
|
||||||
this.vm = new ScreenShareViewModel(
|
this.vm = new ScreenShareViewModel(
|
||||||
id,
|
id,
|
||||||
member,
|
member,
|
||||||
this.participant.asObservable(),
|
this.participant$.asObservable(),
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
liveKitRoom,
|
liveKitRoom,
|
||||||
participant.isLocal,
|
participant.isLocal,
|
||||||
@@ -354,8 +357,8 @@ function findMatrixRoomMember(
|
|||||||
|
|
||||||
// TODO: Move wayyyy more business logic from the call and lobby views into here
|
// TODO: Move wayyyy more business logic from the call and lobby views into here
|
||||||
export class CallViewModel extends ViewModel {
|
export class CallViewModel extends ViewModel {
|
||||||
public readonly localVideo: Observable<LocalVideoTrack | null> =
|
public readonly localVideo$: Observable<LocalVideoTrack | null> =
|
||||||
observeTrackReference(
|
observeTrackReference$(
|
||||||
of(this.livekitRoom.localParticipant),
|
of(this.livekitRoom.localParticipant),
|
||||||
Track.Source.Camera,
|
Track.Source.Camera,
|
||||||
).pipe(
|
).pipe(
|
||||||
@@ -368,16 +371,16 @@ export class CallViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* The raw list of RemoteParticipants as reported by LiveKit
|
* The raw list of RemoteParticipants as reported by LiveKit
|
||||||
*/
|
*/
|
||||||
private readonly rawRemoteParticipants: Observable<RemoteParticipant[]> =
|
private readonly rawRemoteParticipants$: Observable<RemoteParticipant[]> =
|
||||||
connectedParticipantsObserver(this.livekitRoom).pipe(this.scope.state());
|
connectedParticipantsObserver(this.livekitRoom).pipe(this.scope.state());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists of RemoteParticipants to "hold" on display, even if LiveKit claims that
|
* Lists of RemoteParticipants to "hold" on display, even if LiveKit claims that
|
||||||
* they've left
|
* they've left
|
||||||
*/
|
*/
|
||||||
private readonly remoteParticipantHolds: Observable<RemoteParticipant[][]> =
|
private readonly remoteParticipantHolds$: Observable<RemoteParticipant[][]> =
|
||||||
this.connectionState.pipe(
|
this.connectionState$.pipe(
|
||||||
withLatestFrom(this.rawRemoteParticipants),
|
withLatestFrom(this.rawRemoteParticipants$),
|
||||||
mergeMap(([s, ps]) => {
|
mergeMap(([s, ps]) => {
|
||||||
// Whenever we switch focuses, we should retain all the previous
|
// Whenever we switch focuses, we should retain all the previous
|
||||||
// participants for at least POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS ms to
|
// participants for at least POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS ms to
|
||||||
@@ -389,7 +392,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
// Wait for time to pass and the connection state to have changed
|
// Wait for time to pass and the connection state to have changed
|
||||||
forkJoin([
|
forkJoin([
|
||||||
timer(POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS),
|
timer(POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS),
|
||||||
this.connectionState.pipe(
|
this.connectionState$.pipe(
|
||||||
filter((s) => s !== ECAddonConnectionState.ECSwitchingFocus),
|
filter((s) => s !== ECAddonConnectionState.ECSwitchingFocus),
|
||||||
take(1),
|
take(1),
|
||||||
),
|
),
|
||||||
@@ -412,9 +415,9 @@ export class CallViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* The RemoteParticipants including those that are being "held" on the screen
|
* The RemoteParticipants including those that are being "held" on the screen
|
||||||
*/
|
*/
|
||||||
private readonly remoteParticipants: Observable<RemoteParticipant[]> =
|
private readonly remoteParticipants$: Observable<RemoteParticipant[]> =
|
||||||
combineLatest(
|
combineLatest(
|
||||||
[this.rawRemoteParticipants, this.remoteParticipantHolds],
|
[this.rawRemoteParticipants$, this.remoteParticipantHolds$],
|
||||||
(raw, holds) => {
|
(raw, holds) => {
|
||||||
const result = [...raw];
|
const result = [...raw];
|
||||||
const resultIds = new Set(result.map((p) => p.identity));
|
const resultIds = new Set(result.map((p) => p.identity));
|
||||||
@@ -436,10 +439,10 @@ export class CallViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* List of MediaItems that we want to display
|
* List of MediaItems that we want to display
|
||||||
*/
|
*/
|
||||||
private readonly mediaItems: Observable<MediaItem[]> = combineLatest([
|
private readonly mediaItems$: Observable<MediaItem[]> = combineLatest([
|
||||||
this.remoteParticipants,
|
this.remoteParticipants$,
|
||||||
observeParticipantMedia(this.livekitRoom.localParticipant),
|
observeParticipantMedia(this.livekitRoom.localParticipant),
|
||||||
duplicateTiles.value,
|
duplicateTiles.value$,
|
||||||
// Also react to changes in the MatrixRTC session list.
|
// Also react to changes in the MatrixRTC session list.
|
||||||
// The session list will also be update if a room membership changes.
|
// The session list will also be update if a room membership changes.
|
||||||
// No additional RoomState event listener needs to be set up.
|
// No additional RoomState event listener needs to be set up.
|
||||||
@@ -447,6 +450,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.matrixRTCSession,
|
this.matrixRTCSession,
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
).pipe(startWith(null)),
|
).pipe(startWith(null)),
|
||||||
|
showNonMemberTiles.value$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
scan(
|
scan(
|
||||||
(
|
(
|
||||||
@@ -456,6 +460,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
{ participant: localParticipant },
|
{ participant: localParticipant },
|
||||||
duplicateTiles,
|
duplicateTiles,
|
||||||
_membershipsChanged,
|
_membershipsChanged,
|
||||||
|
showNonMemberTiles,
|
||||||
],
|
],
|
||||||
) => {
|
) => {
|
||||||
const newItems = new Map(
|
const newItems = new Map(
|
||||||
@@ -493,9 +498,17 @@ export class CallViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
for (let i = 0; i < 1 + duplicateTiles; i++) {
|
for (let i = 0; i < 1 + duplicateTiles; i++) {
|
||||||
const indexedMediaId = `${livekitParticipantId}:${i}`;
|
const indexedMediaId = `${livekitParticipantId}:${i}`;
|
||||||
const prevMedia = prevItems.get(indexedMediaId);
|
let prevMedia = prevItems.get(indexedMediaId);
|
||||||
if (prevMedia && prevMedia instanceof UserMedia) {
|
if (prevMedia && prevMedia instanceof UserMedia) {
|
||||||
prevMedia.updateParticipant(participant);
|
prevMedia.updateParticipant(participant);
|
||||||
|
if (prevMedia.vm.member === undefined) {
|
||||||
|
// We have a previous media created because of the `debugShowNonMember` flag.
|
||||||
|
// In this case we actually replace the media item.
|
||||||
|
// This "hack" never occurs if we do not use the `debugShowNonMember` debugging
|
||||||
|
// option and if we always find a room member for each rtc member (which also
|
||||||
|
// only fails if we have a fundamental problem)
|
||||||
|
prevMedia = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
yield [
|
yield [
|
||||||
indexedMediaId,
|
indexedMediaId,
|
||||||
@@ -531,7 +544,55 @@ export class CallViewModel extends ViewModel {
|
|||||||
}.bind(this)(),
|
}.bind(this)(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return newItems;
|
// Generate non member items (items without a corresponding MatrixRTC member)
|
||||||
|
// Those items should not be rendered, they are participants in LiveKit that do not have a corresponding
|
||||||
|
// MatrixRTC members. This cannot be any good:
|
||||||
|
// - A malicious user impersonates someone
|
||||||
|
// - Someone injects abusive content
|
||||||
|
// - The user cannot have encryption keys so it makes no sense to participate
|
||||||
|
// We can only trust users that have a MatrixRTC member event.
|
||||||
|
//
|
||||||
|
// This is still available as a debug option. This can be useful
|
||||||
|
// - If one wants to test scalability using the LiveKit CLI.
|
||||||
|
// - If an experimental project does not yet do the MatrixRTC bits.
|
||||||
|
// - If someone wants to debug if the LiveKit connection works but MatrixRTC room state failed to arrive.
|
||||||
|
const newNonMemberItems = showNonMemberTiles
|
||||||
|
? new Map(
|
||||||
|
function* (this: CallViewModel): Iterable<[string, MediaItem]> {
|
||||||
|
for (const participant of remoteParticipants) {
|
||||||
|
for (let i = 0; i < 1 + duplicateTiles; i++) {
|
||||||
|
const maybeNonMemberParticipantId =
|
||||||
|
participant.identity + ":" + i;
|
||||||
|
if (!newItems.has(maybeNonMemberParticipantId)) {
|
||||||
|
const nonMemberId = maybeNonMemberParticipantId;
|
||||||
|
yield [
|
||||||
|
nonMemberId,
|
||||||
|
prevItems.get(nonMemberId) ??
|
||||||
|
new UserMedia(
|
||||||
|
nonMemberId,
|
||||||
|
undefined,
|
||||||
|
participant,
|
||||||
|
this.encryptionSystem,
|
||||||
|
this.livekitRoom,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.bind(this)(),
|
||||||
|
)
|
||||||
|
: new Map();
|
||||||
|
if (newNonMemberItems.size > 0) {
|
||||||
|
logger.debug("Added NonMember items: ", newNonMemberItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedNew = new Map([
|
||||||
|
...newNonMemberItems.entries(),
|
||||||
|
...newItems.entries(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const [id, t] of prevItems) if (!combinedNew.has(id)) t.destroy();
|
||||||
|
return combinedNew;
|
||||||
},
|
},
|
||||||
new Map<string, MediaItem>(),
|
new Map<string, MediaItem>(),
|
||||||
),
|
),
|
||||||
@@ -545,13 +606,13 @@ export class CallViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* List of MediaItems that we want to display, that are of type UserMedia
|
* List of MediaItems that we want to display, that are of type UserMedia
|
||||||
*/
|
*/
|
||||||
private readonly userMedia: Observable<UserMedia[]> = this.mediaItems.pipe(
|
private readonly userMedia$: Observable<UserMedia[]> = this.mediaItems$.pipe(
|
||||||
map((mediaItems) =>
|
map((mediaItems) =>
|
||||||
mediaItems.filter((m): m is UserMedia => m instanceof UserMedia),
|
mediaItems.filter((m): m is UserMedia => m instanceof UserMedia),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
public readonly memberChanges = this.userMedia
|
public readonly memberChanges$ = this.userMedia$
|
||||||
.pipe(map((mediaItems) => mediaItems.map((m) => m.id)))
|
.pipe(map((mediaItems) => mediaItems.map((m) => m.id)))
|
||||||
.pipe(
|
.pipe(
|
||||||
scan<string[], { ids: string[]; joined: string[]; left: string[] }>(
|
scan<string[], { ids: string[]; joined: string[]; left: string[] }>(
|
||||||
@@ -567,22 +628,22 @@ export class CallViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* List of MediaItems that we want to display, that are of type ScreenShare
|
* List of MediaItems that we want to display, that are of type ScreenShare
|
||||||
*/
|
*/
|
||||||
private readonly screenShares: Observable<ScreenShare[]> =
|
private readonly screenShares$: Observable<ScreenShare[]> =
|
||||||
this.mediaItems.pipe(
|
this.mediaItems$.pipe(
|
||||||
map((mediaItems) =>
|
map((mediaItems) =>
|
||||||
mediaItems.filter((m): m is ScreenShare => m instanceof ScreenShare),
|
mediaItems.filter((m): m is ScreenShare => m instanceof ScreenShare),
|
||||||
),
|
),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly spotlightSpeaker: Observable<UserMediaViewModel | null> =
|
private readonly spotlightSpeaker$: Observable<UserMediaViewModel | null> =
|
||||||
this.userMedia.pipe(
|
this.userMedia$.pipe(
|
||||||
switchMap((mediaItems) =>
|
switchMap((mediaItems) =>
|
||||||
mediaItems.length === 0
|
mediaItems.length === 0
|
||||||
? of([])
|
? of([])
|
||||||
: combineLatest(
|
: combineLatest(
|
||||||
mediaItems.map((m) =>
|
mediaItems.map((m) =>
|
||||||
m.vm.speaking.pipe(map((s) => [m, s] as const)),
|
m.vm.speaking$.pipe(map((s) => [m, s] as const)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -611,64 +672,68 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly grid: Observable<UserMediaViewModel[]> = this.userMedia.pipe(
|
private readonly grid$: Observable<UserMediaViewModel[]> =
|
||||||
switchMap((mediaItems) => {
|
this.userMedia$.pipe(
|
||||||
const bins = mediaItems.map((m) =>
|
switchMap((mediaItems) => {
|
||||||
combineLatest(
|
const bins = mediaItems.map((m) =>
|
||||||
[
|
combineLatest(
|
||||||
m.speaker,
|
[
|
||||||
m.presenter,
|
m.speaker$,
|
||||||
m.vm.videoEnabled,
|
m.presenter$,
|
||||||
m.vm instanceof LocalUserMediaViewModel
|
m.vm.videoEnabled$,
|
||||||
? m.vm.alwaysShow
|
m.vm instanceof LocalUserMediaViewModel
|
||||||
: of(false),
|
? m.vm.alwaysShow$
|
||||||
],
|
: of(false),
|
||||||
(speaker, presenter, video, alwaysShow) => {
|
],
|
||||||
let bin: SortingBin;
|
(speaker, presenter, video, alwaysShow) => {
|
||||||
if (m.vm.local)
|
let bin: SortingBin;
|
||||||
bin = alwaysShow
|
if (m.vm.local)
|
||||||
? SortingBin.SelfAlwaysShown
|
bin = alwaysShow
|
||||||
: SortingBin.SelfNotAlwaysShown;
|
? SortingBin.SelfAlwaysShown
|
||||||
else if (presenter) bin = SortingBin.Presenters;
|
: SortingBin.SelfNotAlwaysShown;
|
||||||
else if (speaker) bin = SortingBin.Speakers;
|
else if (presenter) bin = SortingBin.Presenters;
|
||||||
else if (video) bin = SortingBin.Video;
|
else if (speaker) bin = SortingBin.Speakers;
|
||||||
else bin = SortingBin.NoVideo;
|
else if (video) bin = SortingBin.Video;
|
||||||
|
else bin = SortingBin.NoVideo;
|
||||||
|
|
||||||
return [m, bin] as const;
|
return [m, bin] as const;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Sort the media by bin order and generate a tile for each one
|
// Sort the media by bin order and generate a tile for each one
|
||||||
return bins.length === 0
|
return bins.length === 0
|
||||||
? of([])
|
? of([])
|
||||||
: combineLatest(bins, (...bins) =>
|
: combineLatest(bins, (...bins) =>
|
||||||
bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm),
|
bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m.vm),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
distinctUntilChanged(shallowEquals),
|
||||||
|
this.scope.state(),
|
||||||
|
);
|
||||||
|
|
||||||
private readonly spotlight: Observable<MediaViewModel[]> =
|
private readonly spotlight$: Observable<MediaViewModel[]> =
|
||||||
this.screenShares.pipe(
|
this.screenShares$.pipe(
|
||||||
switchMap((screenShares) => {
|
switchMap((screenShares) => {
|
||||||
if (screenShares.length > 0) {
|
if (screenShares.length > 0) {
|
||||||
return of(screenShares.map((m) => m.vm));
|
return of(screenShares.map((m) => m.vm));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.spotlightSpeaker.pipe(
|
return this.spotlightSpeaker$.pipe(
|
||||||
map((speaker) => (speaker ? [speaker] : [])),
|
map((speaker) => (speaker ? [speaker] : [])),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
distinctUntilChanged(shallowEquals),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly pip: Observable<UserMediaViewModel | null> = combineLatest([
|
private readonly pip$: Observable<UserMediaViewModel | null> = combineLatest([
|
||||||
this.screenShares,
|
this.screenShares$,
|
||||||
this.spotlightSpeaker,
|
this.spotlightSpeaker$,
|
||||||
this.mediaItems,
|
this.mediaItems$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(([screenShares, spotlight, mediaItems]) => {
|
switchMap(([screenShares, spotlight, mediaItems]) => {
|
||||||
if (screenShares.length > 0) {
|
if (screenShares.length > 0) {
|
||||||
return this.spotlightSpeaker;
|
return this.spotlightSpeaker$;
|
||||||
}
|
}
|
||||||
if (!spotlight || spotlight.local) {
|
if (!spotlight || spotlight.local) {
|
||||||
return of(null);
|
return of(null);
|
||||||
@@ -685,7 +750,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
if (!localUserMediaViewModel) {
|
if (!localUserMediaViewModel) {
|
||||||
return of(null);
|
return of(null);
|
||||||
}
|
}
|
||||||
return localUserMediaViewModel.alwaysShow.pipe(
|
return localUserMediaViewModel.alwaysShow$.pipe(
|
||||||
map((alwaysShow) => {
|
map((alwaysShow) => {
|
||||||
if (alwaysShow) {
|
if (alwaysShow) {
|
||||||
return localUserMediaViewModel;
|
return localUserMediaViewModel;
|
||||||
@@ -698,19 +763,19 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly hasRemoteScreenShares: Observable<boolean> =
|
private readonly hasRemoteScreenShares$: Observable<boolean> =
|
||||||
this.spotlight.pipe(
|
this.spotlight$.pipe(
|
||||||
map((spotlight) =>
|
map((spotlight) =>
|
||||||
spotlight.some((vm) => !vm.local && vm instanceof ScreenShareViewModel),
|
spotlight.some((vm) => !vm.local && vm instanceof ScreenShareViewModel),
|
||||||
),
|
),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly pipEnabled: Observable<boolean> = setPipEnabled.pipe(
|
private readonly pipEnabled$: Observable<boolean> = setPipEnabled$.pipe(
|
||||||
startWith(false),
|
startWith(false),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly naturalWindowMode: Observable<WindowMode> = fromEvent(
|
private readonly naturalWindowMode$: Observable<WindowMode> = fromEvent(
|
||||||
window,
|
window,
|
||||||
"resize",
|
"resize",
|
||||||
).pipe(
|
).pipe(
|
||||||
@@ -732,30 +797,30 @@ export class CallViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* The general shape of the window.
|
* The general shape of the window.
|
||||||
*/
|
*/
|
||||||
public readonly windowMode: Observable<WindowMode> = this.pipEnabled.pipe(
|
public readonly windowMode$: Observable<WindowMode> = this.pipEnabled$.pipe(
|
||||||
switchMap((pip) => (pip ? of<WindowMode>("pip") : this.naturalWindowMode)),
|
switchMap((pip) => (pip ? of<WindowMode>("pip") : this.naturalWindowMode$)),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly spotlightExpandedToggle = new Subject<void>();
|
private readonly spotlightExpandedToggle$ = new Subject<void>();
|
||||||
public readonly spotlightExpanded: Observable<boolean> =
|
public readonly spotlightExpanded$: Observable<boolean> =
|
||||||
this.spotlightExpandedToggle.pipe(
|
this.spotlightExpandedToggle$.pipe(
|
||||||
accumulate(false, (expanded) => !expanded),
|
accumulate(false, (expanded) => !expanded),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly gridModeUserSelection = new Subject<GridMode>();
|
private readonly gridModeUserSelection$ = new Subject<GridMode>();
|
||||||
/**
|
/**
|
||||||
* The layout mode of the media tile grid.
|
* The layout mode of the media tile grid.
|
||||||
*/
|
*/
|
||||||
public readonly gridMode: Observable<GridMode> =
|
public readonly gridMode$: Observable<GridMode> =
|
||||||
// If the user hasn't selected spotlight and somebody starts screen sharing,
|
// If the user hasn't selected spotlight and somebody starts screen sharing,
|
||||||
// automatically switch to spotlight mode and reset when screen sharing ends
|
// automatically switch to spotlight mode and reset when screen sharing ends
|
||||||
this.gridModeUserSelection.pipe(
|
this.gridModeUserSelection$.pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
switchMap((userSelection) =>
|
switchMap((userSelection) =>
|
||||||
(userSelection === "spotlight"
|
(userSelection === "spotlight"
|
||||||
? EMPTY
|
? EMPTY
|
||||||
: combineLatest([this.hasRemoteScreenShares, this.windowMode]).pipe(
|
: combineLatest([this.hasRemoteScreenShares$, this.windowMode$]).pipe(
|
||||||
skip(userSelection === null ? 0 : 1),
|
skip(userSelection === null ? 0 : 1),
|
||||||
map(
|
map(
|
||||||
([hasScreenShares, windowMode]): GridMode =>
|
([hasScreenShares, windowMode]): GridMode =>
|
||||||
@@ -770,43 +835,41 @@ export class CallViewModel extends ViewModel {
|
|||||||
);
|
);
|
||||||
|
|
||||||
public setGridMode(value: GridMode): void {
|
public setGridMode(value: GridMode): void {
|
||||||
this.gridModeUserSelection.next(value);
|
this.gridModeUserSelection$.next(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly gridLayoutMedia: Observable<GridLayoutMedia> = combineLatest(
|
private readonly gridLayoutMedia$: Observable<GridLayoutMedia> =
|
||||||
[this.grid, this.spotlight],
|
combineLatest([this.grid$, this.spotlight$], (grid, spotlight) => ({
|
||||||
(grid, spotlight) => ({
|
|
||||||
type: "grid",
|
type: "grid",
|
||||||
spotlight: spotlight.some((vm) => vm instanceof ScreenShareViewModel)
|
spotlight: spotlight.some((vm) => vm instanceof ScreenShareViewModel)
|
||||||
? spotlight
|
? spotlight
|
||||||
: undefined,
|
: undefined,
|
||||||
grid,
|
grid,
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
private readonly spotlightLandscapeLayoutMedia: Observable<SpotlightLandscapeLayoutMedia> =
|
private readonly spotlightLandscapeLayoutMedia$: Observable<SpotlightLandscapeLayoutMedia> =
|
||||||
combineLatest([this.grid, this.spotlight], (grid, spotlight) => ({
|
combineLatest([this.grid$, this.spotlight$], (grid, spotlight) => ({
|
||||||
type: "spotlight-landscape",
|
type: "spotlight-landscape",
|
||||||
spotlight,
|
spotlight,
|
||||||
grid,
|
grid,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
private readonly spotlightPortraitLayoutMedia: Observable<SpotlightPortraitLayoutMedia> =
|
private readonly spotlightPortraitLayoutMedia$: Observable<SpotlightPortraitLayoutMedia> =
|
||||||
combineLatest([this.grid, this.spotlight], (grid, spotlight) => ({
|
combineLatest([this.grid$, this.spotlight$], (grid, spotlight) => ({
|
||||||
type: "spotlight-portrait",
|
type: "spotlight-portrait",
|
||||||
spotlight,
|
spotlight,
|
||||||
grid,
|
grid,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
private readonly spotlightExpandedLayoutMedia: Observable<SpotlightExpandedLayoutMedia> =
|
private readonly spotlightExpandedLayoutMedia$: Observable<SpotlightExpandedLayoutMedia> =
|
||||||
combineLatest([this.spotlight, this.pip], (spotlight, pip) => ({
|
combineLatest([this.spotlight$, this.pip$], (spotlight, pip) => ({
|
||||||
type: "spotlight-expanded",
|
type: "spotlight-expanded",
|
||||||
spotlight,
|
spotlight,
|
||||||
pip: pip ?? undefined,
|
pip: pip ?? undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
private readonly oneOnOneLayoutMedia: Observable<OneOnOneLayoutMedia | null> =
|
private readonly oneOnOneLayoutMedia$: Observable<OneOnOneLayoutMedia | null> =
|
||||||
this.mediaItems.pipe(
|
this.mediaItems$.pipe(
|
||||||
map((mediaItems) => {
|
map((mediaItems) => {
|
||||||
if (mediaItems.length !== 2) return null;
|
if (mediaItems.length !== 2) return null;
|
||||||
const local = mediaItems.find((vm) => vm.vm.local)?.vm as
|
const local = mediaItems.find((vm) => vm.vm.local)?.vm as
|
||||||
@@ -824,129 +887,125 @@ export class CallViewModel extends ViewModel {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly pipLayoutMedia: Observable<LayoutMedia> =
|
private readonly pipLayoutMedia$: Observable<LayoutMedia> =
|
||||||
this.spotlight.pipe(map((spotlight) => ({ type: "pip", spotlight })));
|
this.spotlight$.pipe(map((spotlight) => ({ type: "pip", spotlight })));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The media to be used to produce a layout.
|
* The media to be used to produce a layout.
|
||||||
*/
|
*/
|
||||||
private readonly layoutMedia: Observable<LayoutMedia> = this.windowMode.pipe(
|
private readonly layoutMedia$: Observable<LayoutMedia> =
|
||||||
switchMap((windowMode) => {
|
this.windowMode$.pipe(
|
||||||
switch (windowMode) {
|
switchMap((windowMode) => {
|
||||||
case "normal":
|
switch (windowMode) {
|
||||||
return this.gridMode.pipe(
|
case "normal":
|
||||||
switchMap((gridMode) => {
|
return this.gridMode$.pipe(
|
||||||
switch (gridMode) {
|
switchMap((gridMode) => {
|
||||||
case "grid":
|
switch (gridMode) {
|
||||||
return this.oneOnOneLayoutMedia.pipe(
|
case "grid":
|
||||||
switchMap((oneOnOne) =>
|
return this.oneOnOneLayoutMedia$.pipe(
|
||||||
oneOnOne === null ? this.gridLayoutMedia : of(oneOnOne),
|
switchMap((oneOnOne) =>
|
||||||
),
|
oneOnOne === null
|
||||||
);
|
? this.gridLayoutMedia$
|
||||||
case "spotlight":
|
: of(oneOnOne),
|
||||||
return this.spotlightExpanded.pipe(
|
),
|
||||||
switchMap((expanded) =>
|
);
|
||||||
expanded
|
case "spotlight":
|
||||||
? this.spotlightExpandedLayoutMedia
|
return this.spotlightExpanded$.pipe(
|
||||||
: this.spotlightLandscapeLayoutMedia,
|
switchMap((expanded) =>
|
||||||
),
|
expanded
|
||||||
);
|
? this.spotlightExpandedLayoutMedia$
|
||||||
}
|
: this.spotlightLandscapeLayoutMedia$,
|
||||||
}),
|
),
|
||||||
);
|
);
|
||||||
case "narrow":
|
}
|
||||||
return this.oneOnOneLayoutMedia.pipe(
|
}),
|
||||||
switchMap((oneOnOne) =>
|
);
|
||||||
oneOnOne === null
|
case "narrow":
|
||||||
? combineLatest(
|
return this.oneOnOneLayoutMedia$.pipe(
|
||||||
[this.grid, this.spotlight],
|
switchMap((oneOnOne) =>
|
||||||
(grid, spotlight) =>
|
oneOnOne === null
|
||||||
grid.length > smallMobileCallThreshold ||
|
? combineLatest(
|
||||||
spotlight.some((vm) => vm instanceof ScreenShareViewModel)
|
[this.grid$, this.spotlight$],
|
||||||
? this.spotlightPortraitLayoutMedia
|
(grid, spotlight) =>
|
||||||
: this.gridLayoutMedia,
|
grid.length > smallMobileCallThreshold ||
|
||||||
).pipe(switchAll())
|
spotlight.some(
|
||||||
: // The expanded spotlight layout makes for a better one-on-one
|
(vm) => vm instanceof ScreenShareViewModel,
|
||||||
// experience in narrow windows
|
)
|
||||||
this.spotlightExpandedLayoutMedia,
|
? this.spotlightPortraitLayoutMedia$
|
||||||
),
|
: this.gridLayoutMedia$,
|
||||||
);
|
).pipe(switchAll())
|
||||||
case "flat":
|
: // The expanded spotlight layout makes for a better one-on-one
|
||||||
return this.gridMode.pipe(
|
// experience in narrow windows
|
||||||
switchMap((gridMode) => {
|
this.spotlightExpandedLayoutMedia$,
|
||||||
switch (gridMode) {
|
),
|
||||||
case "grid":
|
);
|
||||||
// Yes, grid mode actually gets you a "spotlight" layout in
|
case "flat":
|
||||||
// this window mode.
|
return this.gridMode$.pipe(
|
||||||
return this.spotlightLandscapeLayoutMedia;
|
switchMap((gridMode) => {
|
||||||
case "spotlight":
|
switch (gridMode) {
|
||||||
return this.spotlightExpandedLayoutMedia;
|
case "grid":
|
||||||
}
|
// Yes, grid mode actually gets you a "spotlight" layout in
|
||||||
}),
|
// this window mode.
|
||||||
);
|
return this.spotlightLandscapeLayoutMedia$;
|
||||||
case "pip":
|
case "spotlight":
|
||||||
return this.pipLayoutMedia;
|
return this.spotlightExpandedLayoutMedia$;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
this.scope.state(),
|
);
|
||||||
);
|
case "pip":
|
||||||
|
return this.pipLayoutMedia$;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.scope.state(),
|
||||||
|
);
|
||||||
|
|
||||||
public readonly layoutInternals: Observable<
|
// There is a cyclical dependency here: the layout algorithms want to know
|
||||||
|
// which tiles are on screen, but to know which tiles are on screen we have to
|
||||||
|
// first render a layout. To deal with this we assume initially that no tiles
|
||||||
|
// are visible, and loop the data back into the layouts with a Subject.
|
||||||
|
private readonly visibleTiles$ = new Subject<number>();
|
||||||
|
private readonly setVisibleTiles = (value: number): void =>
|
||||||
|
this.visibleTiles$.next(value);
|
||||||
|
|
||||||
|
public readonly layoutInternals$: Observable<
|
||||||
LayoutScanState & { layout: Layout }
|
LayoutScanState & { layout: Layout }
|
||||||
> = this.layoutMedia.pipe(
|
> = combineLatest([
|
||||||
// Each layout will produce a set of tiles, and these tiles have an
|
this.layoutMedia$,
|
||||||
// observable indicating whether they're visible. We loop this information
|
this.visibleTiles$.pipe(startWith(0), distinctUntilChanged()),
|
||||||
// back into the layout process by using switchScan.
|
]).pipe(
|
||||||
switchScan<
|
scan<
|
||||||
LayoutMedia,
|
[LayoutMedia, number],
|
||||||
LayoutScanState,
|
LayoutScanState & { layout: Layout },
|
||||||
Observable<LayoutScanState & { layout: Layout }>
|
LayoutScanState
|
||||||
>(
|
>(
|
||||||
({ tiles: prevTiles, visibleTiles }, media) => {
|
({ tiles: prevTiles }, [media, visibleTiles]) => {
|
||||||
let layout: Layout;
|
let layout: Layout;
|
||||||
let newTiles: TileStore;
|
let newTiles: TileStore;
|
||||||
switch (media.type) {
|
switch (media.type) {
|
||||||
case "grid":
|
case "grid":
|
||||||
case "spotlight-landscape":
|
case "spotlight-landscape":
|
||||||
case "spotlight-portrait":
|
case "spotlight-portrait":
|
||||||
[layout, newTiles] = gridLikeLayout(media, visibleTiles, prevTiles);
|
[layout, newTiles] = gridLikeLayout(
|
||||||
break;
|
|
||||||
case "spotlight-expanded":
|
|
||||||
[layout, newTiles] = spotlightExpandedLayout(
|
|
||||||
media,
|
media,
|
||||||
visibleTiles,
|
visibleTiles,
|
||||||
|
this.setVisibleTiles,
|
||||||
prevTiles,
|
prevTiles,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "spotlight-expanded":
|
||||||
|
[layout, newTiles] = spotlightExpandedLayout(media, prevTiles);
|
||||||
|
break;
|
||||||
case "one-on-one":
|
case "one-on-one":
|
||||||
[layout, newTiles] = oneOnOneLayout(media, visibleTiles, prevTiles);
|
[layout, newTiles] = oneOnOneLayout(media, prevTiles);
|
||||||
break;
|
break;
|
||||||
case "pip":
|
case "pip":
|
||||||
[layout, newTiles] = pipLayout(media, visibleTiles, prevTiles);
|
[layout, newTiles] = pipLayout(media, prevTiles);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take all of the 'visible' observables and combine them into one big
|
return { layout, tiles: newTiles };
|
||||||
// observable array
|
|
||||||
const visibilities =
|
|
||||||
newTiles.gridTiles.length === 0
|
|
||||||
? of([])
|
|
||||||
: combineLatest(newTiles.gridTiles.map((tile) => tile.visible));
|
|
||||||
return visibilities.pipe(
|
|
||||||
map((visibilities) => ({
|
|
||||||
layout: layout,
|
|
||||||
tiles: newTiles,
|
|
||||||
visibleTiles: new Set(
|
|
||||||
newTiles.gridTiles.filter((_tile, i) => visibilities[i]),
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
layout: null,
|
|
||||||
tiles: TileStore.empty(),
|
|
||||||
visibleTiles: new Set(),
|
|
||||||
},
|
},
|
||||||
|
{ layout: null, tiles: TileStore.empty() },
|
||||||
),
|
),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
@@ -954,7 +1013,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* The layout of tiles in the call interface.
|
* The layout of tiles in the call interface.
|
||||||
*/
|
*/
|
||||||
public readonly layout: Observable<Layout> = this.layoutInternals.pipe(
|
public readonly layout$: Observable<Layout> = this.layoutInternals$.pipe(
|
||||||
map(({ layout }) => layout),
|
map(({ layout }) => layout),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
@@ -962,18 +1021,18 @@ export class CallViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* The current generation of the tile store, exposed for debugging purposes.
|
* The current generation of the tile store, exposed for debugging purposes.
|
||||||
*/
|
*/
|
||||||
public readonly tileStoreGeneration: Observable<number> =
|
public readonly tileStoreGeneration$: Observable<number> =
|
||||||
this.layoutInternals.pipe(
|
this.layoutInternals$.pipe(
|
||||||
map(({ tiles }) => tiles.generation),
|
map(({ tiles }) => tiles.generation),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
public showSpotlightIndicators: Observable<boolean> = this.layout.pipe(
|
public showSpotlightIndicators$: Observable<boolean> = this.layout$.pipe(
|
||||||
map((l) => l.type !== "grid"),
|
map((l) => l.type !== "grid"),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
public showSpeakingIndicators: Observable<boolean> = this.layout.pipe(
|
public showSpeakingIndicators$: Observable<boolean> = this.layout$.pipe(
|
||||||
switchMap((l) => {
|
switchMap((l) => {
|
||||||
switch (l.type) {
|
switch (l.type) {
|
||||||
case "spotlight-landscape":
|
case "spotlight-landscape":
|
||||||
@@ -981,7 +1040,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
// If the spotlight is showing the active speaker, we can do without
|
// If the spotlight is showing the active speaker, we can do without
|
||||||
// speaking indicators as they're a redundant visual cue. But if
|
// speaking indicators as they're a redundant visual cue. But if
|
||||||
// screen sharing feeds are in the spotlight we still need them.
|
// screen sharing feeds are in the spotlight we still need them.
|
||||||
return l.spotlight.media.pipe(
|
return l.spotlight.media$.pipe(
|
||||||
map((models: MediaViewModel[]) =>
|
map((models: MediaViewModel[]) =>
|
||||||
models.some((m) => m instanceof ScreenShareViewModel),
|
models.some((m) => m instanceof ScreenShareViewModel),
|
||||||
),
|
),
|
||||||
@@ -1000,11 +1059,11 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
public readonly toggleSpotlightExpanded: Observable<(() => void) | null> =
|
public readonly toggleSpotlightExpanded$: Observable<(() => void) | null> =
|
||||||
this.windowMode.pipe(
|
this.windowMode$.pipe(
|
||||||
switchMap((mode) =>
|
switchMap((mode) =>
|
||||||
mode === "normal"
|
mode === "normal"
|
||||||
? this.layout.pipe(
|
? this.layout$.pipe(
|
||||||
map(
|
map(
|
||||||
(l) =>
|
(l) =>
|
||||||
l.type === "spotlight-landscape" ||
|
l.type === "spotlight-landscape" ||
|
||||||
@@ -1015,50 +1074,50 @@ export class CallViewModel extends ViewModel {
|
|||||||
),
|
),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((enabled) =>
|
map((enabled) =>
|
||||||
enabled ? (): void => this.spotlightExpandedToggle.next() : null,
|
enabled ? (): void => this.spotlightExpandedToggle$.next() : null,
|
||||||
),
|
),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly screenTap = new Subject<void>();
|
private readonly screenTap$ = new Subject<void>();
|
||||||
private readonly controlsTap = new Subject<void>();
|
private readonly controlsTap$ = new Subject<void>();
|
||||||
private readonly screenHover = new Subject<void>();
|
private readonly screenHover$ = new Subject<void>();
|
||||||
private readonly screenUnhover = new Subject<void>();
|
private readonly screenUnhover$ = new Subject<void>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for when the user taps the call view.
|
* Callback for when the user taps the call view.
|
||||||
*/
|
*/
|
||||||
public tapScreen(): void {
|
public tapScreen(): void {
|
||||||
this.screenTap.next();
|
this.screenTap$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for when the user taps the call's controls.
|
* Callback for when the user taps the call's controls.
|
||||||
*/
|
*/
|
||||||
public tapControls(): void {
|
public tapControls(): void {
|
||||||
this.controlsTap.next();
|
this.controlsTap$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for when the user hovers over the call view.
|
* Callback for when the user hovers over the call view.
|
||||||
*/
|
*/
|
||||||
public hoverScreen(): void {
|
public hoverScreen(): void {
|
||||||
this.screenHover.next();
|
this.screenHover$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for when the user stops hovering over the call view.
|
* Callback for when the user stops hovering over the call view.
|
||||||
*/
|
*/
|
||||||
public unhoverScreen(): void {
|
public unhoverScreen(): void {
|
||||||
this.screenUnhover.next();
|
this.screenUnhover$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly showHeader: Observable<boolean> = this.windowMode.pipe(
|
public readonly showHeader$: Observable<boolean> = this.windowMode$.pipe(
|
||||||
map((mode) => mode !== "pip" && mode !== "flat"),
|
map((mode) => mode !== "pip" && mode !== "flat"),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
public readonly showFooter: Observable<boolean> = this.windowMode.pipe(
|
public readonly showFooter$: Observable<boolean> = this.windowMode$.pipe(
|
||||||
switchMap((mode) => {
|
switchMap((mode) => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "pip":
|
case "pip":
|
||||||
@@ -1073,9 +1132,9 @@ export class CallViewModel extends ViewModel {
|
|||||||
if (isFirefox()) return of(true);
|
if (isFirefox()) return of(true);
|
||||||
// Show/hide the footer in response to interactions
|
// Show/hide the footer in response to interactions
|
||||||
return merge(
|
return merge(
|
||||||
this.screenTap.pipe(map(() => "tap screen" as const)),
|
this.screenTap$.pipe(map(() => "tap screen" as const)),
|
||||||
this.controlsTap.pipe(map(() => "tap controls" as const)),
|
this.controlsTap$.pipe(map(() => "tap controls" as const)),
|
||||||
this.screenHover.pipe(map(() => "hover" as const)),
|
this.screenHover$.pipe(map(() => "hover" as const)),
|
||||||
).pipe(
|
).pipe(
|
||||||
switchScan((state, interaction) => {
|
switchScan((state, interaction) => {
|
||||||
switch (interaction) {
|
switch (interaction) {
|
||||||
@@ -1098,7 +1157,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
// Show on hover and hide after a timeout
|
// Show on hover and hide after a timeout
|
||||||
return race(
|
return race(
|
||||||
timer(showFooterMs),
|
timer(showFooterMs),
|
||||||
this.screenUnhover.pipe(take(1)),
|
this.screenUnhover$.pipe(take(1)),
|
||||||
).pipe(
|
).pipe(
|
||||||
map(() => false),
|
map(() => false),
|
||||||
startWith(true),
|
startWith(true),
|
||||||
@@ -1117,7 +1176,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
private readonly matrixRTCSession: MatrixRTCSession,
|
private readonly matrixRTCSession: MatrixRTCSession,
|
||||||
private readonly livekitRoom: LivekitRoom,
|
private readonly livekitRoom: LivekitRoom,
|
||||||
private readonly encryptionSystem: EncryptionSystem,
|
private readonly encryptionSystem: EncryptionSystem,
|
||||||
private readonly connectionState: Observable<ECConnectionState>,
|
private readonly connectionState$: Observable<ECConnectionState>,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type Layout, type LayoutMedia } from "./CallViewModel";
|
import { type Layout, type LayoutMedia } from "./CallViewModel";
|
||||||
import { type TileStore } from "./TileStore";
|
import { type TileStore } from "./TileStore";
|
||||||
import { type GridTileViewModel } from "./TileViewModel";
|
|
||||||
|
|
||||||
export type GridLikeLayoutType =
|
export type GridLikeLayoutType =
|
||||||
| "grid"
|
| "grid"
|
||||||
@@ -20,7 +19,8 @@ export type GridLikeLayoutType =
|
|||||||
*/
|
*/
|
||||||
export function gridLikeLayout(
|
export function gridLikeLayout(
|
||||||
media: LayoutMedia & { type: GridLikeLayoutType },
|
media: LayoutMedia & { type: GridLikeLayoutType },
|
||||||
visibleTiles: Set<GridTileViewModel>,
|
visibleTiles: number,
|
||||||
|
setVisibleTiles: (value: number) => void,
|
||||||
prevTiles: TileStore,
|
prevTiles: TileStore,
|
||||||
): [Layout & { type: GridLikeLayoutType }, TileStore] {
|
): [Layout & { type: GridLikeLayoutType }, TileStore] {
|
||||||
const update = prevTiles.from(visibleTiles);
|
const update = prevTiles.from(visibleTiles);
|
||||||
@@ -37,6 +37,7 @@ export function gridLikeLayout(
|
|||||||
type: media.type,
|
type: media.type,
|
||||||
spotlight: tiles.spotlightTile,
|
spotlight: tiles.spotlightTile,
|
||||||
grid: tiles.gridTiles,
|
grid: tiles.gridTiles,
|
||||||
|
setVisibleTiles,
|
||||||
} as Layout & { type: GridLikeLayoutType },
|
} as Layout & { type: GridLikeLayoutType },
|
||||||
tiles,
|
tiles,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ test("control a participant's volume", async () => {
|
|||||||
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
|
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expectObservable(vm.localVolume).toBe("ab(cd)(ef)g", {
|
expectObservable(vm.localVolume$).toBe("ab(cd)(ef)g", {
|
||||||
a: 1,
|
a: 1,
|
||||||
b: 0,
|
b: 0,
|
||||||
c: 0.6,
|
c: 0.6,
|
||||||
@@ -69,7 +69,7 @@ test("toggle fit/contain for a participant's video", async () => {
|
|||||||
a: () => vm.toggleFitContain(),
|
a: () => vm.toggleFitContain(),
|
||||||
b: () => vm.toggleFitContain(),
|
b: () => vm.toggleFitContain(),
|
||||||
});
|
});
|
||||||
expectObservable(vm.cropVideo).toBe("abc", {
|
expectObservable(vm.cropVideo$).toBe("abc", {
|
||||||
a: true,
|
a: true,
|
||||||
b: false,
|
b: false,
|
||||||
c: true,
|
c: true,
|
||||||
@@ -82,7 +82,7 @@ test("local media remembers whether it should always be shown", async () => {
|
|||||||
await withLocalMedia(rtcMembership, {}, (vm) =>
|
await withLocalMedia(rtcMembership, {}, (vm) =>
|
||||||
withTestScheduler(({ expectObservable, schedule }) => {
|
withTestScheduler(({ expectObservable, schedule }) => {
|
||||||
schedule("-a|", { a: () => vm.setAlwaysShow(false) });
|
schedule("-a|", { a: () => vm.setAlwaysShow(false) });
|
||||||
expectObservable(vm.alwaysShow).toBe("ab", { a: true, b: false });
|
expectObservable(vm.alwaysShow$).toBe("ab", { a: true, b: false });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// Next local media should start out *not* always shown
|
// Next local media should start out *not* always shown
|
||||||
@@ -93,7 +93,7 @@ test("local media remembers whether it should always be shown", async () => {
|
|||||||
(vm) =>
|
(vm) =>
|
||||||
withTestScheduler(({ expectObservable, schedule }) => {
|
withTestScheduler(({ expectObservable, schedule }) => {
|
||||||
schedule("-a|", { a: () => vm.setAlwaysShow(true) });
|
schedule("-a|", { a: () => vm.setAlwaysShow(true) });
|
||||||
expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true });
|
expectObservable(vm.alwaysShow$).toBe("ab", { a: false, b: true });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ export function useDisplayName(vm: MediaViewModel): string {
|
|||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function observeTrackReference(
|
export function observeTrackReference$(
|
||||||
participant: Observable<Participant | undefined>,
|
participant$: Observable<Participant | undefined>,
|
||||||
source: Track.Source,
|
source: Track.Source,
|
||||||
): Observable<TrackReferenceOrPlaceholder | undefined> {
|
): Observable<TrackReferenceOrPlaceholder | undefined> {
|
||||||
return participant.pipe(
|
return participant$.pipe(
|
||||||
switchMap((p) => {
|
switchMap((p) => {
|
||||||
if (p) {
|
if (p) {
|
||||||
return observeParticipantMedia(p).pipe(
|
return observeParticipantMedia(p).pipe(
|
||||||
@@ -96,7 +96,7 @@ export function observeTrackReference(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function observeRemoteTrackReceivingOkay(
|
function observeRemoteTrackReceivingOkay$(
|
||||||
participant: Participant,
|
participant: Participant,
|
||||||
source: Track.Source,
|
source: Track.Source,
|
||||||
): Observable<boolean | undefined> {
|
): Observable<boolean | undefined> {
|
||||||
@@ -111,7 +111,7 @@ function observeRemoteTrackReceivingOkay(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return combineLatest([
|
return combineLatest([
|
||||||
observeTrackReference(of(participant), source),
|
observeTrackReference$(of(participant), source),
|
||||||
interval(1000).pipe(startWith(0)),
|
interval(1000).pipe(startWith(0)),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(async ([trackReference]) => {
|
switchMap(async ([trackReference]) => {
|
||||||
@@ -168,7 +168,7 @@ function observeRemoteTrackReceivingOkay(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function encryptionErrorObservable(
|
function encryptionErrorObservable$(
|
||||||
room: LivekitRoom,
|
room: LivekitRoom,
|
||||||
participant: Participant,
|
participant: Participant,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
@@ -209,13 +209,13 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
/**
|
/**
|
||||||
* The LiveKit video track for this media.
|
* The LiveKit video track for this media.
|
||||||
*/
|
*/
|
||||||
public readonly video: Observable<TrackReferenceOrPlaceholder | undefined>;
|
public readonly video$: Observable<TrackReferenceOrPlaceholder | undefined>;
|
||||||
/**
|
/**
|
||||||
* Whether there should be a warning that this media is unencrypted.
|
* Whether there should be a warning that this media is unencrypted.
|
||||||
*/
|
*/
|
||||||
public readonly unencryptedWarning: Observable<boolean>;
|
public readonly unencryptedWarning$: Observable<boolean>;
|
||||||
|
|
||||||
public readonly encryptionStatus: Observable<EncryptionStatus>;
|
public readonly encryptionStatus$: Observable<EncryptionStatus>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this media corresponds to the local participant.
|
* Whether this media corresponds to the local participant.
|
||||||
@@ -235,7 +235,7 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
public readonly member: RoomMember | undefined,
|
public readonly member: RoomMember | undefined,
|
||||||
// We don't necessarily have a participant if a user connects via MatrixRTC but not (yet) through
|
// We don't necessarily have a participant if a user connects via MatrixRTC but not (yet) through
|
||||||
// livekit.
|
// livekit.
|
||||||
protected readonly participant: Observable<
|
protected readonly participant$: Observable<
|
||||||
LocalParticipant | RemoteParticipant | undefined
|
LocalParticipant | RemoteParticipant | undefined
|
||||||
>,
|
>,
|
||||||
|
|
||||||
@@ -245,21 +245,21 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
const audio = observeTrackReference(participant, audioSource).pipe(
|
const audio$ = observeTrackReference$(participant$, audioSource).pipe(
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
this.video = observeTrackReference(participant, videoSource).pipe(
|
this.video$ = observeTrackReference$(participant$, videoSource).pipe(
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
this.unencryptedWarning = combineLatest(
|
this.unencryptedWarning$ = combineLatest(
|
||||||
[audio, this.video],
|
[audio$, this.video$],
|
||||||
(a, v) =>
|
(a, v) =>
|
||||||
encryptionSystem.kind !== E2eeType.NONE &&
|
encryptionSystem.kind !== E2eeType.NONE &&
|
||||||
(a?.publication?.isEncrypted === false ||
|
(a?.publication?.isEncrypted === false ||
|
||||||
v?.publication?.isEncrypted === false),
|
v?.publication?.isEncrypted === false),
|
||||||
).pipe(this.scope.state());
|
).pipe(this.scope.state());
|
||||||
|
|
||||||
this.encryptionStatus = this.participant.pipe(
|
this.encryptionStatus$ = this.participant$.pipe(
|
||||||
switchMap((participant): Observable<EncryptionStatus> => {
|
switchMap((participant): Observable<EncryptionStatus> => {
|
||||||
if (!participant) {
|
if (!participant) {
|
||||||
return of(EncryptionStatus.Connecting);
|
return of(EncryptionStatus.Connecting);
|
||||||
@@ -270,20 +270,20 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
return of(EncryptionStatus.Okay);
|
return of(EncryptionStatus.Okay);
|
||||||
} else if (encryptionSystem.kind === E2eeType.PER_PARTICIPANT) {
|
} else if (encryptionSystem.kind === E2eeType.PER_PARTICIPANT) {
|
||||||
return combineLatest([
|
return combineLatest([
|
||||||
encryptionErrorObservable(
|
encryptionErrorObservable$(
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
participant,
|
participant,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
"MissingKey",
|
"MissingKey",
|
||||||
),
|
),
|
||||||
encryptionErrorObservable(
|
encryptionErrorObservable$(
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
participant,
|
participant,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
"InvalidKey",
|
"InvalidKey",
|
||||||
),
|
),
|
||||||
observeRemoteTrackReceivingOkay(participant, audioSource),
|
observeRemoteTrackReceivingOkay$(participant, audioSource),
|
||||||
observeRemoteTrackReceivingOkay(participant, videoSource),
|
observeRemoteTrackReceivingOkay$(participant, videoSource),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([keyMissing, keyInvalid, audioOkay, videoOkay]) => {
|
map(([keyMissing, keyInvalid, audioOkay, videoOkay]) => {
|
||||||
if (keyMissing) return EncryptionStatus.KeyMissing;
|
if (keyMissing) return EncryptionStatus.KeyMissing;
|
||||||
@@ -296,14 +296,14 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return combineLatest([
|
return combineLatest([
|
||||||
encryptionErrorObservable(
|
encryptionErrorObservable$(
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
participant,
|
participant,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
"InvalidKey",
|
"InvalidKey",
|
||||||
),
|
),
|
||||||
observeRemoteTrackReceivingOkay(participant, audioSource),
|
observeRemoteTrackReceivingOkay$(participant, audioSource),
|
||||||
observeRemoteTrackReceivingOkay(participant, videoSource),
|
observeRemoteTrackReceivingOkay$(participant, videoSource),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(
|
map(
|
||||||
([keyInvalid, audioOkay, videoOkay]):
|
([keyInvalid, audioOkay, videoOkay]):
|
||||||
@@ -339,7 +339,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
|||||||
/**
|
/**
|
||||||
* Whether the participant is speaking.
|
* Whether the participant is speaking.
|
||||||
*/
|
*/
|
||||||
public readonly speaking = this.participant.pipe(
|
public readonly speaking$ = this.participant$.pipe(
|
||||||
switchMap((p) =>
|
switchMap((p) =>
|
||||||
p
|
p
|
||||||
? observeParticipantEvents(p, ParticipantEvent.IsSpeakingChanged).pipe(
|
? observeParticipantEvents(p, ParticipantEvent.IsSpeakingChanged).pipe(
|
||||||
@@ -353,49 +353,49 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
|||||||
/**
|
/**
|
||||||
* Whether this participant is sending audio (i.e. is unmuted on their side).
|
* Whether this participant is sending audio (i.e. is unmuted on their side).
|
||||||
*/
|
*/
|
||||||
public readonly audioEnabled: Observable<boolean>;
|
public readonly audioEnabled$: Observable<boolean>;
|
||||||
/**
|
/**
|
||||||
* Whether this participant is sending video.
|
* Whether this participant is sending video.
|
||||||
*/
|
*/
|
||||||
public readonly videoEnabled: Observable<boolean>;
|
public readonly videoEnabled$: Observable<boolean>;
|
||||||
|
|
||||||
private readonly _cropVideo = new BehaviorSubject(true);
|
private readonly _cropVideo$ = new BehaviorSubject(true);
|
||||||
/**
|
/**
|
||||||
* Whether the tile video should be contained inside the tile or be cropped to fit.
|
* Whether the tile video should be contained inside the tile or be cropped to fit.
|
||||||
*/
|
*/
|
||||||
public readonly cropVideo: Observable<boolean> = this._cropVideo;
|
public readonly cropVideo$: Observable<boolean> = this._cropVideo$;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
id: string,
|
id: string,
|
||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: Observable<LocalParticipant | RemoteParticipant | undefined>,
|
participant$: Observable<LocalParticipant | RemoteParticipant | undefined>,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
id,
|
id,
|
||||||
member,
|
member,
|
||||||
participant,
|
participant$,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
Track.Source.Microphone,
|
Track.Source.Microphone,
|
||||||
Track.Source.Camera,
|
Track.Source.Camera,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
const media = participant.pipe(
|
const media$ = participant$.pipe(
|
||||||
switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)),
|
switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
this.audioEnabled = media.pipe(
|
this.audioEnabled$ = media$.pipe(
|
||||||
map((m) => m?.microphoneTrack?.isMuted === false),
|
map((m) => m?.microphoneTrack?.isMuted === false),
|
||||||
);
|
);
|
||||||
this.videoEnabled = media.pipe(
|
this.videoEnabled$ = media$.pipe(
|
||||||
map((m) => m?.cameraTrack?.isMuted === false),
|
map((m) => m?.cameraTrack?.isMuted === false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleFitContain(): void {
|
public toggleFitContain(): void {
|
||||||
this._cropVideo.next(!this._cropVideo.value);
|
this._cropVideo$.next(!this._cropVideo$.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get local(): boolean {
|
public get local(): boolean {
|
||||||
@@ -410,7 +410,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
/**
|
/**
|
||||||
* Whether the video should be mirrored.
|
* Whether the video should be mirrored.
|
||||||
*/
|
*/
|
||||||
public readonly mirror = this.video.pipe(
|
public readonly mirror$ = this.video$.pipe(
|
||||||
switchMap((v) => {
|
switchMap((v) => {
|
||||||
const track = v?.publication?.track;
|
const track = v?.publication?.track;
|
||||||
if (!(track instanceof LocalTrack)) return of(false);
|
if (!(track instanceof LocalTrack)) return of(false);
|
||||||
@@ -428,17 +428,17 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
* Whether to show this tile in a highly visible location near the start of
|
* Whether to show this tile in a highly visible location near the start of
|
||||||
* the grid.
|
* the grid.
|
||||||
*/
|
*/
|
||||||
public readonly alwaysShow = alwaysShowSelf.value;
|
public readonly alwaysShow$ = alwaysShowSelf.value$;
|
||||||
public readonly setAlwaysShow = alwaysShowSelf.setValue;
|
public readonly setAlwaysShow = alwaysShowSelf.setValue;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
id: string,
|
id: string,
|
||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: Observable<LocalParticipant | undefined>,
|
participant$: Observable<LocalParticipant | undefined>,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super(id, member, participant, encryptionSystem, livekitRoom);
|
super(id, member, participant$, encryptionSystem, livekitRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,18 +446,18 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
* A remote participant's user media.
|
* A remote participant's user media.
|
||||||
*/
|
*/
|
||||||
export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
||||||
private readonly locallyMutedToggle = new Subject<void>();
|
private readonly locallyMutedToggle$ = new Subject<void>();
|
||||||
private readonly localVolumeAdjustment = new Subject<number>();
|
private readonly localVolumeAdjustment$ = new Subject<number>();
|
||||||
private readonly localVolumeCommit = new Subject<void>();
|
private readonly localVolumeCommit$ = new Subject<void>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The volume to which this participant's audio is set, as a scalar
|
* The volume to which this participant's audio is set, as a scalar
|
||||||
* multiplier.
|
* multiplier.
|
||||||
*/
|
*/
|
||||||
public readonly localVolume: Observable<number> = merge(
|
public readonly localVolume$: Observable<number> = merge(
|
||||||
this.locallyMutedToggle.pipe(map(() => "toggle mute" as const)),
|
this.locallyMutedToggle$.pipe(map(() => "toggle mute" as const)),
|
||||||
this.localVolumeAdjustment,
|
this.localVolumeAdjustment$,
|
||||||
this.localVolumeCommit.pipe(map(() => "commit" as const)),
|
this.localVolumeCommit$.pipe(map(() => "commit" as const)),
|
||||||
).pipe(
|
).pipe(
|
||||||
accumulate({ volume: 1, committedVolume: 1 }, (state, event) => {
|
accumulate({ volume: 1, committedVolume: 1 }, (state, event) => {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
@@ -487,7 +487,7 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
/**
|
/**
|
||||||
* Whether this participant's audio is disabled.
|
* Whether this participant's audio is disabled.
|
||||||
*/
|
*/
|
||||||
public readonly locallyMuted: Observable<boolean> = this.localVolume.pipe(
|
public readonly locallyMuted$: Observable<boolean> = this.localVolume$.pipe(
|
||||||
map((volume) => volume === 0),
|
map((volume) => volume === 0),
|
||||||
this.scope.state(),
|
this.scope.state(),
|
||||||
);
|
);
|
||||||
@@ -495,29 +495,29 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
public constructor(
|
public constructor(
|
||||||
id: string,
|
id: string,
|
||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: Observable<RemoteParticipant | undefined>,
|
participant$: Observable<RemoteParticipant | undefined>,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super(id, member, participant, encryptionSystem, livekitRoom);
|
super(id, member, participant$, encryptionSystem, livekitRoom);
|
||||||
|
|
||||||
// Sync the local volume with LiveKit
|
// Sync the local volume with LiveKit
|
||||||
combineLatest([
|
combineLatest([
|
||||||
participant,
|
participant$,
|
||||||
this.localVolume.pipe(this.scope.bind()),
|
this.localVolume$.pipe(this.scope.bind()),
|
||||||
]).subscribe(([p, volume]) => p && p.setVolume(volume));
|
]).subscribe(([p, volume]) => p && p.setVolume(volume));
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleLocallyMuted(): void {
|
public toggleLocallyMuted(): void {
|
||||||
this.locallyMutedToggle.next();
|
this.locallyMutedToggle$.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLocalVolume(value: number): void {
|
public setLocalVolume(value: number): void {
|
||||||
this.localVolumeAdjustment.next(value);
|
this.localVolumeAdjustment$.next(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public commitLocalVolume(): void {
|
public commitLocalVolume(): void {
|
||||||
this.localVolumeCommit.next();
|
this.localVolumeCommit$.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +528,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
|
|||||||
public constructor(
|
public constructor(
|
||||||
id: string,
|
id: string,
|
||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: Observable<LocalParticipant | RemoteParticipant>,
|
participant$: Observable<LocalParticipant | RemoteParticipant>,
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
livekitRoom: LivekitRoom,
|
livekitRoom: LivekitRoom,
|
||||||
public readonly local: boolean,
|
public readonly local: boolean,
|
||||||
@@ -536,7 +536,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
|
|||||||
super(
|
super(
|
||||||
id,
|
id,
|
||||||
member,
|
member,
|
||||||
participant,
|
participant$,
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
Track.Source.ScreenShareAudio,
|
Track.Source.ScreenShareAudio,
|
||||||
Track.Source.ScreenShare,
|
Track.Source.ScreenShare,
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ type MonoTypeOperator = <T>(o: Observable<T>) => Observable<T>;
|
|||||||
* A scope which limits the execution lifetime of its bound Observables.
|
* A scope which limits the execution lifetime of its bound Observables.
|
||||||
*/
|
*/
|
||||||
export class ObservableScope {
|
export class ObservableScope {
|
||||||
private readonly ended = new Subject<void>();
|
private readonly ended$ = new Subject<void>();
|
||||||
|
|
||||||
private readonly bindImpl: MonoTypeOperator = takeUntil(this.ended);
|
private readonly bindImpl: MonoTypeOperator = takeUntil(this.ended$);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds an Observable to this scope, so that it completes when the scope
|
* Binds an Observable to this scope, so that it completes when the scope
|
||||||
@@ -31,8 +31,8 @@ export class ObservableScope {
|
|||||||
return this.bindImpl;
|
return this.bindImpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly stateImpl: MonoTypeOperator = (o) =>
|
private readonly stateImpl: MonoTypeOperator = (o$) =>
|
||||||
o.pipe(
|
o$.pipe(
|
||||||
this.bind(),
|
this.bind(),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
shareReplay({ bufferSize: 1, refCount: false }),
|
shareReplay({ bufferSize: 1, refCount: false }),
|
||||||
@@ -51,7 +51,7 @@ export class ObservableScope {
|
|||||||
* Ends the scope, causing any bound Observables to complete.
|
* Ends the scope, causing any bound Observables to complete.
|
||||||
*/
|
*/
|
||||||
public end(): void {
|
public end(): void {
|
||||||
this.ended.next();
|
this.ended$.next();
|
||||||
this.ended.complete();
|
this.ended$.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,15 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type OneOnOneLayout, type OneOnOneLayoutMedia } from "./CallViewModel";
|
import { type OneOnOneLayout, type OneOnOneLayoutMedia } from "./CallViewModel";
|
||||||
import { type TileStore } from "./TileStore";
|
import { type TileStore } from "./TileStore";
|
||||||
import { type GridTileViewModel } from "./TileViewModel";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produces a one-on-one layout with the given media.
|
* Produces a one-on-one layout with the given media.
|
||||||
*/
|
*/
|
||||||
export function oneOnOneLayout(
|
export function oneOnOneLayout(
|
||||||
media: OneOnOneLayoutMedia,
|
media: OneOnOneLayoutMedia,
|
||||||
visibleTiles: Set<GridTileViewModel>,
|
|
||||||
prevTiles: TileStore,
|
prevTiles: TileStore,
|
||||||
): [OneOnOneLayout, TileStore] {
|
): [OneOnOneLayout, TileStore] {
|
||||||
const update = prevTiles.from(visibleTiles);
|
const update = prevTiles.from(2);
|
||||||
update.registerGridTile(media.local);
|
update.registerGridTile(media.local);
|
||||||
update.registerGridTile(media.remote);
|
update.registerGridTile(media.remote);
|
||||||
const tiles = update.build();
|
const tiles = update.build();
|
||||||
|
|||||||
@@ -7,17 +7,15 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type PipLayout, type PipLayoutMedia } from "./CallViewModel";
|
import { type PipLayout, type PipLayoutMedia } from "./CallViewModel";
|
||||||
import { type TileStore } from "./TileStore";
|
import { type TileStore } from "./TileStore";
|
||||||
import { type GridTileViewModel } from "./TileViewModel";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produces a picture-in-picture layout with the given media.
|
* Produces a picture-in-picture layout with the given media.
|
||||||
*/
|
*/
|
||||||
export function pipLayout(
|
export function pipLayout(
|
||||||
media: PipLayoutMedia,
|
media: PipLayoutMedia,
|
||||||
visibleTiles: Set<GridTileViewModel>,
|
|
||||||
prevTiles: TileStore,
|
prevTiles: TileStore,
|
||||||
): [PipLayout, TileStore] {
|
): [PipLayout, TileStore] {
|
||||||
const update = prevTiles.from(visibleTiles);
|
const update = prevTiles.from(0);
|
||||||
update.registerSpotlight(media.spotlight, true);
|
update.registerSpotlight(media.spotlight, true);
|
||||||
const tiles = update.build();
|
const tiles = update.build();
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ import {
|
|||||||
type SpotlightExpandedLayoutMedia,
|
type SpotlightExpandedLayoutMedia,
|
||||||
} from "./CallViewModel";
|
} from "./CallViewModel";
|
||||||
import { type TileStore } from "./TileStore";
|
import { type TileStore } from "./TileStore";
|
||||||
import { type GridTileViewModel } from "./TileViewModel";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produces an expanded spotlight layout with the given media.
|
* Produces an expanded spotlight layout with the given media.
|
||||||
*/
|
*/
|
||||||
export function spotlightExpandedLayout(
|
export function spotlightExpandedLayout(
|
||||||
media: SpotlightExpandedLayoutMedia,
|
media: SpotlightExpandedLayoutMedia,
|
||||||
visibleTiles: Set<GridTileViewModel>,
|
|
||||||
prevTiles: TileStore,
|
prevTiles: TileStore,
|
||||||
): [SpotlightExpandedLayout, TileStore] {
|
): [SpotlightExpandedLayout, TileStore] {
|
||||||
const update = prevTiles.from(visibleTiles);
|
const update = prevTiles.from(1);
|
||||||
update.registerSpotlight(media.spotlight, true);
|
update.registerSpotlight(media.spotlight, true);
|
||||||
if (media.pip !== undefined) update.registerGridTile(media.pip);
|
if (media.pip !== undefined) update.registerGridTile(media.pip);
|
||||||
const tiles = update.build();
|
const tiles = update.build();
|
||||||
|
|||||||
@@ -18,31 +18,31 @@ function debugEntries(entries: GridTileData[]): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let DEBUG_ENABLED = false;
|
let DEBUG_ENABLED = false;
|
||||||
debugTileLayout.value.subscribe((value) => (DEBUG_ENABLED = value));
|
debugTileLayout.value$.subscribe((value) => (DEBUG_ENABLED = value));
|
||||||
|
|
||||||
class SpotlightTileData {
|
class SpotlightTileData {
|
||||||
private readonly media_: BehaviorSubject<MediaViewModel[]>;
|
private readonly media$: BehaviorSubject<MediaViewModel[]>;
|
||||||
public get media(): MediaViewModel[] {
|
public get media(): MediaViewModel[] {
|
||||||
return this.media_.value;
|
return this.media$.value;
|
||||||
}
|
}
|
||||||
public set media(value: MediaViewModel[]) {
|
public set media(value: MediaViewModel[]) {
|
||||||
this.media_.next(value);
|
this.media$.next(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly maximised_: BehaviorSubject<boolean>;
|
private readonly maximised$: BehaviorSubject<boolean>;
|
||||||
public get maximised(): boolean {
|
public get maximised(): boolean {
|
||||||
return this.maximised_.value;
|
return this.maximised$.value;
|
||||||
}
|
}
|
||||||
public set maximised(value: boolean) {
|
public set maximised(value: boolean) {
|
||||||
this.maximised_.next(value);
|
this.maximised$.next(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly vm: SpotlightTileViewModel;
|
public readonly vm: SpotlightTileViewModel;
|
||||||
|
|
||||||
public constructor(media: MediaViewModel[], maximised: boolean) {
|
public constructor(media: MediaViewModel[], maximised: boolean) {
|
||||||
this.media_ = new BehaviorSubject(media);
|
this.media$ = new BehaviorSubject(media);
|
||||||
this.maximised_ = new BehaviorSubject(maximised);
|
this.maximised$ = new BehaviorSubject(maximised);
|
||||||
this.vm = new SpotlightTileViewModel(this.media_, this.maximised_);
|
this.vm = new SpotlightTileViewModel(this.media$, this.maximised$);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
@@ -51,19 +51,19 @@ class SpotlightTileData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GridTileData {
|
class GridTileData {
|
||||||
private readonly media_: BehaviorSubject<UserMediaViewModel>;
|
private readonly media$: BehaviorSubject<UserMediaViewModel>;
|
||||||
public get media(): UserMediaViewModel {
|
public get media(): UserMediaViewModel {
|
||||||
return this.media_.value;
|
return this.media$.value;
|
||||||
}
|
}
|
||||||
public set media(value: UserMediaViewModel) {
|
public set media(value: UserMediaViewModel) {
|
||||||
this.media_.next(value);
|
this.media$.next(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly vm: GridTileViewModel;
|
public readonly vm: GridTileViewModel;
|
||||||
|
|
||||||
public constructor(media: UserMediaViewModel) {
|
public constructor(media: UserMediaViewModel) {
|
||||||
this.media_ = new BehaviorSubject(media);
|
this.media$ = new BehaviorSubject(media);
|
||||||
this.vm = new GridTileViewModel(this.media_);
|
this.vm = new GridTileViewModel(this.media$);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
@@ -101,7 +101,7 @@ export class TileStore {
|
|||||||
* Creates a builder which can be used to update the collection, passing
|
* Creates a builder which can be used to update the collection, passing
|
||||||
* ownership of the tiles to the updated collection.
|
* ownership of the tiles to the updated collection.
|
||||||
*/
|
*/
|
||||||
public from(visibleTiles: Set<GridTileViewModel>): TileStoreBuilder {
|
public from(visibleTiles: number): TileStoreBuilder {
|
||||||
return new TileStoreBuilder(
|
return new TileStoreBuilder(
|
||||||
this.spotlight,
|
this.spotlight,
|
||||||
this.grid,
|
this.grid,
|
||||||
@@ -123,7 +123,10 @@ export class TileStoreBuilder {
|
|||||||
"speaking" in this.prevSpotlight.media[0] &&
|
"speaking" in this.prevSpotlight.media[0] &&
|
||||||
this.prevSpotlight.media[0];
|
this.prevSpotlight.media[0];
|
||||||
|
|
||||||
private readonly prevGridByMedia = new Map(
|
private readonly prevGridByMedia: Map<
|
||||||
|
MediaViewModel,
|
||||||
|
[GridTileData, number]
|
||||||
|
> = new Map(
|
||||||
this.prevGrid.map((entry, i) => [entry.media, [entry, i]] as const),
|
this.prevGrid.map((entry, i) => [entry.media, [entry, i]] as const),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,7 +149,7 @@ export class TileStoreBuilder {
|
|||||||
spotlight: SpotlightTileData | null,
|
spotlight: SpotlightTileData | null,
|
||||||
grid: GridTileData[],
|
grid: GridTileData[],
|
||||||
) => TileStore,
|
) => TileStore,
|
||||||
private readonly visibleTiles: Set<GridTileViewModel>,
|
private readonly visibleTiles: number,
|
||||||
/**
|
/**
|
||||||
* A number incremented on each update, just for debugging purposes.
|
* A number incremented on each update, just for debugging purposes.
|
||||||
*/
|
*/
|
||||||
@@ -204,10 +207,8 @@ export class TileStoreBuilder {
|
|||||||
const prev = this.prevGridByMedia.get(this.spotlight.media[0]);
|
const prev = this.prevGridByMedia.get(this.spotlight.media[0]);
|
||||||
if (prev !== undefined) {
|
if (prev !== undefined) {
|
||||||
const [entry, prevIndex] = prev;
|
const [entry, prevIndex] = prev;
|
||||||
const previouslyVisible = this.visibleTiles.has(entry.vm);
|
const previouslyVisible = prevIndex < this.visibleTiles;
|
||||||
const nowVisible = this.visibleTiles.has(
|
const nowVisible = this.numGridEntries < this.visibleTiles;
|
||||||
this.prevGrid[this.numGridEntries]?.vm,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If it doesn't need to move between the visible/invisible sections of
|
// If it doesn't need to move between the visible/invisible sections of
|
||||||
// the grid, then we can keep it where it was and swap the media
|
// the grid, then we can keep it where it was and swap the media
|
||||||
@@ -236,17 +237,15 @@ export class TileStoreBuilder {
|
|||||||
const prev = this.prevGridByMedia.get(media);
|
const prev = this.prevGridByMedia.get(media);
|
||||||
if (prev === undefined) {
|
if (prev === undefined) {
|
||||||
// Create a new tile
|
// Create a new tile
|
||||||
(this.visibleTiles.has(this.prevGrid[this.numGridEntries]?.vm)
|
(this.numGridEntries < this.visibleTiles
|
||||||
? this.visibleGridEntries
|
? this.visibleGridEntries
|
||||||
: this.invisibleGridEntries
|
: this.invisibleGridEntries
|
||||||
).push(new GridTileData(media));
|
).push(new GridTileData(media));
|
||||||
} else {
|
} else {
|
||||||
// Reuse the existing tile
|
// Reuse the existing tile
|
||||||
const [entry, prevIndex] = prev;
|
const [entry, prevIndex] = prev;
|
||||||
const previouslyVisible = this.visibleTiles.has(entry.vm);
|
const previouslyVisible = prevIndex < this.visibleTiles;
|
||||||
const nowVisible = this.visibleTiles.has(
|
const nowVisible = this.numGridEntries < this.visibleTiles;
|
||||||
this.prevGrid[this.numGridEntries]?.vm,
|
|
||||||
);
|
|
||||||
// If it doesn't need to move between the visible/invisible sections of
|
// If it doesn't need to move between the visible/invisible sections of
|
||||||
// the grid, then we can keep it exactly where it was previously
|
// the grid, then we can keep it exactly where it was previously
|
||||||
if (previouslyVisible === nowVisible)
|
if (previouslyVisible === nowVisible)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BehaviorSubject, type Observable } from "rxjs";
|
import { type Observable } from "rxjs";
|
||||||
|
|
||||||
import { ViewModel } from "./ViewModel";
|
import { ViewModel } from "./ViewModel";
|
||||||
import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel";
|
import { type MediaViewModel, type UserMediaViewModel } from "./MediaViewModel";
|
||||||
@@ -18,23 +18,15 @@ function createId(): string {
|
|||||||
export class GridTileViewModel extends ViewModel {
|
export class GridTileViewModel extends ViewModel {
|
||||||
public readonly id = createId();
|
public readonly id = createId();
|
||||||
|
|
||||||
private readonly visible_ = new BehaviorSubject(false);
|
public constructor(public readonly media$: Observable<UserMediaViewModel>) {
|
||||||
/**
|
|
||||||
* Whether the tile is visible within the current viewport.
|
|
||||||
*/
|
|
||||||
public readonly visible: Observable<boolean> = this.visible_;
|
|
||||||
|
|
||||||
public setVisible = (value: boolean): void => this.visible_.next(value);
|
|
||||||
|
|
||||||
public constructor(public readonly media: Observable<UserMediaViewModel>) {
|
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SpotlightTileViewModel extends ViewModel {
|
export class SpotlightTileViewModel extends ViewModel {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly media: Observable<MediaViewModel[]>,
|
public readonly media$: Observable<MediaViewModel[]>,
|
||||||
public readonly maximised: Observable<boolean>,
|
public readonly maximised$: Observable<boolean>,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { describe, test } from "vitest";
|
import { describe, test } from "vitest";
|
||||||
|
|
||||||
import { withTestScheduler } from "../utils/test";
|
import { withTestScheduler } from "../utils/test";
|
||||||
import { observeSpeaker } from "./observeSpeaker";
|
import { observeSpeaker$ } from "./observeSpeaker";
|
||||||
|
|
||||||
const yesNo = {
|
const yesNo = {
|
||||||
y: true,
|
y: true,
|
||||||
@@ -22,40 +22,36 @@ describe("observeSpeaker", () => {
|
|||||||
// should default to false when no input is given
|
// should default to false when no input is given
|
||||||
const speakingInputMarbles = "";
|
const speakingInputMarbles = "";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("after no speaking", () => {
|
test("after no speaking", () => {
|
||||||
const speakingInputMarbles = "n";
|
const speakingInputMarbles = "n";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("with speaking for 1ms", () => {
|
test("with speaking for 1ms", () => {
|
||||||
const speakingInputMarbles = "y n";
|
const speakingInputMarbles = "y n";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("with speaking for 999ms", () => {
|
test("with speaking for 999ms", () => {
|
||||||
const speakingInputMarbles = "y 999ms n";
|
const speakingInputMarbles = "y 999ms n";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,20 +59,18 @@ describe("observeSpeaker", () => {
|
|||||||
const speakingInputMarbles =
|
const speakingInputMarbles =
|
||||||
"y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n";
|
"y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n 199ms y 199ms n";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("with consecutive speaking then stops speaking", () => {
|
test("with consecutive speaking then stops speaking", () => {
|
||||||
const speakingInputMarbles = "y y y y y y y y y y n";
|
const speakingInputMarbles = "y y y y y y y y y y n";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -87,10 +81,9 @@ describe("observeSpeaker", () => {
|
|||||||
const speakingInputMarbles = " y";
|
const speakingInputMarbles = " y";
|
||||||
const expectedOutputMarbles = "n 999ms y";
|
const expectedOutputMarbles = "n 999ms y";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,10 +91,9 @@ describe("observeSpeaker", () => {
|
|||||||
const speakingInputMarbles = " y 1s n ";
|
const speakingInputMarbles = " y 1s n ";
|
||||||
const expectedOutputMarbles = "n 999ms y 60s n";
|
const expectedOutputMarbles = "n 999ms y 60s n";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,10 +101,9 @@ describe("observeSpeaker", () => {
|
|||||||
const speakingInputMarbles = " y 5s n ";
|
const speakingInputMarbles = " y 5s n ";
|
||||||
const expectedOutputMarbles = "n 999ms y 64s n";
|
const expectedOutputMarbles = "n 999ms y 64s n";
|
||||||
withTestScheduler(({ hot, expectObservable }) => {
|
withTestScheduler(({ hot, expectObservable }) => {
|
||||||
expectObservable(observeSpeaker(hot(speakingInputMarbles, yesNo))).toBe(
|
expectObservable(
|
||||||
expectedOutputMarbles,
|
observeSpeaker$(hot(speakingInputMarbles, yesNo)),
|
||||||
yesNo,
|
).toBe(expectedOutputMarbles, yesNo);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ import {
|
|||||||
* Require 1 second of continuous speaking to become a speaker, and 60 second of
|
* Require 1 second of continuous speaking to become a speaker, and 60 second of
|
||||||
* continuous silence to stop being considered a speaker
|
* continuous silence to stop being considered a speaker
|
||||||
*/
|
*/
|
||||||
export function observeSpeaker(
|
export function observeSpeaker$(
|
||||||
isSpeakingObservable: Observable<boolean>,
|
isSpeakingObservable$: Observable<boolean>,
|
||||||
): Observable<boolean> {
|
): Observable<boolean> {
|
||||||
const distinct = isSpeakingObservable.pipe(distinctUntilChanged());
|
const distinct$ = isSpeakingObservable$.pipe(distinctUntilChanged());
|
||||||
|
|
||||||
return distinct.pipe(
|
return distinct$.pipe(
|
||||||
// Either change to the new value after the timer or re-emit the same value if it toggles back
|
// Either change to the new value after the timer or re-emit the same value if it toggles back
|
||||||
// (audit will return the latest (toggled back) value) before the timeout.
|
// (audit will return the latest (toggled back) value) before the timeout.
|
||||||
audit((s) =>
|
audit((s) =>
|
||||||
merge(timer(s ? 1000 : 60000), distinct.pipe(filter((s1) => s1 !== s))),
|
merge(timer(s ? 1000 : 60000), distinct$.pipe(filter((s1) => s1 !== s))),
|
||||||
),
|
),
|
||||||
// Filter the re-emissions (marked as: | ) that happen if we toggle quickly (<1s) from false->true->false|->..
|
// Filter the re-emissions (marked as: | ) that happen if we toggle quickly (<1s) from false->true->false|->..
|
||||||
startWith(false),
|
startWith(false),
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const video = useObservableEagerState(vm.video);
|
const video = useObservableEagerState(vm.video$);
|
||||||
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
|
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning$);
|
||||||
const encryptionStatus = useObservableEagerState(vm.encryptionStatus);
|
const encryptionStatus = useObservableEagerState(vm.encryptionStatus$);
|
||||||
const audioEnabled = useObservableEagerState(vm.audioEnabled);
|
const audioEnabled = useObservableEagerState(vm.audioEnabled$);
|
||||||
const videoEnabled = useObservableEagerState(vm.videoEnabled);
|
const videoEnabled = useObservableEagerState(vm.videoEnabled$);
|
||||||
const speaking = useObservableEagerState(vm.speaking);
|
const speaking = useObservableEagerState(vm.speaking$);
|
||||||
const cropVideo = useObservableEagerState(vm.cropVideo);
|
const cropVideo = useObservableEagerState(vm.cropVideo$);
|
||||||
const onSelectFitContain = useCallback(
|
const onSelectFitContain = useCallback(
|
||||||
(e: Event) => {
|
(e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -198,8 +198,8 @@ interface LocalUserMediaTileProps extends TileProps {
|
|||||||
const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
|
const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
|
||||||
({ vm, onOpenProfile, ...props }, ref) => {
|
({ vm, onOpenProfile, ...props }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const mirror = useObservableEagerState(vm.mirror);
|
const mirror = useObservableEagerState(vm.mirror$);
|
||||||
const alwaysShow = useObservableEagerState(vm.alwaysShow);
|
const alwaysShow = useObservableEagerState(vm.alwaysShow$);
|
||||||
const latestAlwaysShow = useLatest(alwaysShow);
|
const latestAlwaysShow = useLatest(alwaysShow);
|
||||||
const onSelectAlwaysShow = useCallback(
|
const onSelectAlwaysShow = useCallback(
|
||||||
(e: Event) => {
|
(e: Event) => {
|
||||||
@@ -249,8 +249,8 @@ const RemoteUserMediaTile = forwardRef<
|
|||||||
RemoteUserMediaTileProps
|
RemoteUserMediaTileProps
|
||||||
>(({ vm, ...props }, ref) => {
|
>(({ vm, ...props }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const locallyMuted = useObservableEagerState(vm.locallyMuted);
|
const locallyMuted = useObservableEagerState(vm.locallyMuted$);
|
||||||
const localVolume = useObservableEagerState(vm.localVolume);
|
const localVolume = useObservableEagerState(vm.localVolume$);
|
||||||
const onSelectMute = useCallback(
|
const onSelectMute = useCallback(
|
||||||
(e: Event) => {
|
(e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -316,7 +316,7 @@ export const GridTile = forwardRef<HTMLDivElement, GridTileProps>(
|
|||||||
({ vm, onOpenProfile, ...props }, theirRef) => {
|
({ vm, onOpenProfile, ...props }, theirRef) => {
|
||||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||||
const ref = useMergedRefs(ourRef, theirRef);
|
const ref = useMergedRefs(ourRef, theirRef);
|
||||||
const media = useObservableEagerState(vm.media);
|
const media = useObservableEagerState(vm.media$);
|
||||||
const displayName = useDisplayName(media);
|
const displayName = useDisplayName(media);
|
||||||
|
|
||||||
if (media instanceof LocalUserMediaViewModel) {
|
if (media instanceof LocalUserMediaViewModel) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const SpotlightLocalUserMediaItem = forwardRef<
|
|||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
SpotlightLocalUserMediaItemProps
|
SpotlightLocalUserMediaItemProps
|
||||||
>(({ vm, ...props }, ref) => {
|
>(({ vm, ...props }, ref) => {
|
||||||
const mirror = useObservableEagerState(vm.mirror);
|
const mirror = useObservableEagerState(vm.mirror$);
|
||||||
return <MediaView ref={ref} mirror={mirror} {...props} />;
|
return <MediaView ref={ref} mirror={mirror} {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,8 +86,8 @@ const SpotlightUserMediaItem = forwardRef<
|
|||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
SpotlightUserMediaItemProps
|
SpotlightUserMediaItemProps
|
||||||
>(({ vm, ...props }, ref) => {
|
>(({ vm, ...props }, ref) => {
|
||||||
const videoEnabled = useObservableEagerState(vm.videoEnabled);
|
const videoEnabled = useObservableEagerState(vm.videoEnabled$);
|
||||||
const cropVideo = useObservableEagerState(vm.cropVideo);
|
const cropVideo = useObservableEagerState(vm.cropVideo$);
|
||||||
|
|
||||||
const baseProps: SpotlightUserMediaItemBaseProps &
|
const baseProps: SpotlightUserMediaItemBaseProps &
|
||||||
RefAttributes<HTMLDivElement> = {
|
RefAttributes<HTMLDivElement> = {
|
||||||
@@ -110,7 +110,7 @@ interface SpotlightItemProps {
|
|||||||
vm: MediaViewModel;
|
vm: MediaViewModel;
|
||||||
targetWidth: number;
|
targetWidth: number;
|
||||||
targetHeight: number;
|
targetHeight: number;
|
||||||
intersectionObserver: Observable<IntersectionObserver>;
|
intersectionObserver$: Observable<IntersectionObserver>;
|
||||||
/**
|
/**
|
||||||
* Whether this item should act as a scroll snapping point.
|
* Whether this item should act as a scroll snapping point.
|
||||||
*/
|
*/
|
||||||
@@ -124,7 +124,7 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
|||||||
vm,
|
vm,
|
||||||
targetWidth,
|
targetWidth,
|
||||||
targetHeight,
|
targetHeight,
|
||||||
intersectionObserver,
|
intersectionObserver$,
|
||||||
snap,
|
snap,
|
||||||
"aria-hidden": ariaHidden,
|
"aria-hidden": ariaHidden,
|
||||||
},
|
},
|
||||||
@@ -133,15 +133,15 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
|||||||
const ourRef = useRef<HTMLDivElement | null>(null);
|
const ourRef = useRef<HTMLDivElement | null>(null);
|
||||||
const ref = useMergedRefs(ourRef, theirRef);
|
const ref = useMergedRefs(ourRef, theirRef);
|
||||||
const displayName = useDisplayName(vm);
|
const displayName = useDisplayName(vm);
|
||||||
const video = useObservableEagerState(vm.video);
|
const video = useObservableEagerState(vm.video$);
|
||||||
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
|
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning$);
|
||||||
const encryptionStatus = useObservableEagerState(vm.encryptionStatus);
|
const encryptionStatus = useObservableEagerState(vm.encryptionStatus$);
|
||||||
|
|
||||||
// Hook this item up to the intersection observer
|
// Hook this item up to the intersection observer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = ourRef.current!;
|
const element = ourRef.current!;
|
||||||
let prevIo: IntersectionObserver | null = null;
|
let prevIo: IntersectionObserver | null = null;
|
||||||
const subscription = intersectionObserver.subscribe((io) => {
|
const subscription = intersectionObserver$.subscribe((io) => {
|
||||||
prevIo?.unobserve(element);
|
prevIo?.unobserve(element);
|
||||||
io.observe(element);
|
io.observe(element);
|
||||||
prevIo = io;
|
prevIo = io;
|
||||||
@@ -150,7 +150,7 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
|||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
prevIo?.unobserve(element);
|
prevIo?.unobserve(element);
|
||||||
};
|
};
|
||||||
}, [intersectionObserver]);
|
}, [intersectionObserver$]);
|
||||||
|
|
||||||
const baseProps: SpotlightItemBaseProps & RefAttributes<HTMLDivElement> = {
|
const baseProps: SpotlightItemBaseProps & RefAttributes<HTMLDivElement> = {
|
||||||
ref,
|
ref,
|
||||||
@@ -208,10 +208,10 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
theirRef,
|
theirRef,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [ourRef, root] = useObservableRef<HTMLDivElement | null>(null);
|
const [ourRef, root$] = useObservableRef<HTMLDivElement | null>(null);
|
||||||
const ref = useMergedRefs(ourRef, theirRef);
|
const ref = useMergedRefs(ourRef, theirRef);
|
||||||
const maximised = useObservableEagerState(vm.maximised);
|
const maximised = useObservableEagerState(vm.maximised$);
|
||||||
const media = useObservableEagerState(vm.media);
|
const media = useObservableEagerState(vm.media$);
|
||||||
const [visibleId, setVisibleId] = useState<string | undefined>(
|
const [visibleId, setVisibleId] = useState<string | undefined>(
|
||||||
media[0]?.id,
|
media[0]?.id,
|
||||||
);
|
);
|
||||||
@@ -225,9 +225,9 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
// hooked up to the root element and the items. Because the items will run
|
// hooked up to the root element and the items. Because the items will run
|
||||||
// their effects before their parent does, we need to do this dance with an
|
// their effects before their parent does, we need to do this dance with an
|
||||||
// Observable to actually give them the intersection observer.
|
// Observable to actually give them the intersection observer.
|
||||||
const intersectionObserver = useInitial<Observable<IntersectionObserver>>(
|
const intersectionObserver$ = useInitial<Observable<IntersectionObserver>>(
|
||||||
() =>
|
() =>
|
||||||
root.pipe(
|
root$.pipe(
|
||||||
map(
|
map(
|
||||||
(r) =>
|
(r) =>
|
||||||
new IntersectionObserver(
|
new IntersectionObserver(
|
||||||
@@ -295,7 +295,7 @@ export const SpotlightTile = forwardRef<HTMLDivElement, Props>(
|
|||||||
vm={vm}
|
vm={vm}
|
||||||
targetWidth={targetWidth}
|
targetWidth={targetWidth}
|
||||||
targetHeight={targetHeight}
|
targetHeight={targetHeight}
|
||||||
intersectionObserver={intersectionObserver}
|
intersectionObserver$={intersectionObserver$}
|
||||||
// This is how we get the container to scroll to the right media
|
// This is how we get the container to scroll to the right media
|
||||||
// when the previous/next buttons are clicked: we temporarily
|
// when the previous/next buttons are clicked: we temporarily
|
||||||
// remove all scroll snap points except for just the one media
|
// remove all scroll snap points except for just the one media
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, test, vitest } from "vitest";
|
import { expect, test, vitest, afterEach } from "vitest";
|
||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { afterEach } from "node:test";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { deviceStub, MediaDevicesContext } from "./livekit/MediaDevicesContext";
|
import { deviceStub, MediaDevicesContext } from "./livekit/MediaDevicesContext";
|
||||||
@@ -101,7 +100,8 @@ test("will use the correct device", () => {
|
|||||||
audioInput: deviceStub,
|
audioInput: deviceStub,
|
||||||
audioOutput: {
|
audioOutput: {
|
||||||
selectedId: "chosen-device",
|
selectedId: "chosen-device",
|
||||||
available: [],
|
selectedGroupId: "",
|
||||||
|
available: new Map(),
|
||||||
select: () => {},
|
select: () => {},
|
||||||
},
|
},
|
||||||
videoInput: deviceStub,
|
videoInput: deviceStub,
|
||||||
|
|||||||
16
src/utils/array.ts
Normal file
16
src/utils/array.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether two arrays are equal by shallow comparison.
|
||||||
|
*/
|
||||||
|
export function shallowEquals<A>(first: A[], second: A[]): boolean {
|
||||||
|
if (first.length !== second.length) return false;
|
||||||
|
for (let i = 0; i < first.length; i++)
|
||||||
|
if (first[i] !== second[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -15,10 +15,10 @@ const nothing = Symbol("nothing");
|
|||||||
* callback will not be invoked.
|
* callback will not be invoked.
|
||||||
*/
|
*/
|
||||||
export function finalizeValue<T>(callback: (finalValue: T) => void) {
|
export function finalizeValue<T>(callback: (finalValue: T) => void) {
|
||||||
return (source: Observable<T>): Observable<T> =>
|
return (source$: Observable<T>): Observable<T> =>
|
||||||
defer(() => {
|
defer(() => {
|
||||||
let finalValue: T | typeof nothing = nothing;
|
let finalValue: T | typeof nothing = nothing;
|
||||||
return source.pipe(
|
return source$.pipe(
|
||||||
tap((value) => (finalValue = value)),
|
tap((value) => (finalValue = value)),
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
if (finalValue !== nothing) callback(finalValue);
|
if (finalValue !== nothing) callback(finalValue);
|
||||||
@@ -35,6 +35,6 @@ export function accumulate<State, Event>(
|
|||||||
initial: State,
|
initial: State,
|
||||||
update: (state: State, event: Event) => State,
|
update: (state: State, event: Event) => State,
|
||||||
) {
|
) {
|
||||||
return (events: Observable<Event>): Observable<State> =>
|
return (events$: Observable<Event>): Observable<State> =>
|
||||||
events.pipe(scan(update, initial), startWith(initial));
|
events$.pipe(scan(update, initial), startWith(initial));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ export function withTestScheduler(
|
|||||||
continuation({
|
continuation({
|
||||||
...helpers,
|
...helpers,
|
||||||
schedule(marbles, actions) {
|
schedule(marbles, actions) {
|
||||||
const actionsObservable = helpers
|
const actionsObservable$ = helpers
|
||||||
.cold(marbles)
|
.cold(marbles)
|
||||||
.pipe(map((value) => actions[value]()));
|
.pipe(map((value) => actions[value]()));
|
||||||
const results = Object.fromEntries(
|
const results = Object.fromEntries(
|
||||||
Object.keys(actions).map((value) => [value, undefined] as const),
|
Object.keys(actions).map((value) => [value, undefined] as const),
|
||||||
);
|
);
|
||||||
// Run the actions and verify that none of them error
|
// Run the actions and verify that none of them error
|
||||||
helpers.expectObservable(actionsObservable).toBe(marbles, results);
|
helpers.expectObservable(actionsObservable$).toBe(marbles, results);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -157,16 +157,16 @@ export function mockMatrixRoom(room: Partial<MatrixRoom>): MatrixRoom {
|
|||||||
export function mockLivekitRoom(
|
export function mockLivekitRoom(
|
||||||
room: Partial<LivekitRoom>,
|
room: Partial<LivekitRoom>,
|
||||||
{
|
{
|
||||||
remoteParticipants,
|
remoteParticipants$,
|
||||||
}: { remoteParticipants?: Observable<RemoteParticipant[]> } = {},
|
}: { remoteParticipants$?: Observable<RemoteParticipant[]> } = {},
|
||||||
): LivekitRoom {
|
): LivekitRoom {
|
||||||
const livekitRoom = {
|
const livekitRoom = {
|
||||||
...mockEmitter(),
|
...mockEmitter(),
|
||||||
...room,
|
...room,
|
||||||
} as Partial<LivekitRoom> as LivekitRoom;
|
} as Partial<LivekitRoom> as LivekitRoom;
|
||||||
if (remoteParticipants) {
|
if (remoteParticipants$) {
|
||||||
livekitRoom.remoteParticipants = new Map();
|
livekitRoom.remoteParticipants = new Map();
|
||||||
remoteParticipants.subscribe((newRemoteParticipants) => {
|
remoteParticipants$.subscribe((newRemoteParticipants) => {
|
||||||
livekitRoom.remoteParticipants.clear();
|
livekitRoom.remoteParticipants.clear();
|
||||||
newRemoteParticipants.forEach((p) => {
|
newRemoteParticipants.forEach((p) => {
|
||||||
livekitRoom.remoteParticipants.set(p.identity, p);
|
livekitRoom.remoteParticipants.set(p.identity, p);
|
||||||
@@ -238,7 +238,7 @@ export async function withRemoteMedia(
|
|||||||
{
|
{
|
||||||
kind: E2eeType.PER_PARTICIPANT,
|
kind: E2eeType.PER_PARTICIPANT,
|
||||||
},
|
},
|
||||||
mockLivekitRoom({}, { remoteParticipants: of([remoteParticipant]) }),
|
mockLivekitRoom({}, { remoteParticipants$: of([remoteParticipant]) }),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await continuation(vm);
|
await continuation(vm);
|
||||||
@@ -277,9 +277,9 @@ export class MockRTCSession extends TypedEventEmitter<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public withMemberships(
|
public withMemberships(
|
||||||
rtcMembers: Observable<Partial<CallMembership>[]>,
|
rtcMembers$: Observable<Partial<CallMembership>[]>,
|
||||||
): MockRTCSession {
|
): MockRTCSession {
|
||||||
rtcMembers.subscribe((m) => {
|
rtcMembers$.subscribe((m) => {
|
||||||
const old = this.memberships;
|
const old = this.memberships;
|
||||||
// always prepend the local participant
|
// always prepend the local participant
|
||||||
const updated = [this.localMembership, ...(m as CallMembership[])];
|
const updated = [this.localMembership, ...(m as CallMembership[])];
|
||||||
|
|||||||
541
yarn.lock
541
yarn.lock
@@ -1969,10 +1969,10 @@
|
|||||||
"@mediapipe/holistic" "0.5.1675471629"
|
"@mediapipe/holistic" "0.5.1675471629"
|
||||||
"@mediapipe/tasks-vision" "0.10.9"
|
"@mediapipe/tasks-vision" "0.10.9"
|
||||||
|
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm@^9.0.0":
|
"@matrix-org/matrix-sdk-crypto-wasm@^12.0.0":
|
||||||
version "9.1.0"
|
version "12.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz#f889653eb4fafaad2a963654d586bd34de62acd5"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.0.0.tgz#e3a5150ccbb21d5e98ee3882e7057b9f17fb962a"
|
||||||
integrity sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==
|
integrity sha512-nkkXAxUIk9UTso4TbU6Bgqsv/rJShXQXRx0ti/W+AWXHJ2HoH4sL5LsXkc7a8yYGn8tyXqxGPsYA1UeHqLwm0Q==
|
||||||
|
|
||||||
"@matrix-org/olm@3.2.15":
|
"@matrix-org/olm@3.2.15":
|
||||||
version "3.2.15"
|
version "3.2.15"
|
||||||
@@ -2363,6 +2363,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
|
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
|
||||||
integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==
|
integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==
|
||||||
|
|
||||||
|
"@radix-ui/primitive@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
|
||||||
|
integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==
|
||||||
|
|
||||||
"@radix-ui/react-arrow@1.1.0":
|
"@radix-ui/react-arrow@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a"
|
||||||
@@ -2380,11 +2385,26 @@
|
|||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
"@radix-ui/react-slot" "1.1.0"
|
"@radix-ui/react-slot" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-collection@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.1.tgz#be2c7e01d3508e6d4b6d838f492e7d182f17d3b0"
|
||||||
|
integrity sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-context" "1.1.1"
|
||||||
|
"@radix-ui/react-primitive" "2.0.1"
|
||||||
|
"@radix-ui/react-slot" "1.1.1"
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs@1.1.0":
|
"@radix-ui/react-compose-refs@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
|
||||||
integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==
|
integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==
|
||||||
|
|
||||||
|
"@radix-ui/react-compose-refs@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
|
||||||
|
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
|
||||||
|
|
||||||
"@radix-ui/react-context-menu@^2.2.1":
|
"@radix-ui/react-context-menu@^2.2.1":
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.2.tgz#efcddc559fc3011721b65148f062d04027f76c7a"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.2.tgz#efcddc559fc3011721b65148f062d04027f76c7a"
|
||||||
@@ -2408,21 +2428,21 @@
|
|||||||
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
|
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
|
||||||
|
|
||||||
"@radix-ui/react-dialog@^1.0.4", "@radix-ui/react-dialog@^1.1.1":
|
"@radix-ui/react-dialog@^1.0.4", "@radix-ui/react-dialog@^1.1.1":
|
||||||
version "1.1.2"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.3.tgz#87cf49f619a6a0f6219980678be0f7c31978dee1"
|
||||||
integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==
|
integrity sha512-ujGvqQNkZ0J7caQyl8XuZRj2/TIrYcOGwqz5TeD1OMcCdfBuEMP0D12ve+8J5F9XuNUth3FAKFWo/wt0E/GJrQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/primitive" "1.1.0"
|
"@radix-ui/primitive" "1.1.1"
|
||||||
"@radix-ui/react-compose-refs" "1.1.0"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
"@radix-ui/react-context" "1.1.1"
|
"@radix-ui/react-context" "1.1.1"
|
||||||
"@radix-ui/react-dismissable-layer" "1.1.1"
|
"@radix-ui/react-dismissable-layer" "1.1.2"
|
||||||
"@radix-ui/react-focus-guards" "1.1.1"
|
"@radix-ui/react-focus-guards" "1.1.1"
|
||||||
"@radix-ui/react-focus-scope" "1.1.0"
|
"@radix-ui/react-focus-scope" "1.1.1"
|
||||||
"@radix-ui/react-id" "1.1.0"
|
"@radix-ui/react-id" "1.1.0"
|
||||||
"@radix-ui/react-portal" "1.1.2"
|
"@radix-ui/react-portal" "1.1.3"
|
||||||
"@radix-ui/react-presence" "1.1.1"
|
"@radix-ui/react-presence" "1.1.2"
|
||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.1"
|
||||||
"@radix-ui/react-slot" "1.1.0"
|
"@radix-ui/react-slot" "1.1.1"
|
||||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
aria-hidden "^1.1.1"
|
aria-hidden "^1.1.1"
|
||||||
react-remove-scroll "2.6.0"
|
react-remove-scroll "2.6.0"
|
||||||
@@ -2443,6 +2463,17 @@
|
|||||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-dismissable-layer@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz#771594b202f32bc8ffeb278c565f10c513814aee"
|
||||||
|
integrity sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/primitive" "1.1.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-primitive" "2.0.1"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
"@radix-ui/react-use-escape-keydown" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-dropdown-menu@^2.1.1":
|
"@radix-ui/react-dropdown-menu@^2.1.1":
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.2.tgz#acc49577130e3c875ef0133bd1e271ea3392d924"
|
||||||
@@ -2470,6 +2501,15 @@
|
|||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-focus-scope@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz#5c602115d1db1c4fcfa0fae4c3b09bb8919853cb"
|
||||||
|
integrity sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-primitive" "2.0.1"
|
||||||
|
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-form@^0.1.0":
|
"@radix-ui/react-form@^0.1.0":
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-form/-/react-form-0.1.0.tgz#7111a6aa54a2bde0d11fb72643f9ffc871ac58ad"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-form/-/react-form-0.1.0.tgz#7111a6aa54a2bde0d11fb72643f9ffc871ac58ad"
|
||||||
@@ -2544,6 +2584,14 @@
|
|||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-portal@1.1.3":
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.3.tgz#b0ea5141103a1671b715481b13440763d2ac4440"
|
||||||
|
integrity sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-primitive" "2.0.1"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-presence@1.1.1":
|
"@radix-ui/react-presence@1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1"
|
||||||
@@ -2552,6 +2600,14 @@
|
|||||||
"@radix-ui/react-compose-refs" "1.1.0"
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-presence@1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
|
||||||
|
integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-primitive@2.0.0":
|
"@radix-ui/react-primitive@2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884"
|
||||||
@@ -2559,6 +2615,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-slot" "1.1.0"
|
"@radix-ui/react-slot" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-primitive@2.0.1":
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e"
|
||||||
|
integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-slot" "1.1.1"
|
||||||
|
|
||||||
"@radix-ui/react-progress@^1.1.0":
|
"@radix-ui/react-progress@^1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.0.tgz#28c267885ec154fc557ec7a66cb462787312f7e2"
|
||||||
@@ -2590,17 +2653,17 @@
|
|||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.0"
|
||||||
|
|
||||||
"@radix-ui/react-slider@^1.1.2":
|
"@radix-ui/react-slider@^1.1.2":
|
||||||
version "1.2.1"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.2.1.tgz#acb0804309890f3cd7a224b2b0c4c4704f32921b"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-slider/-/react-slider-1.2.2.tgz#4ca883e3f0dea7b97d43c6cbc6c4305c64e75a86"
|
||||||
integrity sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==
|
integrity sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/number" "1.1.0"
|
"@radix-ui/number" "1.1.0"
|
||||||
"@radix-ui/primitive" "1.1.0"
|
"@radix-ui/primitive" "1.1.1"
|
||||||
"@radix-ui/react-collection" "1.1.0"
|
"@radix-ui/react-collection" "1.1.1"
|
||||||
"@radix-ui/react-compose-refs" "1.1.0"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
"@radix-ui/react-context" "1.1.1"
|
"@radix-ui/react-context" "1.1.1"
|
||||||
"@radix-ui/react-direction" "1.1.0"
|
"@radix-ui/react-direction" "1.1.0"
|
||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.1"
|
||||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
"@radix-ui/react-use-previous" "1.1.0"
|
"@radix-ui/react-use-previous" "1.1.0"
|
||||||
@@ -2613,6 +2676,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-compose-refs" "1.1.0"
|
"@radix-ui/react-compose-refs" "1.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-slot@1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3"
|
||||||
|
integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==
|
||||||
|
dependencies:
|
||||||
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref@1.1.0":
|
"@radix-ui/react-use-callback-ref@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
|
||||||
@@ -2657,11 +2727,11 @@
|
|||||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-visually-hidden@^1.0.3":
|
"@radix-ui/react-visually-hidden@^1.0.3":
|
||||||
version "1.1.0"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz#ad47a8572580f7034b3807c8e6740cd41038a5a2"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz#f7b48c1af50dfdc366e92726aee6d591996c5752"
|
||||||
integrity sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==
|
integrity sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-primitive" "2.0.0"
|
"@radix-ui/react-primitive" "2.0.1"
|
||||||
|
|
||||||
"@radix-ui/rect@1.1.0":
|
"@radix-ui/rect@1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
@@ -2727,231 +2797,236 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz#3e7eda4c0c1de6d2415343002d742ff95e38dca7"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz#3e7eda4c0c1de6d2415343002d742ff95e38dca7"
|
||||||
integrity sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==
|
integrity sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi@4.28.0":
|
"@rollup/rollup-android-arm-eabi@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz#462e7ecdd60968bc9eb95a20d185e74f8243ec1b"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz#7f4c4d8cd5ccab6e95d6750dbe00321c1f30791e"
|
||||||
integrity sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==
|
integrity sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64@4.25.0":
|
"@rollup/rollup-android-arm64@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz#04f679231acf7284f1f8a1f7250d0e0944865ba8"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz#04f679231acf7284f1f8a1f7250d0e0944865ba8"
|
||||||
integrity sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==
|
integrity sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64@4.28.0":
|
"@rollup/rollup-android-arm64@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz#78a2b8a8a55f71a295eb860a654ae90a2b168f40"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz#17ea71695fb1518c2c324badbe431a0bd1879f2d"
|
||||||
integrity sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==
|
integrity sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64@4.25.0":
|
"@rollup/rollup-darwin-arm64@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz#ecea723041621747d0772af93b54752edf26467a"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz#ecea723041621747d0772af93b54752edf26467a"
|
||||||
integrity sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==
|
integrity sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64@4.28.0":
|
"@rollup/rollup-darwin-arm64@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz#5b783af714f434f1e66e3cdfa3817e0b99216d84"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz#dac0f0d0cfa73e7d5225ae6d303c13c8979e7999"
|
||||||
integrity sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==
|
integrity sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64@4.25.0":
|
"@rollup/rollup-darwin-x64@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz#28e6e0687092f31e20982fc104779d48c643fc21"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz#28e6e0687092f31e20982fc104779d48c643fc21"
|
||||||
integrity sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==
|
integrity sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64@4.28.0":
|
"@rollup/rollup-darwin-x64@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz#f72484e842521a5261978034e18e20f778a2850d"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz#8f63baa1d31784904a380d2e293fa1ddf53dd4a2"
|
||||||
integrity sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==
|
integrity sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64@4.25.0":
|
"@rollup/rollup-freebsd-arm64@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz#99e9173b8aef3d1ef086983da70413988206e530"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz#99e9173b8aef3d1ef086983da70413988206e530"
|
||||||
integrity sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==
|
integrity sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64@4.28.0":
|
"@rollup/rollup-freebsd-arm64@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz#3c919dff72b2fe344811a609c674a8347b033f62"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz#30ed247e0df6e8858cdc6ae4090e12dbeb8ce946"
|
||||||
integrity sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==
|
integrity sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64@4.25.0":
|
"@rollup/rollup-freebsd-x64@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz#f3a1ef941f8d3c6b2b036484c69a7b2d3d9ebbd7"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz#f3a1ef941f8d3c6b2b036484c69a7b2d3d9ebbd7"
|
||||||
integrity sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==
|
integrity sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64@4.28.0":
|
"@rollup/rollup-freebsd-x64@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz#b62a3a8365b363b3fdfa6da11a9188b6ab4dca7c"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz#57846f382fddbb508412ae07855b8a04c8f56282"
|
||||||
integrity sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==
|
integrity sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf@4.25.0":
|
"@rollup/rollup-linux-arm-gnueabihf@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz#9ba6adcc33f26f2a0c6ee658f0bbda4de8da2f75"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz#9ba6adcc33f26f2a0c6ee658f0bbda4de8da2f75"
|
||||||
integrity sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==
|
integrity sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf@4.28.0":
|
"@rollup/rollup-linux-arm-gnueabihf@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz#0d02cc55bd229bd8ca5c54f65f916ba5e0591c94"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz#378ca666c9dae5e6f94d1d351e7497c176e9b6df"
|
||||||
integrity sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==
|
integrity sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf@4.25.0":
|
"@rollup/rollup-linux-arm-musleabihf@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz#62f2426fa9016ec884f4fa779d7b62d5ba02a41a"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz#62f2426fa9016ec884f4fa779d7b62d5ba02a41a"
|
||||||
integrity sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==
|
integrity sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf@4.28.0":
|
"@rollup/rollup-linux-arm-musleabihf@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz#c51d379263201e88a60e92bd8e90878f0c044425"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz#a692eff3bab330d5c33a5d5813a090c15374cddb"
|
||||||
integrity sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==
|
integrity sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu@4.25.0":
|
"@rollup/rollup-linux-arm64-gnu@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz#f98ec111a231d35e0c6d3404e3d80f67f9d5b9f8"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz#f98ec111a231d35e0c6d3404e3d80f67f9d5b9f8"
|
||||||
integrity sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==
|
integrity sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu@4.28.0":
|
"@rollup/rollup-linux-arm64-gnu@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz#93ce2addc337b5cfa52b84f8e730d2e36eb4339b"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz#6b1719b76088da5ac1ae1feccf48c5926b9e3db9"
|
||||||
integrity sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==
|
integrity sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl@4.25.0":
|
"@rollup/rollup-linux-arm64-musl@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz#4b36ffb8359f959f2c29afd187603c53368b6723"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz#4b36ffb8359f959f2c29afd187603c53368b6723"
|
||||||
integrity sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==
|
integrity sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl@4.28.0":
|
"@rollup/rollup-linux-arm64-musl@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz#730af6ddc091a5ba5baac28a3510691725dc808b"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz#865baf5b6f5ff67acb32e5a359508828e8dc5788"
|
||||||
integrity sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==
|
integrity sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loongarch64-gnu@4.28.1":
|
||||||
|
version "4.28.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz#23c6609ba0f7fa7a7f2038b6b6a08555a5055a87"
|
||||||
|
integrity sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu@4.25.0":
|
"@rollup/rollup-linux-powerpc64le-gnu@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz#52f4b39e6783505d168a745b79d86474fde71680"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz#52f4b39e6783505d168a745b79d86474fde71680"
|
||||||
integrity sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==
|
integrity sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu@4.28.0":
|
"@rollup/rollup-linux-powerpc64le-gnu@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz#b5565aac20b4de60ca1e557f525e76478b5436af"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz#652ef0d9334a9f25b9daf85731242801cb0fc41c"
|
||||||
integrity sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==
|
integrity sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu@4.25.0":
|
"@rollup/rollup-linux-riscv64-gnu@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz#49195be7e6a7d68d482b12461e2ea914e31ff977"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz#49195be7e6a7d68d482b12461e2ea914e31ff977"
|
||||||
integrity sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==
|
integrity sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu@4.28.0":
|
"@rollup/rollup-linux-riscv64-gnu@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz#d488290bf9338bad4ae9409c4aa8a1728835a20b"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz#1eb6651839ee6ebca64d6cc64febbd299e95e6bd"
|
||||||
integrity sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==
|
integrity sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu@4.25.0":
|
"@rollup/rollup-linux-s390x-gnu@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz#4b8d50a205eac7b46cdcb9c50d4a6ae5994c02e0"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz#4b8d50a205eac7b46cdcb9c50d4a6ae5994c02e0"
|
||||||
integrity sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==
|
integrity sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu@4.28.0":
|
"@rollup/rollup-linux-s390x-gnu@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz#eb2e3f3a06acf448115045c11a5a96868c95a556"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz#015c52293afb3ff2a293cf0936b1d43975c1e9cd"
|
||||||
integrity sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==
|
integrity sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu@4.25.0":
|
"@rollup/rollup-linux-x64-gnu@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz#dfcceebc5ccac7fc2db19471996026258c81b55f"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz#dfcceebc5ccac7fc2db19471996026258c81b55f"
|
||||||
integrity sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==
|
integrity sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu@4.28.0":
|
"@rollup/rollup-linux-x64-gnu@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz#065952ef2aea7e837dc7e02aa500feeaff4fc507"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz#b83001b5abed2bcb5e2dbeec6a7e69b194235c1e"
|
||||||
integrity sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==
|
integrity sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl@4.25.0":
|
"@rollup/rollup-linux-x64-musl@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz#192f78bad8429711d63a31dc0a7d3312e2df850e"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz#192f78bad8429711d63a31dc0a7d3312e2df850e"
|
||||||
integrity sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==
|
integrity sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl@4.28.0":
|
"@rollup/rollup-linux-x64-musl@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz#3435d484d05f5c4d1ffd54541b4facce2887103a"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz#6cc7c84cd4563737f8593e66f33b57d8e228805b"
|
||||||
integrity sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==
|
integrity sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc@4.25.0":
|
"@rollup/rollup-win32-arm64-msvc@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz#f4ec076579634f780b4e5896ae7f59f3e38e0c60"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz#f4ec076579634f780b4e5896ae7f59f3e38e0c60"
|
||||||
integrity sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==
|
integrity sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc@4.28.0":
|
"@rollup/rollup-win32-arm64-msvc@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz#69682a2a10d9fedc334f87583cfca83c39c08077"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz#631ffeee094d71279fcd1fe8072bdcf25311bc11"
|
||||||
integrity sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==
|
integrity sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc@4.25.0":
|
"@rollup/rollup-win32-ia32-msvc@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz#5458eab1929827e4f805cefb90bd09ecf7eeed2b"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz#5458eab1929827e4f805cefb90bd09ecf7eeed2b"
|
||||||
integrity sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==
|
integrity sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc@4.28.0":
|
"@rollup/rollup-win32-ia32-msvc@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz#b64470f9ac79abb386829c56750b9a4711be3332"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz#06d1d60d5b9f718e8a6c4a43f82e3f9e3254587f"
|
||||||
integrity sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==
|
integrity sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc@4.25.0":
|
"@rollup/rollup-win32-x64-msvc@4.25.0":
|
||||||
version "4.25.0"
|
version "4.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz#93415e7e707e4b156d77c5950b983b58f4bc33f3"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz#93415e7e707e4b156d77c5950b983b58f4bc33f3"
|
||||||
integrity sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==
|
integrity sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc@4.28.0":
|
"@rollup/rollup-win32-x64-msvc@4.28.1":
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz#cb313feef9ac6e3737067fdf34f42804ac65a6f2"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz#4dff5c4259ebe6c5b4a8f2c5bc3829b7a8447ff0"
|
||||||
integrity sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==
|
integrity sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==
|
||||||
|
|
||||||
"@rtsao/scc@^1.1.0":
|
"@rtsao/scc@^1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
|
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
|
||||||
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
|
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
|
||||||
|
|
||||||
"@sentry-internal/browser-utils@8.43.0":
|
"@sentry-internal/browser-utils@8.45.0":
|
||||||
version "8.43.0"
|
version "8.45.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.43.0.tgz#b064908a537d1cc17d8ddaf0f4c5d712557cbf40"
|
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.45.0.tgz#8e9217b8e8a4242c9a8244dce648289eaa1e38a0"
|
||||||
integrity sha512-5WhJZ3SA5sZVDBwOsChDd5JCzYcwBX7sEqBqEcm3pFru6TUihEnFIJmDIbreIyrQMwUhs3dTxnfnidgjr5z1Ag==
|
integrity sha512-MX/E/C+W5I9jkGD1PsbZ2hpCc7YuizNKmEbuGPxQPfUSIPrdE2wpo6ZfIhEbxq9m/trl1oRCN4PXi3BB7dlYYg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/core" "8.43.0"
|
"@sentry/core" "8.45.0"
|
||||||
|
|
||||||
"@sentry-internal/feedback@8.43.0":
|
"@sentry-internal/feedback@8.45.0":
|
||||||
version "8.43.0"
|
version "8.45.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.43.0.tgz#9477b999c9bca62335eb944a6f7246a96beb0111"
|
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.45.0.tgz#cfd7f54d5089682a2768c1229a5efcda4d9561fe"
|
||||||
integrity sha512-rcGR2kzFu4vLXBQbI9eGJwjyToyjl36O2q/UKbiZBNJ5IFtDvKRLke6jIHq/YqiHPfFGpVtq5M/lYduDfA/eaQ==
|
integrity sha512-WerpfkKrKPAlnQuqjEgKXZtrx68cla7GyOkNOeL40JQbY4/By4Qjx1atUOmgk/FdjrCLPw+jQQY9pXRpMRqqRw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/core" "8.43.0"
|
"@sentry/core" "8.45.0"
|
||||||
|
|
||||||
"@sentry-internal/replay-canvas@8.43.0":
|
"@sentry-internal/replay-canvas@8.45.0":
|
||||||
version "8.43.0"
|
version "8.45.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.43.0.tgz#f5672a08c9eb588afa0bf36f07b9f5c29b5c9920"
|
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.45.0.tgz#46f39402ff0cfee4ae05191af20b4e4fac6f474c"
|
||||||
integrity sha512-rL8G7E1GtozH8VNalRrBQNjYDJ5ChWS/vpQI5hUG11PZfvQFXEVatLvT3uO2l0xIlHm4idTsHOSLTe/usxnogQ==
|
integrity sha512-LZ8kBuzO5gutDiWnCyYEzBMDLq9PIllcsWsXRpKoau0Zqs3DbyRolI11dNnxmUSh7UW21FksxBpqn5yPmUMbag==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry-internal/replay" "8.43.0"
|
"@sentry-internal/replay" "8.45.0"
|
||||||
"@sentry/core" "8.43.0"
|
"@sentry/core" "8.45.0"
|
||||||
|
|
||||||
"@sentry-internal/replay@8.43.0":
|
"@sentry-internal/replay@8.45.0":
|
||||||
version "8.43.0"
|
version "8.45.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.43.0.tgz#4e2e3844f52b47b16bf816d21857921bbfe85d62"
|
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.45.0.tgz#e94d250de235491888694f7cf0f637114adb4b9a"
|
||||||
integrity sha512-geV5/zejLfGGwWHjylzrb1w8NI3U37GMG9/53nmv13FmTXUDF5XF2lh41KXFVYwvp7Ha4bd1FRQ9IU9YtBWskw==
|
integrity sha512-SOFwFpzx0B6lxhLl2hBnxvybo7gdB5TMY8dOHMwXgk5A2+BXvSpvWXnr33yqUlBmC8R3LeFTB3C0plzM5lhkJg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry-internal/browser-utils" "8.43.0"
|
"@sentry-internal/browser-utils" "8.45.0"
|
||||||
"@sentry/core" "8.43.0"
|
"@sentry/core" "8.45.0"
|
||||||
|
|
||||||
"@sentry/babel-plugin-component-annotate@2.22.7":
|
"@sentry/babel-plugin-component-annotate@2.22.7":
|
||||||
version "2.22.7"
|
version "2.22.7"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd"
|
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd"
|
||||||
integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ==
|
integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ==
|
||||||
|
|
||||||
"@sentry/browser@8.43.0":
|
"@sentry/browser@8.45.0":
|
||||||
version "8.43.0"
|
version "8.45.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.43.0.tgz#4eec67bc6fb278727304045b612ac392674cade6"
|
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.45.0.tgz#2e8f7b8b1a7860863aae4d716b9748a21789f0e0"
|
||||||
integrity sha512-LGvLLnfmR8+AEgFmd7Q7KHiOTiV0P1Lvio2ENDELhEqJOIiICauttibVmig+AW02qg4kMeywvleMsUYaZv2RVA==
|
integrity sha512-Y+BcfpXY1eEkOYOzgLGkx1YH940uMAymYOxfSZSvC+Vx6xHuaGT05mIFef/aeZbyu2AUs6JjdvD1BRBZlHg78w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry-internal/browser-utils" "8.43.0"
|
"@sentry-internal/browser-utils" "8.45.0"
|
||||||
"@sentry-internal/feedback" "8.43.0"
|
"@sentry-internal/feedback" "8.45.0"
|
||||||
"@sentry-internal/replay" "8.43.0"
|
"@sentry-internal/replay" "8.45.0"
|
||||||
"@sentry-internal/replay-canvas" "8.43.0"
|
"@sentry-internal/replay-canvas" "8.45.0"
|
||||||
"@sentry/core" "8.43.0"
|
"@sentry/core" "8.45.0"
|
||||||
|
|
||||||
"@sentry/bundler-plugin-core@2.22.7":
|
"@sentry/bundler-plugin-core@2.22.7":
|
||||||
version "2.22.7"
|
version "2.22.7"
|
||||||
@@ -3021,18 +3096,18 @@
|
|||||||
"@sentry/cli-win32-i686" "2.39.1"
|
"@sentry/cli-win32-i686" "2.39.1"
|
||||||
"@sentry/cli-win32-x64" "2.39.1"
|
"@sentry/cli-win32-x64" "2.39.1"
|
||||||
|
|
||||||
"@sentry/core@8.43.0":
|
"@sentry/core@8.45.0":
|
||||||
version "8.43.0"
|
version "8.45.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.43.0.tgz#e96a489e87a9999199f5ac27d8860da37c1fa8b4"
|
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.45.0.tgz#a03a1b666989898ce7fb33f9ec279ea08450b317"
|
||||||
integrity sha512-ktyovtjkTMNud+kC/XfqHVCoQKreIKgx/hgeRvzPwuPyd1t1KzYmRL3DBkbcWVnyOPpVTHn+RsEI1eRcVYHtvw==
|
integrity sha512-4YTuBipWSh4JrtSYS5GxUQBAcAgOIkEoFfFbwVcr3ivijOacJLRXTBn3rpcy1CKjBq0PHDGR+2RGRYC+bNAMxg==
|
||||||
|
|
||||||
"@sentry/react@^8.0.0":
|
"@sentry/react@^8.0.0":
|
||||||
version "8.43.0"
|
version "8.45.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.43.0.tgz#ad49bd16b0b1897613ef5cbd2f0a49b2b41f98a9"
|
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.45.0.tgz#9a1bfbbbb3575fffb92796acc28ad5bb93a6855a"
|
||||||
integrity sha512-PsTzLrYio/FOJU537Y5Gj9jJi7OMHEjdttsC9INUxy5062LOd8ObtHsjE0mopLaSYEwUfSROQOBZCwmISh8ByQ==
|
integrity sha512-xuJBDATJKAHOxpR5IBfGFWJxXb05GMPGGpk8UoWai1Mh50laAQ0/WW+5sDAKrCjXoA+JZ6fb3DP8EE2X93n1nw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/browser" "8.43.0"
|
"@sentry/browser" "8.45.0"
|
||||||
"@sentry/core" "8.43.0"
|
"@sentry/core" "8.45.0"
|
||||||
hoist-non-react-statics "^3.3.2"
|
hoist-non-react-statics "^3.3.2"
|
||||||
|
|
||||||
"@sentry/vite-plugin@^2.0.0":
|
"@sentry/vite-plugin@^2.0.0":
|
||||||
@@ -3308,9 +3383,9 @@
|
|||||||
undici-types "~6.19.8"
|
undici-types "~6.19.8"
|
||||||
|
|
||||||
"@types/node@^22.0.0":
|
"@types/node@^22.0.0":
|
||||||
version "22.10.1"
|
version "22.10.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9"
|
||||||
integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==
|
integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~6.20.0"
|
undici-types "~6.20.0"
|
||||||
|
|
||||||
@@ -3337,9 +3412,9 @@
|
|||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/react-dom@^18.3.0":
|
"@types/react-dom@^18.3.0":
|
||||||
version "18.3.3"
|
version "18.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.3.tgz#3654138d0da1b0c7916f6ed0dc1cc2b576d47650"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.5.tgz#45f9f87398c5dcea085b715c58ddcf1faf65f716"
|
||||||
integrity sha512-uTYkxTLkYp41nq/ULXyXMtkNT1vu5fXJoqad6uTNCOGat5t9cLgF4vMNLBXsTOXpdOI44XzKPY1M5RRm0bQHuw==
|
integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==
|
||||||
|
|
||||||
"@types/react-router-dom@^5.3.3":
|
"@types/react-router-dom@^5.3.3":
|
||||||
version "5.3.3"
|
version "5.3.3"
|
||||||
@@ -4075,7 +4150,17 @@ broccoli-plugin@^4.0.7:
|
|||||||
rimraf "^3.0.2"
|
rimraf "^3.0.2"
|
||||||
symlink-or-copy "^1.3.1"
|
symlink-or-copy "^1.3.1"
|
||||||
|
|
||||||
browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2:
|
browserslist@^4.23.1, browserslist@^4.23.3:
|
||||||
|
version "4.24.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.3.tgz#5fc2725ca8fb3c1432e13dac278c7cc103e026d2"
|
||||||
|
integrity sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==
|
||||||
|
dependencies:
|
||||||
|
caniuse-lite "^1.0.30001688"
|
||||||
|
electron-to-chromium "^1.5.73"
|
||||||
|
node-releases "^2.0.19"
|
||||||
|
update-browserslist-db "^1.1.1"
|
||||||
|
|
||||||
|
browserslist@^4.24.0, browserslist@^4.24.2:
|
||||||
version "4.24.2"
|
version "4.24.2"
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580"
|
||||||
integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==
|
integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==
|
||||||
@@ -4141,15 +4226,10 @@ camelcase@^6.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001646:
|
caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669, caniuse-lite@^1.0.30001688:
|
||||||
version "1.0.30001680"
|
version "1.0.30001688"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz#5380ede637a33b9f9f1fc6045ea99bd142f3da5e"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz#f9d3ede749f083ce0db4c13db9d828adaf2e8d0a"
|
||||||
integrity sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==
|
integrity sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001669:
|
|
||||||
version "1.0.30001687"
|
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz#d0ac634d043648498eedf7a3932836beba90ebae"
|
|
||||||
integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==
|
|
||||||
|
|
||||||
caseless@~0.12.0:
|
caseless@~0.12.0:
|
||||||
version "0.12.0"
|
version "0.12.0"
|
||||||
@@ -4440,10 +4520,10 @@ css-blank-pseudo@^7.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss-selector-parser "^7.0.0"
|
postcss-selector-parser "^7.0.0"
|
||||||
|
|
||||||
css-has-pseudo@^7.0.1:
|
css-has-pseudo@^7.0.2:
|
||||||
version "7.0.1"
|
version "7.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.1.tgz#adbb51821e51f7a7c1d2df4d12827870cc311137"
|
resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz#fb42e8de7371f2896961e1f6308f13c2c7019b72"
|
||||||
integrity sha512-EOcoyJt+OsuKfCADgLT7gADZI5jMzIe/AeI6MeAYKiFBDmNmM7kk46DtSfMj5AohUJisqVzopBpnQTlvbyaBWg==
|
integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@csstools/selector-specificity" "^5.0.0"
|
"@csstools/selector-specificity" "^5.0.0"
|
||||||
postcss-selector-parser "^7.0.0"
|
postcss-selector-parser "^7.0.0"
|
||||||
@@ -4475,10 +4555,10 @@ css.escape@^1.5.1:
|
|||||||
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
|
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
|
||||||
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
|
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
|
||||||
|
|
||||||
cssdb@^8.2.1:
|
cssdb@^8.2.3:
|
||||||
version "8.2.1"
|
version "8.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.1.tgz#62a5d9a41e2c86f1d7c35981098fc5ce47c5766c"
|
resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.3.tgz#7e6980bb5a785a9b4eb2a21bd38d50624b56cb46"
|
||||||
integrity sha512-KwEPys7lNsC8OjASI8RrmwOYYDcm0JOW9zQhcV83ejYcQkirTEyeAGui8aO2F5PiS6SLpxuTzl6qlMElIdsgIg==
|
integrity sha512-9BDG5XmJrJQQnJ51VFxXCAtpZ5ebDlAREmO8sxMOVU0aSxN/gocbctjIG5LMh3WBUq+xTlb/jw2LoljBEqraTA==
|
||||||
|
|
||||||
cssesc@^3.0.0:
|
cssesc@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
@@ -4738,10 +4818,10 @@ easy-table@1.2.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
wcwidth "^1.0.1"
|
wcwidth "^1.0.1"
|
||||||
|
|
||||||
electron-to-chromium@^1.5.41:
|
electron-to-chromium@^1.5.41, electron-to-chromium@^1.5.73:
|
||||||
version "1.5.72"
|
version "1.5.73"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.72.tgz#a732805986d3a5b5fedd438ddf4616c7d78ac2df"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz#f32956ce40947fa3c8606726a96cd8fb5bb5f720"
|
||||||
integrity sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw==
|
integrity sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
@@ -5902,13 +5982,6 @@ interpret@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
|
||||||
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
|
integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
|
||||||
|
|
||||||
invariant@^2.2.4:
|
|
||||||
version "2.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
|
||||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
|
||||||
dependencies:
|
|
||||||
loose-envify "^1.0.0"
|
|
||||||
|
|
||||||
is-array-buffer@^3.0.4:
|
is-array-buffer@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
|
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
|
||||||
@@ -6329,9 +6402,9 @@ kleur@^3.0.3:
|
|||||||
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
||||||
|
|
||||||
knip@^5.27.2:
|
knip@^5.27.2:
|
||||||
version "5.39.2"
|
version "5.40.0"
|
||||||
resolved "https://registry.yarnpkg.com/knip/-/knip-5.39.2.tgz#1faacd8d8ef36b509b2f6e396cce85b645abb04e"
|
resolved "https://registry.yarnpkg.com/knip/-/knip-5.40.0.tgz#6da9113d9d0c696fc3e5dc3f3a281db57b4b828a"
|
||||||
integrity sha512-BuvuWRllLWV/r2G4m9ggNH+DZ6gouP/dhtJPXVlMbWNF++w9/EfrF6k2g7YBKCwjzCC+PXmYtpH8S2t8RjnY4Q==
|
integrity sha512-EzBfQDz4YBzYnMLueWnaaVr15mneqZs1c3RanttciuVuRcodlNjzAmR2nch/khlRdVABAxAdMGFxfSvhvcH1NA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@nodelib/fs.walk" "1.2.8"
|
"@nodelib/fs.walk" "1.2.8"
|
||||||
"@snyk/github-codeowners" "1.1.0"
|
"@snyk/github-codeowners" "1.1.0"
|
||||||
@@ -6449,7 +6522,7 @@ long@^5.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
|
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
|
||||||
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
|
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
|
loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
@@ -6529,11 +6602,11 @@ matrix-events-sdk@0.0.1:
|
|||||||
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
||||||
|
|
||||||
matrix-js-sdk@matrix-org/matrix-js-sdk#develop:
|
matrix-js-sdk@matrix-org/matrix-js-sdk#develop:
|
||||||
version "34.12.0"
|
version "34.13.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/edac6a9983bd604c17535a9ae673dc979c7b61c4"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e4182eb75227c283a18704727021e99ced72868d"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0"
|
"@matrix-org/matrix-sdk-crypto-wasm" "^12.0.0"
|
||||||
"@matrix-org/olm" "3.2.15"
|
"@matrix-org/olm" "3.2.15"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
bs58 "^6.0.0"
|
bs58 "^6.0.0"
|
||||||
@@ -6638,9 +6711,9 @@ ms@^2.1.1, ms@^2.1.3:
|
|||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
nanoid@^3.3.7:
|
nanoid@^3.3.7:
|
||||||
version "3.3.7"
|
version "3.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||||
|
|
||||||
natural-compare@^1.4.0:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
@@ -6667,7 +6740,7 @@ node-fetch@^2.6.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^5.0.0"
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
node-releases@^2.0.18:
|
node-releases@^2.0.18, node-releases@^2.0.19:
|
||||||
version "2.0.19"
|
version "2.0.19"
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
||||||
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
||||||
@@ -7168,9 +7241,9 @@ postcss-place@^10.0.0:
|
|||||||
postcss-value-parser "^4.2.0"
|
postcss-value-parser "^4.2.0"
|
||||||
|
|
||||||
postcss-preset-env@^10.0.0:
|
postcss-preset-env@^10.0.0:
|
||||||
version "10.1.1"
|
version "10.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.1.1.tgz#6ee631272353fb1c4a9711943e9b80a178ffce44"
|
resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.1.2.tgz#ea9c25d92045ef06edd78f9945d2586107aab3e3"
|
||||||
integrity sha512-wqqsnBFD6VIwcHHRbhjTOcOi4qRVlB26RwSr0ordPj7OubRRxdWebv/aLjKLRR8zkZrbxZyuus03nOIgC5elMQ==
|
integrity sha512-OqUBZ9ByVfngWhMNuBEMy52Izj07oIFA6K/EOGBlaSv+P12MiE1+S2cqXtS1VuW82demQ/Tzc7typYk3uHunkA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@csstools/postcss-cascade-layers" "^5.0.1"
|
"@csstools/postcss-cascade-layers" "^5.0.1"
|
||||||
"@csstools/postcss-color-function" "^4.0.6"
|
"@csstools/postcss-color-function" "^4.0.6"
|
||||||
@@ -7207,9 +7280,9 @@ postcss-preset-env@^10.0.0:
|
|||||||
autoprefixer "^10.4.19"
|
autoprefixer "^10.4.19"
|
||||||
browserslist "^4.23.1"
|
browserslist "^4.23.1"
|
||||||
css-blank-pseudo "^7.0.1"
|
css-blank-pseudo "^7.0.1"
|
||||||
css-has-pseudo "^7.0.1"
|
css-has-pseudo "^7.0.2"
|
||||||
css-prefers-color-scheme "^10.0.0"
|
css-prefers-color-scheme "^10.0.0"
|
||||||
cssdb "^8.2.1"
|
cssdb "^8.2.3"
|
||||||
postcss-attribute-case-insensitive "^7.0.1"
|
postcss-attribute-case-insensitive "^7.0.1"
|
||||||
postcss-clamp "^4.1.0"
|
postcss-clamp "^4.1.0"
|
||||||
postcss-color-functional-notation "^7.0.6"
|
postcss-color-functional-notation "^7.0.6"
|
||||||
@@ -7421,9 +7494,9 @@ react-error-boundary@^3.1.0:
|
|||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
|
|
||||||
react-i18next@^15.0.0:
|
react-i18next@^15.0.0:
|
||||||
version "15.1.4"
|
version "15.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.4.tgz#65c03c31a5e42202000652e163f22f23a9306a60"
|
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.2.0.tgz#6b51650e1e93eb4d235a4d533fcf61b3bbf4ea10"
|
||||||
integrity sha512-2tai71gmehbvl9ZIqPMqlCCkm/cbeV1G4STpmM3C8Uzo6T2l8jDvZxEVSsQKt8blP9X34iRFP/k1ROqG2296MQ==
|
integrity sha512-iJNc8111EaDtVTVMKigvBtPHyrJV+KblWG73cUxqp+WmJCcwkzhWNFXmkAD5pwP2Z4woeDj/oXDdbjDsb3Gutg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.25.0"
|
"@babel/runtime" "^7.25.0"
|
||||||
html-parse-stringify "^3.0.1"
|
html-parse-stringify "^3.0.1"
|
||||||
@@ -7444,11 +7517,11 @@ react-refresh@^0.14.2:
|
|||||||
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
|
integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
|
||||||
|
|
||||||
react-remove-scroll-bar@^2.3.6:
|
react-remove-scroll-bar@^2.3.6:
|
||||||
version "2.3.6"
|
version "2.3.8"
|
||||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c"
|
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223"
|
||||||
integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==
|
integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
react-style-singleton "^2.2.1"
|
react-style-singleton "^2.2.2"
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
react-remove-scroll@2.6.0:
|
react-remove-scroll@2.6.0:
|
||||||
@@ -7490,13 +7563,12 @@ react-router@5.3.4:
|
|||||||
tiny-invariant "^1.0.2"
|
tiny-invariant "^1.0.2"
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
|
|
||||||
react-style-singleton@^2.2.1:
|
react-style-singleton@^2.2.1, react-style-singleton@^2.2.2:
|
||||||
version "2.2.1"
|
version "2.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
|
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388"
|
||||||
integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==
|
integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
get-nonce "^1.0.0"
|
get-nonce "^1.0.0"
|
||||||
invariant "^2.2.4"
|
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
react-use-clipboard@^1.0.7:
|
react-use-clipboard@^1.0.7:
|
||||||
@@ -7788,30 +7860,31 @@ rollup@^4.20.0:
|
|||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
rollup@^4.23.0:
|
rollup@^4.23.0:
|
||||||
version "4.28.0"
|
version "4.28.1"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.0.tgz#eb8d28ed43ef60a18f21d0734d230ee79dd0de77"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.1.tgz#7718ba34d62b449dfc49adbfd2f312b4fe0df4de"
|
||||||
integrity sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==
|
integrity sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "1.0.6"
|
"@types/estree" "1.0.6"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@rollup/rollup-android-arm-eabi" "4.28.0"
|
"@rollup/rollup-android-arm-eabi" "4.28.1"
|
||||||
"@rollup/rollup-android-arm64" "4.28.0"
|
"@rollup/rollup-android-arm64" "4.28.1"
|
||||||
"@rollup/rollup-darwin-arm64" "4.28.0"
|
"@rollup/rollup-darwin-arm64" "4.28.1"
|
||||||
"@rollup/rollup-darwin-x64" "4.28.0"
|
"@rollup/rollup-darwin-x64" "4.28.1"
|
||||||
"@rollup/rollup-freebsd-arm64" "4.28.0"
|
"@rollup/rollup-freebsd-arm64" "4.28.1"
|
||||||
"@rollup/rollup-freebsd-x64" "4.28.0"
|
"@rollup/rollup-freebsd-x64" "4.28.1"
|
||||||
"@rollup/rollup-linux-arm-gnueabihf" "4.28.0"
|
"@rollup/rollup-linux-arm-gnueabihf" "4.28.1"
|
||||||
"@rollup/rollup-linux-arm-musleabihf" "4.28.0"
|
"@rollup/rollup-linux-arm-musleabihf" "4.28.1"
|
||||||
"@rollup/rollup-linux-arm64-gnu" "4.28.0"
|
"@rollup/rollup-linux-arm64-gnu" "4.28.1"
|
||||||
"@rollup/rollup-linux-arm64-musl" "4.28.0"
|
"@rollup/rollup-linux-arm64-musl" "4.28.1"
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.28.0"
|
"@rollup/rollup-linux-loongarch64-gnu" "4.28.1"
|
||||||
"@rollup/rollup-linux-riscv64-gnu" "4.28.0"
|
"@rollup/rollup-linux-powerpc64le-gnu" "4.28.1"
|
||||||
"@rollup/rollup-linux-s390x-gnu" "4.28.0"
|
"@rollup/rollup-linux-riscv64-gnu" "4.28.1"
|
||||||
"@rollup/rollup-linux-x64-gnu" "4.28.0"
|
"@rollup/rollup-linux-s390x-gnu" "4.28.1"
|
||||||
"@rollup/rollup-linux-x64-musl" "4.28.0"
|
"@rollup/rollup-linux-x64-gnu" "4.28.1"
|
||||||
"@rollup/rollup-win32-arm64-msvc" "4.28.0"
|
"@rollup/rollup-linux-x64-musl" "4.28.1"
|
||||||
"@rollup/rollup-win32-ia32-msvc" "4.28.0"
|
"@rollup/rollup-win32-arm64-msvc" "4.28.1"
|
||||||
"@rollup/rollup-win32-x64-msvc" "4.28.0"
|
"@rollup/rollup-win32-ia32-msvc" "4.28.1"
|
||||||
|
"@rollup/rollup-win32-x64-msvc" "4.28.1"
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
rrweb-cssom@^0.7.1:
|
rrweb-cssom@^0.7.1:
|
||||||
@@ -7891,9 +7964,9 @@ safe-regex-test@^1.0.3:
|
|||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
sass@^1.42.1:
|
sass@^1.42.1:
|
||||||
version "1.82.0"
|
version "1.83.0"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.0.tgz#e36842c0b88a94ed336fd16249b878a0541d536f"
|
||||||
integrity sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q==
|
integrity sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar "^4.0.0"
|
chokidar "^4.0.0"
|
||||||
immutable "^5.0.2"
|
immutable "^5.0.2"
|
||||||
@@ -8689,9 +8762,9 @@ use-callback-ref@^1.3.0:
|
|||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
use-sidecar@^1.1.2:
|
use-sidecar@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
|
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb"
|
||||||
integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==
|
integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-node-es "^1.1.0"
|
detect-node-es "^1.1.0"
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
@@ -8836,9 +8909,9 @@ vite@^5.0.0:
|
|||||||
fsevents "~2.3.3"
|
fsevents "~2.3.3"
|
||||||
|
|
||||||
vite@^6.0.0:
|
vite@^6.0.0:
|
||||||
version "6.0.2"
|
version "6.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.2.tgz#7a22630c73c7b663335ddcdb2390971ffbc14993"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.3.tgz#cc01f403e326a9fc1e064235df8a6de084c8a491"
|
||||||
integrity sha512-XdQ+VsY2tJpBsKGs0wf3U/+azx8BBpYRHFAyKm5VeEZNOJZRB63q7Sc8Iup3k0TrN3KO6QgyzFf+opSbfY1y0g==
|
integrity sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.24.0"
|
esbuild "^0.24.0"
|
||||||
postcss "^8.4.49"
|
postcss "^8.4.49"
|
||||||
@@ -9180,6 +9253,6 @@ zod-validation-error@^3.0.3:
|
|||||||
integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==
|
integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==
|
||||||
|
|
||||||
zod@^3.22.4:
|
zod@^3.22.4:
|
||||||
version "3.24.0"
|
version "3.24.1"
|
||||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.0.tgz#babb32313f7c5f4a99812feee806d186b4f76bde"
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
|
||||||
integrity sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w==
|
integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==
|
||||||
|
|||||||
Reference in New Issue
Block a user