Compare commits

..

3 Commits

Author SHA1 Message Date
Hristo Terezov
303f81f427 fix(participants-pane): replace :first-child with :first-of-type to silence emotion SSR warning
emotion/stylis emits a console error for :first-child selectors as they can
cause hydration mismatches in SSR. All three occurrences that trigger on
conference load are in ParticipantsPane. The semantics are equivalent since
all sibling elements are the same type.
2026-02-18 08:30:23 -06:00
Hristo Terezov
23a65fbf2a fix(ui): migrate ReactDOM.render to createRoot for React 18 compatibility
ReactDOM.render is deprecated in React 18 and causes console errors on every
render. Migrate to createRoot API across the main entry point and the legacy
video layout modules, storing root references to avoid recreating them on
repeated calls.
2026-02-18 08:25:25 -06:00
Hristo Terezov
5fee521938 fix(ui): replace kebab-case CSS properties with camelCase in style objects
Vendor-prefixed CSS properties like -webkit-appearance and -moz-appearance
used as object keys in emotion/tss-react style objects cause console errors
because JavaScript CSS-in-JS libraries require camelCase property names.

Replace all occurrences across Input, Chat, Filmstrip, and Slider components.
2026-02-18 08:25:08 -06:00
105 changed files with 1037 additions and 4396 deletions

View File

@@ -96,53 +96,23 @@ 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();
// 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;
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;
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
@@ -166,11 +136,10 @@ 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);

View File

@@ -1600,8 +1600,6 @@ var config = {
// An option to get for user info (name, picture, email) in the token outside the user context.
// Can be used with Firebase tokens.
// tokenGetUserInfoOutOfContext: false,
// An option to pass the token in the iframe API directly instead of using the redirect flow.
// tokenAuthInline: false,
// You can put an array of values to target different entity types in the invite dialog.
// Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"

View File

@@ -45,7 +45,7 @@ body {
.jitsi-icon {
&-default svg {
fill: var(--icon-svg-fill, white);
fill: var(--icon-default-color, white);
}
}

View File

@@ -2,8 +2,8 @@
.reactions-menu {
width: 330px;
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));
background: #242528;
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
border-radius: 6px;
padding: 16px;
@@ -14,7 +14,7 @@
top: 3px;
& .toolbox-icon.toggled {
background-color: var(--reactions-menu-button-toggled, #000000);
background-color: #000000;
}
}
}

View File

@@ -106,7 +106,7 @@
}
#preview {
background: var(--prejoin-preview-background, #040404);
background: #040404;
display: flex;
align-items: center;
justify-content: center;

2
debian/control vendored
View File

@@ -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, ca-certificates-java, lua-sec, lua-basexx, lua-luaossl, lua-cjson, lua-inspect
Depends: openssl, prosody (>= 0.12.0) | prosody-trunk | prosody-0.12 | prosody-13.0, 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

View File

@@ -284,17 +284,13 @@ case "$1" in
# and drop the wait and the prosody restart
sleep 1
invoke-rc.d prosody restart || true
fi
;;
triggered)
for trigger in $2; do
if [ "$trigger" = "update-ca-certificates-java" ]; then
echo "Java certificates updated, restarting Jitsi components..."
# 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
systemctl restart jitsi-videobridge2.service >/dev/null || true
systemctl restart jicofo.service >/dev/null || true
fi
done
exit 0
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)

View File

@@ -1 +0,0 @@
interest-noawait update-ca-certificates-java

View File

@@ -227,9 +227,6 @@
"video_ssrc": "Video-SSRC:",
"yes": "Ja"
},
"customPanel": {
"close": "Schließen"
},
"dateUtils": {
"earlier": "Früher",
"today": "Heute",
@@ -592,7 +589,6 @@
"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"
@@ -1319,7 +1315,6 @@
"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",
@@ -1425,11 +1420,9 @@
"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

File diff suppressed because it is too large Load Diff

View File

@@ -114,9 +114,6 @@
"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": "jai téléversé un fichier",
"fileDeleted": "Un fichier a été supprimé",
"guestsChatIndicator": "(invité)",
"lobbyChatMessageTo": "Message de salle d'attente à {{recipient}}",
"message": "Message",
@@ -126,16 +123,8 @@
"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",
@@ -227,9 +216,6 @@
"video_ssrc": "Video SSRC :",
"yes": "oui"
},
"customPanel": {
"close": "Fermer"
},
"dateUtils": {
"earlier": "Plus tôt",
"today": "Aujourd'hui",
@@ -536,7 +522,6 @@
"tokenAuthFailedWithReasons": "Désolé, vous nêtes pas autorisé à rejoindre lappel. 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",
@@ -584,12 +569,10 @@
"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.",
@@ -981,9 +964,6 @@
"by": "Par {{ name }}",
"closeButton": "Fermer le sondage",
"create": {
"accessibilityLabel": {
"send": "Envoyer le sondage"
},
"addOption": "Ajouter une option",
"answerPlaceholder": "Option {{index}}",
"cancel": "Annuler",
@@ -992,7 +972,8 @@
"pollQuestion": "Question du sondage",
"questionPlaceholder": "Poser une question",
"removeOption": "Supprimer l'option",
"save": "Enregistrer"
"save": "Enregistrer",
"send": "Envoyer"
},
"errors": {
"notUniqueOption": "Les options doivent être uniques"
@@ -1318,7 +1299,6 @@
"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",
@@ -1424,11 +1404,9 @@
"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é",
@@ -1443,7 +1421,6 @@
"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",
@@ -1479,7 +1456,6 @@
"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",

View File

@@ -1,14 +1,10 @@
{
"addPeople": {
"accessibilityLabel": {
"meetingLink": "मीटिंग लिंक: {{url}}"
},
"add": "आमंत्रित करें",
"addContacts": "संपर्क सूची से आमंत्रित करे",
"contacts": "संपर्क",
"copyInvite": "मीटिंग के आमंत्रण कि प्रतिलिपि बनाये",
"copyLink": "मीटिंग कि लिंक कि प्रतिलिपि बनाये",
"copyStream": "सीधे प्रसारण कि लिंक कि प्रतिलिपि बनाये",
"copyStream": "सीधे प्रसारण कि लिंक कि प्रतिलिपि बनाये",
"countryNotSupported": "अभी हम इस गतव्य के लिये सक्षम न‍ही है ।",
"countryReminder": "यू.एस. के बाहर से काल कर रहे है तो कृपया सुनिश्चित करे कि अपने देश के कोड़ से प्रारंभ कर रहे है !",
"defaultEmail": "अपना ई-मेल पता लिखें",
@@ -39,7 +35,6 @@
},
"audioDevices": {
"bluetooth": "ब्लूटूथ",
"car": "कार ऑडियो",
"headphones": "हेडफ़ोन",
"none": "कोई ऑडियो डिवाइस उपलब्ध नहीं",
"phone": "फ़ोन",
@@ -48,47 +43,9 @@
"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": "क्या आप इस इवेंट में एक जित्सी लिंक जोड़ना चाहते हैं?",
"confirmAddLink": "क्या आप इस इवेंट में एक Jitsi लिंक जोड़ना चाहते हैं?",
"error": {
"appConfiguration": "कैलेंडर एकीकरण ठीक से कॉन्फ़िगर नहीं किया गया है।",
"generic": "एक त्रुटि हुई है। कृपया अपनी कैलेंडर सेटिंग जांचें या कैलेंडर को रीफ़्रेश करने का प्रयास करें।",
@@ -104,72 +61,28 @@
"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": "चैट का उपयोग करने के लिए एक उपनाम दर्ज करें",
"titleWith1Features": "{{feature1}} का उपयोग करने के लिए एक उपनाम दर्ज करें",
"titleWith2Features": "{{feature1}} और {{feature2}} का उपयोग करने के लिए एक उपनाम दर्ज करें",
"titleWith3Features": "{{feature1}}, {{feature2}} और {{feature3}} का उपयोग करने के लिए एक उपनाम दर्ज करें",
"titleWith4Features": "{{feature1}}, {{feature2}}, {{feature3}} और {{feature4}} का उपयोग करने के लिए एक उपनाम दर्ज करें",
"titleWithCC": "चैट और बंद कैप्शन का उपयोग करने के लिए एक उपनाम दर्ज करें",
"titleWithPolls": "चैट और पोल का उपयोग करने के लिए एक उपनाम दर्ज करें",
"titleWithPollsAndCC": "चैट, पोल और बंद कैप्शन का उपयोग करने के लिए एक उपनाम दर्ज करें",
"titleWithPollsAndCCAndFileSharing": "चैट, पोल, बंद कैप्शन और फ़ाइलों का उपयोग करने के लिए एक उपनाम दर्ज करें"
"titleWithPolls": "चैट का उपयोग करने के लिए एक उपनाम दर्ज करें"
},
"noMessagesMessage": "अभी तक मीटिंग में कोई संदेश नहीं आया है। वार्तालाप प्रारंभ करें!",
"privateNotice": "{{recipient}} के लिए निजी संदेश",
"sendButton": "भेजें",
"smileysPanel": "इमोजी पैनल",
"systemDisplayName": "सिस्टम",
"tabs": {
"chat": "चैट",
"closedCaptions": "सीसी",
"fileSharing": "फ़ाइलें",
"polls": "पोल"
},
"title": "चैट",
"titleWithCC": "सीसी",
"titleWithFeatures": "चैट और",
"titleWithFileSharing": "फ़ाइलें",
"titleWithPolls": "पोल",
"titleWithPolls": "चैट",
"you": "आप"
},
"chromeExtensionBanner": {
"buttonText": "क्रोम एक्सटेंशन इंस्टॉल करें",
"buttonTextEdge": "Edge एक्सटेंशन इंस्टॉल करें",
"close": "बंद करें",
"dontShowAgain": "मुझे यह फिर से न दिखाएं"
},
"closedCaptionsTab": {
"emptyState": "मॉडरेटर द्वारा शुरू किए जाने पर बंद कैप्शन की सामग्री उपलब्ध होगी",
"startClosedCaptionsButton": "बंद कैप्शन शुरू करें"
"dontShowAgain": "मुझे यह फिर से न दिखाएं",
"installExtensionText": ",गूगल कैलेंडर और ऑफिस 365 एकीकरण के लिए एक्सटेंशन इंस्टॉल करें"
},
"connectingOverlay": {
"joiningRoom": "आपको आपकी मीटिंग से कनेक्ट किया जा रहा है…"
@@ -197,7 +110,6 @@
"bridgeCount": "सर्वर गणना: ",
"codecs": "कोडेक (ए/वी): ",
"connectedTo": "से जुड़ा हुआ है :",
"e2eeVerified": "E2EE सत्यापित:",
"framerate": "फ्रेम दर:",
"less": "कम दिखाएं",
"localaddress": "स्थानीय पता:",
@@ -224,8 +136,7 @@
"status": "सम्पर्क:",
"transport": "ट्रांसपोर्ट :",
"transport_plural": "ट्रांसपोर्ट्स:",
"video_ssrc": "वीडियो एस.आर.सी.सी.:",
"yes": "हाँ"
"video_ssrc": "वीडियो एस.आर.सी.सी.:"
},
"dateUtils": {
"earlier": "पिछला कल",
@@ -235,25 +146,14 @@
"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}} में आपकी मीटिंग शुरू की जा रही हैं…",
"titleNew": "आपकी मीटिंग शुरू की जा रही है…",
"tryAgainButton": "डेस्कटॉप में फिर से प्रयास करें",
"unsupportedBrowser": "ऐसा लगता है कि आप ऐसे ब्राउज़र का उपयोग कर रहे हैं जिसे हम सपोर्ट नहीं करते।"
"tryAgainButton": "डेस्कटॉप में फिर से प्रयास करें"
},
"defaultLink": "उदाहरण {{url}}",
"defaultNickname": "उदा. सतीष कुमार",
@@ -264,12 +164,6 @@
"microphonePermission": "माइक्रोफ़ोन अनुमति प्राप्त करने में त्रुटि"
},
"deviceSelection": {
"hid": {
"callControl": "कॉल नियंत्रण",
"connectedDevices": "कनेक्टेड डिवाइस:",
"deleteDevice": "डिवाइस हटाएँ",
"pairDevice": "डिवाइस जोड़ें"
},
"noPermission": "अनुमति नहीं दी गई",
"previewUnavailable": "पूर्वदर्शन अनुपलब्ध",
"selectADevice": "डिवाइस का चयन करें",
@@ -284,23 +178,16 @@
"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": "एप्लिकेशन विंडो",
@@ -325,7 +212,7 @@
"connectErrorWithMsg": "उफ़! कुछ गड़बड़ हो गई और हम सम्मेलन से नहीं जुड़ सके: {{msg}}",
"connecting": "संपर्क जोड़ा जा रहा है ",
"contactSupport": "सहयोग के लिए संपर्क करें",
"copied": "प्रतिलिपि बनाई गयी",
"copied": "प्रतिलिपि बनाई गयी ",
"copy": "प्रतिलिपि बनाये",
"dismiss": "खारिज करें",
"displayNameRequired": "नमस्ते! आपका नाम क्या है?",
@@ -392,7 +279,7 @@
"readMore": "अधिक",
"recording": "रिकॉर्डिंग",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "संभव नहीं है जब एक लाइव स्ट्रीम सक्रिय है",
"recordingDisabledTooltip": "रिकॉर्डिंग शुरू करना अक्षम करें|",
"recordingDisabledTooltip": "रिकॉर्डिंग शुरू करना अक्षम करें.",
"rejoinNow": "पुनः जुड़े",
"remoteControlAllowedMessage": "{{user}} ने आपका रिमोट कंट्रोल अनुरोध स्वीकार कर लिया!",
"remoteControlDeniedMessage": "{{user}} ने आपका रिमोट कंट्रोल अनुरोध अस्वीकार कर दिया!",
@@ -408,7 +295,7 @@
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
"retry": "पुनः प्रयास करें",
"screenSharingAudio": "ऑडियो साझा करें",
"screenSharingFailed": "स्क्रीन शेयरिंग शुरू नहीं हो पाई।",
"screenSharingFailed": "उफ़! कुछ गड़बड़ हो गई, हम स्क्रीन शेयरिंग शुरू करने में सक्षम नहीं थे!",
"screenSharingFailedTitle": "स्क्रीन साझा करना विफल हुआ!",
"screenSharingPermissionDeniedError": "उफ़! आपकी स्क्रीन शेयरिंग अनुमतियों में कुछ गड़बड़ हो गई है। कृपया पुनः लोड करें और पुनः प्रयास करें।",
"sendPrivateMessage": "आपने हाल ही में एक निजी संदेश प्राप्त किया है। क्या आप उसका निजी रूप से जवाब देने का इरादा रखते हैं? या आप अपना संदेश समूह को भेजना चाहते हैं?",
@@ -460,34 +347,6 @@
"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": "सहायता केंद्र"
},
@@ -503,7 +362,6 @@
"addPassword": "$t(lockRoomPassword)जोड़ें",
"cancelPassword": "$t(lockRoomPassword)रद्द करें",
"conferenceURL": "लिंक:",
"copyNumber": "नंबर कॉपी करें",
"country": "देश",
"dialANumber": "अपनी मीटिंग में शामिल होने के लिए, इनमें से किसी एक नंबर को डायल करें और फिर पिन डालें।",
"dialInConferenceID": "पिन:",
@@ -526,16 +384,11 @@
"noRoom": "डायल-इन करने के लिए कोई कक्ष निर्दिष्ट नहीं किया गया।",
"numbers": "डायल-इन नंबर",
"password": "$t(lockRoomPasswordUppercase):",
"reachedLimit": "आप अपने प्लान की सीमा तक पहुँच चुके हैं।",
"sip": "SIP पता",
"sipAudioOnly": "केवल ऑडियो SIP पता",
"title": "साझा करें",
"tooltip": "इस मीटिंग के लिए लिंक और डायल-इन जानकारी साझा करें",
"upgradeOptions": "कृपया अपग्रेड विकल्पों की जाँच करें",
"whiteboardError": "व्हाइटबोर्ड लोड करने में त्रुटि। कृपया बाद में पुनः प्रयास करें।"
"tooltip": "इस मीटिंग के लिए लिंक और डायल-इन जानकारी साझा करें"
},
"inlineDialogFailure": {
"msg": "कुछ समस्या हुई।",
"msg": "We stumbled a bit.",
"retry": "पुनः प्रयास करें",
"support": "सहायता",
"supportMsg": "ऐसा बार बार हो रहा हो, तो सम्पर्क करे "
@@ -564,10 +417,6 @@
"toggleShortcuts": "कीबोर्ड शॉर्टकट दिखाएं या छिपाएं",
"videoMute": "अपना कैमरा प्रारंभ या बंद करें"
},
"largeVideo": {
"screenIsShared": "आप अपनी स्क्रीन साझा कर रहे हैं",
"showMeWhatImSharing": "मुझे दिखाएँ कि मैं क्या साझा कर रहा हूँ"
},
"liveStreaming": {
"busy": "हम स्ट्रीमिंग संसाधनों को मुक्त करने पर काम कर रहे हैं। कृपया कुछ मिनटों में पुनः प्रयास करें।",
"busyTitle": "सभी स्ट्रीमर वर्तमान में व्यस्त हैं",
@@ -612,7 +461,7 @@
"emailField": "अपना ईमेल पता दर्ज करें",
"enableDialogPasswordField": "पासवर्ड सेट करें (वैकल्पिक)",
"enableDialogSubmit": "सक्षम करें",
"enableDialogText": "लॉबी मोड से आप अपनी मीटिंग को सुरक्षित रख सकते हैं। केवल मॉडरेटर की अनुमति मिलने के बाद ही लोग इसमें शामिल हो पाएंगे।",
"enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approval by a moderator.",
"enterPasswordButton": "मीटिंग पासवर्ड दर्ज करें",
"enterPasswordTitle": "मीटिंग में शामिल होने के लिए पासवर्ड दर्ज करें",
"invalidPassword": "अमान्य पासवर्ड",
@@ -626,8 +475,8 @@
"knockTitle": "कोई व्यक्ति बैठक में शामिल होना चाहता है",
"knockingParticipantList": "प्रतिभागी सूची दस्तक",
"nameField": "अपना नाम दर्ज करें",
"notificationLobbyAccessDenied": "{{targetParticipantName}} को {{originParticipantName}} ने मीटिंग में शामिल होने की अनुमति नहीं दी।",
"notificationLobbyAccessGranted": "{{targetParticipantName}} को {{originParticipantName}} ने मीटिंग में शामिल होने की अनुमति दी।",
"notificationLobbyAccessDenied": "{{targetParticipantName}} has been rejected to join by {{originParticipantName}}",
"notificationLobbyAccessGranted": "{{targetParticipantName}} has been allowed to join by {{originParticipantName}}",
"notificationLobbyDisabled": "लॉबी को {{originParticipantName}}द्वारा अक्षम कर दिया गया",
"notificationLobbyEnabled": "लॉबी को {{originParticipantName}}द्वारा सक्षम किया गया",
"notificationTitle": "लॉबी",
@@ -660,12 +509,9 @@
"no": "नहीं",
"participant": "प्रतिभागी",
"participantStats": "प्रतिभागी आँकड़े",
"selectTabTitle": "🎥 रिकॉर्डिंग के लिए कृपया इस टैब को चुनें",
"sessionToken": "सत्र टोकन",
"start": "रिकॉर्डिंग प्रारंभ करें",
"stop": "रिकॉर्डिंग बंद करें",
"stopping": "रिकॉर्डिंग बंद की जा रही है",
"wait": "कृपया प्रतीक्षा करें, आपकी रिकॉर्डिंग सेव की जा रही है",
"yes": "हाँ"
},
"lockRoomPassword": "पासवर्ड",
@@ -676,30 +522,11 @@
},
"me": "मैं",
"notify": {
"OldElectronAPPTitle": "सुरक्षा में खामी",
"allowAll": "सभी की अनुमति दें",
"allowAudio": "ऑडियो की अनुमति दें",
"allowDesktop": "स्क्रीन शेयरिंग की अनुमति दें",
"allowVideo": "वीडियो की अनुमति दें",
"allowedUnmute": "आप माइक्रोफोन अनम्यूट कर सकते हैं, कैमरा चालू कर सकते हैं या स्क्रीन साझा कर सकते हैं।",
"audioUnmuteBlockedDescription": "सिस्टम सीमाओं के कारण माइक्रोफोन अनम्यूट अस्थायी रूप से ब्लॉक कर दिया गया है।",
"audioUnmuteBlockedTitle": "माइक्रोफोन अनम्यूट ब्लॉक!",
"chatMessages": "चैट संदेश",
"OldElectronAPPTitle": "Security vulnerability!",
"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}}!",
@@ -707,29 +534,7 @@
"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": "जब आप बोलने के लिए तैयार हों, तो आप हमेशा अनम्यूट कर सकते हैं। बैठक में शोर कम रखने के लिए बोलने के बाद म्यूट कर दें।",
@@ -738,85 +543,24 @@
"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}}द्वारा अक्षम कर दिया गया है!",
"videoUnmuteBlockedDescription": "सिस्टम सीमाओं के कारण कैमरा अनम्यूट और डेस्कटॉप शेयरिंग अस्थायी रूप से ब्लॉक कर दी गई है।",
"videoUnmuteBlockedTitle": "कैमरा अनम्यूट और डेस्कटॉप शेयरिंग ब्लॉक!",
"viewParticipants": "प्रतिभागियों को देखें",
"viewVisitors": "दर्शकों को देखें",
"waitingParticipants": "{{waitingParticipants}} लोग",
"waitingVisitors": "कतार में प्रतीक्षारत दर्शक: {{waitingVisitors}}",
"waitingVisitorsTitle": "मीटिंग अभी लाइव नहीं है!",
"whiteboardLimitDescription": "कृपया अपनी प्रगति सहेजें, क्योंकि उपयोगकर्ता सीमा जल्द ही पहुँच जाएगी और व्हाइटबोर्ड बंद हो जाएगा।",
"whiteboardLimitTitle": "व्हाइटबोर्ड उपयोग"
"videoMutedRemotelyTitle": "आपका कैमरा {{participantDisplayName}}द्वारा अक्षम कर दिया गया है!"
},
"participantsPane": {
"actions": {
"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}})"
"reject": "स्वीकार"
}
},
"passwordDigitsOnly": "अधिकतम {{number}} अंक",
@@ -873,7 +617,6 @@
"joinAudioByPhone": "फोन ऑडियो के साथ जुड़ें",
"joinMeeting": "मीटिंग में शामिल हों",
"joinWithoutAudio": "ऑडियो के बिना जुड़ें",
"keyboardShortcuts": "कीबोर्ड शॉर्टकट सक्षम करें",
"linkCopied": "लिंक क्लिपबोर्ड पर कॉपी किया गया",
"lookGood": "ऐसा लगता है कि आपका माइक्रोफ़ोन ठीक से काम कर रहा है",
"or": "या",
@@ -899,10 +642,9 @@
"ringing": "Ringing…"
},
"profile": {
"avatar": "अवतार",
"setDisplayNameLabel": "अपना नाम सेट करें",
"setEmailInput": "ई-मेल दर्ज करें",
"setEmailLabel": "Gravatar ईमेल",
"setEmailLabel": "Set अपना ग्रेवार्ट ईमेल सेट करें",
"title": "प्रोफ़ाइल"
},
"raisedHand": "बोलना चाहेंगे",
@@ -918,12 +660,6 @@
"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": "लाइव",
@@ -941,26 +677,18 @@
"signOut": "साइन आउट करें",
"title": "रिकॉर्डिंग",
"unavailable": "ओह! {{serviceName}} वर्तमान में अनुपलब्ध है। हम इस समस्या को हल करने पर काम कर रहे हैं। कृपया बाद में पुनः प्रयास करें।",
"unavailableTitle": "रिकॉर्डिंग उपलब्ध नहीं है",
"uploadToCloud": "क्लाउड पर अपलोड करें"
"unavailableTitle": "रिकॉर्डिंग उपलब्ध नहीं है"
},
"sectionList": {
"pullToRefresh": "रीफ़्रेश करने के लिए नीचे खींचें"
},
"security": {
"about": "आप अपनी मीटिंग में $t(lockRoomPassword) जोड़ सकते हैं। सहभागियों को मीटिंग में शामिल होने से पहले $t(lockRoomPassword) प्रदान करना होगा।",
"aboutReadOnly": "मॉडरेटर मीटिंग में $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.",
"insecureRoomNameWarning": "कमरे का नाम असुरक्षित है। अनचाहे सहभागियों की कॉन्फ्रेंस में शामिल हो सकते हैं। सुरक्षा बटन का उपयोग करके अपनी मीटिंग को सुरक्षित बनाने का विचार करें। ",
"title": "सुरक्षा विकल्प",
"unsafeRoomActions": {
"meeting": "सुरक्षा बटन का उपयोग करके अपनी मीटिंग को सुरक्षित करने पर विचार करें।",
"prejoin": "कृपया अधिक विशिष्ट मीटिंग नाम का उपयोग करने पर विचार करें।",
"welcome": "कृपया अधिक विशिष्ट मीटिंग नाम का उपयोग करें या दिए गए सुझावों में से किसी एक को चुनें।"
}
"securityOptions": "Security options"
},
"settings": {
"audio": "ऑडियो",
"buttonLabel": "सेटिंग्स",
"calendar": {
"about": "{{appName}} कैलेंडर एकीकरण आपके कैलेंडर तक सुरक्षित रूप से पहुंचने के लिए उपयोग किया जाता है ताकि यह आगामी कार्यक्रम पढ़ सके।",
"disconnect": "डिस्कनेक्ट करें",
@@ -970,35 +698,23 @@
},
"devices": "डिवाइस",
"followMe": "हर कोई मेरा अनुसरण करेगा",
"incomingMessage": "आने वाला संदेश",
"language": "भाषा",
"loggedIn": "{{name}} के रूप में लॉग इन किया",
"maxStageParticipants": "मुख्य स्टेज पर पिन किए जा सकने वाले प्रतिभागियों की अधिकतम संख्या",
"microphones": "माइक्रोफोन",
"moderator": "होस्ट",
"more": "अधिक",
"moderator": "Moderator",
"more": "More",
"name": "नाम",
"noDevice": "कोई नहीं",
"notifications": "सूचना",
"participantJoined": "प्रतिभागी जुड़े",
"participantKnocking": "प्रतिभागी लॉबी में आए",
"participantLeft": "प्रतिभागी बाहर गए",
"playSounds": "ध्वनि चलाएँ",
"reactions": "मीटिंग प्रतिक्रियाएँ",
"sameAsSystem": "सिस्टम के समान ({{label}})",
"selectAudioOutput": "ऑडियो आउटपुट",
"selectCamera": "कैमरा",
"selectMic": "माइक्रोफोन",
"shortcuts": "शॉर्टकट्स",
"speakers": "वक्ता",
"speakers": "Speakers",
"startAudioMuted": "सभी लोग म्यूट से शुरू करेंगे",
"startVideoMuted": "सभी लोग छिपे हुए शुरू करेंगे",
"talkWhileMuted": "म्यूट रहते हुए बोलें",
"title": "सेटिंग",
"video": "वीडियो"
"title": "सेटिंग"
},
"settingsView": {
"advanced": "उन्नत",
"advanced": "Advanced",
"alertCancel": "रद्द करें",
"alertOk": "ओके",
"alertTitle": "चेतावनी",
@@ -1114,14 +830,14 @@
"exitTileView": "टाइल दृश्य से बाहर निकलें",
"feedback": "प्रतिक्रिया छोड़ें",
"hangup": "छोड़ें",
"help": "सहायता",
"help": "Help",
"invite": "लोगों को आमंत्रित करें",
"lobbyButtonDisable": "लॉबी मोड को अक्षम करें",
"lobbyButtonEnable": "लॉबी मोड सक्षम करें",
"login": "लॉग इन",
"logout": "लॉगआउट",
"lowerYourHand": "अपना हाथ नीचे करें",
"moreActions": "अधिक कार्रवाइयाँ",
"moreActions": "More actions",
"moreOptions": "अधिक विकल्प",
"mute": "म्यूट / अनम्यूट",
"muteEveryone": "सभी को म्यूट करें",
@@ -1142,21 +858,19 @@
"security": "सुरक्षा विकल्प",
"selectBackground": "पृष्ठभूमि का चयन करें",
"shareRoom": "किसी को आमंत्रित करें",
"shareaudio": "डियो साझा करें",
"sharedvideo": "एक वीडियो साझा करें",
"sharedvideo": "एक YouTube वीडियो साझा करें",
"shortcuts": "शॉर्टकट देखें",
"speakerStats": "स्पीकर आँकड़े",
"startScreenSharing": "स्क्रीन साझाकरण प्रारंभ करें",
"startSubtitles": "सबटाइटल शुरू करें",
"startSubtitles": "Start subtitles",
"stopScreenSharing": "स्क्रीन शेयरिंग बंद करो",
"stopSharedVideo": "वीडियो बंद करें",
"stopSharedVideo": "YouTube वीडियो बंद करें",
"stopSubtitles": "उपशीर्षक बंद करें",
"talkWhileMutedPopup": "बोलने की कोशिश कर रहा है? आप मौन हैं",
"tileViewToggle": "टॉगल टाइल दृश्य",
"toggleCamera": "कैमरा टॉगल करें",
"videoSettings": "वीडियो सेटिंग्स",
"videomute": "स्टॉप कैमरा",
"videounmute": "स्टार्ट कैमरा"
"videomute": "स्टार्ट / स्टॉप कैमरा"
},
"transcribing": {
"ccButtonTooltip": "सबटाइटल शुरू / बंद करें",
@@ -1205,11 +919,10 @@
"domuteOthers": "सभी को म्यूट करें",
"domuteVideo": "कैमरा अक्षम करें",
"domuteVideoOfOthers": "अन्य सभी के लिए कैमरा बंद करें",
"flip": "उलटना",
"grantModerator": "संचालक बनाएं",
"hideSelfView": "स्वयं का दृश्य छिपाएँ",
"flip": "Flip",
"grantModerator": "Grant Moderator",
"kick": "निकालें",
"moderator": "संचालक",
"moderator": "Moderator",
"mute": "प्रतिभागी मौन है",
"muted": "म्यूटेड",
"remoteControl": "स्टार्ट / स्टॉप रिमोट कंट्रोल",
@@ -1245,20 +958,8 @@
"headerSubtitle": "सुरक्षित और उच्च गुणवत्ता बैठकें",
"headerTitle": "जित्सी मीट",
"info": "डायल-इन जानकारी",
"jitsiOnMobile": "मोबाइल पर जित्सी हमारे एप्लिकेशन डाउनलोड करें और कहीं से भी एक बैठक शुरू करें",
"jitsiOnMobile": "मोबाइल पर Jitsi हमारे एप्लिकेशन डाउनलोड करें और कहीं से भी एक बैठक शुरू करें",
"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": "हाल का",
@@ -1271,13 +972,6 @@
"sendFeedback": "फ़ीडबैक भेजें",
"startMeeting": "मीटिंग प्रारंभ करें",
"terms": "शर्तें",
"title": "सुरक्षित, पूरी तरह से चित्रित, और पूरी तरह से मुक्त वीडियो कॉन्फ्रेंसिंग",
"upcomingMeetings": "आपकी आगामी मीटिंग्स"
},
"whiteboard": {
"accessibilityLabel": {
"heading": "व्हाइटबोर्ड"
},
"screenTitle": "व्हाइटबोर्ड"
"title": "सुरक्षित, पूरी तरह से चित्रित, और पूरी तरह से मुक्त वीडियो कॉन्फ्रेंसिंग"
}
}

View File

@@ -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 Salesforce",
"linkMeetingTitle": "Vergadering koppelen aan Verkoop",
"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 in Salesforce",
"searchInSalesforce": "Zoeken bij Verkoop",
"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": "Starten ",
"start": "Start ",
"startLiveStreaming": "Livestream starten",
"startRecording": "Opname starten",
"startRemoteControlErrorMessage": "Er is een fout opgetreden tijdens het starten van de sessie van extern beheer.",
@@ -592,7 +592,6 @@
"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"
@@ -848,12 +847,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 Salesforce",
"linkToSalesforceDescription": "U kunt de samenvatting van de vergadering koppelen aan een Salesforce-object.",
"linkToSalesforceError": "Koppelen aan Salesforce is mislukt",
"linkToSalesforce": "Koppelen aan Verkoop",
"linkToSalesforceDescription": "U kunt de samenvatting van de vergadering koppelen aan een Verkoop-object.",
"linkToSalesforceError": "Koppelen aan Verkoop is mislukt",
"linkToSalesforceKey": "Deze vergadering koppelen",
"linkToSalesforceProgress": "Vergadering koppelen aan Salesforce…",
"linkToSalesforceSuccess": "De vergadering is gekoppeld aan Salesforce",
"linkToSalesforceProgress": "Vergadering koppelen aan Verkoop…",
"linkToSalesforceSuccess": "De vergadering is gekoppeld aan Verkoop",
"localRecordingStarted": "{{name}} heeft een lokale opname gestart.",
"localRecordingStopped": "{{name}} heeft een lokale opname gestopt.",
"me": "Ik",
@@ -889,7 +888,7 @@
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) verwijderd door een andere deelnemer",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) ingesteld door een ander deelnemer",
"raiseHandAction": "Hand opsteken",
"raisedHand": "Wil graag deelnemen.",
"raisedHand": "Wilgraag deelnemen.",
"raisedHands": "{{participantName}} en {{raisedHands}} meer mensen",
"reactionSounds": "Geluiden uitschakelen",
"reactionSoundsForAll": "Geluiden uitschakelen voor iedereen",
@@ -1097,11 +1096,11 @@
"setEmailLabel": "Gravatar e-mailadres",
"title": "Profiel"
},
"raisedHand": "Wil graag spreken",
"raisedHand": "Zou graag willen spreken",
"raisedHandsLabel": "Aantal handen omhoog",
"record": {
"already": {
"linked": "De vergadering is al gekoppeld aan dit Salesforce-object."
"linked": "De vergadering is al gekoppeld aan dit Verkoop-object."
},
"type": {
"account": "Account",
@@ -1348,7 +1347,7 @@
"laugh": "Lachen",
"leaveConference": "Vergadering verlaten",
"like": "Duim omhoog",
"linkToSalesforce": "Koppelen aan Salesforce",
"linkToSalesforce": "Koppelen met Verkoop",
"lobbyButton": "Wachtruimte in- of uitschakelen",
"localRecording": "Besturingselementen voor lokale opname in- of uitschakelen",
"lockRoom": "Wachtwoord voor vergadering in- of uitschakelen",
@@ -1455,7 +1454,7 @@
"leaveBreakoutRoom": "Aparte vergaderruimte verlaten",
"leaveConference": "Vergadering verlaten",
"like": "Duim omhoog",
"linkToSalesforce": "Koppelen aan Salesforce",
"linkToSalesforce": "Koppelen aan Verkoop",
"lobbyButtonDisable": "Wachtruimte uitschakelen",
"lobbyButtonEnable": "Wachtruimte inschakelen",
"login": "Aanmelden",
@@ -1631,30 +1630,30 @@
"chatIndicator": "(toeschouwer)",
"joinMeeting": {
"description": "U bent momenteel een toeschouwer bij deze conferentie.",
"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."
"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."
},
"labelTooltip": "Aantal toeschouwers: {{count}}",
"labelTooltip": "Number of viewers: {{count}}",
"notification": {
"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"
"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"
},
"waitingMessage": "U zult deelnemen aan de vergadering zodra deze live is!"
"waitingMessage": "You'll join the meeting as soon as it is live!"
},
"volumeSlider": "Volume-schuifknop",
"volumeSlider": "Volume slider",
"welcomepage": {
"accessibilityLabel": {
"join": "Tik om deel te nemen",
"roomname": "Voer naam van ruimte in"
},
"addMeetingName": "Geef de vergadering een naam",
"addMeetingName": "Add Meeting name",
"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",
@@ -1674,16 +1673,16 @@
"join": "AANMAKEN / DEELNEMEN",
"logo": {
"calendar": "Agenda logo",
"desktopPreviewThumbnail": "Bureaubladvoorbeeld miniatuur",
"desktopPreviewThumbnail": "Desktop preview thumbnail",
"googleLogo": "Google logo",
"logoDeepLinking": "Jitsi meet logo",
"microsoftLogo": "Microsoft logo",
"policyLogo": "Policy logo"
},
"meetingsAccessibilityLabel": "Vergaderingen",
"mobileDownLoadLinkAndroid": "Download mobiele app voor Android",
"mobileDownLoadLinkFDroid": "Download mobiele app voor F-Droid",
"mobileDownLoadLinkIos": "Download mobiele app voor iOS",
"meetingsAccessibilityLabel": "Meetings",
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
"mobileDownLoadLinkIos": "Download mobile app for 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",

View File

@@ -383,8 +383,6 @@
"lockRoom": "Add meeting $t(lockRoomPassword)",
"lockTitle": "Lock failed",
"login": "Login",
"loginFailed": "Login failed.",
"loginOnResume": "Your authentication session has expired. You need to login again to continue the meeting.",
"loginQuestion": "Are you sure you want to login and leave the conference?",
"logoutQuestion": "Are you sure you want to logout and leave the conference?",
"logoutTitle": "Logout",
@@ -594,7 +592,6 @@
"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"

View File

@@ -2253,32 +2253,6 @@ 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.
*

View File

@@ -133,8 +133,6 @@ 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',

View File

@@ -3,7 +3,7 @@
import Logger from '@jitsi/logger';
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
@@ -166,7 +166,8 @@ export default class LargeVideoManager {
this.removePresenceLabel();
ReactDOM.unmountComponentAtNode(this._dominantSpeakerAvatarContainer);
this._avatarRoot?.unmount();
this._avatarRoot = null;
this.container.style.display = 'none';
}
@@ -518,14 +519,16 @@ export default class LargeVideoManager {
* Updates the src of the dominant speaker avatar
*/
updateAvatar() {
ReactDOM.render(
if (!this._avatarRoot) {
this._avatarRoot = createRoot(this._dominantSpeakerAvatarContainer);
}
this._avatarRoot.render(
<Provider store = { APP.store }>
<Avatar
id = "dominantSpeakerAvatar"
participantId = { this.id }
size = { 200 } />
</Provider>,
this._dominantSpeakerAvatarContainer
</Provider>
);
}
@@ -559,15 +562,18 @@ export default class LargeVideoManager {
const presenceLabelContainer = document.getElementById('remotePresenceMessage');
if (presenceLabelContainer) {
ReactDOM.render(
if (!this._presenceLabelRoot) {
this._presenceLabelRoot = createRoot(presenceLabelContainer);
}
this._presenceLabelRoot.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<PresenceLabel
participantID = { id }
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer);
</Provider>
);
}
}
@@ -577,11 +583,8 @@ export default class LargeVideoManager {
* @returns {void}
*/
removePresenceLabel() {
const presenceLabelContainer = document.getElementById('remotePresenceMessage');
if (presenceLabelContainer) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
}
this._presenceLabelRoot?.unmount();
this._presenceLabelRoot = null;
}
/**

View File

@@ -4,7 +4,7 @@
import Logger from '@jitsi/logger';
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { browser } from '../../../react/features/base/lib-jitsi-meet';
import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip/constants';
@@ -659,7 +659,12 @@ export class VideoContainer extends LargeContainer {
return;
}
ReactDOM.render(
const container = document.getElementById('largeVideoBackgroundContainer');
if (!this._backgroundRoot) {
this._backgroundRoot = createRoot(container);
}
this._backgroundRoot.render(
<LargeVideoBackground
hidden = { this._hideBackground || this._isHidden }
mirror = {
@@ -669,8 +674,7 @@ export class VideoContainer extends LargeContainer {
}
orientationFit = { this._backgroundOrientation }
videoElement = { this.video }
videoTrack = { this.stream } />,
document.getElementById('largeVideoBackgroundContainer')
videoTrack = { this.stream } />
);
}
}

26
package-lock.json generated
View File

@@ -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/v2131.0.0+6912eed8/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/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/v2131.0.0+6912eed8/lib-jitsi-meet.tgz",
"integrity": "sha512-oTQHdvxPkevpn0CNRiiKaojb6PQxRJMdrmV5N7yrWsiqUA5qw+eMQ7A251oahaSfpW5yLz2CM4l+OHGADWrBUQ==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "^2.6.7",
@@ -19062,7 +19062,6 @@
"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",
@@ -19092,15 +19091,6 @@
"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",
@@ -40858,8 +40848,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2131.0.0+6912eed8/lib-jitsi-meet.tgz",
"integrity": "sha512-oTQHdvxPkevpn0CNRiiKaojb6PQxRJMdrmV5N7yrWsiqUA5qw+eMQ7A251oahaSfpW5yLz2CM4l+OHGADWrBUQ==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
"requires": {
"@jitsi/js-utils": "^2.6.7",
"@jitsi/logger": "2.1.1",
@@ -40870,7 +40860,6 @@
"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",
@@ -40898,11 +40887,6 @@
"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=="
}
}
},

View File

@@ -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/v2131.0.0+6912eed8/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",

View File

@@ -113,13 +113,12 @@ export function maybeRedirectToTokenAuthUrl(
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
if (!isTokenAuthEnabled(state)) {
if (!isTokenAuthEnabled(config)) {
return false;
}
// if tokenAuthUrl check jwt if is about to expire go through the url to get new token
const jwt = state['features/base/jwt'].jwt;
const refreshToken = state['features/base/jwt'].refreshToken;
const expirationDate = getJwtExpirationDate(jwt);
// if there is jwt and its expiration time is less than 3 minutes away
@@ -138,8 +137,7 @@ export function maybeRedirectToTokenAuthUrl(
videoMuted
},
room,
tenant,
refreshToken
tenant
)
.then((tokenAuthServiceUrl: string | undefined) => {
if (!tokenAuthServiceUrl) {

View File

@@ -11,9 +11,8 @@ const route = {
* store.
*
* @param {any} _stateful - Used on web.
* @param {any} _dispatch - Used on web.
* @returns {Promise<Object>}
*/
export function _getRouteToRender(_stateful?: any): Promise<object> {
export function _getRouteToRender(_stateful?: any) {
return Promise.resolve(route);
}

View File

@@ -1,10 +1,13 @@
// @ts-expect-error
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
import { getTokenAuthUrl } from '../authentication/functions.web';
import { IStateful } from '../base/app/types';
import { isRoomValid } from '../base/conference/functions';
import { isSupportedBrowser } from '../base/environment/environment';
import { browser } from '../base/lib-jitsi-meet';
import { toState } from '../base/redux/functions';
import { parseURIString } from '../base/util/uri';
import Conference from '../conference/components/web/Conference';
import { getDeepLinkingPage } from '../deep-linking/functions';
import UnsupportedDesktopBrowser from '../unsupported-browser/components/UnsupportedDesktopBrowser';
@@ -20,10 +23,9 @@ import { IReduxState } from './types';
*
* @param {(Function|Object)} stateful - THe redux store, state, or
* {@code getState} function.
* @param {Dispatch} dispatch - The Redux dispatch function.
* @returns {Promise<Object>}
*/
export function _getRouteToRender(stateful: IStateful): Promise<object> {
export function _getRouteToRender(stateful: IStateful) {
const state = toState(stateful);
return _getWebConferenceRoute(state) || _getWebWelcomePageRoute(state);
@@ -34,10 +36,9 @@ export function _getRouteToRender(stateful: IStateful): Promise<object> {
* a valid conference is being joined.
*
* @param {Object} state - The redux state.
* @param {Dispatch} dispatch - The Redux dispatch function.
* @returns {Promise|undefined}
*/
function _getWebConferenceRoute(state: IReduxState): Promise<any> | undefined {
function _getWebConferenceRoute(state: IReduxState) {
const room = state['features/base/conference'].room;
if (!isRoomValid(room)) {
@@ -45,6 +46,36 @@ function _getWebConferenceRoute(state: IReduxState): Promise<any> | undefined {
}
const route = _getEmptyRoute();
const config = state['features/base/config'];
// if we have auto redirect enabled, and we have previously logged in successfully
// let's redirect to the auth url to get the token and login again
if (!browser.isElectron() && config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const { startAudioOnly } = config;
return getTokenAuthUrl(
config,
locationURL,
{
audioMuted: false,
audioOnlyEnabled: startAudioOnly,
skipPrejoin: false,
videoMuted: false
},
room,
tenant
)
.then((url: string | undefined) => {
route.href = url;
return route;
})
.catch(() => Promise.resolve(route));
}
// Update the location if it doesn't match. This happens when a room is
// joined from the welcome page. The reason for doing this instead of using

View File

@@ -1,5 +1,4 @@
import '../base/app/middleware';
import '../base/jwt/middleware.web';
import '../base/config/middleware';
import '../base/connection/middleware';
import '../base/devices/middleware';

View File

@@ -88,13 +88,3 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string) {
Linking.openURL(tokenAuthServiceUrl);
};
}
/**
* Not used.
*
* @param {string} tokenAuthServiceUrl - Authentication service URL.
* @returns {Promise<any>} Resolves.
*/
export function loginWithPopup(tokenAuthServiceUrl: string): Promise<any> {
return Promise.resolve(tokenAuthServiceUrl);
}

View File

@@ -1,14 +1,10 @@
import { maybeRedirectToWelcomePage } from '../app/actions.web';
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { setJWT } from '../base/jwt/actions';
import { browser } from '../base/lib-jitsi-meet';
import { showErrorNotification } from '../notifications/actions';
import { CANCEL_LOGIN } from './actionTypes';
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
import { isTokenAuthInline } from './functions.any';
import logger from './logger';
export * from './actions.any';
@@ -50,147 +46,6 @@ export function redirectToDefaultLocation() {
return (dispatch: IStore['dispatch']) => dispatch(maybeRedirectToWelcomePage());
}
/**
* Generates a cryptographic nonce.
*
* @returns {string} The generated nonce.
*/
function generateNonce(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
/**
* Performs login with a popup window.
*
* @param {string} tokenAuthServiceUrl - Authentication service URL.
* @returns {Promise<any>} A promise that resolves with the authentication
* result or rejects with an error.
*/
export function loginWithPopup(tokenAuthServiceUrl: string): Promise<any> {
return new Promise<any>((resolve, reject) => {
// Open popup
const width = 500;
const height = 600;
const left = window.screen.width / 2 - width / 2;
const top = window.screen.height / 2 - height / 2;
let nonceParam = '';
try {
const nonce = generateNonce();
sessionStorage.setItem('oauth_nonce', nonce);
nonceParam = `&nonce=${nonce}`;
} catch (e) {
if (e instanceof DOMException && e.name === 'SecurityError') {
logger.warn(
'sessionStorage access denied (cross-origin or restricted context) enable it to improve security',
e);
} else {
logger.error('Unable to save nonce in session storage', e);
}
}
const popup = window.open(
`${tokenAuthServiceUrl}${nonceParam}`,
`Auth-${Date.now()}`,
`width=${width},height=${height},left=${left},top=${top}`
);
if (!popup) {
reject(new Error('Popup blocked'));
return;
}
let closedPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
const cleanup = (handler: (event: MessageEvent) => void) => {
window.removeEventListener('message', handler);
clearInterval(closedPollInterval);
popup.close();
try {
sessionStorage.removeItem('oauth_nonce');
} catch (e) {
// ignore
}
try {
sessionStorage.removeItem('code_verifier');
} catch (e) {
// ignore
}
};
const handler = (event: MessageEvent) => {
// Verify origin
if (event.origin !== window.location.origin) {
return;
}
if (event.data.type === 'oauth-success') {
cleanup(handler);
resolve({
accessToken: event.data.accessToken,
idToken: event.data.idToken,
refreshToken: event.data.refreshToken
});
} else if (event.data.type === 'oauth-error') {
cleanup(handler);
reject(new Error(event.data.error));
}
};
// Listen for messages from the popup
window.addEventListener('message', handler);
// Detect manual popup close before authentication completes
closedPollInterval = setInterval(() => {
if (popup.closed) {
cleanup(handler);
reject(new Error('Login cancelled'));
}
}, 500);
});
}
/**
* Performs silent logout by loading the token authentication logout service URL in an
* invisible iframe.
*
* @param {string} tokenAuthLogoutServiceUrl - Logout service URL.
* @returns {Promise<any>} A promise that resolves when logout is complete.
*/
export function silentLogout(tokenAuthLogoutServiceUrl: string): any {
return new Promise<void>(resolve => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = tokenAuthLogoutServiceUrl;
document.body.appendChild(iframe);
// Listen for logout completion
const handler = (event: any) => {
if (event.origin !== window.location.origin) return;
if (event.data.type === 'logout-success') {
window.removeEventListener('message', handler);
document.body.removeChild(iframe);
resolve();
}
};
window.addEventListener('message', handler);
});
}
/**
* Opens token auth URL page.
*
@@ -208,42 +63,6 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
}
};
if (!browser.isElectron() && isTokenAuthInline(getState()['features/base/config'])) {
loginWithPopup(tokenAuthServiceUrl)
.then((result: { accessToken: string; idToken: string; refreshToken?: string; }) => {
// @ts-ignore
const token: string = result.accessToken;
const idToken: string = result.idToken;
const refreshToken: string | undefined = result.refreshToken;
// @ts-ignore
dispatch(setJWT(token, idToken, refreshToken));
logger.info('Reconnecting to conference with new token.');
const { connection } = getState()['features/base/connection'];
connection?.refreshToken(token).then(
() => {
const { membersOnly } = getState()['features/base/conference'];
membersOnly?.join();
})
.catch((err: any) => {
dispatch(setJWT());
logger.error(err);
});
})
.catch(err => {
dispatch(showErrorNotification({
titleKey: 'dialog.loginFailed'
}));
logger.error(err);
});
return;
}
// Show warning for leaving conference only when in a conference.
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, {

View File

@@ -1,30 +1,15 @@
import { IReduxState } from '../app/types';
import { IConfig } from '../base/config/configType';
import { parseURLParams } from '../base/util/parseURLParams';
import { getBackendSafeRoomName } from '../base/util/uri';
import { isVpaasMeeting } from '../jaas/functions';
/**
* Checks if the token for authentication URL is available and the meeting is not jaas.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export const isTokenAuthEnabled = (state: IReduxState): boolean => {
const config = state['features/base/config'];
return typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length > 0
&& !isVpaasMeeting(state);
};
/**
* Checks if the token authentication should be done inline.
* Checks if the token for authentication is available.
*
* @param {Object} config - Configuration state object from store.
* @returns {boolean}
*/
export const isTokenAuthInline = (config: IConfig): boolean =>
config.tokenAuthInline === true;
export const isTokenAuthEnabled = (config: IConfig): boolean =>
typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length > 0;
/**
* Returns the state that we can add as a parameter to the tokenAuthUrl.
@@ -38,7 +23,6 @@ export const isTokenAuthInline = (config: IConfig): boolean =>
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
* @param {string?} refreshToken - The refresh token if available.
*
* @returns {Object} The state object.
*/
@@ -51,10 +35,8 @@ export const _getTokenAuthState = (
videoMuted: boolean | undefined;
},
roomName: string | undefined,
tenant: string | undefined,
refreshToken?: string): object => {
tenant: string | undefined): object => {
const state = {
refreshToken,
room: roomName,
roomSafe: getBackendSafeRoomName(roomName),
tenant

View File

@@ -23,7 +23,6 @@ export * from './functions.any';
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
* @param {string?} refreshToken - The refreshToken if any.
*
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
@@ -40,9 +39,7 @@ export const getTokenAuthUrl = (
},
roomName: string | undefined,
// eslint-disable-next-line max-params
tenant: string | undefined,
// eslint-disable-next-line max-params
refreshToken?: string | undefined): Promise<string | undefined> => {
tenant: string | undefined): Promise<string | undefined> => {
const {
audioMuted = false,
@@ -67,8 +64,7 @@ export const getTokenAuthUrl = (
videoMuted
},
roomName,
tenant,
refreshToken
tenant
);
// Append ios=true or android=true to the token URL.

View File

@@ -4,7 +4,6 @@ import { IConfig } from '../base/config/configType';
import { browser } from '../base/lib-jitsi-meet';
import { _getTokenAuthState } from './functions.any';
import logger from './logger';
export * from './functions.any';
@@ -42,7 +41,6 @@ function _cryptoRandom() {
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
* @param {string?} refreshToken - The refresh token if available.
*
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
@@ -58,10 +56,9 @@ export const getTokenAuthUrl = (
videoMuted: boolean | undefined;
},
roomName: string | undefined,
// eslint-disable max-params
tenant: string | undefined,
refreshToken?: string): Promise<string | undefined> => {
// eslint-enable max-params
// eslint-disable-next-line max-params
tenant: string | undefined): Promise<string | undefined> => {
const {
audioMuted = false,
audioOnlyEnabled = false,
@@ -85,8 +82,7 @@ export const getTokenAuthUrl = (
videoMuted
},
roomName,
tenant,
refreshToken
tenant
);
if (browser.isElectron()) {
@@ -107,17 +103,7 @@ export const getTokenAuthUrl = (
codeVerifier += POSSIBLE_CHARS.charAt(Math.floor(_cryptoRandom() * POSSIBLE_CHARS.length));
}
try {
window.sessionStorage.setItem('code_verifier', codeVerifier);
} catch (e) {
if (e instanceof DOMException && e.name === 'SecurityError') {
logger.warn(
'sessionStorage access denied (cross-origin or restricted context) enable it to improve security',
e);
} else {
logger.error('Unable to save code verifier in session storage', e);
}
}
window.sessionStorage.setItem('code_verifier', codeVerifier);
return window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
.then(digest => {

View File

@@ -1,4 +1,5 @@
import { IStore } from '../app/types';
import { APP_WILL_NAVIGATE } from '../base/app/actionTypes';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
@@ -16,7 +17,6 @@ import { MEDIA_TYPE } from '../base/media/constants';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { isLocalTrackMuted } from '../base/tracks/functions.any';
import { parseURIString } from '../base/util/uri';
import { PREJOIN_JOINING_IN_PROGRESS } from '../prejoin/actionTypes';
import { openLogoutDialog } from '../settings/actions';
import {
@@ -130,7 +130,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = getState();
const config = state['features/base/config'];
if (isTokenAuthEnabled(state)
if (isTokenAuthEnabled(config)
&& config.tokenAuthUrlAutoRedirect
&& state['features/base/jwt'].jwt) {
// auto redirect is turned on and we have successfully logged in
@@ -187,11 +187,7 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case PREJOIN_JOINING_IN_PROGRESS: {
if (!action.value) {
break;
}
case APP_WILL_NAVIGATE: {
const { dispatch, getState } = store;
const state = getState();
const config = state['features/base/config'];
@@ -292,7 +288,6 @@ function _handleLogin({ dispatch, getState }: IStore) {
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
const refreshToken = state['features/base/jwt'].refreshToken;
if (!room) {
logger.warn('Cannot handle login, room is undefined!');
@@ -300,7 +295,7 @@ function _handleLogin({ dispatch, getState }: IStore) {
return;
}
if (!isTokenAuthEnabled(state)) {
if (!isTokenAuthEnabled(config)) {
dispatch(openLoginDialog());
return;
@@ -316,8 +311,7 @@ function _handleLogin({ dispatch, getState }: IStore) {
videoMuted
},
room,
tenant,
refreshToken
tenant
)
.then((tokenAuthServiceUrl: string | undefined) => {
if (!tokenAuthServiceUrl) {

View File

@@ -354,7 +354,7 @@ export function e2eRttChanged(participant: Object, rtt: number) {
* authLogin: string
* }}
*/
export function authStatusChanged(authEnabled: boolean, authLogin?: string) {
export function authStatusChanged(authEnabled: boolean, authLogin: string) {
return {
type: AUTH_STATUS_CHANGED,
authEnabled,

View File

@@ -14,7 +14,6 @@ import { sendAnalytics } from '../../analytics/functions';
import { reloadNow } from '../../app/actions';
import { IStore } from '../../app/types';
import { login } from '../../authentication/actions.any';
import { isTokenAuthEnabled } from '../../authentication/functions.any';
import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name/actions';
import { isVpaasMeeting } from '../../jaas/functions';
@@ -267,11 +266,8 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
descriptionKey = 'dialog.errorRoomCreationRestriction';
} else if (type === JitsiConferenceErrors.AUTH_ERROR_TYPES.ROOM_UNAUTHENTICATED_ACCESS_DISABLED) {
titleKey = 'dialog.unauthenticatedAccessDisabled';
if (isTokenAuthEnabled(getState())) {
customActionNameKey = [ 'toolbar.login' ];
customActionHandler = [ () => dispatch(login()) ]; // show login button if not jaas
}
customActionNameKey = [ 'toolbar.login' ];
customActionHandler = [ () => dispatch(login()) ];
}
dispatch(showErrorNotification({
@@ -406,8 +402,7 @@ async function _connectionEstablished({ dispatch, getState }: IStore, next: Func
email = getLocalParticipant(getState())?.email;
}
// it may happen to be already set
dispatch(authStatusChanged(true, email || getState()['features/base/conference'].authLogin || ''));
dispatch(authStatusChanged(true, email || ''));
}
// FIXME: Workaround for the web version. Currently, the creation of the

View File

@@ -616,7 +616,6 @@ export interface IConfig {
disabled?: boolean;
numberOfVisibleTiles?: number;
};
tokenAuthInline?: boolean;
tokenAuthUrl?: string;
tokenAuthUrlAutoRedirect?: string;
tokenGetUserInfoOutOfContext?: boolean;

View File

@@ -51,16 +51,6 @@ export const CONNECTION_PROPERTIES_UPDATED = 'CONNECTION_PROPERTIES_UPDATED';
*/
export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT';
/**
* The type of (redux) action which signals that the token for a connection is expired.
*
* {
* type: CONNECTION_TOKEN_EXPIRED,
* connection: JitsiConnection
* }
*/
export const CONNECTION_TOKEN_EXPIRED = 'CONNECTION_TOKEN_EXPIRED';
/**
* The type of (redux) action which sets the location URL of the application,
* connection, conference, etc.

View File

@@ -17,7 +17,6 @@ import {
CONNECTION_ESTABLISHED,
CONNECTION_FAILED,
CONNECTION_PROPERTIES_UPDATED,
CONNECTION_TOKEN_EXPIRED,
CONNECTION_WILL_CONNECT,
SET_LOCATION_URL,
SET_PREFER_VISITOR
@@ -240,9 +239,6 @@ export function _connectInternal(id?: string, password?: string) {
connection.addEventListener(
JitsiConnectionEvents.PROPERTIES_UPDATED,
_onPropertiesUpdate);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_TOKEN_EXPIRED,
_onTokenExpired);
/**
* Unsubscribe the connection instance from
@@ -327,16 +323,6 @@ export function _connectInternal(id?: string, password?: string) {
dispatch(redirect(vnode, focusJid, username));
}
/**
* Connection will resume.
*
* @private
* @returns {void}
*/
function _onTokenExpired(): void {
dispatch(_connectionTokenExpired(connection));
}
/**
* Connection properties were updated.
*
@@ -378,23 +364,6 @@ function _connectionWillConnect(connection: Object) {
};
}
/**
* Create an action for when a connection token is expired.
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} token is expired.
* @private
* @returns {{
* type: CONNECTION_TOKEN_EXPIRED,
* connection: JitsiConnection
* }}
*/
function _connectionTokenExpired(connection: Object) {
return {
type: CONNECTION_TOKEN_EXPIRED,
connection
};
}
/**
* Create an action for when connection properties are updated.
*

View File

@@ -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 { conferenceLeft } from '../conference/actions.native';
import { conferenceWillLeave } 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());
};
}

View File

@@ -23,7 +23,6 @@ export interface IConnectionState {
getJid: () => string;
getLogs: () => Object;
initJitsiConference: Function;
refreshToken: Function;
removeFeature: Function;
};
error?: ConnectionFailedError;

View File

@@ -1 +0,0 @@
export * from './utils.any';

View File

@@ -29,3 +29,4 @@ export function isIpadMobileBrowser() {
// @ts-ignore
return isIosMobileBrowser() && Platform.isPad;
}

View File

@@ -1,37 +0,0 @@
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';

View File

@@ -20,21 +20,15 @@ export function setDelayedLoadOfAvatarUrl(avatarUrl?: string) {
* Stores a specific JSON Web Token (JWT) into the redux store.
*
* @param {string} [jwt] - The JSON Web Token (JWT) to store.
* @param {string} idToken - The ID Token to store.
* @param {string} refreshToken - The Refresh Token to store.
* @returns {{
* type: SET_JWT,
* jwt: (string|undefined),
* idToken: (string|undefined),
* refreshToken: (string|undefined)
* jwt: (string|undefined)
* }}
*/
export function setJWT(jwt?: string, idToken?: string, refreshToken?: string) {
export function setJWT(jwt?: string) {
return {
type: SET_JWT,
jwt,
idToken,
refreshToken
jwt
};
}

View File

@@ -4,7 +4,6 @@ import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { isVpaasMeeting } from '../../jaas/functions';
import { authStatusChanged } from '../conference/actions.any';
import { getCurrentConference } from '../conference/functions';
import { SET_CONFIG } from '../config/actionTypes';
import { CONNECTION_ESTABLISHED, SET_LOCATION_URL } from '../connection/actionTypes';
@@ -40,8 +39,6 @@ StateListenerRegistry.register(
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
switch (action.type) {
case SET_CONFIG:
case SET_LOCATION_URL:
@@ -49,6 +46,7 @@ MiddlewareRegistry.register(store => next => action => {
// have decided to store in the feature jwt
return _setConfigOrLocationURL(store, next, action);
case CONNECTION_ESTABLISHED: {
const state = store.getState();
const delayedLoadOfAvatarUrl = state['features/base/jwt'].delayedLoadOfAvatarUrl;
if (delayedLoadOfAvatarUrl) {
@@ -58,7 +56,6 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(setDelayedLoadOfAvatarUrl());
store.dispatch(setKnownAvatarUrl(delayedLoadOfAvatarUrl));
}
break;
}
case SET_JWT:
return _setJWT(store, next, action);
@@ -152,7 +149,7 @@ function _setConfigOrLocationURL({ dispatch, getState }: IStore, next: Function,
*/
function _setJWT(store: IStore, next: Function, action: AnyAction) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { idToken, jwt, refreshToken, type, ...actionPayload } = action;
const { jwt, type, ...actionPayload } = action;
if (!Object.keys(actionPayload).length) {
const state = store.getState();
@@ -213,32 +210,24 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
if (context.user && context.user.role === 'visitor') {
action.preferVisitor = true;
}
} else if (jwtPayload.name || jwtPayload.picture || jwtPayload.email) {
if (tokenGetUserInfoOutOfContext) {
// there are some tokens (firebase) having picture and name on the main level.
_overwriteLocalParticipant(store, {
avatarURL: jwtPayload.picture,
name: jwtPayload.name,
email: jwtPayload.email
});
}
store.dispatch(authStatusChanged(true, jwtPayload.email));
} else if (tokenGetUserInfoOutOfContext
&& (jwtPayload.name || jwtPayload.picture || jwtPayload.email)) {
// there are some tokens (firebase) having picture and name on the main level.
_overwriteLocalParticipant(store, {
avatarURL: jwtPayload.picture,
name: jwtPayload.name,
email: jwtPayload.email
});
}
}
} else {
if (typeof APP === 'undefined') {
// The logic of restoring JWT overrides make sense only on mobile.
// On Web it should eventually be restored from storage, but there's
// no such use case yet.
} else if (typeof APP === 'undefined') {
// The logic of restoring JWT overrides make sense only on mobile.
// On Web it should eventually be restored from storage, but there's
// no such use case yet.
const { user } = state['features/base/jwt'];
const { user } = state['features/base/jwt'];
user && _undoOverwriteLocalParticipant(store, user);
}
// clears authLogin
store.dispatch(authStatusChanged(true));
user && _undoOverwriteLocalParticipant(store, user);
}
}

View File

@@ -1,100 +0,0 @@
import { IStore } from '../../app/types';
import { loginWithPopup } from '../../authentication/actions';
import LoginQuestionDialog from '../../authentication/components/web/LoginQuestionDialog';
import { getTokenAuthUrl, isTokenAuthEnabled, isTokenAuthInline } from '../../authentication/functions';
import { hideNotification, showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../../notifications/constants';
import { CONNECTION_TOKEN_EXPIRED } from '../connection/actionTypes';
import { openDialog } from '../dialog/actions';
import { browser } from '../lib-jitsi-meet';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { parseURIString } from '../util/uri';
import { setJWT } from './actions';
import logger from './logger';
const PROMPT_LOGIN_NOTIFICATION_ID = 'PROMPT_LOGIN_NOTIFICATION_ID';
/**
* Middleware to handle token expiration on web - prompts the user to re-authenticate.
*
* @param {Store} store - The redux store.
* @private
* @returns {Function}
*/
MiddlewareRegistry.register((store: IStore) => next => action => {
if (action.type === CONNECTION_TOKEN_EXPIRED) {
const state = store.getState();
const jwt = state['features/base/jwt'].jwt;
const refreshToken = state['features/base/jwt'].refreshToken;
if (typeof APP !== 'undefined' && jwt && isTokenAuthEnabled(state)) {
const { connection, locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const room = state['features/base/conference'].room;
const dispatch = store.dispatch;
getTokenAuthUrl(
state['features/base/config'],
locationURL,
{
audioMuted: false,
audioOnlyEnabled: false,
skipPrejoin: false,
videoMuted: false
},
room,
tenant,
refreshToken
)
.then((url: string | undefined) => {
if (url) {
dispatch(showNotification({
descriptionKey: 'dialog.loginOnResume',
titleKey: 'dialog.login',
uid: PROMPT_LOGIN_NOTIFICATION_ID,
customActionNameKey: [ 'dialog.login' ],
customActionHandler: [ () => {
store.dispatch(hideNotification(PROMPT_LOGIN_NOTIFICATION_ID));
if (isTokenAuthInline(state['features/base/config'])) {
loginWithPopup(url)
.then((result: { accessToken: string; idToken: string; refreshToken?: string; }) => {
const token: string = result.accessToken;
const idToken: string = result.idToken;
const newRefreshToken: string | undefined = result.refreshToken;
dispatch(setJWT(token, idToken, newRefreshToken || refreshToken));
connection?.refreshToken(token)
.catch((err: any) => {
dispatch(setJWT());
logger.error(err);
});
}).catch(logger.error);
} else {
dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, {
handler: () => {
// Give time for the dialog to close.
setTimeout(() => {
if (browser.isElectron()) {
window.open(url, '_blank');
} else {
window.location.href = url;
}
}, 500);
}
}));
}
} ],
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
}
})
.catch(logger.error);
}
}
return next(action);
});

View File

@@ -11,10 +11,8 @@ export interface IJwtState {
};
delayedLoadOfAvatarUrl?: string;
group?: string;
idToken?: string;
jwt?: string;
knownAvatarUrl?: string;
refreshToken?: string;
server?: string;
tenant?: string;
user?: {

View File

@@ -60,6 +60,66 @@ 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.

View File

@@ -68,7 +68,6 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
];
const DEFAULT_STATE = {
activeSpeakers: new Set<string>(),
dominantSpeaker: undefined,
fakeParticipants: new Map(),
local: undefined,
@@ -83,10 +82,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;
@@ -101,6 +100,7 @@ export interface IParticipantsState {
remoteVideoSources: Set<string>;
sortedRemoteParticipants: Map<string, string>;
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
speakersList: Map<string, string>;
}
/**
@@ -157,8 +157,22 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
const { participant } = action;
const { id, previousSpeakers = [] } = participant;
const { dominantSpeaker, local } = state;
const activeSpeakers = new Set(previousSpeakers
.filter((speakerId: string) => state.remote.has(speakerId) && (speakerId !== local?.id)));
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]));
// Only one dominant speaker is allowed.
if (dominantSpeaker) {
@@ -169,7 +183,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
return {
...state,
dominantSpeaker: id, // @ts-ignore
activeSpeakers
speakersList: new Map(sortedSpeakersList)
};
}
@@ -424,7 +438,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
}
// Remove the participant from the list of speakers.
state.activeSpeakers.delete(id);
state.speakersList.has(id) && state.speakersList.delete(id);
if (pinnedParticipant === id) {
state.pinnedParticipant = undefined;

View File

@@ -155,7 +155,6 @@ 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
@@ -221,9 +220,9 @@ export const colorMap = {
// Welcome Page
welcomeBackground: 'surface01', // Welcome page background (same as uiBackground)
welcomeCard: 'surface02', // Welcome page tab bar background
welcomeCard: 'ui01', // Welcome page tab bar background
welcomeTabActive: 'icon01', // Welcome page active tab icon
welcomeTabInactive: 'ui03', // Welcome page inactive tab icon
welcomeTabInactive: 'icon03', // Welcome page inactive tab icon
// ----- Form Components -----
@@ -357,9 +356,6 @@ 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
@@ -432,7 +428,7 @@ export const colorMap = {
securityDialogBorder: 'ui07', // Security dialog border color
// Deep Linking
deepLinkingBackground: 'uiBackground', // Deep linking page content pane background (#1e1e1e)
deepLinkingBackground: 'ui01', // Deep linking page content pane background
deepLinkingBorder: 'ui03', // Deep linking page content pane border
deepLinkingText: 'text01', // Deep linking page text
deepLinkingSeparator: 'ui03', // Deep linking separator line
@@ -509,9 +505,6 @@ export const colorMap = {
// High-contrast
icon04: 'surface01',
// SVG fill color
iconSvgFill: 'icon01',
// Error
iconError: 'action03',

View File

@@ -81,11 +81,7 @@ const useStyles = makeStyles()(theme => {
'&:focus': {
outline: 0,
boxShadow: `0px 0px 0px 2px ${theme.palette.inputFieldFocus}`,
'&::placeholder': {
opacity: 0
}
boxShadow: `0px 0px 0px 2px ${theme.palette.inputFieldFocus}`
},
'&:disabled': {
@@ -111,12 +107,12 @@ const useStyles = makeStyles()(theme => {
},
'input::-webkit-outer-spin-button, input::-webkit-inner-spin-button': {
'-webkit-appearance': 'none',
WebkitAppearance: 'none',
margin: 0
},
'input[type=number]': {
'-moz-appearance': 'textfield'
MozAppearance: 'textfield'
},
icon: {

View File

@@ -13,25 +13,19 @@ 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-icon-active-color': theme.palette.toolbarIconActive,
'--toolbar-button-active-color': theme.palette.toolbarButtonActive,
'--toolbar-icon-color': theme.palette.toolbarIcon,
'--toolbar-icon-hover-color': theme.palette.toolbarIconHover,
'--toolbox-background-color': theme.palette.toolboxBackground
'--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
},
'.empty-list': {

View File

@@ -26,7 +26,6 @@ export interface IPalette {
icon02: string;
icon03: string;
icon04: string;
iconSvgFill: string;
iconError: string;
link01: string;
link01Active: string;
@@ -154,7 +153,6 @@ export interface IPalette {
prejoinDialogBackground: string;
prejoinDialogDelimiter: string;
prejoinDialogDelimiterText: string;
prejoinPreviewBackground: string;
prejoinRecordingWarningText: string;
prejoinRoomNameText: string;
prejoinTitleText: string;
@@ -315,9 +313,6 @@ export interface IPalette {
pollsVotersText: string;
reactionsMenuBackground: string;
reactionsMenuBorder: string;
reactionsMenuButtonToggled: string;
reactionsMenuBoxShadow1: string;
reactionsMenuBoxShadow2: string;
recordingBackground: string;
recordingHighlightButton: string;
recordingHighlightButtonDisabled: string;

View File

@@ -24,13 +24,3 @@ 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;
}

View File

@@ -10,17 +10,3 @@ 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;
}
}

View File

@@ -4,7 +4,6 @@ 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';
@@ -24,16 +23,7 @@ import {
setUserChatWidth,
toggleChat
} from '../../actions.web';
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 { CHAT_SIZE, ChatTabs, OPTION_GROUPCHAT, SMALL_WIDTH_THRESHOLD } from '../../constants';
import { getChatMaxSize, getFocusedTab, isChatDisabled } from '../../functions';
import { IChatProps as AbstractProps } from '../../types';
@@ -114,12 +104,7 @@ interface IProps extends AbstractProps {
_width: number;
}
const useStyles = makeStyles<{
_isResizing: boolean;
isTouch: boolean;
resizeEnabled: boolean;
width: number;
}>()((theme, { _isResizing, isTouch, resizeEnabled, width }) => {
const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme, { _isResizing, width }) => {
return {
container: {
backgroundColor: theme.palette.chatBackground,
@@ -130,15 +115,11 @@ const useStyles = makeStyles<{
width: `${width}px`,
zIndex: 300,
// 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'
}
'&:hover, &:focus-within': {
'& .dragHandleContainer': {
visibility: 'visible'
}
}),
},
'@media (max-width: 580px)': {
height: '100dvh',
@@ -151,7 +132,7 @@ const useStyles = makeStyles<{
'*': {
userSelect: 'text',
'-webkit-user-select': 'text'
WebkitUserSelect: 'text'
}
},
@@ -202,23 +183,16 @@ const useStyles = makeStyles<{
dragHandleContainer: {
height: '100%',
// 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`,
width: '9px',
backgroundColor: 'transparent',
position: 'absolute',
cursor: 'col-resize',
display: resizeEnabled ? 'flex' : 'none', // Hide if resize not enabled
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
// 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`,
visibility: 'hidden',
right: '4px',
top: 0,
// Prevent touch scrolling while dragging
touchAction: 'none',
'&:hover': {
'& .dragHandle': {
@@ -236,15 +210,10 @@ const useStyles = makeStyles<{
},
dragHandle: {
// Keep the same visual appearance on all devices
backgroundColor: theme.palette.icon02,
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
})
height: '100px',
width: '3px',
borderRadius: '1px'
},
privateMessageRecipientsList: {
@@ -277,10 +246,7 @@ const Chat = ({
return null;
}
// 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 { classes, cx } = useStyles({ _isResizing, width: _width });
const [ isMouseDown, setIsMouseDown ] = useState(false);
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
const [ dragChatWidth, setDragChatWidth ] = useState<number | null>(null);
@@ -316,21 +282,16 @@ const Chat = ({
}, [ participants, defaultRemoteDisplayName, t, notifyTimestamp ]);
/**
* Handles pointer down on the drag handle.
* Supports both mouse and touch events via Pointer Events API.
* Handles mouse down on the drag handle.
*
* @param {React.PointerEvent} e - The pointer down event.
* @param {MouseEvent} e - The mouse down event.
* @returns {void}
*/
const onDragHandlePointerDown = useCallback((e: React.PointerEvent) => {
const onDragHandleMouseDown = useCallback((e: React.MouseEvent) => {
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);
// Store the initial pointer position and chat width
// Store the initial mouse position and chat width
setIsMouseDown(true);
setMousePosition(e.clientX);
setDragChatWidth(_width);
@@ -338,7 +299,7 @@ const Chat = ({
// Indicate that resizing is in progress
dispatch(setChatIsResizing(true));
// Add visual feedback that we're dragging (cursor for mouse, not visible on touch)
// Add visual feedback that we're dragging
document.body.style.cursor = 'col-resize';
// Disable text selection during resize
@@ -346,12 +307,11 @@ const Chat = ({
}, [ _width, dispatch ]);
/**
* Drag handle pointer up handler.
* Supports both mouse and touch events via Pointer Events API.
* Drag handle mouse up handler.
*
* @returns {void}
*/
const onDragPointerUp = useCallback(() => {
const onDragMouseUp = useCallback(() => {
if (isMouseDown) {
setIsMouseDown(false);
dispatch(setChatIsResizing(false));
@@ -363,13 +323,12 @@ const Chat = ({
}, [ isMouseDown, dispatch ]);
/**
* Handles drag handle pointer move.
* Supports both mouse and touch events via Pointer Events API.
* Handles drag handle mouse move.
*
* @param {PointerEvent} e - The pointermove event.
* @param {MouseEvent} e - The mousemove event.
* @returns {void}
*/
const onChatResize = useCallback(throttle((e: PointerEvent) => {
const onChatResize = useCallback(throttle((e: MouseEvent) => {
if (isMouseDown && mousePosition !== null && dragChatWidth !== null) {
// For chat panel resizing on the left edge:
// - Dragging left (decreasing X coordinate) should make the panel wider
@@ -393,14 +352,14 @@ const Chat = ({
// Set up event listeners when component mounts
useEffect(() => {
document.addEventListener('pointerup', onDragPointerUp);
document.addEventListener('pointermove', onChatResize);
document.addEventListener('mouseup', onDragMouseUp);
document.addEventListener('mousemove', onChatResize);
return () => {
document.removeEventListener('pointerup', onDragPointerUp);
document.removeEventListener('pointermove', onChatResize);
document.removeEventListener('mouseup', onDragMouseUp);
document.removeEventListener('mousemove', onChatResize);
};
}, [ onDragPointerUp, onChatResize ]);
}, [ onDragMouseUp, onChatResize ]);
/**
* Sends a text message.
@@ -641,7 +600,7 @@ const Chat = ({
(isMouseDown || _isResizing) && 'visible',
'dragHandleContainer'
) }
onPointerDown = { onDragHandlePointerDown }>
onMouseDown = { onDragHandleMouseDown }>
<div className = { cx(classes.dragHandle, 'dragHandle') } />
</div>
</div> : null

View File

@@ -33,23 +33,6 @@ 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.

View File

@@ -227,8 +227,7 @@ class Conference extends AbstractConference<IProps, State> {
const {
_audioOnlyEnabled,
_showLobby,
_startCarMode,
navigation
_startCarMode
} = this.props;
if (!prevProps._showLobby && _showLobby) {
@@ -237,10 +236,10 @@ class Conference extends AbstractConference<IProps, State> {
if (prevProps._showLobby && !_showLobby) {
if (_audioOnlyEnabled && _startCarMode) {
navigation.navigate(screen.conference.carmode);
} else {
navigate(screen.conference.main);
return;
}
navigate(screen.conference.main);
}
}

View File

@@ -6,7 +6,6 @@ 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';
@@ -227,20 +226,16 @@ const FileSharing = () => {
)
}
{
<Tooltip
containerClassName = { classes.uploadButton }
content = { isUploadEnabled ? t('fileSharing.uploadFile') : t('fileSharing.uploadDisabled') }
position = 'top'>
isUploadEnabled && (
<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>
);

View File

@@ -72,9 +72,6 @@ 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) => {
@@ -86,11 +83,6 @@ 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);
});
});
}
});
@@ -160,8 +152,6 @@ MiddlewareRegistry.register(store => next => action => {
privateMessage: false,
timestamp: file.timestamp
}));
APP.API.notifyFileUploaded(file);
}
return result;
@@ -188,9 +178,6 @@ 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();

View File

@@ -60,19 +60,16 @@ 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,
* fullyVisibleCount: number
* endIndex: number
* }}
*/
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number, fullyVisibleCount?: number) {
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
return {
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
startIndex,
endIndex,
fullyVisibleCount
endIndex
};
}

View File

@@ -10,7 +10,6 @@ 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,
@@ -191,7 +190,7 @@ class Filmstrip extends PureComponent<IProps> {
* @returns {void}
*/
_onViewableItemsChanged({ viewableItems = [] }: { viewableItems: ViewToken[]; }) {
const { _aspectRatio, _clientWidth, _clientHeight, _disableSelfView } = this.props;
const { _disableSelfView } = this.props;
if (!this._separateLocalThumbnail && !_disableSelfView && viewableItems[0]?.index === 0) {
// Skip the local thumbnail.
@@ -206,31 +205,13 @@ 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, fullyVisibleCount));
this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
}
/**

View File

@@ -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, isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
import { isMobileBrowser } 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,19 +32,13 @@ 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,
TOUCH_DRAG_HANDLE_PADDING
TOP_FILMSTRIP_HEIGHT
} from '../../constants';
import { calculateFullyVisibleParticipantsCount } from '../../functions.any';
import {
getVerticalViewMaxWidth,
isFilmstripDisabled,
@@ -58,21 +52,6 @@ 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.
@@ -82,14 +61,6 @@ const NON_TOUCH_DEVICE_PANEL = {
* @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',
@@ -123,7 +94,7 @@ function styles(theme: Theme, props: IProps) {
margin: 0,
border: 'none',
'-webkit-appearance': 'none',
WebkitAppearance: 'none' as const,
'& svg': {
fill: theme.palette.icon01
@@ -151,27 +122,23 @@ function styles(theme: Theme, props: IProps) {
right: 0,
bottom: 0,
// 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
},
'&: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',
@@ -220,22 +187,14 @@ function styles(theme: Theme, props: IProps) {
dragHandleContainer: {
height: '100%',
width: `${DRAG_HANDLE_WIDTH}px`,
width: '9px',
backgroundColor: 'transparent',
position: 'relative' as const,
cursor: 'col-resize',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
// 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',
visibility: 'hidden' as const,
'&:hover': {
'& .dragHandle': {
@@ -254,21 +213,20 @@ function styles(theme: Theme, props: IProps) {
'&.top-panel': {
order: 2,
width: '100%',
height: `${DRAG_HANDLE_WIDTH}px`,
height: '9px',
cursor: 'row-resize',
'& .dragHandle': {
height: `${DRAG_HANDLE_TOP_PANEL_HEIGHT}px`,
width: `${DRAG_HANDLE_TOP_PANEL_WIDTH}px`
height: '3px',
width: '100px'
}
}
},
dragHandle: {
// Keep the same visual appearance on all devices
backgroundColor: theme.palette.filmstripDragHandle,
height: `${DRAG_HANDLE_HEIGHT}px`,
width: `${DRAG_HANDLE_WIDTH / 3}px`,
height: '100px',
width: '3px',
borderRadius: '1px'
}
};
@@ -355,11 +313,6 @@ export interface IProps extends WithTranslation {
*/
_isToolboxVisible: Boolean;
/**
* Whether the device has touch capability.
*/
_isTouchDevice?: boolean;
/**
* Whether or not the current layout is vertical filmstrip.
*/
@@ -405,11 +358,6 @@ 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.
*/
@@ -543,10 +491,8 @@ 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._onDragHandlePointerDown = this._onDragHandlePointerDown.bind(this);
this._onDragHandleClick = this._onDragHandleClick.bind(this);
this._onDragHandleTouchStart = this._onDragHandleTouchStart.bind(this);
this._onDragPointerUp = this._onDragPointerUp.bind(this);
this._onDragHandleMouseDown = this._onDragHandleMouseDown.bind(this);
this._onDragMouseUp = this._onDragMouseUp.bind(this);
this._onFilmstripResize = this._onFilmstripResize.bind(this);
this._throttledResize = throttle(
@@ -570,10 +516,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
handler: this._onShortcutToggleFilmstrip
}));
document.addEventListener('pointerup', this._onDragPointerUp);
document.addEventListener('mouseup', this._onDragMouseUp);
// @ts-ignore
document.addEventListener('pointermove', this._throttledResize);
document.addEventListener('mousemove', this._throttledResize);
}
/**
@@ -584,10 +530,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
override componentWillUnmount() {
this.props.dispatch(unregisterShortcut('F'));
document.removeEventListener('pointerup', this._onDragPointerUp);
document.removeEventListener('mouseup', this._onDragMouseUp);
// @ts-ignore
document.removeEventListener('pointermove', this._throttledResize);
document.removeEventListener('mousemove', this._throttledResize);
}
/**
@@ -732,9 +678,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
(isMouseDown || _alwaysShowResizeBar) && 'visible',
_topPanelFilmstrip && 'top-panel')
}
onClick = { this._onDragHandleClick }
onPointerDown = { this._onDragHandlePointerDown }
onTouchStart = { this._onDragHandleTouchStart }>
onMouseDown = { this._onDragHandleMouseDown }>
<div className = { clsx(classes.dragHandle, 'dragHandle') } />
</div>
{filmstrip}
@@ -747,23 +691,14 @@ class Filmstrip extends PureComponent <IProps, IState> {
}
/**
* Handles pointer down on the drag handle.
* Supports both mouse and touch events via Pointer Events API.
* Handles mouse down on the drag handle.
*
* @param {React.PointerEvent} e - The pointer down event.
* @param {MouseEvent} e - The mouse down event.
* @returns {void}
*/
_onDragHandlePointerDown(e: React.PointerEvent) {
_onDragHandleMouseDown(e: React.MouseEvent) {
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,
@@ -774,33 +709,11 @@ class Filmstrip extends PureComponent <IProps, IState> {
}
/**
* 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.
* Drag handle mouse up handler.
*
* @returns {void}
*/
_onDragPointerUp() {
_onDragMouseUp() {
if (this.state.isMouseDown) {
this.setState({
isMouseDown: false
@@ -810,13 +723,12 @@ class Filmstrip extends PureComponent <IProps, IState> {
}
/**
* Handles drag handle pointer move.
* Supports both mouse and touch events via Pointer Events API.
* Handles drag handle mouse move.
*
* @param {PointerEvent} e - The pointermove event.
* @param {MouseEvent} e - The mousemove event.
* @returns {void}
*/
_onFilmstripResize(e: PointerEvent) {
_onFilmstripResize(e: React.MouseEvent) {
if (this.state.isMouseDown) {
const {
dispatch,
@@ -945,33 +857,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
*/
_onListItemsRendered({ visibleStartIndex, visibleStopIndex }: {
visibleStartIndex: number; visibleStopIndex: number; }) {
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 { dispatch } = this.props;
const { startIndex, stopIndex } = this._calculateIndices(visibleStartIndex, visibleStopIndex);
dispatch(setVisibleRemoteParticipants(startIndex, stopIndex, fullyVisibleCount));
dispatch(setVisibleRemoteParticipants(startIndex, stopIndex));
}
/**
@@ -1274,4 +1163,4 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
};
}
export default translate(connect(_mapStateToProps)(withStyles(Filmstrip, styles)));
export default withStyles(translate(connect(_mapStateToProps)(Filmstrip)), styles);

View File

@@ -353,7 +353,7 @@ const defaultStyles = (theme: Theme) => {
tintBackground: {
position: 'absolute' as const,
zIndex: 0,
zIndex: 1,
width: '100%',
height: '100%',
backgroundColor: theme.palette.thumbnailTintBackground,

View File

@@ -269,19 +269,6 @@ 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.
*/
@@ -311,9 +298,3 @@ 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;

View File

@@ -1,5 +1,8 @@
import { IReduxState, IStore } from '../app/types';
import { getParticipantById, getVirtualScreenshareParticipantOwnerId } from '../base/participants/functions';
import {
getActiveSpeakersToBeDisplayed,
getVirtualScreenshareParticipantOwnerId
} from '../base/participants/functions';
import { setRemoteParticipants } from './actions';
import { isFilmstripScrollVisible } from './functions';
@@ -30,70 +33,44 @@ 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 = new Array<string>();
const { fullyVisibleRemoteParticipantsCount } = state['features/filmstrip'];
const speakers = getActiveSpeakersToBeDisplayed(state);
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,
...speakers,
...Array.from(speakers.keys()),
...Array.from(remoteParticipants.keys())
];
@@ -131,31 +108,3 @@ 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));
}

View File

@@ -2,7 +2,7 @@ import { Theme } from '@mui/material/styles';
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { isTouchDevice, shouldEnableResize } from '../base/environment/utils';
import { isMobileBrowser } from '../base/environment/utils';
import { MEDIA_TYPE } from '../base/media/constants';
import {
getLocalParticipant,
@@ -30,7 +30,6 @@ import {
DEFAULT_LOCAL_TILE_ASPECT_RATIO,
DISPLAY_AVATAR,
DISPLAY_VIDEO,
DRAG_HANDLE_WIDTH,
FILMSTRIP_GRID_BREAKPOINT,
FILMSTRIP_TYPE,
INDICATORS_TOOLTIP_POSITION,
@@ -46,7 +45,6 @@ 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';
@@ -623,7 +621,6 @@ 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}
@@ -632,7 +629,7 @@ export function isFilmstripResizable(state: IReduxState) {
const { filmstrip } = state['features/base/config'];
const _currentLayout = getCurrentLayout(state);
return !filmstrip?.disableResizable && shouldEnableResize()
return !filmstrip?.disableResizable && !isMobileBrowser()
&& (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW || _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
}
@@ -665,13 +662,8 @@ 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.
// 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);
// Also adding 7px for the scrollbar. Also adding 9px for the drag handle.
maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? 9 : 4);
return maxWidth;
}

View File

@@ -158,15 +158,6 @@ 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.
*
@@ -210,7 +201,6 @@ export interface IFilmstripState {
pinned?: boolean;
}>;
enabled: boolean;
fullyVisibleRemoteParticipantsCount: number;
horizontalViewDimensions: {
hasScroll?: boolean;
local?: IDimensions;
@@ -311,7 +301,7 @@ ReducerRegistry.register<IFilmstripState>(
}
};
case SET_VISIBLE_REMOTE_PARTICIPANTS: {
const { endIndex, startIndex, fullyVisibleCount } = action;
const { endIndex, startIndex } = action;
const { remoteParticipants } = state;
const visibleRemoteParticipants = new Set(remoteParticipants.slice(startIndex, endIndex + 1));
@@ -319,8 +309,7 @@ ReducerRegistry.register<IFilmstripState>(
...state,
visibleParticipantsStartIndex: startIndex,
visibleParticipantsEndIndex: endIndex,
visibleRemoteParticipants,
fullyVisibleRemoteParticipantsCount: fullyVisibleCount ?? visibleRemoteParticipants.size
visibleRemoteParticipants
};
}
case PARTICIPANT_LEFT: {

View File

@@ -70,15 +70,10 @@ 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

View File

@@ -16,8 +16,7 @@ import {
import { INVITE_TYPES } from './constants';
import {
invitePeopleAndChatRooms,
inviteSipEndpoints,
isDialInEnabled
inviteSipEndpoints
} from './functions';
import logger from './logger';
import { IInvitee } from './types';
@@ -211,7 +210,7 @@ export function updateDialInNumbers() {
const { numbersFetched } = state['features/invite'];
const mucURL = hosts?.muc;
if (numbersFetched || !isDialInEnabled(state)) {
if (numbersFetched || !dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
// URLs for fetching dial in numbers not defined
return;
}

View File

@@ -496,26 +496,6 @@ 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.
*
@@ -654,7 +634,7 @@ export function getShareInfoText(
const { locationURL = {} } = state['features/base/connection'];
const mucURL = hosts?.muc;
if (skipDialIn || !isDialInEnabled(state)) {
if (skipDialIn || !dialInConfCodeUrl || !dialInNumbersUrl || !mucURL) {
// URLs for fetching dial in numbers not defined.
return Promise.resolve(infoText);
}

View File

@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { IReduxState, IStore } from '../../app/types';
import { conferenceWillJoin } from '../../base/conference/actions.any';
import { conferenceWillJoin } from '../../base/conference/actions';
import { getConferenceName } from '../../base/conference/functions';
import { IJitsiConference } from '../../base/conference/reducer';
import { getSecurityUiConfig } from '../../base/config/functions.any';
@@ -11,8 +11,9 @@ 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.any';
import { isDeviceStatusVisible } from '../../prejoin/functions';
import { cancelKnocking, joinWithPassword, onSendMessage, setPasswordJoinFailed, startKnocking } from '../actions';
import { getLobbyConfig } from '../functions';
export const SCREEN_STATES = {
EDIT: 1,
@@ -27,6 +28,11 @@ export interface IProps {
*/
_deviceStatusVisible: boolean;
/**
* Whether to show the hangup button.
*/
_hangUp?: boolean;
/**
* Indicates whether the message that display name is required is shown.
*/
@@ -450,6 +456,7 @@ 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'];
@@ -461,6 +468,7 @@ 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,

View File

@@ -19,6 +19,7 @@ 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, {
@@ -220,12 +221,19 @@ 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>
);
}

View File

@@ -336,7 +336,6 @@ 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,

View File

@@ -2,6 +2,7 @@ import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_LEAVE,
SET_PASSWORD
} from '../base/conference/actionTypes';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
@@ -69,6 +70,7 @@ ReducerRegistry.register<ILobbyState>('features/lobby', (state = DEFAULT_STATE,
}
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:
case CONFERENCE_WILL_LEAVE:
return {
...state,
isDisplayNameRequiredError: false,

View File

@@ -54,7 +54,7 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
fontWeight: 600,
height: '100%',
[[ '& > *:first-child', '& > *:last-child' ] as any]: {
[[ '& > *:first-of-type', '& > *:last-of-type' ] as any]: {
flexShrink: 0
},
@@ -116,11 +116,11 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
antiCollapse: {
fontSize: 0,
'&:first-child': {
'&:first-of-type': {
display: 'none'
},
'&:first-child + *': {
'&:first-of-type + *': {
marginTop: 0
}
},

View File

@@ -33,9 +33,11 @@ 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';
@@ -60,6 +62,9 @@ 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 || '');
@@ -181,6 +186,11 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
styles = { styles.buttonStylesBorderless } />
<VideoMuteButton
styles = { styles.buttonStylesBorderless } />
{
(knocking ? showHangUpLobby : showHangUpPrejoin)
&& <HangupButton
styles = { styles.buttonStylesBorderless } />
}
</View>
{
showDisplayNameInput && <Input

View File

@@ -5,14 +5,9 @@ import { connect, useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { loginWithPopup } from '../../../authentication/actions.web';
import { getTokenAuthUrl, isTokenAuthEnabled, isTokenAuthInline } from '../../../authentication/functions.web';
import Avatar from '../../../base/avatar/components/Avatar';
import { IConfig } from '../../../base/config/configType';
import { isNameReadOnly } from '../../../base/config/functions.web';
import { IconArrowDown, IconArrowUp, IconPhoneRinging, IconVolumeOff } from '../../../base/icons/svg';
import { setJWT } from '../../../base/jwt/actions';
import { browser } from '../../../base/lib-jitsi-meet';
import { isVideoMutedByUser } from '../../../base/media/functions';
import { getLocalParticipant } from '../../../base/participants/functions';
import Popover from '../../../base/popover/components/Popover.web';
@@ -25,7 +20,6 @@ import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import isInsecureRoomName from '../../../base/util/isInsecureRoomName';
import { parseURIString } from '../../../base/util/uri';
import { openDisplayNamePrompt } from '../../../display-name/actions';
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
import {
@@ -127,16 +121,6 @@ interface IProps {
*/
showUnsafeRoomWarning: boolean;
/**
* The configuration for token pre-authentication, if applicable.
*/
tokenPreAuthConfig?: {
config: IConfig;
locationURL: URL;
refreshToken: string | undefined;
room: string;
};
/**
* Whether the user has approved to join a room with unsafe name.
*/
@@ -242,7 +226,6 @@ const Prejoin = ({
showErrorOnJoin,
showRecordingWarning,
showUnsafeRoomWarning,
tokenPreAuthConfig,
unsafeRoomConsent,
updateSettings: dispatchUpdateSettings,
videoTrack
@@ -276,56 +259,7 @@ const Prejoin = ({
logger.info('Prejoin join button clicked.');
// if we have auto redirect enabled, and we have previously logged in successfully
// let's redirect to the auth url to get the token and login again
if (tokenPreAuthConfig) {
const { tenant } = parseURIString(tokenPreAuthConfig.locationURL.href) || {};
const { startAudioOnly } = tokenPreAuthConfig.config;
const refreshToken = tokenPreAuthConfig.refreshToken;
getTokenAuthUrl(
config,
tokenPreAuthConfig.locationURL,
{
audioMuted: false,
audioOnlyEnabled: startAudioOnly,
skipPrejoin: false,
videoMuted: false
},
tokenPreAuthConfig.room,
tenant,
refreshToken
)
.then((url: string | undefined) => {
if (isTokenAuthInline(config)) {
if (url) {
return loginWithPopup(url)
.then((result: { accessToken: string; idToken: string; refreshToken?: string; }) => {
// @ts-ignore
const token: string = result.accessToken;
const idToken: string = result.idToken;
const newRefreshToken: string | undefined = result.refreshToken;
// @ts-ignore
dispatch(setJWT(token, idToken, newRefreshToken || refreshToken));
})
.then(() => joinConference());
}
} else {
if (url) {
window.location.href = url;
} else {
joinConference();
}
}
})
.catch(err => {
logger.error('Error in login', err);
joinConference();
});
} else {
joinConference();
}
joinConference();
};
/**
@@ -568,13 +502,7 @@ function mapStateToProps(state: IReduxState) {
const { joiningInProgress } = state['features/prejoin'];
const { room } = state['features/base/conference'];
const { unsafeRoomConsent } = state['features/base/premeeting'];
const config = state['features/base/config'];
const { showPrejoinWarning: showRecordingWarning } = config.recordings ?? {};
const preTokenAuthenticate = !browser.isElectron()
&& isTokenAuthEnabled(state)
&& config.tokenAuthUrlAutoRedirect && state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room; // skip if jaas
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { showPrejoinWarning: showRecordingWarning } = state['features/base/config'].recordings ?? {};
return {
deviceStatusVisible: isDeviceStatusVisible(state),
@@ -590,12 +518,6 @@ function mapStateToProps(state: IReduxState) {
showErrorOnJoin,
showRecordingWarning: Boolean(showRecordingWarning),
showUnsafeRoomWarning: isInsecureRoomName(room) && isUnsafeRoomWarningEnabled(state),
tokenPreAuthConfig: preTokenAuthenticate ? {
config,
locationURL,
refreshToken: state['features/base/jwt'].refreshToken,
room
} : undefined,
unsafeRoomConsent,
videoTrack: getLocalJitsiVideoTrack(state)
};

View File

@@ -103,13 +103,13 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent {
<div className = 'recording-header space-top'>
<label
className = 'recording-title'
htmlFor = 'recording-switch-audio-video'>
htmlFor = 'recording-switch-transcription'>
{ t('recording.recordAudioAndVideo') }
</label>
<Switch
checked = { shouldRecordAudioAndVideo }
className = 'recording-switch'
id = 'recording-switch-audio-video'
id = 'recording-switch-transcription'
onChange = { this._onRecordAudioAndVideoSwitchChange } />
</div>
</>

View File

@@ -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, isEmbeddedFromSameDomain } from '../base/util/embedUtils';
import { isEmbedded } 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() || isEmbeddedFromSameDomain());
return LocalRecordingManager.isSupported() && !isEmbedded();
}
/**

View File

@@ -18,11 +18,12 @@ export function openLogoutDialog() {
const state = getState();
const { conference } = state['features/base/conference'];
const logoutUrl = state['features/base/config'].tokenLogoutUrl;
const config = state['features/base/config'];
const logoutUrl = config.tokenLogoutUrl;
dispatch(openDialog('LogoutDialog', LogoutDialog, {
onLogout() {
if (isTokenAuthEnabled(state)) {
if (isTokenAuthEnabled(config)) {
if (logoutUrl) {
Linking.openURL(logoutUrl);
}

View File

@@ -1,8 +1,8 @@
import { batch } from 'react-redux';
import { IStore } from '../app/types';
import { setTokenAuthUrlSuccess, silentLogout } from '../authentication/actions.web';
import { isTokenAuthEnabled, isTokenAuthInline } from '../authentication/functions';
import { setTokenAuthUrlSuccess } from '../authentication/actions.web';
import { isTokenAuthEnabled } from '../authentication/functions';
import {
setStartMutedPolicy,
setStartReactionsMuted
@@ -11,7 +11,6 @@ import { getConferenceState } from '../base/conference/functions';
import { hangup } from '../base/connection/actions.web';
import { openDialog } from '../base/dialog/actions';
import i18next from '../base/i18n/i18next';
import { setJWT } from '../base/jwt/actions';
import { browser } from '../base/lib-jitsi-meet';
import { getNormalizedDisplayName } from '../base/participants/functions';
import { updateSettings } from '../base/settings/actions';
@@ -38,7 +37,6 @@ import {
getProfileTabProps,
getShortcutsTabProps
} from './functions.web';
import logger from './logger';
/**
* Opens {@code LogoutDialog}.
@@ -53,28 +51,11 @@ export function openLogoutDialog() {
const logoutUrl = config.tokenLogoutUrl;
const { conference } = state['features/base/conference'];
const { jwt, idToken } = state['features/base/jwt'];
if (!browser.isElectron() && logoutUrl && isTokenAuthInline(config)) {
let url = logoutUrl;
if (idToken) {
url += `${logoutUrl.indexOf('?') === -1 ? '?' : '&'}id_token_hint=${idToken}`;
}
silentLogout(url)
.then(() => {
dispatch(setJWT());
dispatch(setTokenAuthUrlSuccess(false));
})
.catch(() => logger.error('logout failed'));
return;
}
const { jwt } = state['features/base/jwt'];
dispatch(openDialog('LogoutDialog', LogoutDialog, {
onLogout() {
if (isTokenAuthEnabled(state) && config.tokenAuthUrlAutoRedirect && jwt) {
if (isTokenAuthEnabled(config) && config.tokenAuthUrlAutoRedirect && jwt) {
// user is logging out remove auto redirect indication
dispatch(setTokenAuthUrlSuccess(false));

View File

@@ -272,13 +272,6 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
component: ProfileTab,
labelKey: 'profile.title',
props: getProfileTabProps(state),
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getProfileTabProps>) => {
return {
...newProps,
displayName: tabState?.displayName,
email: tabState?.email
};
},
submit: submitProfileTab,
icon: IconUser
});

View File

@@ -244,11 +244,11 @@ const AudioSettingsContent = ({
* @returns {void}
*/
const _setTracks = async () => {
if (!measureAudioLevels || browser.isWebKitBased()) {
if (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;
}

View File

@@ -86,7 +86,7 @@ const useStyles = makeStyles()(theme => {
slider: {
// Use an additional class here to override global CSS specificity
'&.custom-slider': {
'-webkit-appearance': 'none',
WebkitAppearance: 'none',
background: 'transparent',
height,
left: 0,
@@ -104,11 +104,11 @@ const useStyles = makeStyles()(theme => {
},
'&::-webkit-slider-runnable-track': {
'-webkit-appearance': 'none',
WebkitAppearance: 'none',
...inputTrack
},
'&::-webkit-slider-thumb': {
'-webkit-appearance': 'none',
WebkitAppearance: 'none',
position: 'relative',
top: -6,
...inputThumb

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { IconCalendar, IconGear, IconRestore } from '../base/icons/svg';
import BaseTheme from '../base/ui/components/BaseTheme.native';
import BaseTheme from '../base/ui/components/BaseTheme';
import TabIcon from './components/TabIcon';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { App } from './features/app/components/App.web';
import { getLogger } from './features/base/logging/functions';
@@ -72,14 +72,17 @@ globalNS.entryPoints = {
WHITEBOARD: WhiteboardApp
};
const _roots = new Map();
globalNS.renderEntryPoint = ({
Component,
props = {},
elementId = 'react'
}) => {
/* eslint-disable-next-line react/no-deprecated */
ReactDOM.render(
<Component { ...props } />,
document.getElementById(elementId)
);
const element = document.getElementById(elementId);
if (!_roots.has(elementId)) {
_roots.set(elementId, createRoot(element));
}
_roots.get(elementId).render(<Component { ...props } />);
};

View File

@@ -89,27 +89,5 @@
- speakerStats - A table containing speaker statistics for occupants in the room. The keys are occupant JIDs and the values are objects with properties like dominantSpeakerId, faceLandmarks, and sessionId. Used by mod_speakerstats_component.lua to manage speaker statistics in the room.
- visitors_destroy_timer - A timer used to destroy the room when there are no main occupants or visitors left. It is set by mod_fmuc.lua to clean up the room after a certain period of inactivity.
# session fields added by jitsi
- jitsi_meet_context_user - The context from the jwt token, added after token verify.
- jitsi_meet_context_group - The group from the jwt context, added after token verify.
- jitsi_meet_context_features - The features from the context, added after token verify.
- jitsi_meet_context_room - The room settings from the jwt context, added after token verify.
- jitsi_meet_room - The room name in jwt token, added after token verify.
- jitsi_meet_str_tenant - The tenant in the context. Added after token verify.
- jitsi_meet_domain - The domain in the jwt ('sub' claim). Added after token verify. Can be the domain if not tenant is used or the tenant itself in lowercase.
- customusername - from a query parameter to be used with combination with "pre-jitsi-authentication" event to pre-set a known jid to a session.
- jitsi_web_query_room - room name from the query.
- jitsi_web_query_prefix - the tenant from the query specified as a param named 'prefix'.
- auth_token - The token, set before verify and cleared if verification fails.
- jitsi_meet_tenant_mismatch - The tenant field from the token and the query param for tenant do not match.
- previd - Used for stream resumption.
- user_region - the region header from the http request received.
- user_agent_header - the user agent header from the http request received.
- jitsi_throttle - used by rate limit module.
- jitsi_throttle_counter - used by rate limit module.
- force_permissions_update - Indicate that on next self-presence update the permissions should be resent to the client. Used by mod_jitsi_permissions.lua to manage permissions updates for the session.
- granted_jitsi_meet_context_user_id - when affiliation was changed (grant moderation) this holds the id of the actor.
- granted_jitsi_meet_context_group_id - when affiliation was changed (grant moderation) this holds the group of the actor.
#### Notes:
When modules need to store data they should do it in the room object in _data or directly. The data needs to be a simple as strings or table of strings, they should not add objects like room, sessions or occupants that cannot be serialized. Attaching data to the room object makes reloading modules safe and guarantees data will be wiped once the room is destroyed.

View File

@@ -7,8 +7,6 @@ local new_sasl = require "util.sasl".new;
local sasl = require "util.sasl";
local sessions = prosody.full_sessions;
module:depends("jitsi_session");
-- define auth provider
local provider = {};
@@ -40,13 +38,10 @@ function provider.get_sasl_handler(session)
-- Custom session matching so we can resume session even with randomly
-- generated user IDs.
local function get_username(self, message)
local resuming = false;
if (session.previd ~= nil) then
for _, session1 in pairs(sessions) do
if (session1.resumption_token == session.previd) then
self.username = session1.username;
resuming = true;
break;
end
end
@@ -54,10 +49,6 @@ function provider.get_sasl_handler(session)
self.username = message;
end
if not resuming then
session.auth_token = nil;
end
return true;
end

View File

@@ -26,6 +26,41 @@ local provider = {};
local host = module.host;
-- Extract 'token' param from URL when session is created
function init_session(event)
local session, request = event.session, event.request;
local query = request.url.query;
local token = nil;
-- extract token from Authorization header
if request.headers["authorization"] then
-- assumes the header value starts with "Bearer "
token = request.headers["authorization"]:sub(8,#request.headers["authorization"])
end
-- allow override of token via query parameter
if query ~= nil then
local params = formdecode(query);
-- The following fields are filled in the session, by extracting them
-- from the query and no validation is being done.
-- After validating auth_token will be cleaned in case of error and few
-- other fields will be extracted from the token and set in the session
if params and params.token then
token = params.token;
end
end
-- in either case set auth_token in the session
session.auth_token = token;
session.user_agent_header = request.headers['user_agent'];
end
module:hook_global("bosh-session", init_session);
module:hook_global("websocket-session", init_session);
module:hook("pre-resource-unbind", function (e)
local error, session = e.error, e.session;
@@ -60,60 +95,41 @@ function provider.delete_user(username)
return nil;
end
function first_stage_auth(session)
-- retrieve custom public key from server and save it on the session
local pre_event_result = prosody.events.fire_event("pre-jitsi-authentication-fetch-key", session);
if pre_event_result ~= nil and pre_event_result.res == false then
module:log("warn",
"Error verifying token on pre authentication stage:%s, reason:%s", pre_event_result.error, pre_event_result.reason);
session.auth_token = nil;
measure_pre_fetch_fail(1);
return pre_event_result;
end
local res, error, reason = token_util:process_and_verify_token(session);
if res == false then
module:log("warn",
"Error verifying token err:%s, reason:%s tenant:%s room:%s user_agent:%s",
error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room,
session.user_agent_header);
session.auth_token = nil;
measure_verify_fail(1);
return { res = res, error = error, reason = reason };
end
local shouldAllow = prosody.events.fire_event("jitsi-access-ban-check", session);
if shouldAllow == false then
module:log("warn", "user is banned")
measure_ban(1);
return { res = false, error = "not-allowed", reason = "user is banned" };
end
return { verify_result = res, custom_username = prosody.events.fire_event("pre-jitsi-authentication", session) };
end
function second_stage_auth(session)
local post_event_result = prosody.events.fire_event("post-jitsi-authentication", session);
if post_event_result ~= nil and post_event_result.res == false then
module:log("warn",
"Error verifying token on post authentication stage :%s, reason:%s", post_event_result.error, post_event_result.reason);
session.auth_token = nil;
measure_post_auth_fail(1);
return post_event_result;
end
end
function provider.get_sasl_handler(session)
local function get_username_from_token(self, message)
local s1_result = first_stage_auth(session);
if s1_result.res == false then
return s1_result.res, s1_result.error, s1_result.reason;
-- retrieve custom public key from server and save it on the session
local pre_event_result = prosody.events.fire_event("pre-jitsi-authentication-fetch-key", session);
if pre_event_result ~= nil and pre_event_result.res == false then
module:log("warn",
"Error verifying token on pre authentication stage:%s, reason:%s", pre_event_result.error, pre_event_result.reason);
session.auth_token = nil;
measure_pre_fetch_fail(1);
return pre_event_result.res, pre_event_result.error, pre_event_result.reason;
end
if s1_result.custom_username then
self.username = s1_result.custom_username;
local res, error, reason = token_util:process_and_verify_token(session);
if res == false then
module:log("warn",
"Error verifying token err:%s, reason:%s tenant:%s room:%s user_agent:%s",
error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room,
session.user_agent_header);
session.auth_token = nil;
measure_verify_fail(1);
return res, error, reason;
end
local shouldAllow = prosody.events.fire_event("jitsi-access-ban-check", session);
if shouldAllow == false then
module:log("warn", "user is banned")
measure_ban(1);
return false, "not-allowed", "user is banned";
end
local customUsername = prosody.events.fire_event("pre-jitsi-authentication", session);
if customUsername then
self.username = customUsername;
elseif session.previd ~= nil then
for _, session1 in pairs(sessions) do
if (session1.resumption_token == session.previd) then
@@ -125,14 +141,17 @@ function provider.get_sasl_handler(session)
self.username = message;
end
local s2_result = second_stage_auth(session);
if s2_result and s2_result.res ~= nil then
return s2_result.res, s2_result.error, s2_result.reason;
local post_event_result = prosody.events.fire_event("post-jitsi-authentication", session);
if post_event_result ~= nil and post_event_result.res == false then
module:log("warn",
"Error verifying token on post authentication stage :%s, reason:%s", post_event_result.error, post_event_result.reason);
session.auth_token = nil;
measure_post_auth_fail(1);
return post_event_result.res, post_event_result.error, post_event_result.reason;
end
measure_success(1);
session._jitsi_auth_done = true;
return s1_result.verify_result;
return res;
end
return new_sasl(host, { anonymous = get_username_from_token });
@@ -158,47 +177,3 @@ local function anonymous(self, message)
end
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
module:hook_global('c2s-session-updated', function (event)
local session, from_session = event.session, event.from_session;
if not from_session.auth_token then
return;
end
-- we care to handle sessions from other hosts (anonymous hosts)
if module.host ~= event.from_session.host then
-- Handle session updates (e.g., when a session is resumed on some anonymous host with a token we need to do all the checks here)
session.auth_token = event.from_session.auth_token;
local s1_result = first_stage_auth(session);
if s1_result.res == false then
event.session:close();
return;
end
local s2_result = second_stage_auth(session);
if s2_result and s2_result.res == false then
event.session:close();
return;
end
session._jitsi_auth_done = true;
end
if not session._jitsi_auth_done then
module:log('warn', 'Impossible case hit where session did not pass auth flow');
event.session:close();
return;
end
-- copy all the custom fields we set in the session
session.auth_token = from_session.auth_token;
session.jitsi_meet_context_user = from_session.jitsi_meet_context_user;
session.jitsi_meet_context_group = from_session.jitsi_meet_context_group;
session.jitsi_meet_context_features = from_session.jitsi_meet_context_features;
session.jitsi_meet_context_room = from_session.jitsi_meet_context_room;
session.jitsi_meet_room = from_session.jitsi_meet_room;
session.jitsi_meet_str_tenant = from_session.jitsi_meet_str_tenant;
session.jitsi_meet_domain = from_session.jitsi_meet_domain;
session.jitsi_meet_tenant_mismatch = from_session.jitsi_meet_tenant_mismatch;
end, 1);

View File

@@ -95,27 +95,26 @@ 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 feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call';
local error_message = nil;
local is_sender_in_room = room:get_occupant_jid(stanza.attr.from) ~= 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') then
error_message = "feature not allowed";
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
if error_message then
module:log("warn", "Filtering stanza dial, %s, stanza:%s", error_message, tostring(stanza));
local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call';
local is_session_allowed = is_feature_allowed(
feature,
session.jitsi_meet_context_features,
room:get_affiliation(stanza.attr.from) == 'owner');
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));
session.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
end

View File

@@ -11,14 +11,6 @@ function init_session(event)
local session, request = event.session, event.request;
local query = request.url.query;
local token = nil;
-- extract token from Authorization header
if request.headers["authorization"] then
-- assumes the header value starts with "Bearer "
token = request.headers["authorization"]:sub(8,#request.headers["authorization"])
end
if query ~= nil then
local params = formdecode(query);
@@ -32,23 +24,9 @@ function init_session(event)
-- The room name and optional prefix from the web query
session.jitsi_web_query_room = params.room;
session.jitsi_web_query_prefix = params.prefix or "";
-- The following fields are filled in the session, by extracting them
-- from the query and no validation is being done.
-- After validating auth_token will be cleaned in case of error and few
-- other fields will be extracted from the token and set in the session
if params and params.token then
token = params.token;
end
end
session.user_region = request.headers[region_header_name];
-- in either case set auth_token in the session
session.auth_token = token;
session.user_agent_header = request.headers['user_agent'];
end
module:hook_global("bosh-session", init_session, 1);

View File

@@ -16,7 +16,6 @@ local is_transcriber = util.is_transcriber;
local QUEUE_MAX_SIZE = 500;
module:depends("jitsi_permissions");
module:depends("jitsi_session");
-- Common module for all logic that can be loaded under the conference muc component.
--

View File

@@ -57,10 +57,7 @@ module:hook('muc-occupant-pre-join', function (event)
local has_host = false;
for _, o in room:each_occupant() do
-- the main virtual host that requires tokens
if jid.host(o.bare_jid) == muc_domain_base
-- or this is anonymous that upgraded by passing token which we validated
or prosody.full_sessions[o.jid].auth_token then
if jid.host(o.bare_jid) == muc_domain_base then
room.has_host = true;
end
end

View File

@@ -33,7 +33,7 @@ function insertTextMsg(id, msg) {
const el = document.getElementById(id);
if (el) {
el.textContent = msg;
el.innerHTML = msg;
}
}

View File

@@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logged Out</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
text-align: center;
padding: 20px;
}
.success {
color: #27ae60;
font-size: 48px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="success"></div>
<p>You have been logged out successfully.</p>
</div>
<script>
// Notify parent window that logout is complete
if (window.parent) {
window.parent.postMessage({
type: 'logout-success'
}, window.location.origin);
}
</script>
</body>
</html>

View File

@@ -1,364 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSO Authentication</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
text-align: center;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.spinner {
display: none;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
display: none;
color: #e74c3c;
margin-top: 20px;
}
.success {
display: none;
color: #27ae60;
font-size: 48px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div id="spinner" class="spinner"></div>
<p id="spinner-msg" style="display: none;">Completing authentication...</p>
<div id="success" class="success"></div>
<p id="message-for-opener" style="display: none;">Authentication successful! You can close this window.</p>
<div id="error" class="error"></div>
</div>
<script>
<!--#include virtual="/config.js" -->
(function() {
/**
* Decodes a JWT token and returns the payload
* @param {string} token - The JWT token to decode
* @returns {object} The decoded payload
*/
function decodeJWT(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
} catch (error) {
throw new Error('Failed to decode JWT: ' + error.message);
}
}
/**
* Validates that the nonce in the ID token matches the stored nonce
* @param {string} idToken - The ID token to validate
* @param {string} expectedNonce - The nonce stored in sessionStorage
*/
function validateNonce(idToken, expectedNonce) {
const payload = decodeJWT(idToken);
if (!payload.nonce) {
throw new Error('ID token does not contain a nonce claim');
}
if (payload.nonce !== expectedNonce) {
throw new Error('Nonce validation failed: ID token nonce does not match stored nonce');
}
}
async function handleAuthorizationCode() {
try {
// Parse query parameters (Authorization Code Flow uses query params, not hash)
const urlParams = new URLSearchParams(window.location.search);
// Get state parameter first to check for refresh token
const stateParam = urlParams.get('state');
if (!stateParam) {
throw new Error('No state parameter received');
}
const state = JSON.parse(decodeURIComponent(stateParam));
// Get SSO configuration from config
const { sso } = config;
if (!sso || !sso.tokenService || !sso.clientId) {
throw new Error('Missing SSO configuration (tokenService or clientId)');
}
// Check if we have a refresh token in state - use it to refresh tokens
if (state.refreshToken) {
console.log('Using refresh token to get new tokens');
// Show spinner while refreshing tokens
document.getElementById('spinner').style.display = 'block';
document.getElementById('spinner-msg').style.display = 'block';
// Use refresh token to get new tokens
const tokenResponse = await fetch(`https://${sso.tokenService}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: state.refreshToken,
client_id: sso.clientId
})
});
if (!tokenResponse.ok) {
const errorData = await tokenResponse.json().catch(() => ({}));
throw new Error(errorData.error_description || errorData.error || 'Token refresh failed');
}
const tokens = await tokenResponse.json();
const accessToken = tokens.access_token;
const idToken = tokens.id_token;
const refreshToken = tokens.refresh_token || state.refreshToken; // Use new refresh token if provided, otherwise keep old one
if (!accessToken || !idToken) {
throw new Error('Missing tokens in refresh response');
}
// Validate nonce if available in sessionStorage (optional for refresh flows)
const refreshNonce = sessionStorage.getItem('oauth_nonce');
if (refreshNonce) {
try {
validateNonce(idToken, refreshNonce);
} catch (nonceError) {
console.warn('Nonce validation failed during refresh:', nonceError.message);
// Don't fail the refresh flow if nonce validation fails
}
}
const message = {
type: 'oauth-success',
accessToken: accessToken,
idToken: idToken,
refreshToken: refreshToken
};
// Send message to parent window
if (window.opener) {
document.getElementById('spinner').style.display = 'none';
document.getElementById('spinner-msg').style.display = 'none';
document.getElementById('success').style.display = 'block';
document.getElementById('message-for-opener').style.display = 'block';
window.opener.postMessage(message, window.location.origin);
} else if (window.parent && window.parent !== window) {
console.log('Sending refreshed tokens to parent (iframe)');
window.parent.postMessage(message, window.location.origin);
} else {
// Standalone page - redirect to room
const tenant = state.tenant || '';
const roomSafe = state.roomSafe || state.room || '';
if (!roomSafe) {
throw new Error('No room specified in state');
}
const protocol = window.location.protocol;
const host = window.location.host;
const path = tenant ? `/${tenant}/${roomSafe}` : `/${roomSafe}`;
const hashParams = new URLSearchParams();
for (const [key, value] of Object.entries(state)) {
if (key.startsWith('config.')) {
hashParams.append(key, String(value));
}
}
hashParams.append('jwt', `"${accessToken}"`);
const redirectUrl = `${protocol}//${host}${path}#${hashParams.toString()}`;
window.location.href = redirectUrl;
}
return; // Exit after handling refresh token
}
// No refresh token in state - proceed with authorization code flow
// Check for OAuth errors
const error = urlParams.get('error');
if (error) {
const errorDescription = urlParams.get('error_description');
console.error('OAuth error:', error, errorDescription);
if (window.opener) {
window.opener.postMessage({
type: 'oauth-error',
error: errorDescription || error
}, window.location.origin);
} else if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: 'oauth-error',
error: errorDescription || error
}, window.location.origin);
}
return;
}
// Get authorization code
const code = urlParams.get('code');
if (!code) {
throw new Error('No authorization code received');
}
// Retrieve code_verifier and oauth_nonce from sessionStorage (set during login initiation)
const codeVerifier = sessionStorage.getItem('code_verifier');
if (!codeVerifier) {
throw new Error('No PKCE code verifier found in session');
}
const oauthNonce = sessionStorage.getItem('oauth_nonce');
if (!oauthNonce) {
throw new Error('No OAuth nonce found in session');
}
// Show spinner while exchanging code for tokens
document.getElementById('spinner').style.display = 'block';
document.getElementById('spinner-msg').style.display = 'block';
// Exchange authorization code for tokens
const tokenResponse = await fetch(`https://${sso.tokenService}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: window.location.origin + window.location.pathname,
client_id: sso.clientId,
code_verifier: codeVerifier
})
});
if (!tokenResponse.ok) {
const errorData = await tokenResponse.json().catch(() => ({}));
throw new Error(errorData.error_description || errorData.error || 'Token exchange failed');
}
const tokens = await tokenResponse.json();
const accessToken = tokens.access_token;
const idToken = tokens.id_token;
const refreshToken = tokens.refresh_token;
if (!accessToken || !idToken) {
throw new Error('Missing tokens in response');
}
// Validate nonce in ID token
validateNonce(idToken, oauthNonce);
const message = {
type: 'oauth-success',
accessToken: accessToken,
idToken: idToken,
refreshToken: refreshToken
};
// Send message to parent window
if (window.opener) {
// Opened via window.open() - popup
document.getElementById('spinner').style.display = 'none';
document.getElementById('spinner-msg').style.display = 'none';
document.getElementById('success').style.display = 'block';
document.getElementById('message-for-opener').style.display = 'block';
window.opener.postMessage(message, window.location.origin);
} else if (window.parent && window.parent !== window) {
// Loaded in iframe
console.log('Sending message to parent (iframe)');
window.parent.postMessage(message, window.location.origin);
} else {
// Standalone page - redirect to room
// Extract required values
const tenant = state.tenant || '';
const roomSafe = state.roomSafe || state.room || '';
if (!roomSafe) {
throw new Error('No room specified in state');
}
// Build the new URL
const protocol = window.location.protocol;
const host = window.location.host;
// Construct path: /tenant/roomSafe
const path = tenant ? `/${tenant}/${roomSafe}` : `/${roomSafe}`;
// Build hash parameters - only config.* values and jwt
const hashParams = new URLSearchParams();
// Add only config.* parameters from state
for (const [key, value] of Object.entries(state)) {
if (key.startsWith('config.')) {
hashParams.append(key, String(value));
}
}
// Add access_token as jwt with quotes
hashParams.append('jwt', `"${accessToken}"`);
// Build final URL
const redirectUrl = `${protocol}//${host}${path}#${hashParams.toString()}`;
console.log('Redirecting to:', redirectUrl);
// Redirect
window.location.href = redirectUrl;
}
} catch (error) {
console.error('SSO Error:', error);
const errorMessage = {
type: 'oauth-error',
error: error.message
};
if (window.opener) {
window.opener.postMessage(errorMessage, window.location.origin);
} else if (window.parent && window.parent !== window) {
window.parent.postMessage(errorMessage, window.location.origin);
} else {
document.getElementById('error').textContent = `Error: ${error.message}`;
document.getElementById('error').style.display = 'block';
document.getElementById('spinner').style.display = 'none';
document.getElementById('spinner-msg').style.display = 'none';
}
}
}
// Start authentication flow
handleAuthorizationCode();
})();
</script>
</body>
</html>

View File

@@ -35,8 +35,6 @@ 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.

View File

@@ -38,9 +38,8 @@ export default class WebhookProxy {
this.ws.on('error', console.error);
this.ws.on('open', () => {
this.ws.on('open', function open() {
console.log('WebhookProxy connected');
this.logInfo('connected');
});
this.ws.on('message', (data: any) => {

View File

@@ -22,12 +22,6 @@ 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.
*/

View File

@@ -1,4 +1,4 @@
import { P1, P2, P3, P4, P5, P6, Participant } from './Participant';
import { P1, P2, P3, P4, Participant } from './Participant';
import { config } from './TestsConfig';
import { IJoinOptions, IParticipantOptions } from './types';
@@ -122,50 +122,6 @@ 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.
*
@@ -288,16 +244,10 @@ export async function checkForScreensharingTile(sharer: Participant, observer: P
}
/**
* Hangs up all participants (p1, p2, p3, p4, p5, and p6)
* Hangs up all participants (p1, p2, p3 and p4)
* @returns {Promise<void>}
*/
export function hangupAllParticipants() {
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()));
return Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup(), ctx.p3?.hangup(), ctx.p4?.hangup() ]
.map(p => p ?? Promise.resolve()));
}

View File

@@ -7,16 +7,13 @@ import { IToken, ITokenOptions } from './token';
export type IContext = {
/**
* The up-to-six browser instances provided by the framework. These can be initialized using
* The up-to-four 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;
/**
@@ -42,7 +39,7 @@ export type IParticipantOptions = {
/** Whether it should use the iFrame API. */
iFrameApi?: boolean;
/** Determines the browser instance to use. */
name: 'p1' | 'p2' | 'p3' | 'p4' | 'p5' | 'p6';
name: 'p1' | 'p2' | 'p3' | 'p4';
/** An optional token to use. */
token?: IToken;
};

Some files were not shown because too many files have changed in this diff Show More