mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-03-01 09:10:20 +00:00
Compare commits
32 Commits
fix/consol
...
9056
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c48834a116 | ||
|
|
244b5f9dd3 | ||
|
|
18504d4ad8 | ||
|
|
fcf8a4bbe0 | ||
|
|
f66fe9fbbd | ||
|
|
caee687cc3 | ||
|
|
8530c1f2fc | ||
|
|
3b2e3526c4 | ||
|
|
aee5cb9200 | ||
|
|
df0e55eda3 | ||
|
|
f7d64341f8 | ||
|
|
156dd24b14 | ||
|
|
91481aa245 | ||
|
|
e94153df16 | ||
|
|
3c85aa2350 | ||
|
|
c9a220e1c3 | ||
|
|
5f11e2e9f4 | ||
|
|
45ee515b6b | ||
|
|
56f7792604 | ||
|
|
230559c493 | ||
|
|
900bece988 | ||
|
|
a06a338de5 | ||
|
|
eb1e4f01e7 | ||
|
|
641a401b4b | ||
|
|
6e3c256905 | ||
|
|
36a755776e | ||
|
|
7bd38be8a4 | ||
|
|
c88bfa1b9a | ||
|
|
6a443b0f8f | ||
|
|
b4cc466608 | ||
|
|
bde8dca825 | ||
|
|
2ab86cffb8 |
@@ -96,23 +96,53 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
|
||||
public static void addTopBottomInsets(@NonNull Window w, @NonNull View v) {
|
||||
|
||||
// Enable edge-to-edge mode
|
||||
androidx.core.view.WindowCompat.setDecorFitsSystemWindows(w, false);
|
||||
|
||||
// Make system bars transparent so content is visible underneath
|
||||
w.setStatusBarColor(android.graphics.Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(android.graphics.Color.TRANSPARENT);
|
||||
|
||||
View decorView = w.getDecorView();
|
||||
|
||||
decorView.post(() -> {
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
|
||||
if (insets != null) {
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
||||
params.topMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
|
||||
params.bottomMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||
// Get display metrics for calculating density-independent caps
|
||||
final android.util.DisplayMetrics metrics = v.getContext().getResources().getDisplayMetrics();
|
||||
final int screenHeight = metrics.heightPixels;
|
||||
final float density = metrics.density;
|
||||
|
||||
// Listen for window inset changes
|
||||
// when system bars visibility is toggled or when the device rotates
|
||||
ViewCompat.setOnApplyWindowInsetsListener(decorView, (view, windowInsets) -> {
|
||||
|
||||
// Get the actual inset values reported by the system
|
||||
int statusBarInset = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||
int navBarInset = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
|
||||
|
||||
// Calculate maximum allowed inset values to prevent device-specific bugs
|
||||
final int maxTopInset = Math.min((int)(60 * density), (int)(screenHeight * 0.10));
|
||||
final int maxBottomInset = Math.min((int)(120 * density), (int)(screenHeight * 0.10));
|
||||
|
||||
int topInset = Math.min(statusBarInset, maxTopInset);
|
||||
int bottomInset = Math.min(navBarInset, maxBottomInset);
|
||||
|
||||
// Apply calculated insets
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
||||
|
||||
// Update margins only if they've changed
|
||||
if (params.topMargin != topInset || params.bottomMargin != bottomInset) {
|
||||
params.topMargin = topInset;
|
||||
params.bottomMargin = bottomInset;
|
||||
v.setLayoutParams(params);
|
||||
|
||||
decorView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
|
||||
view.setBackgroundColor(JitsiMeetView.BACKGROUND_COLOR);
|
||||
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
|
||||
view.setBackgroundColor(JitsiMeetView.BACKGROUND_COLOR);
|
||||
|
||||
// Return CONSUMED to prevent double-application of margins
|
||||
return WindowInsetsCompat.CONSUMED;
|
||||
});
|
||||
|
||||
// Manually trigger the inset listener to apply margins immediately
|
||||
ViewCompat.requestApplyInsets(decorView);
|
||||
}
|
||||
|
||||
// Overrides
|
||||
@@ -136,10 +166,11 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
|
||||
setContentView(R.layout.activity_jitsi_meet);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
|
||||
&& getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
addTopBottomInsets(getWindow(), findViewById(android.R.id.content));
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
|
||||
&& getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
addTopBottomInsets(getWindow(), findViewById(android.R.id.content));
|
||||
}
|
||||
|
||||
this.jitsiView = findViewById(R.id.jitsiView);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ body {
|
||||
|
||||
.jitsi-icon {
|
||||
&-default svg {
|
||||
fill: var(--icon-default-color, white);
|
||||
fill: var(--icon-svg-fill, white);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
.reactions-menu {
|
||||
width: 330px;
|
||||
background: #242528;
|
||||
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
|
||||
background: var(--reactions-menu-background, #242528);
|
||||
box-shadow: 0px 3px 16px var(--reactions-menu-box-shadow-1, rgba(0, 0, 0, 0.6)), 0px 0px 4px 1px var(--reactions-menu-box-shadow-2, rgba(0, 0, 0, 0.25));
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
top: 3px;
|
||||
|
||||
& .toolbox-icon.toggled {
|
||||
background-color: #000000;
|
||||
background-color: var(--reactions-menu-button-toggled, #000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
}
|
||||
|
||||
#preview {
|
||||
background: #040404;
|
||||
background: var(--prejoin-preview-background, #040404);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -34,7 +34,7 @@ Description: Configuration for web serving of Jitsi Meet
|
||||
|
||||
Package: jitsi-meet-prosody
|
||||
Architecture: all
|
||||
Depends: openssl, prosody (>= 0.12.0) | prosody-trunk | prosody-0.12 | prosody-13.0, lua-sec, lua-basexx, lua-luaossl, lua-cjson, lua-inspect
|
||||
Depends: openssl, prosody (>= 0.12.0) | prosody-trunk | prosody-0.12 | prosody-13.0, ca-certificates-java, lua-sec, lua-basexx, lua-luaossl, lua-cjson, lua-inspect
|
||||
Replaces: jitsi-meet-tokens
|
||||
Description: Prosody configuration for Jitsi Meet
|
||||
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
|
||||
|
||||
12
debian/jitsi-meet-prosody.postinst
vendored
12
debian/jitsi-meet-prosody.postinst
vendored
@@ -284,13 +284,17 @@ case "$1" in
|
||||
# and drop the wait and the prosody restart
|
||||
sleep 1
|
||||
invoke-rc.d prosody restart || true
|
||||
|
||||
# In case we had updated the certificates and restarted prosody, let's restart and the bridge and jicofo if possible
|
||||
if [ -d /run/systemd/system ] && [ "$CERT_ADDED_TO_TRUST" = "true" ]; then
|
||||
fi
|
||||
;;
|
||||
triggered)
|
||||
for trigger in $2; do
|
||||
if [ "$trigger" = "update-ca-certificates-java" ]; then
|
||||
echo "Java certificates updated, restarting Jitsi components..."
|
||||
systemctl restart jitsi-videobridge2.service >/dev/null || true
|
||||
systemctl restart jicofo.service >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
|
||||
1
debian/jitsi-meet-prosody.triggers
vendored
Normal file
1
debian/jitsi-meet-prosody.triggers
vendored
Normal file
@@ -0,0 +1 @@
|
||||
interest-noawait update-ca-certificates-java
|
||||
@@ -227,6 +227,9 @@
|
||||
"video_ssrc": "Video-SSRC:",
|
||||
"yes": "Ja"
|
||||
},
|
||||
"customPanel": {
|
||||
"close": "Schließen"
|
||||
},
|
||||
"dateUtils": {
|
||||
"earlier": "Früher",
|
||||
"today": "Heute",
|
||||
@@ -589,6 +592,7 @@
|
||||
"newFileNotification": "{{ participantName }} hat Datei '{{ fileName }}' hochgeladen",
|
||||
"removeFile": "Entfernen",
|
||||
"removeFileSuccess": "Datei erfolgreich entfernt",
|
||||
"uploadDisabled": "Keine Berechtigung, Dateien hochzuladen. Bitte bei der Moderation anfragen.",
|
||||
"uploadFailedDescription": "Bitte versuchen Sie es erneut.",
|
||||
"uploadFailedTitle": "Dateiupload fehlgeschlagen",
|
||||
"uploadFile": "Datei hochladen"
|
||||
@@ -1315,6 +1319,7 @@
|
||||
"chat": "Chatfenster öffnen / schließen",
|
||||
"clap": "Klatschen",
|
||||
"closeChat": "Chat schließen",
|
||||
"closeCustomPanel": "Schließen",
|
||||
"closeMoreActions": "„Weitere Einstellungen“ schließen",
|
||||
"closeParticipantsPane": "Anwesendenliste schließen",
|
||||
"closedCaptions": "Untertitel",
|
||||
@@ -1420,9 +1425,11 @@
|
||||
"chat": "Chat öffnen / schließen",
|
||||
"clap": "Klatschen",
|
||||
"closeChat": "Chat schließen",
|
||||
"closeCustomPanel": "Schließen",
|
||||
"closeParticipantsPane": "Anwesenheitsliste schließen",
|
||||
"closeReactionsMenu": "Interaktionsmenü schließen",
|
||||
"closedCaptions": "Untertitel",
|
||||
"copilot": "Copilot",
|
||||
"disableNoiseSuppression": "Rauschunterdrückung deaktivieren",
|
||||
"disableReactionSounds": "Sie können die Interaktionstöne für diese Konferenz deaktivieren",
|
||||
"documentClose": "Geteiltes Dokument schließen",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1209
lang/main-fi.json
1209
lang/main-fi.json
File diff suppressed because it is too large
Load Diff
@@ -114,6 +114,9 @@
|
||||
"error": "Erreur : votre message n'a pas été envoyé. Raison : {{error}}",
|
||||
"everyone": "Tout le monde",
|
||||
"fieldPlaceHolder": "Tapez votre message ici",
|
||||
"fileAccessibleTitle": "{{user}} a téléversé un fichier",
|
||||
"fileAccessibleTitleMe": "j’ai téléversé un fichier",
|
||||
"fileDeleted": "Un fichier a été supprimé",
|
||||
"guestsChatIndicator": "(invité)",
|
||||
"lobbyChatMessageTo": "Message de salle d'attente à {{recipient}}",
|
||||
"message": "Message",
|
||||
@@ -123,8 +126,16 @@
|
||||
"messagebox": "Envoyer un message",
|
||||
"newMessages": "Nouveaux messages",
|
||||
"nickname": {
|
||||
"featureChat": "chat",
|
||||
"featureClosedCaptions": "sous-titres",
|
||||
"featureFileSharing": "partage de fichiers",
|
||||
"featurePolls": "sondages",
|
||||
"popover": "Choisissez un pseudonyme",
|
||||
"title": "Entrez un pseudonyme pour utiliser le chat",
|
||||
"titleWith1Features": "Entrez un pseudonyme pour utiliser {{feature1}}",
|
||||
"titleWith2Features": "Entrez un pseudonyme pour utiliser {{feature1}} et {{feature2}}",
|
||||
"titleWith3Features": "Entrez un pseudonyme pour utiliser {{feature1}}, {{feature2}} et {{feature3}}",
|
||||
"titleWith4Features": "Entrez un pseudonyme pour utiliser {{feature1}}, {{feature2}}, {{feature3}} et {{feature4}}",
|
||||
"titleWithCC": "Entrez un pseudonyme pour utiliser le chat et les sous-titres",
|
||||
"titleWithPolls": "Entrez un pseudonyme pour utiliser le chat et les sondages",
|
||||
"titleWithPollsAndCC": "Entrez un pseudonyme pour utiliser le chat, les sondages et les sous-titres",
|
||||
@@ -216,6 +227,9 @@
|
||||
"video_ssrc": "Video SSRC :",
|
||||
"yes": "oui"
|
||||
},
|
||||
"customPanel": {
|
||||
"close": "Fermer"
|
||||
},
|
||||
"dateUtils": {
|
||||
"earlier": "Plus tôt",
|
||||
"today": "Aujourd'hui",
|
||||
@@ -522,6 +536,7 @@
|
||||
"tokenAuthFailedWithReasons": "Désolé, vous n’êtes pas autorisé à rejoindre l’appel. La raison possible : {{reason}}.",
|
||||
"tokenAuthUnsupported": "Token URL n'est pas supporté.",
|
||||
"transcribing": "Transcription",
|
||||
"unauthenticatedAccessDisabled": "Cet appel nécessite une authentification. Veuillez vous connecter pour continuer.",
|
||||
"unlockRoom": "Supprimer le $t(lockRoomPassword) de la réunion",
|
||||
"user": "Utilisateur",
|
||||
"userIdentifier": "Identifiant utilisateur",
|
||||
@@ -569,10 +584,12 @@
|
||||
"downloadStarted": "Téléchargement de fichier démarré",
|
||||
"dragAndDrop": "Glissez et déposez des fichiers ici ou n'importe où sur l'écran",
|
||||
"fileAlreadyUploaded": "Le fichier a déjà été téléchargé vers cette réunion.",
|
||||
"fileRemovedByOther": "Votre fichier « {{ fileName }} » a été supprimé",
|
||||
"fileTooLargeDescription": "Veuillez vous assurer que le fichier ne dépasse pas {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "Le fichier sélectionné est trop volumineux",
|
||||
"fileUploadProgress": "Progression du téléchargement de fichier",
|
||||
"fileUploadedSuccessfully": "Fichier téléchargé avec succès",
|
||||
"newFileNotification": "{{ participantName }} a partagé « {{ fileName }} »",
|
||||
"removeFile": "Supprimer",
|
||||
"removeFileSuccess": "Fichier supprimé avec succès",
|
||||
"uploadFailedDescription": "Veuillez réessayer.",
|
||||
@@ -964,6 +981,9 @@
|
||||
"by": "Par {{ name }}",
|
||||
"closeButton": "Fermer le sondage",
|
||||
"create": {
|
||||
"accessibilityLabel": {
|
||||
"send": "Envoyer le sondage"
|
||||
},
|
||||
"addOption": "Ajouter une option",
|
||||
"answerPlaceholder": "Option {{index}}",
|
||||
"cancel": "Annuler",
|
||||
@@ -972,8 +992,7 @@
|
||||
"pollQuestion": "Question du sondage",
|
||||
"questionPlaceholder": "Poser une question",
|
||||
"removeOption": "Supprimer l'option",
|
||||
"save": "Enregistrer",
|
||||
"send": "Envoyer"
|
||||
"save": "Enregistrer"
|
||||
},
|
||||
"errors": {
|
||||
"notUniqueOption": "Les options doivent être uniques"
|
||||
@@ -1299,6 +1318,7 @@
|
||||
"chat": "Afficher / Masquer la discussion instantanée",
|
||||
"clap": "Applaudir",
|
||||
"closeChat": "Fermer la discussion instantanée",
|
||||
"closeCustomPanel": "Fermer",
|
||||
"closeMoreActions": "Fermer le menu plus d'actions",
|
||||
"closeParticipantsPane": "Fermer le panneau des participants",
|
||||
"closedCaptions": "Sous-titres",
|
||||
@@ -1404,9 +1424,11 @@
|
||||
"chat": "Ouvrir / Fermer le chat",
|
||||
"clap": "Applaudir",
|
||||
"closeChat": "Fermer le chat",
|
||||
"closeCustomPanel": "Fermer",
|
||||
"closeParticipantsPane": "Fermer le panneau des participants",
|
||||
"closeReactionsMenu": "Fermer le menu réactions",
|
||||
"closedCaptions": "Sous-titres",
|
||||
"copilot": "Copilot",
|
||||
"disableNoiseSuppression": "Arrêter la suppression du bruit",
|
||||
"disableReactionSounds": "Vous pouvez interdire les réactions sonores à cette réunion",
|
||||
"documentClose": "Fermer le document partagé",
|
||||
@@ -1421,6 +1443,7 @@
|
||||
"exitFullScreen": "Quitter le mode plein écran",
|
||||
"exitTileView": "Quitter le mode mosaïque",
|
||||
"feedback": "Laisser des commentaires",
|
||||
"fileSharing": "Partage de fichiers",
|
||||
"giphy": "Activer/désactiver le menu GIPHY",
|
||||
"hangup": "Quitter",
|
||||
"help": "Aide",
|
||||
@@ -1456,6 +1479,7 @@
|
||||
"openReactionsMenu": "Ouvrir le menu Réactions",
|
||||
"participants": "Participants",
|
||||
"pip": "Entrer en mode Picture-in-Picture",
|
||||
"polls": "Sondages",
|
||||
"privateMessage": "Envoyer un message privé",
|
||||
"profile": "Éditer votre profil",
|
||||
"raiseHand": "Lever / Baisser la main",
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"addPeople": {
|
||||
"accessibilityLabel": {
|
||||
"meetingLink": "मीटिंग लिंक: {{url}}"
|
||||
},
|
||||
"add": "आमंत्रित करें",
|
||||
"addContacts": "संपर्क सूची से आमंत्रित करे",
|
||||
"contacts": "संपर्क",
|
||||
"copyInvite": "मीटिंग के आमंत्रण कि प्रतिलिपि बनाये",
|
||||
"copyLink": "मीटिंग कि लिंक कि प्रतिलिपि बनाये",
|
||||
"copyStream": "सीधे प्रसारण कि लिंक कि प्रीतिलिपि बनाये",
|
||||
"copyStream": "सीधे प्रसारण कि लिंक कि प्रतिलिपि बनाये",
|
||||
"countryNotSupported": "अभी हम इस गतव्य के लिये सक्षम नही है ।",
|
||||
"countryReminder": "यू.एस. के बाहर से काल कर रहे है तो कृपया सुनिश्चित करे कि अपने देश के कोड़ से प्रारंभ कर रहे है !",
|
||||
"defaultEmail": "अपना ई-मेल पता लिखें",
|
||||
@@ -35,6 +39,7 @@
|
||||
},
|
||||
"audioDevices": {
|
||||
"bluetooth": "ब्लूटूथ",
|
||||
"car": "कार ऑडियो",
|
||||
"headphones": "हेडफ़ोन",
|
||||
"none": "कोई ऑडियो डिवाइस उपलब्ध नहीं",
|
||||
"phone": "फ़ोन",
|
||||
@@ -43,9 +48,47 @@
|
||||
"audioOnly": {
|
||||
"audioOnly": "लो बैंडविड्थ"
|
||||
},
|
||||
"bandwidthSettings": {
|
||||
"assumedBandwidthBps": "उदाहरण: 10 Mbps के लिए 10000000",
|
||||
"assumedBandwidthBpsWarning": "अधिक मान नेटवर्क समस्याएँ पैदा कर सकते हैं।",
|
||||
"customValue": "कस्टम मान",
|
||||
"customValueEffect": "वास्तविक bps मान सेट करने के लिए",
|
||||
"leaveEmpty": "खाली छोड़ें",
|
||||
"leaveEmptyEffect": "अनुमान लगाने की अनुमति देने के लिए",
|
||||
"possibleValues": "संभावित मान",
|
||||
"setAssumedBandwidthBps": "अनुमानित बैंडविड्थ (bps)",
|
||||
"title": "बैंडविड्थ सेटिंग्स",
|
||||
"zeroEffect": "वीडियो अक्षम करने के लिए"
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"actions": {
|
||||
"add": "ब्रेकआउट रूम जोड़ें",
|
||||
"autoAssign": "ब्रेकआउट रूम में स्वतः असाइन करें",
|
||||
"close": "बंद करें",
|
||||
"join": "जॉइन करें",
|
||||
"leaveBreakoutRoom": "ब्रेकआउट रूम छोड़ें",
|
||||
"more": "और",
|
||||
"remove": "हटाएँ",
|
||||
"rename": "नाम बदलें",
|
||||
"renameBreakoutRoom": "ब्रेकआउट रूम का नाम बदलें",
|
||||
"sendToBreakoutRoom": "प्रतिभागी को भेजें:"
|
||||
},
|
||||
"breakoutList": "ब्रेकआउट सूची",
|
||||
"buttonLabel": "ब्रेकआउट रूम",
|
||||
"defaultName": "ब्रेकआउट रूम #{{index}}",
|
||||
"hideParticipantList": "प्रतिभागी सूची छिपाएँ",
|
||||
"mainRoom": "मुख्य रूम",
|
||||
"notifications": {
|
||||
"joined": "\"{{name}}\" ब्रेकआउट रूम में शामिल हो रहे हैं",
|
||||
"joinedMainRoom": "मुख्य रूम में शामिल हो रहे हैं",
|
||||
"joinedTitle": "ब्रेकआउट रूम"
|
||||
},
|
||||
"showParticipantList": "प्रतिभागी सूची दिखाएँ",
|
||||
"title": "ब्रेकआउट रूम"
|
||||
},
|
||||
"calendarSync": {
|
||||
"addMeetingURL": "एक मीटिंग लिंक जोड़ें",
|
||||
"confirmAddLink": "क्या आप इस इवेंट में एक Jitsi लिंक जोड़ना चाहते हैं?",
|
||||
"confirmAddLink": "क्या आप इस इवेंट में एक जित्सी लिंक जोड़ना चाहते हैं?",
|
||||
"error": {
|
||||
"appConfiguration": "कैलेंडर एकीकरण ठीक से कॉन्फ़िगर नहीं किया गया है।",
|
||||
"generic": "एक त्रुटि हुई है। कृपया अपनी कैलेंडर सेटिंग जांचें या कैलेंडर को रीफ़्रेश करने का प्रयास करें।",
|
||||
@@ -61,28 +104,72 @@
|
||||
"refresh": "कैलेंडर रीफ़्रेश करें",
|
||||
"today": "आज"
|
||||
},
|
||||
"carmode": {
|
||||
"actions": {
|
||||
"selectSoundDevice": "साउंड डिवाइस चुनें"
|
||||
},
|
||||
"labels": {
|
||||
"buttonLabel": "कार मोड",
|
||||
"title": "कार मोड",
|
||||
"videoStopped": "आपका वीडियो बंद है"
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "चैट संदेश भेजना अक्षम है।",
|
||||
"enter": "चैट रूम में प्रवेश करें",
|
||||
"error": "त्रुटि: आपका संदेश नहीं भेजा गया । कारण: {{error}}",
|
||||
"everyone": "सभी",
|
||||
"fieldPlaceHolder": "अपना संदेश यहां लिखें",
|
||||
"fileAccessibleTitle": "{{user}} ने एक फ़ाइल अपलोड की",
|
||||
"fileAccessibleTitleMe": "मैंने एक फ़ाइल अपलोड की",
|
||||
"fileDeleted": "एक फ़ाइल हटा दी गई",
|
||||
"guestsChatIndicator": "(अतिथि)",
|
||||
"messageTo": "{{recipient}} के लिए निजी संदेश",
|
||||
"messagebox": "एक संदेश टाइप करें",
|
||||
"newMessages": "नए संदेश",
|
||||
"nickname": {
|
||||
"featureChat": "चैट",
|
||||
"featureClosedCaptions": "बंद कैप्शन",
|
||||
"featureFileSharing": "फ़ाइल साझा करना",
|
||||
"featurePolls": "पोल",
|
||||
"popover": "एक उपनाम चुनें",
|
||||
"title": "चैट का उपयोग करने के लिए एक उपनाम दर्ज करें",
|
||||
"titleWithPolls": "चैट का उपयोग करने के लिए एक उपनाम दर्ज करें"
|
||||
"titleWith1Features": "{{feature1}} का उपयोग करने के लिए एक उपनाम दर्ज करें",
|
||||
"titleWith2Features": "{{feature1}} और {{feature2}} का उपयोग करने के लिए एक उपनाम दर्ज करें",
|
||||
"titleWith3Features": "{{feature1}}, {{feature2}} और {{feature3}} का उपयोग करने के लिए एक उपनाम दर्ज करें",
|
||||
"titleWith4Features": "{{feature1}}, {{feature2}}, {{feature3}} और {{feature4}} का उपयोग करने के लिए एक उपनाम दर्ज करें",
|
||||
"titleWithCC": "चैट और बंद कैप्शन का उपयोग करने के लिए एक उपनाम दर्ज करें",
|
||||
"titleWithPolls": "चैट और पोल का उपयोग करने के लिए एक उपनाम दर्ज करें",
|
||||
"titleWithPollsAndCC": "चैट, पोल और बंद कैप्शन का उपयोग करने के लिए एक उपनाम दर्ज करें",
|
||||
"titleWithPollsAndCCAndFileSharing": "चैट, पोल, बंद कैप्शन और फ़ाइलों का उपयोग करने के लिए एक उपनाम दर्ज करें"
|
||||
},
|
||||
"noMessagesMessage": "अभी तक मीटिंग में कोई संदेश नहीं आया है। वार्तालाप प्रारंभ करें!",
|
||||
"privateNotice": "{{recipient}} के लिए निजी संदेश",
|
||||
"sendButton": "भेजें",
|
||||
"smileysPanel": "इमोजी पैनल",
|
||||
"systemDisplayName": "सिस्टम",
|
||||
"tabs": {
|
||||
"chat": "चैट",
|
||||
"closedCaptions": "सीसी",
|
||||
"fileSharing": "फ़ाइलें",
|
||||
"polls": "पोल"
|
||||
},
|
||||
"title": "चैट",
|
||||
"titleWithPolls": "चैट",
|
||||
"titleWithCC": "सीसी",
|
||||
"titleWithFeatures": "चैट और",
|
||||
"titleWithFileSharing": "फ़ाइलें",
|
||||
"titleWithPolls": "पोल",
|
||||
"you": "आप"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"buttonText": "क्रोम एक्सटेंशन इंस्टॉल करें",
|
||||
"dontShowAgain": "मुझे यह फिर से न दिखाएं",
|
||||
"installExtensionText": ",गूगल कैलेंडर और ऑफिस 365 एकीकरण के लिए एक्सटेंशन इंस्टॉल करें"
|
||||
"buttonTextEdge": "Edge एक्सटेंशन इंस्टॉल करें",
|
||||
"close": "बंद करें",
|
||||
"dontShowAgain": "मुझे यह फिर से न दिखाएं"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
"emptyState": "मॉडरेटर द्वारा शुरू किए जाने पर बंद कैप्शन की सामग्री उपलब्ध होगी",
|
||||
"startClosedCaptionsButton": "बंद कैप्शन शुरू करें"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "आपको आपकी मीटिंग से कनेक्ट किया जा रहा है…"
|
||||
@@ -110,6 +197,7 @@
|
||||
"bridgeCount": "सर्वर गणना: ",
|
||||
"codecs": "कोडेक (ए/वी): ",
|
||||
"connectedTo": "से जुड़ा हुआ है :",
|
||||
"e2eeVerified": "E2EE सत्यापित:",
|
||||
"framerate": "फ्रेम दर:",
|
||||
"less": "कम दिखाएं",
|
||||
"localaddress": "स्थानीय पता:",
|
||||
@@ -136,7 +224,8 @@
|
||||
"status": "सम्पर्क:",
|
||||
"transport": "ट्रांसपोर्ट :",
|
||||
"transport_plural": "ट्रांसपोर्ट्स:",
|
||||
"video_ssrc": "वीडियो एस.आर.सी.सी.:"
|
||||
"video_ssrc": "वीडियो एस.आर.सी.सी.:",
|
||||
"yes": "हाँ"
|
||||
},
|
||||
"dateUtils": {
|
||||
"earlier": "पिछला कल",
|
||||
@@ -146,14 +235,25 @@
|
||||
"deepLinking": {
|
||||
"appNotInstalled": "आपको अपने फ़ोन पर इस मीटिंग में शामिल होने के लिए {{app}} मोबाइल ऐप की आवश्यकता है। ",
|
||||
"description": "हमने आपकी मीटिंग {{app}} डेस्कटॉप ऐप में लॉन्च करने की कोशिश की। कुछ नहीं हुआ? फिर से कोशिश करें या {{app}} वेब ऐप में लॉन्च करें।",
|
||||
"descriptionNew": "कुछ नहीं हुआ? हमने आपकी मीटिंग को {{app}} डेस्कटॉप ऐप में खोलने की कोशिश की। <br /><br /> आप दोबारा प्रयास कर सकते हैं या इसे वेब पर खोल सकते हैं।",
|
||||
"descriptionWithoutWeb": "हमने आपकी मीटिंग {{app}} डेस्कटॉप ऐप में लॉन्च करने की कोशिश की। कुछ नहीं हुआ?",
|
||||
"downloadApp": "एप्लिकेशन डाउनलोड करें",
|
||||
"downloadMobileApp": "App Store से डाउनलोड करें",
|
||||
"ifDoNotHaveApp": "यदि आपके पास अभी तक ऐप नहीं है:",
|
||||
"ifHaveApp": "यदि आपके पास पहले से ही ऐप है:",
|
||||
"joinInApp": "ऐप का उपयोग करके इस मीटिंग में शामिल हों",
|
||||
"joinInAppNew": "ऐप में जुड़ें",
|
||||
"joinInBrowser": "ब्राउज़र में जुड़ें",
|
||||
"launchMeetingLabel": "आप इस मीटिंग में कैसे जुड़ना चाहते हैं?",
|
||||
"launchWebButton": "वेब में लॉन्च करे",
|
||||
"noDesktopApp": "क्या आपके पास ऐप नहीं है?",
|
||||
"noMobileApp": "क्या आपके पास ऐप नहीं है?",
|
||||
"or": "या",
|
||||
"termsAndConditions": "जारी रखने पर आप हमारी <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>नियम और शर्तों</a> से सहमत होते हैं।",
|
||||
"title": "{{app}} में आपकी मीटिंग शुरू की जा रही हैं…",
|
||||
"tryAgainButton": "डेस्कटॉप में फिर से प्रयास करें"
|
||||
"titleNew": "आपकी मीटिंग शुरू की जा रही है…",
|
||||
"tryAgainButton": "डेस्कटॉप में फिर से प्रयास करें",
|
||||
"unsupportedBrowser": "ऐसा लगता है कि आप ऐसे ब्राउज़र का उपयोग कर रहे हैं जिसे हम सपोर्ट नहीं करते।"
|
||||
},
|
||||
"defaultLink": "उदाहरण {{url}}",
|
||||
"defaultNickname": "उदा. सतीष कुमार",
|
||||
@@ -164,6 +264,12 @@
|
||||
"microphonePermission": "माइक्रोफ़ोन अनुमति प्राप्त करने में त्रुटि"
|
||||
},
|
||||
"deviceSelection": {
|
||||
"hid": {
|
||||
"callControl": "कॉल नियंत्रण",
|
||||
"connectedDevices": "कनेक्टेड डिवाइस:",
|
||||
"deleteDevice": "डिवाइस हटाएँ",
|
||||
"pairDevice": "डिवाइस जोड़ें"
|
||||
},
|
||||
"noPermission": "अनुमति नहीं दी गई",
|
||||
"previewUnavailable": "पूर्वदर्शन अनुपलब्ध",
|
||||
"selectADevice": "डिवाइस का चयन करें",
|
||||
@@ -178,16 +284,23 @@
|
||||
"IamHost": "मैं मेजबान हूँ",
|
||||
"Ok": "ठीक है",
|
||||
"Remove": "निकालें",
|
||||
"Share": "Share",
|
||||
"Share": "साझा करें",
|
||||
"Submit": "सबमिट करें",
|
||||
"Understand": "मैं समझता/समझती हूँ, अभी मुझे म्यूट रखें",
|
||||
"UnderstandAndUnmute": "मैं समझता/समझती हूँ, कृपया मुझे अनम्यूट करें",
|
||||
"WaitForHostMsg": "सम्मेलन अभी तक शुरू नहीं हुआ है। यदि आप मेजबान हैं तो कृपया प्रमाणित करें। अन्यथा, कृपया मेजबान के आने की प्रतीक्षा करें।",
|
||||
"WaitingForHostButton": "मॉडरेटर की प्रतीक्षा करें",
|
||||
"WaitingForHostTitle": "होस्ट की प्रतीक्षा कर रहा है…",
|
||||
"Yes": "हाँ",
|
||||
"accessibilityLabel": {
|
||||
"liveStreaming": "सीधा प्रसारण"
|
||||
},
|
||||
"add": "जोड़ें",
|
||||
"addMeetingNote": "इस मीटिंग के बारे में एक नोट जोड़ें",
|
||||
"addOptionalNote": "एक नोट जोड़ें (वैकल्पिक):",
|
||||
"allow": "अनुमति दें",
|
||||
"allowToggleCameraDialog": "क्या आप {{initiatorName}} को आपके कैमरे का फ़ेसिंग मोड बदलने की अनुमति देते हैं?",
|
||||
"allowToggleCameraTitle": "कैमरा बदलने की अनुमति दें?",
|
||||
"alreadySharedVideoMsg": "एक अन्य प्रतिभागी पहले से ही वीडियो साझा कर रहा है। यह सम्मेलन एक समय में केवल एक साझा की अनुमति देता है।",
|
||||
"alreadySharedVideoTitle": "एक समय में केवल एक साझा वीडियो की अनुमति है",
|
||||
"applicationWindow": "एप्लिकेशन विंडो",
|
||||
@@ -212,7 +325,7 @@
|
||||
"connectErrorWithMsg": "उफ़! कुछ गड़बड़ हो गई और हम सम्मेलन से नहीं जुड़ सके: {{msg}}",
|
||||
"connecting": "संपर्क जोड़ा जा रहा है ",
|
||||
"contactSupport": "सहयोग के लिए संपर्क करें",
|
||||
"copied": "प्रतिलिपि बनाई गयी ",
|
||||
"copied": "प्रतिलिपि बनाई गयी",
|
||||
"copy": "प्रतिलिपि बनाये",
|
||||
"dismiss": "खारिज करें",
|
||||
"displayNameRequired": "नमस्ते! आपका नाम क्या है?",
|
||||
@@ -279,7 +392,7 @@
|
||||
"readMore": "अधिक",
|
||||
"recording": "रिकॉर्डिंग",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "संभव नहीं है जब एक लाइव स्ट्रीम सक्रिय है",
|
||||
"recordingDisabledTooltip": "रिकॉर्डिंग शुरू करना अक्षम करें.",
|
||||
"recordingDisabledTooltip": "रिकॉर्डिंग शुरू करना अक्षम करें|",
|
||||
"rejoinNow": "पुनः जुड़े",
|
||||
"remoteControlAllowedMessage": "{{user}} ने आपका रिमोट कंट्रोल अनुरोध स्वीकार कर लिया!",
|
||||
"remoteControlDeniedMessage": "{{user}} ने आपका रिमोट कंट्रोल अनुरोध अस्वीकार कर दिया!",
|
||||
@@ -295,7 +408,7 @@
|
||||
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
|
||||
"retry": "पुनः प्रयास करें",
|
||||
"screenSharingAudio": "ऑडियो साझा करें",
|
||||
"screenSharingFailed": "उफ़! कुछ गड़बड़ हो गई, हम स्क्रीन शेयरिंग शुरू करने में सक्षम नहीं थे!",
|
||||
"screenSharingFailed": "स्क्रीन शेयरिंग शुरू नहीं हो पाई।",
|
||||
"screenSharingFailedTitle": "स्क्रीन साझा करना विफल हुआ!",
|
||||
"screenSharingPermissionDeniedError": "उफ़! आपकी स्क्रीन शेयरिंग अनुमतियों में कुछ गड़बड़ हो गई है। कृपया पुनः लोड करें और पुनः प्रयास करें।",
|
||||
"sendPrivateMessage": "आपने हाल ही में एक निजी संदेश प्राप्त किया है। क्या आप उसका निजी रूप से जवाब देने का इरादा रखते हैं? या आप अपना संदेश समूह को भेजना चाहते हैं?",
|
||||
@@ -347,6 +460,34 @@
|
||||
"veryBad": "बहुत बुरा",
|
||||
"veryGood": "बहुत अच्छा"
|
||||
},
|
||||
"fileSharing": {
|
||||
"downloadFailedDescription": "कृपया फिर से प्रयास करें।",
|
||||
"downloadFailedTitle": "डाउनलोड विफल",
|
||||
"downloadFile": "डाउनलोड करें",
|
||||
"downloadStarted": "फ़ाइल डाउनलोड शुरू हो गया",
|
||||
"dragAndDrop": "फ़ाइलों को यहाँ या स्क्रीन पर कहीं भी खींचें और छोड़ें",
|
||||
"fileAlreadyUploaded": "फ़ाइल पहले ही इस मीटिंग में अपलोड हो चुकी है।",
|
||||
"fileRemovedByOther": "आपकी फ़ाइल '{{ fileName }}' हटा दी गई",
|
||||
"fileTooLargeDescription": "कृपया सुनिश्चित करें कि फ़ाइल {{ maxFileSize }} से अधिक नहीं है।",
|
||||
"fileTooLargeTitle": "चयनित फ़ाइल बहुत बड़ी है",
|
||||
"fileUploadProgress": "फ़ाइल अपलोड प्रगति",
|
||||
"fileUploadedSuccessfully": "फ़ाइल सफलतापूर्वक अपलोड हो गई",
|
||||
"newFileNotification": "{{ participantName }} ने '{{ fileName }}' साझा की",
|
||||
"removeFile": "हटाएँ",
|
||||
"removeFileSuccess": "फ़ाइल सफलतापूर्वक हटा दी गई",
|
||||
"uploadFailedDescription": "कृपया फिर से प्रयास करें।",
|
||||
"uploadFailedTitle": "अपलोड विफल",
|
||||
"uploadFile": "फ़ाइल साझा करें"
|
||||
},
|
||||
"filmstrip": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "वीडियो थंबनेल"
|
||||
}
|
||||
},
|
||||
"giphy": {
|
||||
"noResults": "कोई परिणाम नहीं मिला :(",
|
||||
"search": "GIPHY खोजें"
|
||||
},
|
||||
"helpView": {
|
||||
"title": "सहायता केंद्र"
|
||||
},
|
||||
@@ -362,6 +503,7 @@
|
||||
"addPassword": "$t(lockRoomPassword)जोड़ें",
|
||||
"cancelPassword": "$t(lockRoomPassword)रद्द करें",
|
||||
"conferenceURL": "लिंक:",
|
||||
"copyNumber": "नंबर कॉपी करें",
|
||||
"country": "देश",
|
||||
"dialANumber": "अपनी मीटिंग में शामिल होने के लिए, इनमें से किसी एक नंबर को डायल करें और फिर पिन डालें।",
|
||||
"dialInConferenceID": "पिन:",
|
||||
@@ -384,11 +526,16 @@
|
||||
"noRoom": "डायल-इन करने के लिए कोई कक्ष निर्दिष्ट नहीं किया गया।",
|
||||
"numbers": "डायल-इन नंबर",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"reachedLimit": "आप अपने प्लान की सीमा तक पहुँच चुके हैं।",
|
||||
"sip": "SIP पता",
|
||||
"sipAudioOnly": "केवल ऑडियो SIP पता",
|
||||
"title": "साझा करें",
|
||||
"tooltip": "इस मीटिंग के लिए लिंक और डायल-इन जानकारी साझा करें"
|
||||
"tooltip": "इस मीटिंग के लिए लिंक और डायल-इन जानकारी साझा करें",
|
||||
"upgradeOptions": "कृपया अपग्रेड विकल्पों की जाँच करें",
|
||||
"whiteboardError": "व्हाइटबोर्ड लोड करने में त्रुटि। कृपया बाद में पुनः प्रयास करें।"
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "We stumbled a bit.",
|
||||
"msg": "कुछ समस्या हुई।",
|
||||
"retry": "पुनः प्रयास करें",
|
||||
"support": "सहायता",
|
||||
"supportMsg": "ऐसा बार बार हो रहा हो, तो सम्पर्क करे "
|
||||
@@ -417,6 +564,10 @@
|
||||
"toggleShortcuts": "कीबोर्ड शॉर्टकट दिखाएं या छिपाएं",
|
||||
"videoMute": "अपना कैमरा प्रारंभ या बंद करें"
|
||||
},
|
||||
"largeVideo": {
|
||||
"screenIsShared": "आप अपनी स्क्रीन साझा कर रहे हैं",
|
||||
"showMeWhatImSharing": "मुझे दिखाएँ कि मैं क्या साझा कर रहा हूँ"
|
||||
},
|
||||
"liveStreaming": {
|
||||
"busy": "हम स्ट्रीमिंग संसाधनों को मुक्त करने पर काम कर रहे हैं। कृपया कुछ मिनटों में पुनः प्रयास करें।",
|
||||
"busyTitle": "सभी स्ट्रीमर वर्तमान में व्यस्त हैं",
|
||||
@@ -461,7 +612,7 @@
|
||||
"emailField": "अपना ईमेल पता दर्ज करें",
|
||||
"enableDialogPasswordField": "पासवर्ड सेट करें (वैकल्पिक)",
|
||||
"enableDialogSubmit": "सक्षम करें",
|
||||
"enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approval by a moderator.",
|
||||
"enableDialogText": "लॉबी मोड से आप अपनी मीटिंग को सुरक्षित रख सकते हैं। केवल मॉडरेटर की अनुमति मिलने के बाद ही लोग इसमें शामिल हो पाएंगे।",
|
||||
"enterPasswordButton": "मीटिंग पासवर्ड दर्ज करें",
|
||||
"enterPasswordTitle": "मीटिंग में शामिल होने के लिए पासवर्ड दर्ज करें",
|
||||
"invalidPassword": "अमान्य पासवर्ड",
|
||||
@@ -475,8 +626,8 @@
|
||||
"knockTitle": "कोई व्यक्ति बैठक में शामिल होना चाहता है",
|
||||
"knockingParticipantList": "प्रतिभागी सूची दस्तक",
|
||||
"nameField": "अपना नाम दर्ज करें",
|
||||
"notificationLobbyAccessDenied": "{{targetParticipantName}} has been rejected to join by {{originParticipantName}}",
|
||||
"notificationLobbyAccessGranted": "{{targetParticipantName}} has been allowed to join by {{originParticipantName}}",
|
||||
"notificationLobbyAccessDenied": "{{targetParticipantName}} को {{originParticipantName}} ने मीटिंग में शामिल होने की अनुमति नहीं दी।",
|
||||
"notificationLobbyAccessGranted": "{{targetParticipantName}} को {{originParticipantName}} ने मीटिंग में शामिल होने की अनुमति दी।",
|
||||
"notificationLobbyDisabled": "लॉबी को {{originParticipantName}}द्वारा अक्षम कर दिया गया",
|
||||
"notificationLobbyEnabled": "लॉबी को {{originParticipantName}}द्वारा सक्षम किया गया",
|
||||
"notificationTitle": "लॉबी",
|
||||
@@ -509,9 +660,12 @@
|
||||
"no": "नहीं",
|
||||
"participant": "प्रतिभागी",
|
||||
"participantStats": "प्रतिभागी आँकड़े",
|
||||
"selectTabTitle": "🎥 रिकॉर्डिंग के लिए कृपया इस टैब को चुनें",
|
||||
"sessionToken": "सत्र टोकन",
|
||||
"start": "रिकॉर्डिंग प्रारंभ करें",
|
||||
"stop": "रिकॉर्डिंग बंद करें",
|
||||
"stopping": "रिकॉर्डिंग बंद की जा रही है",
|
||||
"wait": "कृपया प्रतीक्षा करें, आपकी रिकॉर्डिंग सेव की जा रही है",
|
||||
"yes": "हाँ"
|
||||
},
|
||||
"lockRoomPassword": "पासवर्ड",
|
||||
@@ -522,11 +676,30 @@
|
||||
},
|
||||
"me": "मैं",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "Security vulnerability!",
|
||||
"OldElectronAPPTitle": "सुरक्षा में खामी",
|
||||
"allowAll": "सभी की अनुमति दें",
|
||||
"allowAudio": "ऑडियो की अनुमति दें",
|
||||
"allowDesktop": "स्क्रीन शेयरिंग की अनुमति दें",
|
||||
"allowVideo": "वीडियो की अनुमति दें",
|
||||
"allowedUnmute": "आप माइक्रोफोन अनम्यूट कर सकते हैं, कैमरा चालू कर सकते हैं या स्क्रीन साझा कर सकते हैं।",
|
||||
"audioUnmuteBlockedDescription": "सिस्टम सीमाओं के कारण माइक्रोफोन अनम्यूट अस्थायी रूप से ब्लॉक कर दिया गया है।",
|
||||
"audioUnmuteBlockedTitle": "माइक्रोफोन अनम्यूट ब्लॉक!",
|
||||
"chatMessages": "चैट संदेश",
|
||||
"connectedOneMember": "{{name}} मीटिंग में शामिल हुए",
|
||||
"connectedThreePlusMembers": "{{name}} और {{count}} अन्य लोग मीटिंग में शामिल हुए",
|
||||
"connectedTwoMembers": "{{first}} और {{second}} मीटिंग में शामिल हुआ",
|
||||
"connectionFailed": "कनेक्शन विफल। कृपया बाद में पुनः प्रयास करें!",
|
||||
"dataChannelClosed": "वीडियो की गुणवत्ता प्रभावित हो सकती है",
|
||||
"dataChannelClosedDescription": "ब्रिज चैनल बंद है, इसलिए वीडियो की गुणवत्ता न्यूनतम स्तर तक सीमित हो सकती है।",
|
||||
"dataChannelClosedDescriptionWithAudio": "ब्रिज चैनल बंद है, इसलिए ऑडियो और वीडियो में व्यवधान हो सकता है।",
|
||||
"dataChannelClosedWithAudio": "ऑडियो और वीडियो की गुणवत्ता प्रभावित हो सकती है",
|
||||
"desktopMutedRemotelyTitle": "आपकी स्क्रीन शेयरिंग {{participantDisplayName}} द्वारा रोकी गई है",
|
||||
"disabledIframe": "एंबेडिंग केवल डेमो उद्देश्यों के लिए है, इसलिए यह कॉल {{timeout}} मिनट में डिस्कनेक्ट हो जाएगी।",
|
||||
"disabledIframeSecondaryNative": "एंबेडिंग {{domain}} केवल डेमो उद्देश्यों के लिए है, इसलिए यह कॉल {{timeout}} मिनट में डिस्कनेक्ट हो जाएगी।",
|
||||
"disabledIframeSecondaryWeb": "एंबेडिंग {{domain}} केवल डेमो उद्देश्यों के लिए है, इसलिए यह कॉल {{timeout}} मिनट में डिस्कनेक्ट हो जाएगी। उत्पादन एंबेडिंग के लिए कृपया <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> का उपयोग करें!",
|
||||
"disconnected": "डिस्कनेक्ट",
|
||||
"displayNotifications": "सूचनाएँ दिखाएँ",
|
||||
"dontRemindMe": "मुझे याद न दिलाएँ",
|
||||
"focus": "Conference focus",
|
||||
"focusFail": "{{component}} उपलब्ध नहीं - {{ms}} सेकंड में पुनः प्रयास करें",
|
||||
"grantedTo": "Moderator rights granted to {{to}}!",
|
||||
@@ -534,7 +707,29 @@
|
||||
"invitedThreePlusMembers": "{{name}} और {{count}} अन्य लोगों को आमंत्रित किया गया",
|
||||
"invitedTwoMembers": "{{first}} और {{second}} को आमंत्रित किया गया",
|
||||
"kickParticipant": "{{kicked}} को {{kicker}} द्वारा किक किया गया",
|
||||
"leftOneMember": "{{name}} ने मीटिंग छोड़ दी",
|
||||
"leftThreePlusMembers": "{{name}} और कई अन्य ने मीटिंग छोड़ दी",
|
||||
"leftTwoMembers": "{{first}} और {{second}} ने मीटिंग छोड़ दी",
|
||||
"linkToSalesforce": "Salesforce से लिंक करें",
|
||||
"linkToSalesforceDescription": "आप मीटिंग सारांश को Salesforce ऑब्जेक्ट से लिंक कर सकते हैं।",
|
||||
"linkToSalesforceError": "मीटिंग को Salesforce से लिंक करने में विफल",
|
||||
"linkToSalesforceKey": "इस मीटिंग को लिंक करें",
|
||||
"linkToSalesforceProgress": "मीटिंग को Salesforce से लिंक किया जा रहा है…",
|
||||
"linkToSalesforceSuccess": "मीटिंग सफलतापूर्वक Salesforce से लिंक हो गई",
|
||||
"localRecordingStarted": "{{name}} ने लोकल रिकॉर्डिंग शुरू की।",
|
||||
"localRecordingStopped": "{{name}} ने लोकल रिकॉर्डिंग बंद की।",
|
||||
"me": "मैं",
|
||||
"moderationInEffectCSDescription": "यदि आप अपनी स्क्रीन साझा करना चाहते हैं तो कृपया हाथ उठाएँ।",
|
||||
"moderationInEffectCSTitle": "स्क्रीन शेयरिंग मॉडरेटर द्वारा ब्लॉक की गई है",
|
||||
"moderationInEffectDescription": "यदि आप बोलना चाहते हैं तो कृपया हाथ उठाएँ।",
|
||||
"moderationInEffectTitle": "आपका माइक्रोफोन मॉडरेटर द्वारा म्यूट किया गया है",
|
||||
"moderationInEffectVideoDescription": "यदि आप अपना कैमरा चालू करना चाहते हैं तो कृपया हाथ उठाएँ।",
|
||||
"moderationInEffectVideoTitle": "आपका कैमरा मॉडरेटर द्वारा ब्लॉक किया गया है",
|
||||
"moderationRequestFromModerator": "होस्ट चाहता है कि आप अनम्यूट करें",
|
||||
"moderationRequestFromParticipant": "बोलना चाहता है",
|
||||
"moderationStartedTitle": "मॉडरेशन शुरू हो गई",
|
||||
"moderationStoppedTitle": "मॉडरेशन बंद हो गई",
|
||||
"moderationToggleDescription": "{{participantDisplayName}} द्वारा",
|
||||
"moderator": "मॉडरेटर के अधिकार दिए गए!",
|
||||
"muted": "आपने वार्तालाप को म्यूट करके शुरू किया है।",
|
||||
"mutedRemotelyDescription": "जब आप बोलने के लिए तैयार हों, तो आप हमेशा अनम्यूट कर सकते हैं। बैठक में शोर कम रखने के लिए बोलने के बाद म्यूट कर दें।",
|
||||
@@ -543,24 +738,85 @@
|
||||
"newDeviceAction": "उपयोग करें",
|
||||
"newDeviceAudioTitle": "नए ऑडियो डिवाइस का पता चला",
|
||||
"newDeviceCameraTitle": "नए कैमरे का पता चला",
|
||||
"nextToSpeak": "आप अगली बारी में बोलने वाले हैं",
|
||||
"noiseSuppressionDesktopAudioDescription": "डेस्कटॉप ऑडियो साझा करते समय अतिरिक्त नॉइज़ सप्रेशन सक्षम नहीं किया जा सकता, कृपया इसे अक्षम करें और पुनः प्रयास करें।",
|
||||
"noiseSuppressionFailedTitle": "अतिरिक्त नॉइज़ सप्रेशन शुरू करने में विफल",
|
||||
"noiseSuppressionStereoDescription": "स्टीरियो ऑडियो के साथ अतिरिक्त नॉइज़ सप्रेशन वर्तमान में समर्थित नहीं है।",
|
||||
"oldElectronClientDescription1": "आप जित्सी मीट क्लाइंट के एक पुराने संस्करण का उपयोग करते हुए दिखाई देते हैं, जिसमे सुरक्षा कमजोरियां ज्ञात हैं।",
|
||||
"oldElectronClientDescription2": "नवीनतम बिल्ड",
|
||||
"oldElectronClientDescription3": " अब!",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) किसी अन्य प्रतिभागी द्वारा हटा दिया गया",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) दूसरे प्रतिभागी द्वारा निर्धारित",
|
||||
"raisedHand": "{{name}} बोलना चाहेंगे।",
|
||||
"raisedHands": "{{participantName}} और {{raisedHands}} अन्य लोग",
|
||||
"reactionSounds": "ध्वनि बंद करें",
|
||||
"reactionSoundsForAll": "सभी के लिए ध्वनि बंद करें",
|
||||
"screenShareNoAudio": "विंडो चयन स्क्रीन में 'ऑडियो साझा करें' बॉक्स चयनित नहीं था।",
|
||||
"screenShareNoAudioTitle": "सिस्टम ऑडियो साझा नहीं किया जा सका!",
|
||||
"screenSharingAudioOnlyDescription": "कृपया ध्यान दें कि अपनी स्क्रीन साझा करने से \"सर्वोत्तम प्रदर्शन\" मोड प्रभावित होगा और आप अधिक बैंडविड्थ का उपयोग करेंगे।",
|
||||
"screenSharingAudioOnlyTitle": "\"सर्वोत्तम प्रदर्शन\" मोड",
|
||||
"selfViewTitle": "आप सेटिंग्स से हमेशा सेल्फ-व्यू को अन-हाइड कर",
|
||||
"somebody": "Somebody",
|
||||
"startSilentDescription": "ऑडियो सक्षम करने के लिए मीटिंग को फिर से करें",
|
||||
"startSilentTitle": "आप बिना ऑडियो आउटपुट के साथ शामिल हुए!",
|
||||
"suboptimalBrowserWarning": "हमें डर है कि आपकी मीटिंग अनुभव यहाँ बहुत अच्छा नहीं होने वाला है। हम इसे सुधारने के तरीके ढूंढ़ रहे हैं, लेकिन उस समय तक कृपया <a href='{{recommendedBrowserPageLink}}' target='_blank'>पूरी तरह से समर्थित ब्राउज़र</a> में से एक का प्रयास करें",
|
||||
"suboptimalExperienceTitle": "ब्राउज़र चेतावनी",
|
||||
"suggestRecordingAction": "शुरू करें",
|
||||
"suggestRecordingDescription": "क्या आप रिकॉर्डिंग शुरू करना चाहेंगे?",
|
||||
"suggestRecordingTitle": "इस मीटिंग को रिकॉर्ड करें",
|
||||
"unmute": "अनम्यूट",
|
||||
"videoMutedRemotelyDescription": "आप इसे हमेशा फिर से चालू कर सकते हैं।",
|
||||
"videoMutedRemotelyTitle": "आपका कैमरा {{participantDisplayName}}द्वारा अक्षम कर दिया गया है!"
|
||||
"videoMutedRemotelyTitle": "आपका कैमरा {{participantDisplayName}}द्वारा अक्षम कर दिया गया है!",
|
||||
"videoUnmuteBlockedDescription": "सिस्टम सीमाओं के कारण कैमरा अनम्यूट और डेस्कटॉप शेयरिंग अस्थायी रूप से ब्लॉक कर दी गई है।",
|
||||
"videoUnmuteBlockedTitle": "कैमरा अनम्यूट और डेस्कटॉप शेयरिंग ब्लॉक!",
|
||||
"viewParticipants": "प्रतिभागियों को देखें",
|
||||
"viewVisitors": "दर्शकों को देखें",
|
||||
"waitingParticipants": "{{waitingParticipants}} लोग",
|
||||
"waitingVisitors": "कतार में प्रतीक्षारत दर्शक: {{waitingVisitors}}",
|
||||
"waitingVisitorsTitle": "मीटिंग अभी लाइव नहीं है!",
|
||||
"whiteboardLimitDescription": "कृपया अपनी प्रगति सहेजें, क्योंकि उपयोगकर्ता सीमा जल्द ही पहुँच जाएगी और व्हाइटबोर्ड बंद हो जाएगा।",
|
||||
"whiteboardLimitTitle": "व्हाइटबोर्ड उपयोग"
|
||||
},
|
||||
"participantsPane": {
|
||||
"actions": {
|
||||
"reject": "अस्वीकार"
|
||||
"admit": "स्वीकृत करें",
|
||||
"admitAll": "सभी को स्वीकृत करें",
|
||||
"allow": "नॉन-मॉडरेटर को अनुमति दें:",
|
||||
"allowDesktop": "स्क्रीन शेयरिंग की अनुमति दें",
|
||||
"allowVideo": "वीडियो की अनुमति दें",
|
||||
"askDesktop": "स्क्रीन साझा करने के लिए पूछें",
|
||||
"askUnmute": "अनम्यूट करने के लिए पूछें",
|
||||
"audioModeration": "स्वयं अनम्यूट करें",
|
||||
"blockEveryoneMicCamera": "सभी के माइक्रोफोन और कैमरा ब्लॉक करें",
|
||||
"breakoutRooms": "ब्रेकआउट रूम",
|
||||
"desktopModeration": "स्क्रीन शेयरिंग शुरू करें",
|
||||
"goLive": "लाइव जाएँ",
|
||||
"invite": "किसी को आमंत्रित करें",
|
||||
"lowerAllHands": "सभी हाथ नीचे करें",
|
||||
"lowerHand": "हाथ नीचे करें",
|
||||
"moreModerationActions": "अधिक मॉडरेशन विकल्प",
|
||||
"moreModerationControls": "अधिक मॉडरेशन नियंत्रण",
|
||||
"moreParticipantOptions": "अधिक प्रतिभागी विकल्प",
|
||||
"mute": "म्यूट करें",
|
||||
"muteAll": "सभी को म्यूट करें",
|
||||
"muteEveryoneElse": "बाकी सभी को म्यूट करें",
|
||||
"reject": "अस्वीकार",
|
||||
"stopDesktop": "स्क्रीन शेयरिंग बंद करें",
|
||||
"stopEveryonesDesktop": "सभी की स्क्रीन शेयरिंग बंद करें",
|
||||
"stopEveryonesVideo": "सभी का वीडियो बंद करें",
|
||||
"stopVideo": "वीडियो बंद करें",
|
||||
"unblockEveryoneMicCamera": "सभी के माइक्रोफोन और कैमरा अनब्लॉक करें",
|
||||
"videoModeration": "उनका वीडियो शुरू करें"
|
||||
},
|
||||
"headings": {
|
||||
"lobby": "लॉबी ({{count}})",
|
||||
"participantsList": "मीटिंग प्रतिभागी ({{count}})",
|
||||
"viewerRequests": "दर्शकों के अनुरोध {{count}}",
|
||||
"visitorInQueue": " (प्रतीक्षा में {{count}})",
|
||||
"visitorRequests": " (अनुरोध {{count}})",
|
||||
"visitors": "दर्शक {{count}}",
|
||||
"visitorsList": "दर्शक ({{count}})",
|
||||
"waitingLobby": "लॉबी में प्रतीक्षा कर रहे हैं ({{count}})"
|
||||
}
|
||||
},
|
||||
"passwordDigitsOnly": "अधिकतम {{number}} अंक",
|
||||
@@ -617,6 +873,7 @@
|
||||
"joinAudioByPhone": "फोन ऑडियो के साथ जुड़ें",
|
||||
"joinMeeting": "मीटिंग में शामिल हों",
|
||||
"joinWithoutAudio": "ऑडियो के बिना जुड़ें",
|
||||
"keyboardShortcuts": "कीबोर्ड शॉर्टकट सक्षम करें",
|
||||
"linkCopied": "लिंक क्लिपबोर्ड पर कॉपी किया गया",
|
||||
"lookGood": "ऐसा लगता है कि आपका माइक्रोफ़ोन ठीक से काम कर रहा है",
|
||||
"or": "या",
|
||||
@@ -642,9 +899,10 @@
|
||||
"ringing": "Ringing…"
|
||||
},
|
||||
"profile": {
|
||||
"avatar": "अवतार",
|
||||
"setDisplayNameLabel": "अपना नाम सेट करें",
|
||||
"setEmailInput": "ई-मेल दर्ज करें",
|
||||
"setEmailLabel": "Set अपना ग्रेवार्ट ईमेल सेट करें",
|
||||
"setEmailLabel": "Gravatar ईमेल",
|
||||
"title": "प्रोफ़ाइल"
|
||||
},
|
||||
"raisedHand": "बोलना चाहेंगे",
|
||||
@@ -660,6 +918,12 @@
|
||||
"expandedPending": "रिकॉर्डिंग शुरू की जा रही है…",
|
||||
"failedToStart": "रिकॉर्डिंग शुरू करने में विफलता हुई।",
|
||||
"fileSharingdescription": "रिकॉर्डिंग को बैठक प्रतिभागियों के साथ साझा करें",
|
||||
"highlight": "हाइलाइट",
|
||||
"highlightMoment": "महत्वपूर्ण क्षण",
|
||||
"highlightMomentDisabled": "रिकॉर्डिंग शुरू होने पर आप क्षणों को हाइलाइट कर सकते हैं",
|
||||
"highlightMomentSuccess": "क्षण हाइलाइट किया गया",
|
||||
"highlightMomentSucessDescription": "आपका हाइलाइट किया गया क्षण मीटिंग सारांश में जोड़ा जाएगा।",
|
||||
"inProgress": "रिकॉर्डिंग या लाइव स्ट्रीमिंग प्रगति में है",
|
||||
"limitNotificationDescriptionNative": "उच्च मांग के कारण आपकी रिकॉर्डिंग {{limit}} मिनट तक सीमित रहेगी। असीमित रिकॉर्डिंग के लिए <3>{{app}}</3> आज़माएँ।",
|
||||
"limitNotificationDescriptionWeb": "उच्च मांग के कारण आपकी रिकॉर्डिंग {{limit}} मिनट तक सीमित रहेगी। असीमित रिकॉर्डिंग के लिए <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a> आज़माएँ।",
|
||||
"live": "लाइव",
|
||||
@@ -677,18 +941,26 @@
|
||||
"signOut": "साइन आउट करें",
|
||||
"title": "रिकॉर्डिंग",
|
||||
"unavailable": "ओह! {{serviceName}} वर्तमान में अनुपलब्ध है। हम इस समस्या को हल करने पर काम कर रहे हैं। कृपया बाद में पुनः प्रयास करें।",
|
||||
"unavailableTitle": "रिकॉर्डिंग उपलब्ध नहीं है"
|
||||
"unavailableTitle": "रिकॉर्डिंग उपलब्ध नहीं है",
|
||||
"uploadToCloud": "क्लाउड पर अपलोड करें"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "रीफ़्रेश करने के लिए नीचे खींचें"
|
||||
},
|
||||
"security": {
|
||||
"about": "आप अपनी मीटिंग में $t(lockRoomPassword) जोड़ सकते हैं। सहभागियों को मीटिंग में शामिल होने से पहले $t(lockRoomPassword) प्रदान करना होगा।",
|
||||
"aboutReadOnly": "Moderator participants can add a $t(lockRoomPassword) to the meeting. Participants will need to provide the $t(lockRoomPassword) before they are allowed to join the meeting.",
|
||||
"aboutReadOnly": "मॉडरेटर मीटिंग में $t(lockRoomPassword) जोड़ सकते हैं। प्रतिभागियों को मीटिंग में शामिल होने से पहले यह $t(lockRoomPassword) प्रदान करना होगा।",
|
||||
"insecureRoomNameWarning": "कमरे का नाम असुरक्षित है। अनचाहे सहभागियों की कॉन्फ्रेंस में शामिल हो सकते हैं। सुरक्षा बटन का उपयोग करके अपनी मीटिंग को सुरक्षित बनाने का विचार करें। ",
|
||||
"securityOptions": "Security options"
|
||||
"title": "सुरक्षा विकल्प",
|
||||
"unsafeRoomActions": {
|
||||
"meeting": "सुरक्षा बटन का उपयोग करके अपनी मीटिंग को सुरक्षित करने पर विचार करें।",
|
||||
"prejoin": "कृपया अधिक विशिष्ट मीटिंग नाम का उपयोग करने पर विचार करें।",
|
||||
"welcome": "कृपया अधिक विशिष्ट मीटिंग नाम का उपयोग करें या दिए गए सुझावों में से किसी एक को चुनें।"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"audio": "ऑडियो",
|
||||
"buttonLabel": "सेटिंग्स",
|
||||
"calendar": {
|
||||
"about": "{{appName}} कैलेंडर एकीकरण आपके कैलेंडर तक सुरक्षित रूप से पहुंचने के लिए उपयोग किया जाता है ताकि यह आगामी कार्यक्रम पढ़ सके।",
|
||||
"disconnect": "डिस्कनेक्ट करें",
|
||||
@@ -698,23 +970,35 @@
|
||||
},
|
||||
"devices": "डिवाइस",
|
||||
"followMe": "हर कोई मेरा अनुसरण करेगा",
|
||||
"incomingMessage": "आने वाला संदेश",
|
||||
"language": "भाषा",
|
||||
"loggedIn": "{{name}} के रूप में लॉग इन किया",
|
||||
"maxStageParticipants": "मुख्य स्टेज पर पिन किए जा सकने वाले प्रतिभागियों की अधिकतम संख्या",
|
||||
"microphones": "माइक्रोफोन",
|
||||
"moderator": "Moderator",
|
||||
"more": "More",
|
||||
"moderator": "होस्ट",
|
||||
"more": "अधिक",
|
||||
"name": "नाम",
|
||||
"noDevice": "कोई नहीं",
|
||||
"notifications": "सूचना",
|
||||
"participantJoined": "प्रतिभागी जुड़े",
|
||||
"participantKnocking": "प्रतिभागी लॉबी में आए",
|
||||
"participantLeft": "प्रतिभागी बाहर गए",
|
||||
"playSounds": "ध्वनि चलाएँ",
|
||||
"reactions": "मीटिंग प्रतिक्रियाएँ",
|
||||
"sameAsSystem": "सिस्टम के समान ({{label}})",
|
||||
"selectAudioOutput": "ऑडियो आउटपुट",
|
||||
"selectCamera": "कैमरा",
|
||||
"selectMic": "माइक्रोफोन",
|
||||
"speakers": "Speakers",
|
||||
"shortcuts": "शॉर्टकट्स",
|
||||
"speakers": "वक्ता",
|
||||
"startAudioMuted": "सभी लोग म्यूट से शुरू करेंगे",
|
||||
"startVideoMuted": "सभी लोग छिपे हुए शुरू करेंगे",
|
||||
"title": "सेटिंग"
|
||||
"talkWhileMuted": "म्यूट रहते हुए बोलें",
|
||||
"title": "सेटिंग",
|
||||
"video": "वीडियो"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "Advanced",
|
||||
"advanced": "उन्नत",
|
||||
"alertCancel": "रद्द करें",
|
||||
"alertOk": "ओके",
|
||||
"alertTitle": "चेतावनी",
|
||||
@@ -830,14 +1114,14 @@
|
||||
"exitTileView": "टाइल दृश्य से बाहर निकलें",
|
||||
"feedback": "प्रतिक्रिया छोड़ें",
|
||||
"hangup": "छोड़ें",
|
||||
"help": "Help",
|
||||
"help": "सहायता",
|
||||
"invite": "लोगों को आमंत्रित करें",
|
||||
"lobbyButtonDisable": "लॉबी मोड को अक्षम करें",
|
||||
"lobbyButtonEnable": "लॉबी मोड सक्षम करें",
|
||||
"login": "लॉग इन",
|
||||
"logout": "लॉगआउट",
|
||||
"lowerYourHand": "अपना हाथ नीचे करें",
|
||||
"moreActions": "More actions",
|
||||
"moreActions": "अधिक कार्रवाइयाँ",
|
||||
"moreOptions": "अधिक विकल्प",
|
||||
"mute": "म्यूट / अनम्यूट",
|
||||
"muteEveryone": "सभी को म्यूट करें",
|
||||
@@ -858,19 +1142,21 @@
|
||||
"security": "सुरक्षा विकल्प",
|
||||
"selectBackground": "पृष्ठभूमि का चयन करें",
|
||||
"shareRoom": "किसी को आमंत्रित करें",
|
||||
"sharedvideo": "एक YouTube वीडियो साझा करें",
|
||||
"shareaudio": "ऑडियो साझा करें",
|
||||
"sharedvideo": "एक वीडियो साझा करें",
|
||||
"shortcuts": "शॉर्टकट देखें",
|
||||
"speakerStats": "स्पीकर आँकड़े",
|
||||
"startScreenSharing": "स्क्रीन साझाकरण प्रारंभ करें",
|
||||
"startSubtitles": "Start subtitles",
|
||||
"startSubtitles": "सबटाइटल शुरू करें",
|
||||
"stopScreenSharing": "स्क्रीन शेयरिंग बंद करो",
|
||||
"stopSharedVideo": "YouTube वीडियो बंद करें",
|
||||
"stopSharedVideo": "वीडियो बंद करें",
|
||||
"stopSubtitles": "उपशीर्षक बंद करें",
|
||||
"talkWhileMutedPopup": "बोलने की कोशिश कर रहा है? आप मौन हैं",
|
||||
"tileViewToggle": "टॉगल टाइल दृश्य",
|
||||
"toggleCamera": "कैमरा टॉगल करें",
|
||||
"videoSettings": "वीडियो सेटिंग्स",
|
||||
"videomute": "स्टार्ट / स्टॉप कैमरा"
|
||||
"videomute": "स्टॉप कैमरा",
|
||||
"videounmute": "स्टार्ट कैमरा"
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "सबटाइटल शुरू / बंद करें",
|
||||
@@ -919,10 +1205,11 @@
|
||||
"domuteOthers": "सभी को म्यूट करें",
|
||||
"domuteVideo": "कैमरा अक्षम करें",
|
||||
"domuteVideoOfOthers": "अन्य सभी के लिए कैमरा बंद करें",
|
||||
"flip": "Flip",
|
||||
"grantModerator": "Grant Moderator",
|
||||
"flip": "उलटना",
|
||||
"grantModerator": "संचालक बनाएं",
|
||||
"hideSelfView": "स्वयं का दृश्य छिपाएँ",
|
||||
"kick": "निकालें",
|
||||
"moderator": "Moderator",
|
||||
"moderator": "संचालक",
|
||||
"mute": "प्रतिभागी मौन है",
|
||||
"muted": "म्यूटेड",
|
||||
"remoteControl": "स्टार्ट / स्टॉप रिमोट कंट्रोल",
|
||||
@@ -958,8 +1245,20 @@
|
||||
"headerSubtitle": "सुरक्षित और उच्च गुणवत्ता बैठकें",
|
||||
"headerTitle": "जित्सी मीट",
|
||||
"info": "डायल-इन जानकारी",
|
||||
"jitsiOnMobile": "मोबाइल पर Jitsi – हमारे एप्लिकेशन डाउनलोड करें और कहीं से भी एक बैठक शुरू करें",
|
||||
"jitsiOnMobile": "मोबाइल पर जित्सी – हमारे एप्लिकेशन डाउनलोड करें और कहीं से भी एक बैठक शुरू करें",
|
||||
"join": "बनाये / जुड़े ",
|
||||
"logo": {
|
||||
"calendar": "कैलेंडर लोगो",
|
||||
"desktopPreviewThumbnail": "डेस्कटॉप प्रीव्यू थंबनेल",
|
||||
"googleLogo": "गूगल लोगो",
|
||||
"logoDeepLinking": "जित्सी मीट लोगो",
|
||||
"microsoftLogo": "माइक्रोसॉफ्ट लोगो",
|
||||
"policyLogo": "नीति लोगो"
|
||||
},
|
||||
"meetingsAccessibilityLabel": "मीटिंग्स",
|
||||
"mobileDownLoadLinkAndroid": "एंड्रॉइड के लिए मोबाइल ऐप डाउनलोड करें",
|
||||
"mobileDownLoadLinkFDroid": "F-Droid के लिए मोबाइल ऐप डाउनलोड करें",
|
||||
"mobileDownLoadLinkIos": "iOS के लिए मोबाइल ऐप डाउनलोड करें",
|
||||
"moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.",
|
||||
"privacy": "गोपनीयता",
|
||||
"recentList": "हाल का",
|
||||
@@ -972,6 +1271,13 @@
|
||||
"sendFeedback": "फ़ीडबैक भेजें",
|
||||
"startMeeting": "मीटिंग प्रारंभ करें",
|
||||
"terms": "शर्तें",
|
||||
"title": "सुरक्षित, पूरी तरह से चित्रित, और पूरी तरह से मुक्त वीडियो कॉन्फ्रेंसिंग"
|
||||
"title": "सुरक्षित, पूरी तरह से चित्रित, और पूरी तरह से मुक्त वीडियो कॉन्फ्रेंसिंग",
|
||||
"upcomingMeetings": "आपकी आगामी मीटिंग्स"
|
||||
},
|
||||
"whiteboard": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "व्हाइटबोर्ड"
|
||||
},
|
||||
"screenTitle": "व्हाइटबोर्ड"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"addPeople": {
|
||||
"accessibilityLabel": {
|
||||
"meetingLink": "Meeting link: {{url}}"
|
||||
"meetingLink": "Meeting-link: {{url}}"
|
||||
},
|
||||
"add": "Uitnodigen",
|
||||
"addContacts": "Nodig uw contacten uit",
|
||||
@@ -375,7 +375,7 @@
|
||||
"kickTitle": "Oei! {{participantDisplayName}} heeft u uit de vergadering verwijderd",
|
||||
"learnMore": "meer informatie",
|
||||
"linkMeeting": "Vergadering koppelen",
|
||||
"linkMeetingTitle": "Vergadering koppelen aan Verkoop",
|
||||
"linkMeetingTitle": "Vergadering koppelen aan Salesforce",
|
||||
"liveStreaming": "Livestreamen",
|
||||
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Niet mogelijk tijdens opnemen",
|
||||
"localUserControls": "Lokale gebruikerscontrole",
|
||||
@@ -468,7 +468,7 @@
|
||||
"screenSharingFailed": "Oeps! Er is iets misgegaan, de schermdeling kon niet worden gestart!",
|
||||
"screenSharingFailedTitle": "Schermdeling mislukt!",
|
||||
"screenSharingPermissionDeniedError": "Oeps! Er is iets misgegaan met uw toegangsrechten voor schermdeling. Herlaad en probeer opnieuw.",
|
||||
"searchInSalesforce": "Zoeken bij Verkoop",
|
||||
"searchInSalesforce": "Zoeken in Salesforce",
|
||||
"searchResults": "Zoekresultaten({{count}})",
|
||||
"searchResultsDetailsError": "Er ging iets mis bij het ophalen van eigenaargegevens.",
|
||||
"searchResultsError": "Er ging iets mis bij het ophalen van gegevens",
|
||||
@@ -504,7 +504,7 @@
|
||||
"sharedVideoDialogError": "Fout: Ongeldige of verboden URL",
|
||||
"sharedVideoLinkPlaceholder": "YouTube-link of directe video-link",
|
||||
"show": "Weergeven",
|
||||
"start": "Start ",
|
||||
"start": "Starten ",
|
||||
"startLiveStreaming": "Livestream starten",
|
||||
"startRecording": "Opname starten",
|
||||
"startRemoteControlErrorMessage": "Er is een fout opgetreden tijdens het starten van de sessie van extern beheer.",
|
||||
@@ -592,6 +592,7 @@
|
||||
"newFileNotification": "{{ participantName }} deelde '{{ fileName }}'",
|
||||
"removeFile": "Verwijderen",
|
||||
"removeFileSuccess": "Bestand met succes verwijderd",
|
||||
"uploadDisabled": "Bestanden uploaden is niet toegestaan. Vraag een moderator om toestemming voor die handeling.",
|
||||
"uploadFailedDescription": "Probeer het nog eens.",
|
||||
"uploadFailedTitle": "Upload is mislukt",
|
||||
"uploadFile": "Bestand delen"
|
||||
@@ -847,12 +848,12 @@
|
||||
"leftOneMember": "{{name}} heeft de vergadering verlaten",
|
||||
"leftThreePlusMembers": "{{name}} en vele anderen hebben de vergadering verlaten",
|
||||
"leftTwoMembers": "{{first}} en {{second}} hebben de vergadering verlaten",
|
||||
"linkToSalesforce": "Koppelen aan Verkoop",
|
||||
"linkToSalesforceDescription": "U kunt de samenvatting van de vergadering koppelen aan een Verkoop-object.",
|
||||
"linkToSalesforceError": "Koppelen aan Verkoop is mislukt",
|
||||
"linkToSalesforce": "Koppelen aan Salesforce",
|
||||
"linkToSalesforceDescription": "U kunt de samenvatting van de vergadering koppelen aan een Salesforce-object.",
|
||||
"linkToSalesforceError": "Koppelen aan Salesforce is mislukt",
|
||||
"linkToSalesforceKey": "Deze vergadering koppelen",
|
||||
"linkToSalesforceProgress": "Vergadering koppelen aan Verkoop…",
|
||||
"linkToSalesforceSuccess": "De vergadering is gekoppeld aan Verkoop",
|
||||
"linkToSalesforceProgress": "Vergadering koppelen aan Salesforce…",
|
||||
"linkToSalesforceSuccess": "De vergadering is gekoppeld aan Salesforce",
|
||||
"localRecordingStarted": "{{name}} heeft een lokale opname gestart.",
|
||||
"localRecordingStopped": "{{name}} heeft een lokale opname gestopt.",
|
||||
"me": "Ik",
|
||||
@@ -888,7 +889,7 @@
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) verwijderd door een andere deelnemer",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) ingesteld door een ander deelnemer",
|
||||
"raiseHandAction": "Hand opsteken",
|
||||
"raisedHand": "Wilgraag deelnemen.",
|
||||
"raisedHand": "Wil graag deelnemen.",
|
||||
"raisedHands": "{{participantName}} en {{raisedHands}} meer mensen",
|
||||
"reactionSounds": "Geluiden uitschakelen",
|
||||
"reactionSoundsForAll": "Geluiden uitschakelen voor iedereen",
|
||||
@@ -1096,11 +1097,11 @@
|
||||
"setEmailLabel": "Gravatar e-mailadres",
|
||||
"title": "Profiel"
|
||||
},
|
||||
"raisedHand": "Zou graag willen spreken",
|
||||
"raisedHand": "Wil graag spreken",
|
||||
"raisedHandsLabel": "Aantal handen omhoog",
|
||||
"record": {
|
||||
"already": {
|
||||
"linked": "De vergadering is al gekoppeld aan dit Verkoop-object."
|
||||
"linked": "De vergadering is al gekoppeld aan dit Salesforce-object."
|
||||
},
|
||||
"type": {
|
||||
"account": "Account",
|
||||
@@ -1347,7 +1348,7 @@
|
||||
"laugh": "Lachen",
|
||||
"leaveConference": "Vergadering verlaten",
|
||||
"like": "Duim omhoog",
|
||||
"linkToSalesforce": "Koppelen met Verkoop",
|
||||
"linkToSalesforce": "Koppelen aan Salesforce",
|
||||
"lobbyButton": "Wachtruimte in- of uitschakelen",
|
||||
"localRecording": "Besturingselementen voor lokale opname in- of uitschakelen",
|
||||
"lockRoom": "Wachtwoord voor vergadering in- of uitschakelen",
|
||||
@@ -1454,7 +1455,7 @@
|
||||
"leaveBreakoutRoom": "Aparte vergaderruimte verlaten",
|
||||
"leaveConference": "Vergadering verlaten",
|
||||
"like": "Duim omhoog",
|
||||
"linkToSalesforce": "Koppelen aan Verkoop",
|
||||
"linkToSalesforce": "Koppelen aan Salesforce",
|
||||
"lobbyButtonDisable": "Wachtruimte uitschakelen",
|
||||
"lobbyButtonEnable": "Wachtruimte inschakelen",
|
||||
"login": "Aanmelden",
|
||||
@@ -1630,30 +1631,30 @@
|
||||
"chatIndicator": "(toeschouwer)",
|
||||
"joinMeeting": {
|
||||
"description": "U bent momenteel een toeschouwer bij deze conferentie.",
|
||||
"raiseHand": "Raise your hand",
|
||||
"title": "Joining meeting",
|
||||
"wishToSpeak": "If you wish to speak, please raise your hand below and wait for the moderator's approval."
|
||||
"raiseHand": "Hand opsteken",
|
||||
"title": "Deelnemen aan vergadering",
|
||||
"wishToSpeak": "Als u wilt spreken, steek dan hieronder uw hand op en wacht op goedkeuring van de moderator."
|
||||
},
|
||||
"labelTooltip": "Number of viewers: {{count}}",
|
||||
"labelTooltip": "Aantal toeschouwers: {{count}}",
|
||||
"notification": {
|
||||
"demoteDescription": "Sent here by {{actor}}, raise your hand to participate",
|
||||
"noMainParticipantsDescription": "A participant needs to start the meeting. Please try again in a bit.",
|
||||
"noMainParticipantsTitle": "This meeting hasn't started yet.",
|
||||
"noVisitorLobby": "You cannot join while there is a lobby enabled for the meeting.",
|
||||
"notAllowedPromotion": "A participant needs to allow your request first.",
|
||||
"requestToJoin": "Hand Raised",
|
||||
"requestToJoinDescription": "Your request was sent to the moderators. Hang tight!",
|
||||
"title": "You are a viewer in the meeting"
|
||||
"demoteDescription": "Hierheen gestuurd door {{actor}}, steek uw hand op om deel te nemen",
|
||||
"noMainParticipantsDescription": "Een deelnemer moet de vergadering starten. Probeer het over een tijdje opnieuw.",
|
||||
"noMainParticipantsTitle": "Deze vergadering is nog niet begonnen.",
|
||||
"noVisitorLobby": "U kunt niet deelnemen terwijl er een wachtruimte is ingeschakeld voor de vergadering.",
|
||||
"notAllowedPromotion": "Een deelnemer moet eerst uw verzoek toestaan.",
|
||||
"requestToJoin": "Hand opgestoken",
|
||||
"requestToJoinDescription": "Uw verzoek is naar de moderators gestuurd. Even geduld!",
|
||||
"title": "U bent een toeschouwer in de vergadering"
|
||||
},
|
||||
"waitingMessage": "You'll join the meeting as soon as it is live!"
|
||||
"waitingMessage": "U zult deelnemen aan de vergadering zodra deze live is!"
|
||||
},
|
||||
"volumeSlider": "Volume slider",
|
||||
"volumeSlider": "Volume-schuifknop",
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
"join": "Tik om deel te nemen",
|
||||
"roomname": "Voer naam van ruimte in"
|
||||
},
|
||||
"addMeetingName": "Add Meeting name",
|
||||
"addMeetingName": "Geef de vergadering een naam",
|
||||
"appDescription": "U kunt nu videochatten met het gehele team. Nodig uit wie u maar wilt. {{app}} is een volledig versleutelde, 100% open-source oplossing voor videovergaderingen, die u wanneer u maar wilt gratis kunt gebruiken — zonder dat u een account nodig hebt.",
|
||||
"audioVideoSwitch": {
|
||||
"audio": "Spraak",
|
||||
@@ -1673,16 +1674,16 @@
|
||||
"join": "AANMAKEN / DEELNEMEN",
|
||||
"logo": {
|
||||
"calendar": "Agenda logo",
|
||||
"desktopPreviewThumbnail": "Desktop preview thumbnail",
|
||||
"desktopPreviewThumbnail": "Bureaubladvoorbeeld miniatuur",
|
||||
"googleLogo": "Google logo",
|
||||
"logoDeepLinking": "Jitsi meet logo",
|
||||
"microsoftLogo": "Microsoft logo",
|
||||
"policyLogo": "Policy logo"
|
||||
},
|
||||
"meetingsAccessibilityLabel": "Meetings",
|
||||
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
|
||||
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
|
||||
"mobileDownLoadLinkIos": "Download mobile app for iOS",
|
||||
"meetingsAccessibilityLabel": "Vergaderingen",
|
||||
"mobileDownLoadLinkAndroid": "Download mobiele app voor Android",
|
||||
"mobileDownLoadLinkFDroid": "Download mobiele app voor F-Droid",
|
||||
"mobileDownLoadLinkIos": "Download mobiele app voor iOS",
|
||||
"moderatedMessage": "Of <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">boek een vergadering URL</a> van tevoren waar u de enige moderator bent.",
|
||||
"privacy": "Privacy",
|
||||
"recentList": "Recent",
|
||||
|
||||
@@ -592,6 +592,7 @@
|
||||
"newFileNotification": "{{ participantName }} shared '{{ fileName }}'",
|
||||
"removeFile": "Remove",
|
||||
"removeFileSuccess": "File removed successfully",
|
||||
"uploadDisabled": "Not allowed to upload files. Ask a moderator for permission rights for that operation.",
|
||||
"uploadFailedDescription": "Please try again.",
|
||||
"uploadFailedTitle": "Upload failed",
|
||||
"uploadFile": "Share file"
|
||||
|
||||
@@ -2253,6 +2253,32 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the external application that a file has been uploaded.
|
||||
*
|
||||
* @param {Object} fileMetadata - The file metadata.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyFileUploaded(fileMetadata) {
|
||||
this._sendEvent({
|
||||
name: 'file-uploaded',
|
||||
file: fileMetadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the external application that a file has been deleted.
|
||||
*
|
||||
* @param {string} fileId - The ID of the deleted file.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyFileDeleted(fileId) {
|
||||
this._sendEvent({
|
||||
name: 'file-deleted',
|
||||
fileId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the external application that the audio or video is being shared by a participant.
|
||||
*
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -133,6 +133,8 @@ const events = {
|
||||
'face-landmark-detected': 'faceLandmarkDetected',
|
||||
'feedback-submitted': 'feedbackSubmitted',
|
||||
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
|
||||
'file-deleted': 'fileDeleted',
|
||||
'file-uploaded': 'fileUploaded',
|
||||
'filmstrip-display-changed': 'filmstripDisplayChanged',
|
||||
'incoming-message': 'incomingMessage',
|
||||
'knocking-participant': 'knockingParticipant',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Logger from '@jitsi/logger';
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
@@ -166,8 +166,7 @@ export default class LargeVideoManager {
|
||||
|
||||
this.removePresenceLabel();
|
||||
|
||||
this._avatarRoot?.unmount();
|
||||
this._avatarRoot = null;
|
||||
ReactDOM.unmountComponentAtNode(this._dominantSpeakerAvatarContainer);
|
||||
|
||||
this.container.style.display = 'none';
|
||||
}
|
||||
@@ -519,16 +518,14 @@ export default class LargeVideoManager {
|
||||
* Updates the src of the dominant speaker avatar
|
||||
*/
|
||||
updateAvatar() {
|
||||
if (!this._avatarRoot) {
|
||||
this._avatarRoot = createRoot(this._dominantSpeakerAvatarContainer);
|
||||
}
|
||||
this._avatarRoot.render(
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<Avatar
|
||||
id = "dominantSpeakerAvatar"
|
||||
participantId = { this.id }
|
||||
size = { 200 } />
|
||||
</Provider>
|
||||
</Provider>,
|
||||
this._dominantSpeakerAvatarContainer
|
||||
);
|
||||
}
|
||||
|
||||
@@ -562,18 +559,15 @@ export default class LargeVideoManager {
|
||||
const presenceLabelContainer = document.getElementById('remotePresenceMessage');
|
||||
|
||||
if (presenceLabelContainer) {
|
||||
if (!this._presenceLabelRoot) {
|
||||
this._presenceLabelRoot = createRoot(presenceLabelContainer);
|
||||
}
|
||||
this._presenceLabelRoot.render(
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<PresenceLabel
|
||||
participantID = { id }
|
||||
className = 'presence-label' />
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
);
|
||||
</Provider>,
|
||||
presenceLabelContainer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,8 +577,11 @@ export default class LargeVideoManager {
|
||||
* @returns {void}
|
||||
*/
|
||||
removePresenceLabel() {
|
||||
this._presenceLabelRoot?.unmount();
|
||||
this._presenceLabelRoot = null;
|
||||
const presenceLabelContainer = document.getElementById('remotePresenceMessage');
|
||||
|
||||
if (presenceLabelContainer) {
|
||||
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Logger from '@jitsi/logger';
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { browser } from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip/constants';
|
||||
@@ -659,12 +659,7 @@ export class VideoContainer extends LargeContainer {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('largeVideoBackgroundContainer');
|
||||
|
||||
if (!this._backgroundRoot) {
|
||||
this._backgroundRoot = createRoot(container);
|
||||
}
|
||||
this._backgroundRoot.render(
|
||||
ReactDOM.render(
|
||||
<LargeVideoBackground
|
||||
hidden = { this._hideBackground || this._isHidden }
|
||||
mirror = {
|
||||
@@ -674,7 +669,8 @@ export class VideoContainer extends LargeContainer {
|
||||
}
|
||||
orientationFit = { this._backgroundOrientation }
|
||||
videoElement = { this.video }
|
||||
videoTrack = { this.stream } />
|
||||
videoTrack = { this.stream } />,
|
||||
document.getElementById('largeVideoBackgroundContainer')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -66,7 +66,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2131.0.0+6912eed8/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -19049,8 +19049,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2131.0.0+6912eed8/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-oTQHdvxPkevpn0CNRiiKaojb6PQxRJMdrmV5N7yrWsiqUA5qw+eMQ7A251oahaSfpW5yLz2CM4l+OHGADWrBUQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "^2.6.7",
|
||||
@@ -19062,6 +19062,7 @@
|
||||
"base64-js": "1.5.1",
|
||||
"current-executing-script": "0.1.3",
|
||||
"emoji-regex": "10.4.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"sdp-transform": "2.3.0",
|
||||
"strophe.js": "https://github.com/jitsi/strophejs/releases/download/v1.5-jitsi-3/strophe.js-1.5.0.tgz",
|
||||
@@ -19091,6 +19092,15 @@
|
||||
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lib-jitsi-meet/node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
@@ -40848,8 +40858,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2131.0.0+6912eed8/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-oTQHdvxPkevpn0CNRiiKaojb6PQxRJMdrmV5N7yrWsiqUA5qw+eMQ7A251oahaSfpW5yLz2CM4l+OHGADWrBUQ==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "^2.6.7",
|
||||
"@jitsi/logger": "2.1.1",
|
||||
@@ -40860,6 +40870,7 @@
|
||||
"base64-js": "1.5.1",
|
||||
"current-executing-script": "0.1.3",
|
||||
"emoji-regex": "10.4.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"sdp-transform": "2.3.0",
|
||||
"strophe.js": "https://github.com/jitsi/strophejs/releases/download/v1.5-jitsi-3/strophe.js-1.5.0.tgz",
|
||||
@@ -40887,6 +40898,11 @@
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
|
||||
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="
|
||||
},
|
||||
"jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2131.0.0+6912eed8/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getCustomerDetails } from '../../jaas/actions.any';
|
||||
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
|
||||
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../mobile/navigation/routes';
|
||||
import { conferenceWillLeave } from '../conference/actions.native';
|
||||
import { conferenceLeft } from '../conference/actions.native';
|
||||
import { setJWT } from '../jwt/actions';
|
||||
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
|
||||
@@ -60,7 +60,7 @@ export function connect(id?: string, password?: string) {
|
||||
*/
|
||||
export function hangup(_requestFeedback = false) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
dispatch(conferenceWillLeave());
|
||||
dispatch(appNavigate(undefined));
|
||||
dispatch(conferenceLeft());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,4 +29,3 @@ export function isIpadMobileBrowser() {
|
||||
// @ts-ignore
|
||||
return isIosMobileBrowser() && Platform.isPad;
|
||||
}
|
||||
|
||||
1
react/features/base/environment/utils.native.ts
Normal file
1
react/features/base/environment/utils.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './utils.any';
|
||||
37
react/features/base/environment/utils.web.ts
Normal file
37
react/features/base/environment/utils.web.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { MIN_FILMSTRIP_RESIZE_WIDTH } from '../../filmstrip/constants';
|
||||
|
||||
/**
|
||||
* Detects if the current device has touch capability.
|
||||
* This includes smartphones, tablets, and laptops with touch screens.
|
||||
*
|
||||
* @returns {boolean} True if the device supports touch events.
|
||||
*/
|
||||
export function isTouchDevice(): boolean {
|
||||
// Check maxTouchPoints (most reliable for modern browsers)
|
||||
if ('maxTouchPoints' in navigator) {
|
||||
return navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if resize functionality should be enabled based on device capabilities
|
||||
* and screen size. On touch devices, resize is only enabled for larger screens.
|
||||
* On non-touch devices (desktop), resize is always enabled.
|
||||
*
|
||||
* @returns {boolean} True if resize functionality should be available to the user.
|
||||
*/
|
||||
export function shouldEnableResize(): boolean {
|
||||
const hasTouch = isTouchDevice();
|
||||
|
||||
// On non-touch devices (desktop), always enable resize
|
||||
if (!hasTouch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// On touch devices, only enable if screen is large enough.
|
||||
return window?.innerWidth >= MIN_FILMSTRIP_RESIZE_WIDTH;
|
||||
}
|
||||
|
||||
export * from './utils.any';
|
||||
@@ -60,66 +60,6 @@ const AVATAR_CHECKER_FUNCTIONS = [
|
||||
return null;
|
||||
}
|
||||
];
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
/**
|
||||
* Returns the list of active speakers that should be moved to the top of the sorted list of participants so that the
|
||||
* dominant speaker is visible always on the vertical filmstrip in stage layout.
|
||||
*
|
||||
* @param {Function | Object} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getActiveSpeakersToBeDisplayed(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
sortedRemoteVirtualScreenshareParticipants,
|
||||
speakersList
|
||||
} = state['features/base/participants'];
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
let activeSpeakers = new Map(speakersList);
|
||||
|
||||
// Do not re-sort the active speakers if dominant speaker is currently visible.
|
||||
if (dominantSpeaker && visibleRemoteParticipants.has(dominantSpeaker)) {
|
||||
return activeSpeakers;
|
||||
}
|
||||
let availableSlotsForActiveSpeakers = visibleRemoteParticipants.size;
|
||||
|
||||
if (activeSpeakers.has(dominantSpeaker ?? '')) {
|
||||
activeSpeakers.delete(dominantSpeaker ?? '');
|
||||
}
|
||||
|
||||
// Add dominant speaker to the beginning of the list (not including self) since the active speaker list is always
|
||||
// alphabetically sorted.
|
||||
if (dominantSpeaker && dominantSpeaker !== getLocalParticipant(state)?.id) {
|
||||
const updatedSpeakers = Array.from(activeSpeakers);
|
||||
|
||||
updatedSpeakers.splice(0, 0, [ dominantSpeaker, getParticipantById(state, dominantSpeaker)?.name ?? '' ]);
|
||||
activeSpeakers = new Map(updatedSpeakers);
|
||||
}
|
||||
|
||||
// Remove screenshares from the count.
|
||||
if (sortedRemoteVirtualScreenshareParticipants) {
|
||||
availableSlotsForActiveSpeakers -= sortedRemoteVirtualScreenshareParticipants.size * 2;
|
||||
for (const screenshare of Array.from(sortedRemoteVirtualScreenshareParticipants.keys())) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare as string);
|
||||
|
||||
activeSpeakers.delete(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove fake participants from the count.
|
||||
if (fakeParticipants) {
|
||||
availableSlotsForActiveSpeakers -= fakeParticipants.size;
|
||||
}
|
||||
const truncatedSpeakersList = Array.from(activeSpeakers).slice(0, availableSlotsForActiveSpeakers);
|
||||
|
||||
truncatedSpeakersList.sort((a: any, b: any) => a[1].localeCompare(b[1]));
|
||||
|
||||
return new Map(truncatedSpeakersList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
|
||||
@@ -68,6 +68,7 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
||||
];
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
activeSpeakers: new Set<string>(),
|
||||
dominantSpeaker: undefined,
|
||||
fakeParticipants: new Map(),
|
||||
local: undefined,
|
||||
@@ -82,10 +83,10 @@ const DEFAULT_STATE = {
|
||||
remoteVideoSources: new Set<string>(),
|
||||
sortedRemoteVirtualScreenshareParticipants: new Map(),
|
||||
sortedRemoteParticipants: new Map(),
|
||||
speakersList: new Map()
|
||||
};
|
||||
|
||||
export interface IParticipantsState {
|
||||
activeSpeakers: Set<string>;
|
||||
dominantSpeaker?: string;
|
||||
fakeParticipants: Map<string, IParticipant>;
|
||||
local?: ILocalParticipant;
|
||||
@@ -100,7 +101,6 @@ export interface IParticipantsState {
|
||||
remoteVideoSources: Set<string>;
|
||||
sortedRemoteParticipants: Map<string, string>;
|
||||
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
|
||||
speakersList: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,22 +157,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
const { participant } = action;
|
||||
const { id, previousSpeakers = [] } = participant;
|
||||
const { dominantSpeaker, local } = state;
|
||||
const newSpeakers = [ id, ...previousSpeakers ];
|
||||
const sortedSpeakersList: Array<Array<string>> = [];
|
||||
|
||||
for (const speaker of newSpeakers) {
|
||||
if (speaker !== local?.id) {
|
||||
const remoteParticipant = state.remote.get(speaker);
|
||||
|
||||
remoteParticipant
|
||||
&& sortedSpeakersList.push(
|
||||
[ speaker, _getDisplayName(state, remoteParticipant?.name) ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the remote speaker list sorted alphabetically.
|
||||
sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
const activeSpeakers = new Set(previousSpeakers
|
||||
.filter((speakerId: string) => state.remote.has(speakerId) && (speakerId !== local?.id)));
|
||||
|
||||
// Only one dominant speaker is allowed.
|
||||
if (dominantSpeaker) {
|
||||
@@ -183,7 +169,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
return {
|
||||
...state,
|
||||
dominantSpeaker: id, // @ts-ignore
|
||||
speakersList: new Map(sortedSpeakersList)
|
||||
activeSpeakers
|
||||
};
|
||||
}
|
||||
|
||||
@@ -438,7 +424,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
}
|
||||
|
||||
// Remove the participant from the list of speakers.
|
||||
state.speakersList.has(id) && state.speakersList.delete(id);
|
||||
state.activeSpeakers.delete(id);
|
||||
|
||||
if (pinnedParticipant === id) {
|
||||
state.pinnedParticipant = undefined;
|
||||
|
||||
@@ -155,6 +155,7 @@ export const colorMap = {
|
||||
preMeetingBackground: 'surface02', // Pre-meeting screen container background
|
||||
preMeetingPreview: 'ui01', // Video preview in pre-meeting
|
||||
prejoinDialogBackground: 'uiBackground', // Prejoin dialog background
|
||||
prejoinPreviewBackground: 'uiBackground', // Prejoin video preview background (#040404)
|
||||
prejoinDialogDelimiter: 'ui03', // Prejoin dialog delimiter line
|
||||
prejoinDialogDelimiterText: 'text01', // Prejoin dialog delimiter text
|
||||
prejoinTitleText: 'text01', // Prejoin title text color
|
||||
@@ -220,9 +221,9 @@ export const colorMap = {
|
||||
|
||||
// Welcome Page
|
||||
welcomeBackground: 'surface01', // Welcome page background (same as uiBackground)
|
||||
welcomeCard: 'ui01', // Welcome page tab bar background
|
||||
welcomeCard: 'surface02', // Welcome page tab bar background
|
||||
welcomeTabActive: 'icon01', // Welcome page active tab icon
|
||||
welcomeTabInactive: 'icon03', // Welcome page inactive tab icon
|
||||
welcomeTabInactive: 'ui03', // Welcome page inactive tab icon
|
||||
|
||||
// ----- Form Components -----
|
||||
|
||||
@@ -356,6 +357,9 @@ export const colorMap = {
|
||||
// Reactions
|
||||
reactionsMenuBackground: 'ui01', // Reactions menu background
|
||||
reactionsMenuBorder: 'ui02', // Reactions menu border
|
||||
reactionsMenuButtonToggled: 'surface01', // Reactions menu button toggled state background
|
||||
reactionsMenuBoxShadow1: 'ui09', // Reactions menu box shadow primary
|
||||
reactionsMenuBoxShadow2: 'ui08', // Reactions menu box shadow secondary
|
||||
|
||||
// Recording / Live Stream
|
||||
recordingBackground: 'ui01', // Recording panel background
|
||||
@@ -428,7 +432,7 @@ export const colorMap = {
|
||||
securityDialogBorder: 'ui07', // Security dialog border color
|
||||
|
||||
// Deep Linking
|
||||
deepLinkingBackground: 'ui01', // Deep linking page content pane background
|
||||
deepLinkingBackground: 'uiBackground', // Deep linking page content pane background (#1e1e1e)
|
||||
deepLinkingBorder: 'ui03', // Deep linking page content pane border
|
||||
deepLinkingText: 'text01', // Deep linking page text
|
||||
deepLinkingSeparator: 'ui03', // Deep linking separator line
|
||||
@@ -505,6 +509,9 @@ export const colorMap = {
|
||||
// High-contrast
|
||||
icon04: 'surface01',
|
||||
|
||||
// SVG fill color
|
||||
iconSvgFill: 'icon01',
|
||||
|
||||
// Error
|
||||
iconError: 'action03',
|
||||
|
||||
|
||||
@@ -81,7 +81,11 @@ const useStyles = makeStyles()(theme => {
|
||||
|
||||
'&:focus': {
|
||||
outline: 0,
|
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.inputFieldFocus}`
|
||||
boxShadow: `0px 0px 0px 2px ${theme.palette.inputFieldFocus}`,
|
||||
|
||||
'&::placeholder': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
'&:disabled': {
|
||||
@@ -107,12 +111,12 @@ const useStyles = makeStyles()(theme => {
|
||||
},
|
||||
|
||||
'input::-webkit-outer-spin-button, input::-webkit-inner-spin-button': {
|
||||
WebkitAppearance: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
margin: 0
|
||||
},
|
||||
|
||||
'input[type=number]': {
|
||||
MozAppearance: 'textfield'
|
||||
'-moz-appearance': 'textfield'
|
||||
},
|
||||
|
||||
icon: {
|
||||
|
||||
@@ -13,19 +13,25 @@ export const commonStyles = (theme: Theme) => {
|
||||
return {
|
||||
':root': {
|
||||
// Inject semantic tokens as CSS custom properties for use in SCSS
|
||||
'--toolbox-background-color': theme.palette.toolboxBackground,
|
||||
'--drawer-background-color': theme.palette.drawerBackground,
|
||||
'--icon-svg-fill': theme.palette.iconSvgFill,
|
||||
'--overflow-menu-background-color': theme.palette.overflowMenuBackground,
|
||||
'--overflow-menu-item-disabled-color': theme.palette.overflowMenuItemDisabled,
|
||||
'--overflow-menu-item-hover-color': theme.palette.overflowMenuItemHover,
|
||||
'--overflow-menu-item-icon-color': theme.palette.overflowMenuItemIcon,
|
||||
'--overflow-menu-item-text-color': theme.palette.overflowMenuItemText,
|
||||
'--prejoin-preview-background': theme.palette.prejoinPreviewBackground,
|
||||
'--reactions-menu-background': theme.palette.reactionsMenuBackground,
|
||||
'--reactions-menu-box-shadow-1': theme.palette.reactionsMenuBoxShadow1,
|
||||
'--reactions-menu-box-shadow-2': theme.palette.reactionsMenuBoxShadow2,
|
||||
'--reactions-menu-button-toggled': theme.palette.reactionsMenuButtonToggled,
|
||||
'--toolbar-button-active-color': theme.palette.toolbarButtonActive,
|
||||
'--toolbar-button-color': theme.palette.toolbarButton,
|
||||
'--toolbar-button-hover-color': theme.palette.toolbarButtonHover,
|
||||
'--toolbar-button-active-color': theme.palette.toolbarButtonActive,
|
||||
'--toolbar-icon-active-color': theme.palette.toolbarIconActive,
|
||||
'--toolbar-icon-color': theme.palette.toolbarIcon,
|
||||
'--toolbar-icon-hover-color': theme.palette.toolbarIconHover,
|
||||
'--toolbar-icon-active-color': theme.palette.toolbarIconActive,
|
||||
'--overflow-menu-background-color': theme.palette.overflowMenuBackground,
|
||||
'--overflow-menu-item-text-color': theme.palette.overflowMenuItemText,
|
||||
'--overflow-menu-item-icon-color': theme.palette.overflowMenuItemIcon,
|
||||
'--overflow-menu-item-hover-color': theme.palette.overflowMenuItemHover,
|
||||
'--overflow-menu-item-disabled-color': theme.palette.overflowMenuItemDisabled
|
||||
'--toolbox-background-color': theme.palette.toolboxBackground
|
||||
},
|
||||
|
||||
'.empty-list': {
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface IPalette {
|
||||
icon02: string;
|
||||
icon03: string;
|
||||
icon04: string;
|
||||
iconSvgFill: string;
|
||||
iconError: string;
|
||||
link01: string;
|
||||
link01Active: string;
|
||||
@@ -153,6 +154,7 @@ export interface IPalette {
|
||||
prejoinDialogBackground: string;
|
||||
prejoinDialogDelimiter: string;
|
||||
prejoinDialogDelimiterText: string;
|
||||
prejoinPreviewBackground: string;
|
||||
prejoinRecordingWarningText: string;
|
||||
prejoinRoomNameText: string;
|
||||
prejoinTitleText: string;
|
||||
@@ -313,6 +315,9 @@ export interface IPalette {
|
||||
pollsVotersText: string;
|
||||
reactionsMenuBackground: string;
|
||||
reactionsMenuBorder: string;
|
||||
reactionsMenuButtonToggled: string;
|
||||
reactionsMenuBoxShadow1: string;
|
||||
reactionsMenuBoxShadow2: string;
|
||||
recordingBackground: string;
|
||||
recordingHighlightButton: string;
|
||||
recordingHighlightButtonDisabled: string;
|
||||
|
||||
@@ -24,3 +24,13 @@ const JITSI_MEET_APPS = [
|
||||
export function isEmbedded(): boolean {
|
||||
return !JITSI_MEET_APPS.includes(getBundleId());
|
||||
}
|
||||
|
||||
/**
|
||||
* React Native has no concept of same-domain embedding. SDK consumers are
|
||||
* always treated as cross-domain embeddings.
|
||||
*
|
||||
* @returns {boolean} Always false in React Native.
|
||||
*/
|
||||
export function isEmbeddedFromSameDomain(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -10,3 +10,17 @@ export function isEmbedded(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether we are loaded in iframe with same parent domain.
|
||||
*
|
||||
* @returns {boolean} Whether the current page is loaded in an iframe with same parent domain.
|
||||
*/
|
||||
export function isEmbeddedFromSameDomain(): boolean {
|
||||
try {
|
||||
return window.self.location.host === window.parent.location.host;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconInfo, IconMessage, IconShareDoc, IconSubtitles } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant, getRemoteParticipants, isPrivateChatEnabledSelf } from '../../../base/participants/functions';
|
||||
@@ -23,7 +24,16 @@ import {
|
||||
setUserChatWidth,
|
||||
toggleChat
|
||||
} from '../../actions.web';
|
||||
import { CHAT_SIZE, ChatTabs, OPTION_GROUPCHAT, SMALL_WIDTH_THRESHOLD } from '../../constants';
|
||||
import {
|
||||
CHAT_DRAG_HANDLE_HEIGHT,
|
||||
CHAT_DRAG_HANDLE_OFFSET,
|
||||
CHAT_DRAG_HANDLE_WIDTH,
|
||||
CHAT_SIZE,
|
||||
CHAT_TOUCH_HANDLE_SIZE,
|
||||
ChatTabs,
|
||||
OPTION_GROUPCHAT,
|
||||
SMALL_WIDTH_THRESHOLD
|
||||
} from '../../constants';
|
||||
import { getChatMaxSize, getFocusedTab, isChatDisabled } from '../../functions';
|
||||
import { IChatProps as AbstractProps } from '../../types';
|
||||
|
||||
@@ -104,7 +114,12 @@ interface IProps extends AbstractProps {
|
||||
_width: number;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme, { _isResizing, width }) => {
|
||||
const useStyles = makeStyles<{
|
||||
_isResizing: boolean;
|
||||
isTouch: boolean;
|
||||
resizeEnabled: boolean;
|
||||
width: number;
|
||||
}>()((theme, { _isResizing, isTouch, resizeEnabled, width }) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.palette.chatBackground,
|
||||
@@ -115,11 +130,15 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
|
||||
width: `${width}px`,
|
||||
zIndex: 300,
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible'
|
||||
// On non-touch devices (desktop), show handle on hover
|
||||
// On touch devices, handle is always visible if resize is enabled
|
||||
...(!isTouch && {
|
||||
'&:hover, &:focus-within': {
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible'
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
'@media (max-width: 580px)': {
|
||||
height: '100dvh',
|
||||
@@ -132,7 +151,7 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
|
||||
|
||||
'*': {
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text'
|
||||
'-webkit-user-select': 'text'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -183,16 +202,23 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
|
||||
|
||||
dragHandleContainer: {
|
||||
height: '100%',
|
||||
width: '9px',
|
||||
// Touch devices need larger hit target but positioned to not take extra space
|
||||
width: isTouch ? `${CHAT_TOUCH_HANDLE_SIZE}px` : `${CHAT_DRAG_HANDLE_WIDTH}px`,
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
cursor: 'col-resize',
|
||||
display: 'flex',
|
||||
display: resizeEnabled ? 'flex' : 'none', // Hide if resize not enabled
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
visibility: 'hidden',
|
||||
right: '4px',
|
||||
// On touch devices, always visible if resize enabled. On desktop, hidden by default
|
||||
visibility: (isTouch && resizeEnabled) ? 'visible' : 'hidden',
|
||||
// Position touch handle centered on offset from edge, maintaining same gap as non-touch
|
||||
right: isTouch
|
||||
? `${CHAT_DRAG_HANDLE_OFFSET - Math.floor((CHAT_TOUCH_HANDLE_SIZE - CHAT_DRAG_HANDLE_WIDTH) / 2)}px`
|
||||
: `${CHAT_DRAG_HANDLE_OFFSET}px`,
|
||||
top: 0,
|
||||
// Prevent touch scrolling while dragging
|
||||
touchAction: 'none',
|
||||
|
||||
'&:hover': {
|
||||
'& .dragHandle': {
|
||||
@@ -210,10 +236,15 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
|
||||
},
|
||||
|
||||
dragHandle: {
|
||||
// Keep the same visual appearance on all devices
|
||||
backgroundColor: theme.palette.icon02,
|
||||
height: '100px',
|
||||
width: '3px',
|
||||
borderRadius: '1px'
|
||||
height: `${CHAT_DRAG_HANDLE_HEIGHT}px`,
|
||||
width: `${CHAT_DRAG_HANDLE_WIDTH / 3}px`,
|
||||
borderRadius: '1px',
|
||||
// Make more visible when actively shown
|
||||
...(isTouch && resizeEnabled && {
|
||||
backgroundColor: theme.palette.icon01
|
||||
})
|
||||
},
|
||||
|
||||
privateMessageRecipientsList: {
|
||||
@@ -246,7 +277,10 @@ const Chat = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { classes, cx } = useStyles({ _isResizing, width: _width });
|
||||
// Detect touch capability and screen size for resize functionality
|
||||
const isTouch = isTouchDevice();
|
||||
const resizeEnabled = shouldEnableResize();
|
||||
const { classes, cx } = useStyles({ _isResizing, width: _width, isTouch, resizeEnabled });
|
||||
const [ isMouseDown, setIsMouseDown ] = useState(false);
|
||||
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
|
||||
const [ dragChatWidth, setDragChatWidth ] = useState<number | null>(null);
|
||||
@@ -282,16 +316,21 @@ const Chat = ({
|
||||
}, [ participants, defaultRemoteDisplayName, t, notifyTimestamp ]);
|
||||
|
||||
/**
|
||||
* Handles mouse down on the drag handle.
|
||||
* Handles pointer down on the drag handle.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {MouseEvent} e - The mouse down event.
|
||||
* @param {React.PointerEvent} e - The pointer down event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragHandleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
const onDragHandlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Store the initial mouse position and chat width
|
||||
// Capture the pointer to ensure we receive all pointer events
|
||||
// even if the pointer moves outside the element
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
|
||||
// Store the initial pointer position and chat width
|
||||
setIsMouseDown(true);
|
||||
setMousePosition(e.clientX);
|
||||
setDragChatWidth(_width);
|
||||
@@ -299,7 +338,7 @@ const Chat = ({
|
||||
// Indicate that resizing is in progress
|
||||
dispatch(setChatIsResizing(true));
|
||||
|
||||
// Add visual feedback that we're dragging
|
||||
// Add visual feedback that we're dragging (cursor for mouse, not visible on touch)
|
||||
document.body.style.cursor = 'col-resize';
|
||||
|
||||
// Disable text selection during resize
|
||||
@@ -307,11 +346,12 @@ const Chat = ({
|
||||
}, [ _width, dispatch ]);
|
||||
|
||||
/**
|
||||
* Drag handle mouse up handler.
|
||||
* Drag handle pointer up handler.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragMouseUp = useCallback(() => {
|
||||
const onDragPointerUp = useCallback(() => {
|
||||
if (isMouseDown) {
|
||||
setIsMouseDown(false);
|
||||
dispatch(setChatIsResizing(false));
|
||||
@@ -323,12 +363,13 @@ const Chat = ({
|
||||
}, [ isMouseDown, dispatch ]);
|
||||
|
||||
/**
|
||||
* Handles drag handle mouse move.
|
||||
* Handles drag handle pointer move.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {MouseEvent} e - The mousemove event.
|
||||
* @param {PointerEvent} e - The pointermove event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onChatResize = useCallback(throttle((e: MouseEvent) => {
|
||||
const onChatResize = useCallback(throttle((e: PointerEvent) => {
|
||||
if (isMouseDown && mousePosition !== null && dragChatWidth !== null) {
|
||||
// For chat panel resizing on the left edge:
|
||||
// - Dragging left (decreasing X coordinate) should make the panel wider
|
||||
@@ -352,14 +393,14 @@ const Chat = ({
|
||||
|
||||
// Set up event listeners when component mounts
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', onDragMouseUp);
|
||||
document.addEventListener('mousemove', onChatResize);
|
||||
document.addEventListener('pointerup', onDragPointerUp);
|
||||
document.addEventListener('pointermove', onChatResize);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', onDragMouseUp);
|
||||
document.removeEventListener('mousemove', onChatResize);
|
||||
document.removeEventListener('pointerup', onDragPointerUp);
|
||||
document.removeEventListener('pointermove', onChatResize);
|
||||
};
|
||||
}, [ onDragMouseUp, onChatResize ]);
|
||||
}, [ onDragPointerUp, onChatResize ]);
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
@@ -600,7 +641,7 @@ const Chat = ({
|
||||
(isMouseDown || _isResizing) && 'visible',
|
||||
'dragHandleContainer'
|
||||
) }
|
||||
onMouseDown = { onDragHandleMouseDown }>
|
||||
onPointerDown = { onDragHandlePointerDown }>
|
||||
<div className = { cx(classes.dragHandle, 'dragHandle') } />
|
||||
</div>
|
||||
</div> : null
|
||||
|
||||
@@ -33,6 +33,23 @@ export const MESSAGE_TYPE_REMOTE = 'remote';
|
||||
|
||||
export const SMALL_WIDTH_THRESHOLD = 580;
|
||||
|
||||
/**
|
||||
* Drag handle dimensions for resizable chat.
|
||||
*/
|
||||
export const CHAT_DRAG_HANDLE_WIDTH = 9;
|
||||
export const CHAT_DRAG_HANDLE_HEIGHT = 100;
|
||||
|
||||
/**
|
||||
* Touch target size for chat drag handle on touch devices.
|
||||
* Provides adequate hit area (44px) for comfortable tapping.
|
||||
*/
|
||||
export const CHAT_TOUCH_HANDLE_SIZE = 44;
|
||||
|
||||
/**
|
||||
* Offset from edge for positioning the chat drag handle.
|
||||
*/
|
||||
export const CHAT_DRAG_HANDLE_OFFSET = 4;
|
||||
|
||||
|
||||
/**
|
||||
* Lobby message type.
|
||||
|
||||
@@ -227,7 +227,8 @@ class Conference extends AbstractConference<IProps, State> {
|
||||
const {
|
||||
_audioOnlyEnabled,
|
||||
_showLobby,
|
||||
_startCarMode
|
||||
_startCarMode,
|
||||
navigation
|
||||
} = this.props;
|
||||
|
||||
if (!prevProps._showLobby && _showLobby) {
|
||||
@@ -236,10 +237,10 @@ class Conference extends AbstractConference<IProps, State> {
|
||||
|
||||
if (prevProps._showLobby && !_showLobby) {
|
||||
if (_audioOnlyEnabled && _startCarMode) {
|
||||
return;
|
||||
navigation.navigate(screen.conference.carmode);
|
||||
} else {
|
||||
navigate(screen.conference.main);
|
||||
}
|
||||
|
||||
navigate(screen.conference.main);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCloudUpload } from '../../../base/icons/svg';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
@@ -226,16 +227,20 @@ const FileSharing = () => {
|
||||
)
|
||||
}
|
||||
{
|
||||
isUploadEnabled && (
|
||||
<Tooltip
|
||||
containerClassName = { classes.uploadButton }
|
||||
content = { isUploadEnabled ? t('fileSharing.uploadFile') : t('fileSharing.uploadDisabled') }
|
||||
position = 'top'>
|
||||
<Button
|
||||
accessibilityLabel = { t('fileSharing.uploadFile') }
|
||||
className = { classes.uploadButton }
|
||||
disabled = { !isUploadEnabled }
|
||||
labelKey = { 'fileSharing.uploadFile' }
|
||||
onClick = { handleClick }
|
||||
onKeyPress = { handleKeyPress }
|
||||
ref = { uploadButtonRef }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
)
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -72,6 +72,9 @@ StateListenerRegistry.register(
|
||||
type: _FILE_REMOVED,
|
||||
fileId
|
||||
});
|
||||
|
||||
// Notify external API about file deletion (for remote participants)
|
||||
APP.API.notifyFileDeleted(fileId);
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
|
||||
@@ -83,6 +86,11 @@ StateListenerRegistry.register(
|
||||
files,
|
||||
localParticipantId: localParticipant?.id
|
||||
});
|
||||
|
||||
// Notify external API about existing files (for participants joining late)
|
||||
Object.values(files).forEach((file: IFileMetadata) => {
|
||||
APP.API.notifyFileUploaded(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -152,6 +160,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
privateMessage: false,
|
||||
timestamp: file.timestamp
|
||||
}));
|
||||
|
||||
APP.API.notifyFileUploaded(file);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -178,6 +188,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
fileId
|
||||
});
|
||||
|
||||
// Notify external API about file deletion (for the local participant who deleted it)
|
||||
APP.API.notifyFileDeleted(fileId);
|
||||
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const sessionId = conference.getMeetingUniqueId();
|
||||
|
||||
|
||||
@@ -60,16 +60,19 @@ export function setRemoteParticipants(participants: Array<string>) {
|
||||
*
|
||||
* @param {number} startIndex - The start index from the remote participants array.
|
||||
* @param {number} endIndex - The end index from the remote participants array.
|
||||
* @param {number} fullyVisibleCount - The number of fully visible participants (excluding partially visible).
|
||||
* @returns {{
|
||||
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
* startIndex: number,
|
||||
* endIndex: number
|
||||
* endIndex: number,
|
||||
* fullyVisibleCount: number
|
||||
* }}
|
||||
*/
|
||||
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
|
||||
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number, fullyVisibleCount?: number) {
|
||||
return {
|
||||
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
startIndex,
|
||||
endIndex
|
||||
endIndex,
|
||||
fullyVisibleCount
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { getHideSelfView } from '../../../base/settings/functions.any';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.native';
|
||||
import { setVisibleRemoteParticipants } from '../../actions.native';
|
||||
import { calculateFullyVisibleParticipantsCount } from '../../functions.any';
|
||||
import {
|
||||
getFilmstripDimensions,
|
||||
isFilmstripVisible,
|
||||
@@ -190,7 +191,7 @@ class Filmstrip extends PureComponent<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onViewableItemsChanged({ viewableItems = [] }: { viewableItems: ViewToken[]; }) {
|
||||
const { _disableSelfView } = this.props;
|
||||
const { _aspectRatio, _clientWidth, _clientHeight, _disableSelfView } = this.props;
|
||||
|
||||
if (!this._separateLocalThumbnail && !_disableSelfView && viewableItems[0]?.index === 0) {
|
||||
// Skip the local thumbnail.
|
||||
@@ -205,13 +206,31 @@ class Filmstrip extends PureComponent<IProps> {
|
||||
let startIndex = Number(viewableItems[0].index);
|
||||
let endIndex = Number(viewableItems[viewableItems.length - 1].index);
|
||||
|
||||
// Calculate fully visible count (excluding partially visible tiles)
|
||||
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
|
||||
const { height: thumbnailHeight, width: thumbnailWidth, margin } = styles.thumbnail;
|
||||
const { height, width } = this._getDimensions();
|
||||
|
||||
// Calculate item size and container size based on layout orientation
|
||||
const itemSize = isNarrowAspectRatio
|
||||
? thumbnailWidth + (2 * margin) // Horizontal layout
|
||||
: thumbnailHeight + (2 * margin); // Vertical layout
|
||||
const containerSize = isNarrowAspectRatio ? width : height;
|
||||
|
||||
const fullyVisibleCount = calculateFullyVisibleParticipantsCount(
|
||||
startIndex,
|
||||
endIndex,
|
||||
containerSize,
|
||||
itemSize
|
||||
);
|
||||
|
||||
if (!this._separateLocalThumbnail && !_disableSelfView) {
|
||||
// We are off by one in the remote participants array.
|
||||
startIndex -= 1;
|
||||
endIndex -= 1;
|
||||
}
|
||||
|
||||
this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
|
||||
this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex, fullyVisibleCount));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import { withStyles } from 'tss-react/mui';
|
||||
import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { isMobileBrowser, isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
@@ -32,13 +32,19 @@ import {
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
DEFAULT_FILMSTRIP_WIDTH,
|
||||
DRAG_HANDLE_HEIGHT,
|
||||
DRAG_HANDLE_TOP_PANEL_HEIGHT,
|
||||
DRAG_HANDLE_TOP_PANEL_WIDTH,
|
||||
DRAG_HANDLE_WIDTH,
|
||||
FILMSTRIP_TYPE,
|
||||
MIN_STAGE_VIEW_HEIGHT,
|
||||
MIN_STAGE_VIEW_WIDTH,
|
||||
TILE_HORIZONTAL_MARGIN,
|
||||
TILE_VERTICAL_MARGIN,
|
||||
TOP_FILMSTRIP_HEIGHT
|
||||
TOP_FILMSTRIP_HEIGHT,
|
||||
TOUCH_DRAG_HANDLE_PADDING
|
||||
} from '../../constants';
|
||||
import { calculateFullyVisibleParticipantsCount } from '../../functions.any';
|
||||
import {
|
||||
getVerticalViewMaxWidth,
|
||||
isFilmstripDisabled,
|
||||
@@ -52,6 +58,21 @@ import ThumbnailWrapper from './ThumbnailWrapper';
|
||||
|
||||
|
||||
const BACKGROUND_COLOR = 'rgba(51, 51, 51, .5)';
|
||||
const TOUCH_DEVICE_PADDING = {
|
||||
paddingLeft: `${TOUCH_DRAG_HANDLE_PADDING}px`,
|
||||
paddingRight: `${TOUCH_DRAG_HANDLE_PADDING}px`,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
};
|
||||
const TOUCH_DEVICE_TOP_PANEL_PADDING = {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
paddingTop: `${TOUCH_DRAG_HANDLE_PADDING}px`,
|
||||
paddingBottom: `${TOUCH_DRAG_HANDLE_PADDING}px`
|
||||
};
|
||||
const NON_TOUCH_DEVICE_PANEL = {
|
||||
pading: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
@@ -61,6 +82,14 @@ const BACKGROUND_COLOR = 'rgba(51, 51, 51, .5)';
|
||||
* @returns {Object}
|
||||
*/
|
||||
function styles(theme: Theme, props: IProps) {
|
||||
const { _topPanelFilmstrip: isTopPanel } = props;
|
||||
|
||||
const _isTouchDevice = isTouchDevice();
|
||||
const resizeEnabled = shouldEnableResize();
|
||||
const handlePaddding = _isTouchDevice
|
||||
? (isTopPanel ? TOUCH_DEVICE_TOP_PANEL_PADDING : TOUCH_DEVICE_PADDING)
|
||||
: NON_TOUCH_DEVICE_PANEL;
|
||||
|
||||
const result = {
|
||||
toggleFilmstripContainer: {
|
||||
display: 'flex',
|
||||
@@ -94,7 +123,7 @@ function styles(theme: Theme, props: IProps) {
|
||||
margin: 0,
|
||||
border: 'none',
|
||||
|
||||
WebkitAppearance: 'none' as const,
|
||||
'-webkit-appearance': 'none',
|
||||
|
||||
'& svg': {
|
||||
fill: theme.palette.icon01
|
||||
@@ -122,23 +151,27 @@ function styles(theme: Theme, props: IProps) {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
'& .resizable-filmstrip': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
// On touch devices, handle is always visible via base styles, so no hover needed.
|
||||
// On desktop, show handle on hover/focus.
|
||||
...(!_isTouchDevice && {
|
||||
'&:hover, &:focus-within': {
|
||||
'& .resizable-filmstrip': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
|
||||
'& .filmstrip-hover': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
'& .filmstrip-hover': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
|
||||
'& .toggleFilmstripContainer': {
|
||||
opacity: 1
|
||||
},
|
||||
'& .toggleFilmstripContainer': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible' as const
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible' as const
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
'.horizontal-filmstrip &.hidden': {
|
||||
bottom: '-50px',
|
||||
@@ -187,14 +220,22 @@ function styles(theme: Theme, props: IProps) {
|
||||
|
||||
dragHandleContainer: {
|
||||
height: '100%',
|
||||
width: '9px',
|
||||
width: `${DRAG_HANDLE_WIDTH}px`,
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative' as const,
|
||||
cursor: 'col-resize',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
visibility: 'hidden' as const,
|
||||
// On touch devices, always visible if resize enabled. On desktop, hidden by default
|
||||
visibility: (_isTouchDevice && resizeEnabled) ? 'visible' as const : 'hidden' as const,
|
||||
marginLeft: 0,
|
||||
marginTop: 0,
|
||||
// Touch devices get padding for easier tapping
|
||||
// Vertical filmstrip: left/right padding. Top panel: top/bottom padding.
|
||||
...handlePaddding,
|
||||
// Prevent touch scrolling while dragging
|
||||
touchAction: 'none',
|
||||
|
||||
'&:hover': {
|
||||
'& .dragHandle': {
|
||||
@@ -213,20 +254,21 @@ function styles(theme: Theme, props: IProps) {
|
||||
'&.top-panel': {
|
||||
order: 2,
|
||||
width: '100%',
|
||||
height: '9px',
|
||||
height: `${DRAG_HANDLE_WIDTH}px`,
|
||||
cursor: 'row-resize',
|
||||
|
||||
'& .dragHandle': {
|
||||
height: '3px',
|
||||
width: '100px'
|
||||
height: `${DRAG_HANDLE_TOP_PANEL_HEIGHT}px`,
|
||||
width: `${DRAG_HANDLE_TOP_PANEL_WIDTH}px`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
dragHandle: {
|
||||
// Keep the same visual appearance on all devices
|
||||
backgroundColor: theme.palette.filmstripDragHandle,
|
||||
height: '100px',
|
||||
width: '3px',
|
||||
height: `${DRAG_HANDLE_HEIGHT}px`,
|
||||
width: `${DRAG_HANDLE_WIDTH / 3}px`,
|
||||
borderRadius: '1px'
|
||||
}
|
||||
};
|
||||
@@ -313,6 +355,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_isToolboxVisible: Boolean;
|
||||
|
||||
/**
|
||||
* Whether the device has touch capability.
|
||||
*/
|
||||
_isTouchDevice?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the current layout is vertical filmstrip.
|
||||
*/
|
||||
@@ -358,6 +405,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_resizableFilmstrip: boolean;
|
||||
|
||||
/**
|
||||
* Whether resize functionality should be enabled based on device and screen size.
|
||||
*/
|
||||
_resizeEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* The number of rows in tile view.
|
||||
*/
|
||||
@@ -491,8 +543,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
|
||||
this._onListItemsRendered = this._onListItemsRendered.bind(this);
|
||||
this._onToggleButtonTouch = this._onToggleButtonTouch.bind(this);
|
||||
this._onDragHandleMouseDown = this._onDragHandleMouseDown.bind(this);
|
||||
this._onDragMouseUp = this._onDragMouseUp.bind(this);
|
||||
this._onDragHandlePointerDown = this._onDragHandlePointerDown.bind(this);
|
||||
this._onDragHandleClick = this._onDragHandleClick.bind(this);
|
||||
this._onDragHandleTouchStart = this._onDragHandleTouchStart.bind(this);
|
||||
this._onDragPointerUp = this._onDragPointerUp.bind(this);
|
||||
this._onFilmstripResize = this._onFilmstripResize.bind(this);
|
||||
|
||||
this._throttledResize = throttle(
|
||||
@@ -516,10 +570,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
handler: this._onShortcutToggleFilmstrip
|
||||
}));
|
||||
|
||||
document.addEventListener('mouseup', this._onDragMouseUp);
|
||||
document.addEventListener('pointerup', this._onDragPointerUp);
|
||||
|
||||
// @ts-ignore
|
||||
document.addEventListener('mousemove', this._throttledResize);
|
||||
document.addEventListener('pointermove', this._throttledResize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -530,10 +584,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
override componentWillUnmount() {
|
||||
this.props.dispatch(unregisterShortcut('F'));
|
||||
|
||||
document.removeEventListener('mouseup', this._onDragMouseUp);
|
||||
document.removeEventListener('pointerup', this._onDragPointerUp);
|
||||
|
||||
// @ts-ignore
|
||||
document.removeEventListener('mousemove', this._throttledResize);
|
||||
document.removeEventListener('pointermove', this._throttledResize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,7 +732,9 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
(isMouseDown || _alwaysShowResizeBar) && 'visible',
|
||||
_topPanelFilmstrip && 'top-panel')
|
||||
}
|
||||
onMouseDown = { this._onDragHandleMouseDown }>
|
||||
onClick = { this._onDragHandleClick }
|
||||
onPointerDown = { this._onDragHandlePointerDown }
|
||||
onTouchStart = { this._onDragHandleTouchStart }>
|
||||
<div className = { clsx(classes.dragHandle, 'dragHandle') } />
|
||||
</div>
|
||||
{filmstrip}
|
||||
@@ -691,14 +747,23 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse down on the drag handle.
|
||||
* Handles pointer down on the drag handle.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {MouseEvent} e - The mouse down event.
|
||||
* @param {React.PointerEvent} e - The pointer down event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandleMouseDown(e: React.MouseEvent) {
|
||||
_onDragHandlePointerDown(e: React.PointerEvent) {
|
||||
const { _topPanelFilmstrip, _topPanelHeight, _verticalFilmstripWidth } = this.props;
|
||||
|
||||
// Prevent toolbar from appearing and stop event propagation
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Capture the pointer to ensure we receive all pointer events
|
||||
// even if the pointer moves outside the element
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
|
||||
this.setState({
|
||||
isMouseDown: true,
|
||||
mousePosition: _topPanelFilmstrip ? e.clientY : e.clientX,
|
||||
@@ -709,11 +774,33 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag handle mouse up handler.
|
||||
* Prevents click events on drag handle from triggering toolbar.
|
||||
*
|
||||
* @param {React.MouseEvent} e - The click event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandleClick(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents touch start events on drag handle from triggering toolbar.
|
||||
*
|
||||
* @param {React.TouchEvent} e - The touch start event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandleTouchStart(e: React.TouchEvent) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag handle pointer up handler.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragMouseUp() {
|
||||
_onDragPointerUp() {
|
||||
if (this.state.isMouseDown) {
|
||||
this.setState({
|
||||
isMouseDown: false
|
||||
@@ -723,12 +810,13 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles drag handle mouse move.
|
||||
* Handles drag handle pointer move.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {MouseEvent} e - The mousemove event.
|
||||
* @param {PointerEvent} e - The pointermove event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFilmstripResize(e: React.MouseEvent) {
|
||||
_onFilmstripResize(e: PointerEvent) {
|
||||
if (this.state.isMouseDown) {
|
||||
const {
|
||||
dispatch,
|
||||
@@ -857,10 +945,33 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
*/
|
||||
_onListItemsRendered({ visibleStartIndex, visibleStopIndex }: {
|
||||
visibleStartIndex: number; visibleStopIndex: number; }) {
|
||||
const { dispatch } = this.props;
|
||||
const {
|
||||
dispatch,
|
||||
_currentLayout,
|
||||
_filmstripWidth,
|
||||
_filmstripHeight,
|
||||
_thumbnailWidth,
|
||||
_thumbnailHeight,
|
||||
_isVerticalFilmstrip
|
||||
} = this.props;
|
||||
|
||||
// Calculate fully visible count (excluding partially visible tiles)
|
||||
const isHorizontal = _currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
|
||||
const itemSize = isHorizontal
|
||||
? _thumbnailWidth + TILE_HORIZONTAL_MARGIN
|
||||
: _thumbnailHeight + TILE_VERTICAL_MARGIN;
|
||||
const containerSize = isHorizontal ? _filmstripWidth : _filmstripHeight;
|
||||
|
||||
const fullyVisibleCount = calculateFullyVisibleParticipantsCount(
|
||||
visibleStartIndex,
|
||||
visibleStopIndex,
|
||||
containerSize,
|
||||
itemSize
|
||||
);
|
||||
|
||||
const { startIndex, stopIndex } = this._calculateIndices(visibleStartIndex, visibleStopIndex);
|
||||
|
||||
dispatch(setVisibleRemoteParticipants(startIndex, stopIndex));
|
||||
dispatch(setVisibleRemoteParticipants(startIndex, stopIndex, fullyVisibleCount));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1163,4 +1274,4 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
};
|
||||
}
|
||||
|
||||
export default withStyles(translate(connect(_mapStateToProps)(Filmstrip)), styles);
|
||||
export default translate(connect(_mapStateToProps)(withStyles(Filmstrip, styles)));
|
||||
|
||||
@@ -353,7 +353,7 @@ const defaultStyles = (theme: Theme) => {
|
||||
|
||||
tintBackground: {
|
||||
position: 'absolute' as const,
|
||||
zIndex: 1,
|
||||
zIndex: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: theme.palette.thumbnailTintBackground,
|
||||
|
||||
@@ -269,6 +269,19 @@ export const MIN_STAGE_VIEW_HEIGHT = 700;
|
||||
*/
|
||||
export const MIN_STAGE_VIEW_WIDTH = 800;
|
||||
|
||||
/**
|
||||
* Drag handle dimensions for resizable filmstrip.
|
||||
*/
|
||||
export const DRAG_HANDLE_WIDTH = 9;
|
||||
export const DRAG_HANDLE_HEIGHT = 100;
|
||||
export const DRAG_HANDLE_TOP_PANEL_HEIGHT = 3;
|
||||
export const DRAG_HANDLE_TOP_PANEL_WIDTH = 100;
|
||||
|
||||
/**
|
||||
* Touch padding added to each side of drag handle for easier tapping on touch devices.
|
||||
*/
|
||||
export const TOUCH_DRAG_HANDLE_PADDING = 6;
|
||||
|
||||
/**
|
||||
* Horizontal margin used for the vertical filmstrip.
|
||||
*/
|
||||
@@ -298,3 +311,9 @@ export const MAX_ACTIVE_PARTICIPANTS = 6;
|
||||
* Top filmstrip default height.
|
||||
*/
|
||||
export const TOP_FILMSTRIP_HEIGHT = 180;
|
||||
|
||||
/**
|
||||
* Minimum screen width needed for functional filmstrip resizing.
|
||||
* Calculated as stage minimum + filmstrip minimum (800px + 120px = 920px).
|
||||
*/
|
||||
export const MIN_FILMSTRIP_RESIZE_WIDTH = MIN_STAGE_VIEW_WIDTH + DEFAULT_FILMSTRIP_WIDTH;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import {
|
||||
getActiveSpeakersToBeDisplayed,
|
||||
getVirtualScreenshareParticipantOwnerId
|
||||
} from '../base/participants/functions';
|
||||
import { getParticipantById, getVirtualScreenshareParticipantOwnerId } from '../base/participants/functions';
|
||||
|
||||
import { setRemoteParticipants } from './actions';
|
||||
import { isFilmstripScrollVisible } from './functions';
|
||||
@@ -33,44 +30,70 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
|
||||
}
|
||||
|
||||
const {
|
||||
activeSpeakers,
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
sortedRemoteParticipants
|
||||
} = state['features/base/participants'];
|
||||
const config = state['features/base/config'];
|
||||
const defaultRemoteDisplayName = config?.defaultRemoteDisplayName ?? 'Fellow Jitster';
|
||||
const dominant = dominantSpeaker ? getParticipantById(state, dominantSpeaker) : undefined;
|
||||
let dominantSpeakerSlot = 0;
|
||||
const previousSpeakers = new Set(activeSpeakers);
|
||||
const remoteParticipants = new Map(sortedRemoteParticipants);
|
||||
const screenShareParticipants = sortedRemoteVirtualScreenshareParticipants
|
||||
? [ ...sortedRemoteVirtualScreenshareParticipants.keys() ] : [];
|
||||
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
|
||||
const speakers = getActiveSpeakersToBeDisplayed(state);
|
||||
const speakers = new Array<string>();
|
||||
const { fullyVisibleRemoteParticipantsCount } = state['features/filmstrip'];
|
||||
|
||||
for (const screenshare of screenShareParticipants) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
|
||||
|
||||
remoteParticipants.delete(ownerId);
|
||||
remoteParticipants.delete(screenshare);
|
||||
speakers.delete(ownerId);
|
||||
}
|
||||
|
||||
for (const sharedVideo of sharedVideos) {
|
||||
remoteParticipants.delete(sharedVideo);
|
||||
}
|
||||
for (const speaker of speakers.keys()) {
|
||||
remoteParticipants.delete(speaker);
|
||||
}
|
||||
|
||||
// Always update the order of the thubmnails.
|
||||
const participantsWithScreenShare = screenShareParticipants.reduce<string[]>((acc, screenshare) => {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
|
||||
|
||||
acc.push(ownerId);
|
||||
acc.push(screenshare);
|
||||
remoteParticipants.delete(ownerId);
|
||||
remoteParticipants.delete(screenshare);
|
||||
previousSpeakers.delete(ownerId);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
for (const sharedVideo of sharedVideos) {
|
||||
remoteParticipants.delete(sharedVideo);
|
||||
}
|
||||
|
||||
if (dominant && !dominant.local && participantsWithScreenShare.indexOf(dominant.id) === -1) {
|
||||
dominantSpeakerSlot = 1;
|
||||
remoteParticipants.delete(dominant.id);
|
||||
speakers.push(dominant.id);
|
||||
}
|
||||
|
||||
// Find the number of slots available for speakers. Use fullyVisibleRemoteParticipantsCount to exclude partially
|
||||
// visible tiles, ensuring dominant speaker is placed on a fully visible tile.
|
||||
const slotsForSpeakers
|
||||
= fullyVisibleRemoteParticipantsCount
|
||||
- (screenShareParticipants.length * 2)
|
||||
- sharedVideos.length
|
||||
- dominantSpeakerSlot;
|
||||
|
||||
// Construct the list of speakers to be shown.
|
||||
if (slotsForSpeakers > 0) {
|
||||
Array.from(previousSpeakers).slice(0, slotsForSpeakers).forEach((speakerId: string) => {
|
||||
speakers.push(speakerId);
|
||||
remoteParticipants.delete(speakerId);
|
||||
});
|
||||
speakers.sort((a: string, b: string) => {
|
||||
return (getParticipantById(state, a)?.name ?? defaultRemoteDisplayName)
|
||||
.localeCompare(getParticipantById(state, b)?.name ?? defaultRemoteDisplayName);
|
||||
});
|
||||
}
|
||||
|
||||
// Always update the order of the thumbnails.
|
||||
reorderedParticipants = [
|
||||
...participantsWithScreenShare,
|
||||
...sharedVideos,
|
||||
...Array.from(speakers.keys()),
|
||||
...speakers,
|
||||
...Array.from(remoteParticipants.keys())
|
||||
];
|
||||
|
||||
@@ -108,3 +131,31 @@ export function isTileViewModeDisabled(state: IReduxState) {
|
||||
|
||||
return tileView.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the count of fully visible participants, excluding any partially visible tiles.
|
||||
* This respects the actual rendered items from the list component while accounting for
|
||||
* container padding/gaps.
|
||||
*
|
||||
* @param {number} visibleStartIndex - The start index of visible items.
|
||||
* @param {number} visibleEndIndex - The end index of visible items.
|
||||
* @param {number} containerSize - The width or height of the filmstrip container.
|
||||
* @param {number} itemSize - The width or height of each item including margin.
|
||||
* @returns {number} - The count of fully visible participants (at least 1).
|
||||
*/
|
||||
export function calculateFullyVisibleParticipantsCount(
|
||||
visibleStartIndex: number,
|
||||
visibleEndIndex: number,
|
||||
containerSize: number,
|
||||
itemSize: number
|
||||
): number {
|
||||
// Current visible count from the list component (includes any partially visible tile)
|
||||
const currentVisibleCount = visibleEndIndex - visibleStartIndex + 1;
|
||||
|
||||
// Theoretical max that can fit fully in the container
|
||||
const maxFullyVisible = Math.floor(containerSize / itemSize);
|
||||
|
||||
// Fully visible count is the minimum of actual visible and max that can fit fully
|
||||
// Ensure at least 1 if there are any visible items
|
||||
return Math.max(1, Math.min(currentVisibleCount, maxFullyVisible));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Theme } from '@mui/material/styles';
|
||||
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
import { isTouchDevice, shouldEnableResize } from '../base/environment/utils';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
DEFAULT_LOCAL_TILE_ASPECT_RATIO,
|
||||
DISPLAY_AVATAR,
|
||||
DISPLAY_VIDEO,
|
||||
DRAG_HANDLE_WIDTH,
|
||||
FILMSTRIP_GRID_BREAKPOINT,
|
||||
FILMSTRIP_TYPE,
|
||||
INDICATORS_TOOLTIP_POSITION,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
|
||||
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
|
||||
TILE_VIEW_GRID_VERTICAL_MARGIN,
|
||||
TOUCH_DRAG_HANDLE_PADDING,
|
||||
VERTICAL_VIEW_HORIZONTAL_MARGIN
|
||||
} from './constants';
|
||||
|
||||
@@ -621,6 +623,7 @@ export function getIndicatorsTooltipPosition(thumbnailType?: string) {
|
||||
|
||||
/**
|
||||
* Returns whether or not the filmstrip is resizable.
|
||||
* On touch devices, resize is only enabled for larger screens (tablets, not phones).
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
@@ -629,7 +632,7 @@ export function isFilmstripResizable(state: IReduxState) {
|
||||
const { filmstrip } = state['features/base/config'];
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
|
||||
return !filmstrip?.disableResizable && !isMobileBrowser()
|
||||
return !filmstrip?.disableResizable && shouldEnableResize()
|
||||
&& (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW || _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
|
||||
}
|
||||
|
||||
@@ -662,8 +665,13 @@ export function getVerticalViewMaxWidth(state: IReduxState) {
|
||||
|
||||
// Adding 4px for the border-right and margin-right.
|
||||
// On non-resizable filmstrip add 4px for the left margin and border.
|
||||
// Also adding 7px for the scrollbar. Also adding 9px for the drag handle.
|
||||
maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? 9 : 4);
|
||||
// Also adding 7px for the scrollbar.
|
||||
// Drag handle: DRAG_HANDLE_WIDTH + padding (TOUCH_DRAG_HANDLE_PADDING on each side for touch)
|
||||
const dragHandleWidth = isTouchDevice()
|
||||
? DRAG_HANDLE_WIDTH + (TOUCH_DRAG_HANDLE_PADDING * 2)
|
||||
: DRAG_HANDLE_WIDTH;
|
||||
|
||||
maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? dragHandleWidth : 4);
|
||||
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,15 @@ const DEFAULT_STATE = {
|
||||
*/
|
||||
visibleRemoteParticipants: new Set<string>(),
|
||||
|
||||
/**
|
||||
* The number of fully visible remote participants (excluding partially visible ones).
|
||||
* Used for calculating speaker slots to avoid placing dominant speaker on partially visible tiles.
|
||||
*
|
||||
* @public
|
||||
* @type {number}
|
||||
*/
|
||||
fullyVisibleRemoteParticipantsCount: 0,
|
||||
|
||||
/**
|
||||
* The width of the resizable filmstrip.
|
||||
*
|
||||
@@ -201,6 +210,7 @@ export interface IFilmstripState {
|
||||
pinned?: boolean;
|
||||
}>;
|
||||
enabled: boolean;
|
||||
fullyVisibleRemoteParticipantsCount: number;
|
||||
horizontalViewDimensions: {
|
||||
hasScroll?: boolean;
|
||||
local?: IDimensions;
|
||||
@@ -301,7 +311,7 @@ ReducerRegistry.register<IFilmstripState>(
|
||||
}
|
||||
};
|
||||
case SET_VISIBLE_REMOTE_PARTICIPANTS: {
|
||||
const { endIndex, startIndex } = action;
|
||||
const { endIndex, startIndex, fullyVisibleCount } = action;
|
||||
const { remoteParticipants } = state;
|
||||
const visibleRemoteParticipants = new Set(remoteParticipants.slice(startIndex, endIndex + 1));
|
||||
|
||||
@@ -309,7 +319,8 @@ ReducerRegistry.register<IFilmstripState>(
|
||||
...state,
|
||||
visibleParticipantsStartIndex: startIndex,
|
||||
visibleParticipantsEndIndex: endIndex,
|
||||
visibleRemoteParticipants
|
||||
visibleRemoteParticipants,
|
||||
fullyVisibleRemoteParticipantsCount: fullyVisibleCount ?? visibleRemoteParticipants.size
|
||||
};
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
|
||||
@@ -70,10 +70,15 @@ export function getDialInConferenceID(
|
||||
* phone number strings, as the second one should not be used and is deprecated.
|
||||
*/
|
||||
export function getDialInNumbers(
|
||||
url: string,
|
||||
roomName: string,
|
||||
mucURL: string
|
||||
url?: string,
|
||||
roomName?: string,
|
||||
mucURL?: string
|
||||
): Promise<any> {
|
||||
|
||||
if (!url || !roomName || !mucURL) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
|
||||
// when roomName and mucURL are available
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
import { INVITE_TYPES } from './constants';
|
||||
import {
|
||||
invitePeopleAndChatRooms,
|
||||
inviteSipEndpoints
|
||||
inviteSipEndpoints,
|
||||
isDialInEnabled
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IInvitee } from './types';
|
||||
@@ -210,7 +211,7 @@ export function updateDialInNumbers() {
|
||||
const { numbersFetched } = state['features/invite'];
|
||||
const mucURL = hosts?.muc;
|
||||
|
||||
if (numbersFetched || !dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
|
||||
if (numbersFetched || !isDialInEnabled(state)) {
|
||||
// URLs for fetching dial in numbers not defined
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -496,6 +496,26 @@ export function isDialOutEnabled(state: IReduxState): boolean {
|
||||
return isJwtFeatureEnabled(state, MEET_FEATURES.OUTBOUND_CALL, false) && conference?.isSIPCallingSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if dial out is currently enabled or not.
|
||||
*
|
||||
* @param {IReduxState} state - Current state.
|
||||
* @returns {boolean} Indication of whether dial out is currently enabled.
|
||||
*/
|
||||
export function isDialInEnabled(state: IReduxState): boolean {
|
||||
const dialInDisabled = state['features/base/conference']
|
||||
.conference?.getMetadataHandler()?.getMetadata()?.dialinEnabled === false;
|
||||
|
||||
if (dialInDisabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { dialInConfCodeUrl, dialInNumbersUrl, hosts } = state['features/base/config'];
|
||||
const mucURL = hosts?.muc;
|
||||
|
||||
return Boolean(dialInConfCodeUrl && dialInNumbersUrl && mucURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if inviting sip endpoints is enabled or not.
|
||||
*
|
||||
@@ -634,7 +654,7 @@ export function getShareInfoText(
|
||||
const { locationURL = {} } = state['features/base/connection'];
|
||||
const mucURL = hosts?.muc;
|
||||
|
||||
if (skipDialIn || !dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
|
||||
if (skipDialIn || !isDialInEnabled(state)) {
|
||||
// URLs for fetching dial in numbers not defined.
|
||||
return Promise.resolve(infoText);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { conferenceWillJoin } from '../../base/conference/actions';
|
||||
import { conferenceWillJoin } from '../../base/conference/actions.any';
|
||||
import { getConferenceName } from '../../base/conference/functions';
|
||||
import { IJitsiConference } from '../../base/conference/reducer';
|
||||
import { getSecurityUiConfig } from '../../base/config/functions.any';
|
||||
@@ -11,9 +11,8 @@ import { getLocalParticipant } from '../../base/participants/functions';
|
||||
import { getFieldValue } from '../../base/react/functions';
|
||||
import { updateSettings } from '../../base/settings/actions';
|
||||
import { IMessage } from '../../chat/types';
|
||||
import { isDeviceStatusVisible } from '../../prejoin/functions';
|
||||
import { isDeviceStatusVisible } from '../../prejoin/functions.any';
|
||||
import { cancelKnocking, joinWithPassword, onSendMessage, setPasswordJoinFailed, startKnocking } from '../actions';
|
||||
import { getLobbyConfig } from '../functions';
|
||||
|
||||
export const SCREEN_STATES = {
|
||||
EDIT: 1,
|
||||
@@ -28,11 +27,6 @@ export interface IProps {
|
||||
*/
|
||||
_deviceStatusVisible: boolean;
|
||||
|
||||
/**
|
||||
* Whether to show the hangup button.
|
||||
*/
|
||||
_hangUp?: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether the message that display name is required is shown.
|
||||
*/
|
||||
@@ -456,7 +450,6 @@ export function _mapStateToProps(state: IReduxState) {
|
||||
const { disableLobbyPassword } = getSecurityUiConfig(state);
|
||||
const showCopyUrlButton = inviteEnabledFlag || !disableInviteFunctions;
|
||||
const deviceStatusVisible = isDeviceStatusVisible(state);
|
||||
const { showHangUp = true } = getLobbyConfig(state);
|
||||
const { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
|
||||
const { isLobbyChatActive, lobbyMessageRecipient, messages } = state['features/chat'];
|
||||
const { showModeratorLogin } = state['features/authentication'];
|
||||
@@ -468,7 +461,6 @@ export function _mapStateToProps(state: IReduxState) {
|
||||
_lobbyChatMessages: messages,
|
||||
_lobbyMessageRecipient: lobbyMessageRecipient?.name,
|
||||
_login: showModeratorLogin && !state['features/base/jwt'].jwt,
|
||||
_hangUp: showHangUp,
|
||||
_isLobbyChatActive: isLobbyChatActive,
|
||||
_meetingName: getConferenceName(state),
|
||||
_membersOnlyConference: membersOnly,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { navigate }
|
||||
from '../../../mobile/navigation/components/lobby/LobbyNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import { preJoinStyles } from '../../../prejoin/components/native/styles';
|
||||
import HangupButton from '../../../toolbox/components/HangupButton';
|
||||
import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton';
|
||||
import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton';
|
||||
import AbstractLobbyScreen, {
|
||||
@@ -221,19 +220,12 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderToolbarButtons() {
|
||||
const { _hangUp } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { preJoinStyles.toolboxContainer as ViewStyle }>
|
||||
<AudioMuteButton
|
||||
styles = { preJoinStyles.buttonStylesBorderless } />
|
||||
<VideoMuteButton
|
||||
styles = { preJoinStyles.buttonStylesBorderless } />
|
||||
{
|
||||
_hangUp
|
||||
&& <HangupButton
|
||||
styles = { preJoinStyles.buttonStylesBorderless } />
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,6 +336,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
|
||||
const result = next(action);
|
||||
|
||||
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
|
||||
dispatch(hideLobbyScreen());
|
||||
dispatch(
|
||||
showNotification({
|
||||
appearance: NOTIFICATION_TYPE.ERROR,
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
SET_PASSWORD
|
||||
} from '../base/conference/actionTypes';
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
@@ -70,7 +69,6 @@ ReducerRegistry.register<ILobbyState>('features/lobby', (state = DEFAULT_STATE,
|
||||
}
|
||||
case CONFERENCE_JOINED:
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
return {
|
||||
...state,
|
||||
isDisplayNameRequiredError: false,
|
||||
|
||||
@@ -54,7 +54,7 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
fontWeight: 600,
|
||||
height: '100%',
|
||||
|
||||
[[ '& > *:first-of-type', '& > *:last-of-type' ] as any]: {
|
||||
[[ '& > *:first-child', '& > *:last-child' ] as any]: {
|
||||
flexShrink: 0
|
||||
},
|
||||
|
||||
@@ -116,11 +116,11 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
antiCollapse: {
|
||||
fontSize: 0,
|
||||
|
||||
'&:first-of-type': {
|
||||
'&:first-child': {
|
||||
display: 'none'
|
||||
},
|
||||
|
||||
'&:first-of-type + *': {
|
||||
'&:first-child + *': {
|
||||
marginTop: 0
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,11 +33,9 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { openDisplayNamePrompt } from '../../../display-name/actions';
|
||||
import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground';
|
||||
import LargeVideo from '../../../large-video/components/LargeVideo.native';
|
||||
import { getLobbyConfig } from '../../../lobby/functions';
|
||||
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import { navigateRoot } from '../../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import HangupButton from '../../../toolbox/components/HangupButton';
|
||||
import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton';
|
||||
import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton';
|
||||
import { isDisplayNameRequired, isRoomNameEnabled } from '../../functions.native';
|
||||
@@ -62,9 +60,6 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
const isDisplayNameReadonly = useSelector(isNameReadOnly);
|
||||
const roomName = useSelector((state: IReduxState) => getConferenceName(state));
|
||||
const roomNameEnabled = useSelector((state: IReduxState) => isRoomNameEnabled(state));
|
||||
const { showHangUp: showHangUpLobby = true } = useSelector((state: IReduxState) => getLobbyConfig(state));
|
||||
const { showHangUp: showHangUpPrejoin = true } = useSelector((state: IReduxState) => state['features/base/config'].prejoinConfig || {});
|
||||
const { knocking } = useSelector((state: IReduxState) => state['features/lobby']);
|
||||
const participantName = localParticipant?.name;
|
||||
const [ displayName, setDisplayName ]
|
||||
= useState(participantName || '');
|
||||
@@ -186,11 +181,6 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
styles = { styles.buttonStylesBorderless } />
|
||||
<VideoMuteButton
|
||||
styles = { styles.buttonStylesBorderless } />
|
||||
{
|
||||
(knocking ? showHangUpLobby : showHangUpPrejoin)
|
||||
&& <HangupButton
|
||||
styles = { styles.buttonStylesBorderless } />
|
||||
}
|
||||
</View>
|
||||
{
|
||||
showDisplayNameInput && <Input
|
||||
|
||||
@@ -103,13 +103,13 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent {
|
||||
<div className = 'recording-header space-top'>
|
||||
<label
|
||||
className = 'recording-title'
|
||||
htmlFor = 'recording-switch-transcription'>
|
||||
htmlFor = 'recording-switch-audio-video'>
|
||||
{ t('recording.recordAudioAndVideo') }
|
||||
</label>
|
||||
<Switch
|
||||
checked = { shouldRecordAudioAndVideo }
|
||||
className = 'recording-switch'
|
||||
id = 'recording-switch-transcription'
|
||||
id = 'recording-switch-audio-video'
|
||||
onChange = { this._onRecordAudioAndVideoSwitchChange } />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||
import { getSoundFileSrc } from '../base/media/functions';
|
||||
import { getLocalParticipant, getRemoteParticipants } from '../base/participants/functions';
|
||||
import { registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
import { isEmbedded } from '../base/util/embedUtils';
|
||||
import { isEmbedded, isEmbeddedFromSameDomain } from '../base/util/embedUtils';
|
||||
import { isSpotTV } from '../base/util/spot';
|
||||
import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functions';
|
||||
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
|
||||
@@ -153,7 +153,7 @@ export function getSessionStatusToShow(state: IReduxState, mode: string): string
|
||||
* @returns {boolean} - Whether local recording is supported or not.
|
||||
*/
|
||||
export function supportsLocalRecording() {
|
||||
return LocalRecordingManager.isSupported() && !isEmbedded();
|
||||
return LocalRecordingManager.isSupported() && (!isEmbedded() || isEmbeddedFromSameDomain());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -244,11 +244,11 @@ const AudioSettingsContent = ({
|
||||
* @returns {void}
|
||||
*/
|
||||
const _setTracks = async () => {
|
||||
if (browser.isWebKitBased()) {
|
||||
|
||||
if (!measureAudioLevels || browser.isWebKitBased()) {
|
||||
// It appears that at the time of this writing, creating audio tracks blocks the browser's main thread for
|
||||
// long time on safari. Wasn't able to confirm which part of track creation does the blocking exactly, but
|
||||
// not creating the tracks seems to help and makes the UI much more responsive.
|
||||
// Also skip when audio levels are disabled to avoid activating all microphones unnecessarily.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ const useStyles = makeStyles()(theme => {
|
||||
slider: {
|
||||
// Use an additional class here to override global CSS specificity
|
||||
'&.custom-slider': {
|
||||
WebkitAppearance: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
background: 'transparent',
|
||||
height,
|
||||
left: 0,
|
||||
@@ -104,11 +104,11 @@ const useStyles = makeStyles()(theme => {
|
||||
},
|
||||
|
||||
'&::-webkit-slider-runnable-track': {
|
||||
WebkitAppearance: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
...inputTrack
|
||||
},
|
||||
'&::-webkit-slider-thumb': {
|
||||
WebkitAppearance: 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
position: 'relative',
|
||||
top: -6,
|
||||
...inputThumb
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { IconCalendar, IconGear, IconRestore } from '../base/icons/svg';
|
||||
import BaseTheme from '../base/ui/components/BaseTheme';
|
||||
import BaseTheme from '../base/ui/components/BaseTheme.native';
|
||||
|
||||
import TabIcon from './components/TabIcon';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { App } from './features/app/components/App.web';
|
||||
import { getLogger } from './features/base/logging/functions';
|
||||
@@ -72,17 +72,14 @@ globalNS.entryPoints = {
|
||||
WHITEBOARD: WhiteboardApp
|
||||
};
|
||||
|
||||
const _roots = new Map();
|
||||
|
||||
globalNS.renderEntryPoint = ({
|
||||
Component,
|
||||
props = {},
|
||||
elementId = 'react'
|
||||
}) => {
|
||||
const element = document.getElementById(elementId);
|
||||
|
||||
if (!_roots.has(elementId)) {
|
||||
_roots.set(elementId, createRoot(element));
|
||||
}
|
||||
_roots.get(elementId).render(<Component { ...props } />);
|
||||
/* eslint-disable-next-line react/no-deprecated */
|
||||
ReactDOM.render(
|
||||
<Component { ...props } />,
|
||||
document.getElementById(elementId)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,26 +95,27 @@ module:hook("pre-iq/full", function(event)
|
||||
local room_jid = jid.bare(stanza.attr.to);
|
||||
local room_real_jid = room_jid_match_rewrite(room_jid);
|
||||
local room = main_muc_service.get_room_from_jid(room_real_jid);
|
||||
local is_sender_in_room = room:get_occupant_jid(stanza.attr.from) ~= nil;
|
||||
|
||||
if not room or not is_sender_in_room then
|
||||
module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
|
||||
session.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
|
||||
local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call';
|
||||
local is_session_allowed = is_feature_allowed(
|
||||
local error_message = nil;
|
||||
|
||||
if not room or room:get_occupant_jid(stanza.attr.from) == nil then
|
||||
error_message = "not in room";
|
||||
elseif roomName == nil then
|
||||
error_message = OUT_ROOM_NAME_ATTR_NAME.." header missing";
|
||||
elseif roomName ~= room_jid then
|
||||
error_message = OUT_ROOM_NAME_ATTR_NAME.." header mismatch";
|
||||
elseif (token ~= nil and not token_util:verify_room(session, room_real_jid)) then
|
||||
error_message = "no token or token room mismatch";
|
||||
elseif not is_feature_allowed(
|
||||
feature,
|
||||
session.jitsi_meet_context_features,
|
||||
room:get_affiliation(stanza.attr.from) == 'owner');
|
||||
room:get_affiliation(stanza.attr.from) == 'owner') then
|
||||
|
||||
if roomName == nil
|
||||
or roomName ~= room_jid
|
||||
or (token ~= nil and not token_util:verify_room(session, room_real_jid))
|
||||
or not is_session_allowed
|
||||
then
|
||||
module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
|
||||
error_message = "feature not allowed";
|
||||
end
|
||||
|
||||
if error_message then
|
||||
module:log("warn", "Filtering stanza dial, %s, stanza:%s", error_message, tostring(stanza));
|
||||
session.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ function insertTextMsg(id, msg) {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
if (el) {
|
||||
el.innerHTML = msg;
|
||||
el.textContent = msg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ export const P1 = 'p1';
|
||||
export const P2 = 'p2';
|
||||
export const P3 = 'p3';
|
||||
export const P4 = 'p4';
|
||||
export const P5 = 'p5';
|
||||
export const P6 = 'p6';
|
||||
|
||||
/**
|
||||
* Participant.
|
||||
|
||||
@@ -38,8 +38,9 @@ export default class WebhookProxy {
|
||||
|
||||
this.ws.on('error', console.error);
|
||||
|
||||
this.ws.on('open', function open() {
|
||||
this.ws.on('open', () => {
|
||||
console.log('WebhookProxy connected');
|
||||
this.logInfo('connected');
|
||||
});
|
||||
|
||||
this.ws.on('message', (data: any) => {
|
||||
|
||||
@@ -22,6 +22,12 @@ const defaultExpectations = {
|
||||
liveStreamingEnabled: true,
|
||||
recordingEnabled: true,
|
||||
transcriptionEnabled: true,
|
||||
transcription: {
|
||||
// Expect "async" transcription to be enabled.
|
||||
asyncTranscription: true,
|
||||
// Expect the JaaS transcription webhooks to fire in "async" mode.
|
||||
asyncTranscriptionWebhook: false
|
||||
},
|
||||
/**
|
||||
* Whether the jaas account is configured with the account-level setting to allow unauthenticated users to join.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { P1, P2, P3, P4, Participant } from './Participant';
|
||||
import { P1, P2, P3, P4, P5, P6, Participant } from './Participant';
|
||||
import { config } from './TestsConfig';
|
||||
import { IJoinOptions, IParticipantOptions } from './types';
|
||||
|
||||
@@ -122,6 +122,50 @@ export async function ensureFourParticipants(options?: IJoinOptions): Promise<vo
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are six participants.
|
||||
*
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureSixParticipants(options?: IJoinOptions): Promise<void> {
|
||||
await ensureOneParticipant(options);
|
||||
|
||||
// Join participants in batches
|
||||
await Promise.all([
|
||||
joinParticipant({ name: P2 }, options),
|
||||
joinParticipant({ name: P3 }, options),
|
||||
joinParticipant({ name: P4 }, options)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
joinParticipant({ name: P5 }, options),
|
||||
joinParticipant({ name: P6 }, options)
|
||||
]);
|
||||
|
||||
if (options?.skipInMeetingChecks) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
ctx.p1.waitForIceConnected(),
|
||||
ctx.p2.waitForIceConnected(),
|
||||
ctx.p3.waitForIceConnected(),
|
||||
ctx.p4.waitForIceConnected(),
|
||||
ctx.p5.waitForIceConnected(),
|
||||
ctx.p6.waitForIceConnected()
|
||||
]);
|
||||
await Promise.all([
|
||||
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
|
||||
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
|
||||
ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
|
||||
ctx.p4.waitForSendReceiveData().then(() => ctx.p4.waitForRemoteStreams(1)),
|
||||
ctx.p5.waitForSendReceiveData().then(() => ctx.p5.waitForRemoteStreams(1)),
|
||||
ctx.p6.waitForSendReceiveData().then(() => ctx.p6.waitForRemoteStreams(1))
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that there are two participants.
|
||||
*
|
||||
@@ -244,10 +288,16 @@ export async function checkForScreensharingTile(sharer: Participant, observer: P
|
||||
}
|
||||
|
||||
/**
|
||||
* Hangs up all participants (p1, p2, p3 and p4)
|
||||
* Hangs up all participants (p1, p2, p3, p4, p5, and p6)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function hangupAllParticipants() {
|
||||
return Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup(), ctx.p3?.hangup(), ctx.p4?.hangup() ]
|
||||
.map(p => p ?? Promise.resolve()));
|
||||
return Promise.all([
|
||||
ctx.p1?.hangup(),
|
||||
ctx.p2?.hangup(),
|
||||
ctx.p3?.hangup(),
|
||||
ctx.p4?.hangup(),
|
||||
ctx.p5?.hangup(),
|
||||
ctx.p6?.hangup()
|
||||
].map(p => p ?? Promise.resolve()));
|
||||
}
|
||||
|
||||
@@ -7,13 +7,16 @@ import { IToken, ITokenOptions } from './token';
|
||||
|
||||
export type IContext = {
|
||||
/**
|
||||
* The up-to-four browser instances provided by the framework. These can be initialized using
|
||||
* The up-to-six browser instances provided by the framework. These can be initialized using
|
||||
* ensureOneParticipant, ensureTwoParticipants, etc. from participants.ts.
|
||||
**/
|
||||
p1: Participant;
|
||||
p2: Participant;
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
p5: Participant;
|
||||
p6: Participant;
|
||||
|
||||
/** A room name automatically generated by the framework for convenience. */
|
||||
roomName: string;
|
||||
/**
|
||||
@@ -39,7 +42,7 @@ export type IParticipantOptions = {
|
||||
/** Whether it should use the iFrame API. */
|
||||
iFrameApi?: boolean;
|
||||
/** Determines the browser instance to use. */
|
||||
name: 'p1' | 'p2' | 'p3' | 'p4';
|
||||
name: 'p1' | 'p2' | 'p3' | 'p4' | 'p5' | 'p6';
|
||||
/** An optional token to use. */
|
||||
token?: IToken;
|
||||
};
|
||||
|
||||
@@ -20,13 +20,6 @@ describe('SIP jibri invite', () => {
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
if (true) {
|
||||
// This is temporary until we figure out how to fix it and configure it properly.
|
||||
ctx.skipSuiteTests = 'This test is disabled as the code doesn\'t work anymore.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dialOutUrl) {
|
||||
ctx.skipSuiteTests = 'SIP_JIBRI_DIAL_OUT_URL is not set.';
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
retry: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
for (const asyncTranscriptions of [ false, true ]) {
|
||||
const asyncTranscriptionValues = expectations.jaas.transcription.asyncTranscription ? [ false, true ] : [ false ];
|
||||
|
||||
for (const asyncTranscriptions of asyncTranscriptionValues) {
|
||||
describe(`Transcription (async=${asyncTranscriptions})`, () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
@@ -66,10 +67,12 @@ for (const asyncTranscriptions of [ false, true ]) {
|
||||
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
@@ -78,18 +81,29 @@ for (const asyncTranscriptions of [ false, true ]) {
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received by p1'
|
||||
});
|
||||
// The p1 event can be triggered locally, resulting in the code below executing prematurely. Make sure
|
||||
// transcription is indeed stopped on the backend before proceeding.
|
||||
await p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
.getEventResult('transcribingStatusChanged'), {
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received by p1'
|
||||
});
|
||||
});
|
||||
|
||||
it('set subtitles on and off', async () => {
|
||||
// we need to clear results or the last one will be used, from the previous time subtitles were on
|
||||
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', false);
|
||||
|
||||
@@ -98,6 +112,13 @@ for (const asyncTranscriptions of [ false, true ]) {
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received by p1'
|
||||
});
|
||||
// The p1 event can be triggered locally, resulting in the code below executing prematurely. Make sure
|
||||
// transcription is indeed stopped on the backend before proceeding.
|
||||
await p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
.getEventResult('transcribingStatusChanged'), {
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received by p1'
|
||||
});
|
||||
});
|
||||
|
||||
it('start/stop transcriptions via recording', async () => {
|
||||
@@ -132,7 +153,7 @@ for (const asyncTranscriptions of [ false, true ]) {
|
||||
expect(e.value.on).toBe(true);
|
||||
});
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
@@ -168,15 +189,17 @@ for (const asyncTranscriptions of [ false, true ]) {
|
||||
// let's wait for destroy event before waiting for those that depends on it
|
||||
await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
const event: {
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
|
||||
if (!asyncTranscriptions || expectations.jaas.transcription.asyncTranscriptionWebhook) {
|
||||
const event: {
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
|
||||
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_UPLOADED');
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_UPLOADED');
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -186,10 +209,9 @@ for (const asyncTranscriptions of [ false, true ]) {
|
||||
* @param p1
|
||||
* @param p2
|
||||
* @param webhooksProxy
|
||||
* @param expectName Whether to expect the events to contain the name of the participant. Currently, async
|
||||
* transcriptions do not include the name. TODO: remove this parameter when async transcription events are fixed.
|
||||
* @param asyncTranscription Whether async transciptions are used.
|
||||
*/
|
||||
async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy, expectName = true) {
|
||||
async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy, asyncTranscription = false) {
|
||||
const p1Promise = p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
@@ -202,7 +224,24 @@ async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksPr
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p2 side'
|
||||
});
|
||||
|
||||
const webhookPromise = async () => {
|
||||
const [ p1Event, p2Event ] = await Promise.all([ p1Promise, p2Promise ]);
|
||||
const p1Id = await p1.getEndpointId();
|
||||
|
||||
const p1Transcript = p1Event.data.stable || p1Event.data.final;
|
||||
const p2Transcript = p2Event.data.stable || p2Event.data.final;
|
||||
|
||||
expect(p2Transcript.includes(p1Transcript) || p1Transcript.includes(p2Transcript)).toBe(true);
|
||||
expect(p2Event.data.language).toBe(p1Event.data.language);
|
||||
expect(p2Event.data.messageID).toBe(p1Event.data.messageID);
|
||||
expect(p1Event.data.participant.id).toBe(p1Id);
|
||||
expect(p2Event.data.participant.id).toBe(p1Id);
|
||||
|
||||
if (!asyncTranscription) {
|
||||
expect(p1Event.data.participant.name).toBe(p1.name);
|
||||
expect(p2Event.data.participant.name).toBe(p1.name);
|
||||
}
|
||||
|
||||
if (!asyncTranscription || expectations.jaas.transcription.asyncTranscriptionWebhook) {
|
||||
const event: {
|
||||
data: {
|
||||
final: string;
|
||||
@@ -219,34 +258,14 @@ async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksPr
|
||||
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_CHUNK_RECEIVED');
|
||||
|
||||
event.data.stable = event.data.final;
|
||||
const webhookTranscript = event.data.final;
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
const [ p1Event, p2Event, webhookEvent ] = await Promise.all([ p1Promise, p2Promise, await webhookPromise() ]);
|
||||
const p1Id = await p1.getEndpointId();
|
||||
|
||||
const p1Transcript = p1Event.data.stable || p1Event.data.final;
|
||||
const p2Transcript = p2Event.data.stable || p2Event.data.final;
|
||||
const webhookTranscript = webhookEvent.data.stable || webhookEvent.data.final;
|
||||
|
||||
expect(p2Transcript.includes(p1Transcript) || p1Transcript.includes(p2Transcript)).toBe(true);
|
||||
expect(webhookTranscript.includes(p1Transcript) || p1Transcript.includes(webhookTranscript)).toBe(true);
|
||||
|
||||
expect(p2Event.data.language).toBe(p1Event.data.language);
|
||||
expect(webhookEvent.data.language).toBe(p1Event.data.language);
|
||||
|
||||
expect(p2Event.data.messageID).toBe(p1Event.data.messageID);
|
||||
expect(webhookEvent.data.messageID).toBe(p1Event.data.messageID);
|
||||
|
||||
expect(p1Event.data.participant.id).toBe(p1Id);
|
||||
expect(p2Event.data.participant.id).toBe(p1Id);
|
||||
expect(webhookEvent.data.participant.id).toBe(p1Id);
|
||||
|
||||
if (expectName) {
|
||||
expect(p1Event.data.participant.name).toBe(p1.name);
|
||||
expect(p2Event.data.participant.name).toBe(p1.name);
|
||||
expect(webhookEvent.data.participant.name).toBe(p1.name);
|
||||
expect(webhookTranscript.includes(p1Transcript) || p1Transcript.includes(webhookTranscript)).toBe(true);
|
||||
expect(event.data.language).toBe(p1Event.data.language);
|
||||
expect(event.data.messageID).toBe(p1Event.data.messageID);
|
||||
expect(event.data.participant.id).toBe(p1Id);
|
||||
if (!asyncTranscription) {
|
||||
expect(event.data.participant.name).toBe(p1.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureThreeParticipants } from '../../helpers/participants';
|
||||
import {
|
||||
checkForScreensharingTile,
|
||||
ensureSixParticipants,
|
||||
ensureThreeParticipants,
|
||||
hangupAllParticipants
|
||||
} from '../../helpers/participants';
|
||||
import { muteAudioAndCheck } from '../helpers/mute';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4', 'p5', 'p6' ]
|
||||
});
|
||||
|
||||
describe('Active speaker', () => {
|
||||
it('testActiveSpeaker', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await muteAudioAndCheck(p1, p2);
|
||||
@@ -30,6 +34,306 @@ describe('Active speaker', () => {
|
||||
await assertOneDominantSpeaker(p1);
|
||||
await assertOneDominantSpeaker(p2);
|
||||
await assertOneDominantSpeaker(p3);
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that the dominant speaker appears in the filmstrip in stage view
|
||||
* even when alphabetically last with limited visible slots.
|
||||
* This tests the fix for the bug where dominant speakers at the bottom of
|
||||
* the alphabetically sorted list would not appear when slots were limited.
|
||||
*
|
||||
* Note: This test verifies filmstrip ordering via Redux state
|
||||
* (visibleRemoteParticipants), not large video behavior.
|
||||
*/
|
||||
it.skip('testDominantSpeakerInFilmstripWithLimitedSlots', async () => {
|
||||
await ensureSixParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3, p4, p5, p6 } = ctx;
|
||||
|
||||
// Resize p1's window to limit filmstrip slots to 2-3 tiles
|
||||
// This creates the condition where not all participants fit in the filmstrip
|
||||
await p1.driver.setWindowSize(1024, 600);
|
||||
await p1.driver.pause(1000); // Wait for layout to adjust
|
||||
|
||||
// Set display names to create alphabetical ordering
|
||||
// Names chosen so p6 ("Zoe") is alphabetically last: Alice, Bob, Charlie, Eve, Frank, Zoe
|
||||
await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
|
||||
|
||||
// Test with multiple speakers: Eve (p4), Frank (p5), and Zoe (p6)
|
||||
// This verifies the fix works for different alphabetical positions
|
||||
const speakersToTest = [
|
||||
{ participant: p4, name: 'Eve' },
|
||||
{ participant: p5, name: 'Frank' },
|
||||
{ participant: p6, name: 'Zoe' }
|
||||
];
|
||||
|
||||
for (const { participant, name } of speakersToTest) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await p1.log(`Testing ${name} (${participantId}) as dominant speaker`);
|
||||
|
||||
// Make this participant the dominant speaker by unmuting
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Wait for the dominant speaker to be detected
|
||||
await waitForDominantSpeaker(p1, participantId, name);
|
||||
|
||||
// Verify that the participant appears in the visible remote participants
|
||||
const filmstripState = await getFilmstripState(p1);
|
||||
|
||||
await p1.log(`Dominant speaker: ${filmstripState.dominantSpeaker}`);
|
||||
await p1.log(`Visible remote participants: ${JSON.stringify(filmstripState.visibleRemoteParticipants)}`);
|
||||
await p1.log(`${name} endpoint ID: ${participantId}`);
|
||||
await p1.log(`Total remote: ${filmstripState.remoteParticipants.length}, Visible: ${filmstripState.visibleRemoteParticipants.length}`);
|
||||
|
||||
// Verify we actually have slot limitation (fewer visible than total)
|
||||
expect(filmstripState.visibleRemoteParticipants.length).toBeLessThan(filmstripState.remoteParticipants.length);
|
||||
|
||||
// Assert that the dominant speaker is in the visible participants
|
||||
// This is the key test - even though they may be alphabetically late and slots are limited,
|
||||
// they should still be visible because the fix reserves a slot for dominant speaker
|
||||
expect(filmstripState.visibleRemoteParticipants).toContain(participantId);
|
||||
|
||||
// Verify the dominant speaker thumbnail is visible in the filmstrip
|
||||
await p1.driver.$(`//span[@id='participant_${participantId}']`).waitForDisplayed({
|
||||
timeout: 5_000,
|
||||
timeoutMsg: `${name} dominant speaker thumbnail not visible in filmstrip`
|
||||
});
|
||||
|
||||
// Mute this participant back before testing the next one
|
||||
await participant.getToolbar().clickAudioMuteButton();
|
||||
await p1.driver.pause(2000);
|
||||
}
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test dominant speaker in filmstrip with screensharing active.
|
||||
* Verifies that dominant speaker is still visible when screen shares
|
||||
* take up some of the visible slots.
|
||||
*/
|
||||
it.skip('testDominantSpeakerWithScreensharing', async () => {
|
||||
await ensureSixParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3, p4, p5, p6 } = ctx;
|
||||
|
||||
// Resize p1's window to limit filmstrip slots
|
||||
await p1.driver.setWindowSize(1024, 600);
|
||||
await p1.driver.pause(1000); // Wait for layout to adjust
|
||||
|
||||
// Set display names
|
||||
await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
|
||||
|
||||
// Start screensharing from p2
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
|
||||
// Test with multiple speakers while screensharing is active
|
||||
const speakersToTest = [
|
||||
{ participant: p4, name: 'Eve' },
|
||||
{ participant: p5, name: 'Frank' },
|
||||
{ participant: p6, name: 'Zoe' }
|
||||
];
|
||||
|
||||
for (const { participant, name } of speakersToTest) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await p1.log(`Testing ${name} (${participantId}) as dominant speaker with screensharing`);
|
||||
|
||||
// Make this participant the dominant speaker by unmuting
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Wait for the dominant speaker to be detected
|
||||
await waitForDominantSpeaker(p1, participantId, name);
|
||||
|
||||
// Verify dominant speaker is still visible in filmstrip despite screenshare
|
||||
const filmstripState = await getFilmstripState(p1);
|
||||
|
||||
await p1.log(`Dominant speaker (with screenshare): ${filmstripState.dominantSpeaker}`);
|
||||
await p1.log(`Visible remote participants with screenshare: ${JSON.stringify(filmstripState.visibleRemoteParticipants)}`);
|
||||
await p1.log(`${name} endpoint ID: ${participantId}`);
|
||||
await p1.log(`Total remote: ${filmstripState.remoteParticipants.length}, Visible: ${filmstripState.visibleRemoteParticipants.length}`);
|
||||
|
||||
// Verify we have slot limitation even with screensharing
|
||||
expect(filmstripState.visibleRemoteParticipants.length).toBeLessThan(filmstripState.remoteParticipants.length);
|
||||
|
||||
// The dominant speaker should still be in the visible participants despite screenshare taking slots
|
||||
expect(filmstripState.visibleRemoteParticipants).toContain(participantId);
|
||||
|
||||
// Verify thumbnail visibility
|
||||
await p1.driver.$(`//span[@id='participant_${participantId}']`).waitForDisplayed({
|
||||
timeout: 5_000,
|
||||
timeoutMsg: `${name} not visible with screensharing active`
|
||||
});
|
||||
|
||||
// Mute this participant back before testing the next one
|
||||
await participant.getToolbar().clickAudioMuteButton();
|
||||
await p1.driver.pause(1000);
|
||||
}
|
||||
|
||||
// Clean up - stop screensharing
|
||||
await p2.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that filmstrip maintains stable ordering when multiple speakers alternate.
|
||||
* Verifies that the alphabetical sorting prevents visual reordering when the same
|
||||
* set of speakers take turns speaking.
|
||||
*/
|
||||
it.skip('testFilmstripStableOrderingWithMultipleSpeakers', async () => {
|
||||
await ensureSixParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3, p4, p5, p6 } = ctx;
|
||||
|
||||
// Resize p1's window to limit filmstrip slots
|
||||
await p1.driver.setWindowSize(1024, 600);
|
||||
await p1.driver.pause(1000); // Wait for layout to adjust
|
||||
|
||||
// Set display names
|
||||
await setAlphabeticalDisplayNames(p1, p2, p3, p4, p5, p6);
|
||||
|
||||
// First, have Eve, Frank, and Zoe all speak to get them into the active speakers list
|
||||
const speakersToTest = [
|
||||
{ participant: p4, name: 'Eve' },
|
||||
{ participant: p5, name: 'Frank' },
|
||||
{ participant: p6, name: 'Zoe' }
|
||||
];
|
||||
|
||||
await p1.log('Initial round: getting all three speakers into active speakers list');
|
||||
|
||||
for (const { participant, name } of speakersToTest) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await p1.log(`${name} (${participantId}) speaking for the first time`);
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
await waitForDominantSpeaker(p1, participantId, name);
|
||||
await participant.getToolbar().clickAudioMuteButton();
|
||||
await p1.driver.pause(1000);
|
||||
}
|
||||
|
||||
// Now cycle through them again and verify they maintain alphabetical order (Eve, Frank, Zoe)
|
||||
await p1.log('Second round: verifying stable alphabetical ordering when speakers alternate');
|
||||
|
||||
const states = [];
|
||||
|
||||
for (const { participant, name } of speakersToTest) {
|
||||
const participantId = await participant.getEndpointId();
|
||||
|
||||
await p1.log(`Testing ${name} (${participantId}) for stable ordering`);
|
||||
|
||||
// Make this participant the dominant speaker
|
||||
await participant.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Wait for the dominant speaker to be detected
|
||||
await waitForDominantSpeaker(p1, participantId, name);
|
||||
|
||||
// Capture filmstrip state
|
||||
const filmstripState = await getFilmstripState(p1);
|
||||
|
||||
states.push({ name, id: participantId, state: filmstripState });
|
||||
|
||||
// Mute back
|
||||
await participant.getToolbar().clickAudioMuteButton();
|
||||
await p1.driver.pause(1000);
|
||||
}
|
||||
|
||||
const [ eveState, frankState, zoeState ] = states;
|
||||
|
||||
// Helper function to get participant names in the order they appear
|
||||
const getVisibleParticipantNames = async (visibleIds: string[]) => {
|
||||
return await p1.execute(ids => {
|
||||
const state = APP.store.getState();
|
||||
const participants = state['features/base/participants'];
|
||||
|
||||
return ids.map(id => {
|
||||
const participant = participants.remote.get(id);
|
||||
|
||||
return participant?.name || 'Unknown';
|
||||
});
|
||||
}, visibleIds);
|
||||
};
|
||||
|
||||
// Get the names of visible participants for each state
|
||||
const eveVisibleNames = await getVisibleParticipantNames(eveState.state.visibleRemoteParticipants);
|
||||
const frankVisibleNames = await getVisibleParticipantNames(frankState.state.visibleRemoteParticipants);
|
||||
const zoeVisibleNames = await getVisibleParticipantNames(zoeState.state.visibleRemoteParticipants);
|
||||
|
||||
await p1.log(`Visible participants when Eve is dominant: ${JSON.stringify(eveVisibleNames)}`);
|
||||
await p1.log(`Visible participants when Frank is dominant: ${JSON.stringify(frankVisibleNames)}`);
|
||||
await p1.log(`Visible participants when Zoe is dominant: ${JSON.stringify(zoeVisibleNames)}`);
|
||||
|
||||
await p1.log(`Eve visible count: ${eveState.state.visibleRemoteParticipants.length}, total remote: ${eveState.state.remoteParticipants.length}`);
|
||||
await p1.log(`Frank visible count: ${frankState.state.visibleRemoteParticipants.length}, total remote: ${frankState.state.remoteParticipants.length}`);
|
||||
await p1.log(`Zoe visible count: ${zoeState.state.visibleRemoteParticipants.length}, total remote: ${zoeState.state.remoteParticipants.length}`);
|
||||
|
||||
// Verify that each dominant speaker appears in visible participants
|
||||
expect(eveState.state.visibleRemoteParticipants).toContain(eveState.id);
|
||||
expect(frankState.state.visibleRemoteParticipants).toContain(frankState.id);
|
||||
expect(zoeState.state.visibleRemoteParticipants).toContain(zoeState.id);
|
||||
|
||||
// Helper function to get the relative order of Eve, Frank, and Zoe
|
||||
const getSpeakersOrder = (names: string[]) => {
|
||||
return names.filter(n => [ 'Eve', 'Frank', 'Zoe' ].includes(n));
|
||||
};
|
||||
|
||||
const eveOrder = getSpeakersOrder(eveVisibleNames);
|
||||
const frankOrder = getSpeakersOrder(frankVisibleNames);
|
||||
const zoeOrder = getSpeakersOrder(zoeVisibleNames);
|
||||
|
||||
await p1.log(`Speakers order when Eve is dominant: ${JSON.stringify(eveOrder)}`);
|
||||
await p1.log(`Speakers order when Frank is dominant: ${JSON.stringify(frankOrder)}`);
|
||||
await p1.log(`Speakers order when Zoe is dominant: ${JSON.stringify(zoeOrder)}`);
|
||||
|
||||
// Verify that the dominant speaker is always in the visible list (this tests the bug fix)
|
||||
expect(eveOrder).toContain('Eve');
|
||||
expect(frankOrder).toContain('Frank');
|
||||
expect(zoeOrder).toContain('Zoe');
|
||||
|
||||
// Helper to check if array is alphabetically sorted
|
||||
const isAlphabeticallySorted = (names: string[]) => {
|
||||
for (let i = 0; i < names.length - 1; i++) {
|
||||
if (names[i].localeCompare(names[i + 1]) > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Verify that whatever speakers ARE visible maintain alphabetical order
|
||||
// This is the key test - when the same speakers alternate, visible speakers stay in alphabetical order
|
||||
expect(isAlphabeticallySorted(eveOrder)).toBe(true);
|
||||
expect(isAlphabeticallySorted(frankOrder)).toBe(true);
|
||||
expect(isAlphabeticallySorted(zoeOrder)).toBe(true);
|
||||
|
||||
// Additionally verify order consistency: if multiple speakers are visible in multiple states,
|
||||
// their relative order should be the same
|
||||
// For example, if Eve and Frank are both visible when Zoe speaks, they should be [Eve, Frank]
|
||||
if (eveOrder.includes('Frank') && frankOrder.includes('Eve')) {
|
||||
// Both Eve and Frank visible in both states
|
||||
const eveAndFrankInEveState = eveOrder.filter(n => [ 'Eve', 'Frank' ].includes(n));
|
||||
const eveAndFrankInFrankState = frankOrder.filter(n => [ 'Eve', 'Frank' ].includes(n));
|
||||
|
||||
expect(eveAndFrankInEveState).toEqual(eveAndFrankInFrankState);
|
||||
}
|
||||
|
||||
await p1.log('Filmstrip maintains alphabetical ordering of visible speakers when dominant speaker changes');
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,3 +399,81 @@ async function assertOneDominantSpeaker(participant: Participant) {
|
||||
expect(await participant.driver.$$(
|
||||
'//span[not(contains(@class, "tile-view"))]//span[contains(@class,"dominant-speaker")]').length).toBe(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a participant to be detected as the dominant speaker.
|
||||
*
|
||||
* @param {Participant} observer - The participant observing the dominant speaker state.
|
||||
* @param {string} participantId - The endpoint ID of the expected dominant speaker.
|
||||
* @param {string} participantName - The name of the participant for logging.
|
||||
*/
|
||||
async function waitForDominantSpeaker(
|
||||
observer: Participant,
|
||||
participantId: string,
|
||||
participantName: string
|
||||
): Promise<void> {
|
||||
await observer.driver.waitUntil(
|
||||
async () => {
|
||||
const state = await observer.execute(() => {
|
||||
const participants = APP.store.getState()['features/base/participants'];
|
||||
|
||||
return participants.dominantSpeaker;
|
||||
});
|
||||
|
||||
return state === participantId;
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: `${participantName} (${participantId}) was not detected as dominant speaker within 10 seconds`
|
||||
}
|
||||
);
|
||||
|
||||
// Wait a bit more for filmstrip state to update after dominant speaker changes
|
||||
await observer.driver.pause(1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current filmstrip state from Redux.
|
||||
*
|
||||
* @param {Participant} participant - The participant to query.
|
||||
* @returns {Promise<FilmstripState>} The filmstrip state.
|
||||
*/
|
||||
async function getFilmstripState(participant: Participant): Promise<{
|
||||
dominantSpeaker: string | null;
|
||||
remoteParticipants: string[];
|
||||
visibleRemoteParticipants: string[];
|
||||
}> {
|
||||
return await participant.execute(() => {
|
||||
const state = APP.store.getState();
|
||||
const filmstrip = state['features/filmstrip'];
|
||||
const participants = state['features/base/participants'];
|
||||
|
||||
return {
|
||||
dominantSpeaker: participants.dominantSpeaker,
|
||||
remoteParticipants: filmstrip.remoteParticipants,
|
||||
visibleRemoteParticipants: Array.from(filmstrip.visibleRemoteParticipants)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set display names for all 6 participants to create alphabetical ordering.
|
||||
*/
|
||||
async function setAlphabeticalDisplayNames(
|
||||
p1: Participant,
|
||||
p2: Participant,
|
||||
p3: Participant,
|
||||
p4: Participant,
|
||||
p5: Participant,
|
||||
p6: Participant
|
||||
): Promise<void> {
|
||||
await p1.setLocalDisplayName('Alice');
|
||||
await p2.setLocalDisplayName('Bob');
|
||||
await p3.setLocalDisplayName('Charlie');
|
||||
await p4.setLocalDisplayName('Eve');
|
||||
await p5.setLocalDisplayName('Frank');
|
||||
await p6.setLocalDisplayName('Zoe');
|
||||
|
||||
// Wait for display names to propagate
|
||||
await p1.driver.pause(2000);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { expectations } from '../../helpers/expectations';
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
retry: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ const specs = [
|
||||
*/
|
||||
function generateCapabilitiesFromSpecs(): Record<string, any> {
|
||||
const allSpecFiles: string[] = [];
|
||||
const browsers = [ 'p1', 'p2', 'p3', 'p4' ];
|
||||
const browsers = [ 'p1', 'p2', 'p3', 'p4', 'p5', 'p6' ];
|
||||
|
||||
for (const pattern of specs) {
|
||||
const matches = glob.sync(pattern, { cwd: path.join(__dirname) });
|
||||
@@ -87,7 +87,9 @@ function generateCapabilitiesFromSpecs(): Record<string, any> {
|
||||
p1: new Set(),
|
||||
p2: new Set(),
|
||||
p3: new Set(),
|
||||
p4: new Set()
|
||||
p4: new Set(),
|
||||
p5: new Set(),
|
||||
p6: new Set()
|
||||
};
|
||||
|
||||
for (const file of allSpecFiles) {
|
||||
|
||||
Reference in New Issue
Block a user