mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-30 01:07:46 +00:00
Compare commits
43 Commits
5703
...
saghul-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e3463735b | ||
|
|
7dd9785e48 | ||
|
|
315c884f0d | ||
|
|
8590315244 | ||
|
|
6b260c27a5 | ||
|
|
197dbfbbcb | ||
|
|
847dc2a7bb | ||
|
|
7f26e7f927 | ||
|
|
6247d45b9e | ||
|
|
04f9ad32e1 | ||
|
|
348414cc84 | ||
|
|
804573e9aa | ||
|
|
9dd3941388 | ||
|
|
f341a4e4eb | ||
|
|
1c462996f5 | ||
|
|
1316c48964 | ||
|
|
1080404de9 | ||
|
|
55ef73875e | ||
|
|
afeb3e0e69 | ||
|
|
ca227df7ec | ||
|
|
fb843a7ecf | ||
|
|
af985d8fd8 | ||
|
|
8d62e010e0 | ||
|
|
1c4283eeca | ||
|
|
c64d1a97c1 | ||
|
|
6159504478 | ||
|
|
f2efdfcbc1 | ||
|
|
6682167800 | ||
|
|
b084aae212 | ||
|
|
a436a889a9 | ||
|
|
9816be4745 | ||
|
|
b9e182b7cc | ||
|
|
da2b920dbe | ||
|
|
d22d95e8a5 | ||
|
|
d44660527b | ||
|
|
5cc3fade8f | ||
|
|
f8340bfd41 | ||
|
|
a7bf037363 | ||
|
|
a42483c84b | ||
|
|
476ddeac41 | ||
|
|
12bc054386 | ||
|
|
32aa40b396 | ||
|
|
11bbedf9ac |
@@ -135,7 +135,6 @@ import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './
|
||||
import { suspendDetected } from './react/features/power-monitor';
|
||||
import {
|
||||
initPrejoin,
|
||||
isPrejoinPageEnabled,
|
||||
isPrejoinPageVisible,
|
||||
makePrecallTest,
|
||||
setJoiningInProgress,
|
||||
@@ -797,7 +796,7 @@ export default {
|
||||
logger.warn('initial device list initialization failed', error);
|
||||
}
|
||||
|
||||
if (isPrejoinPageEnabled(APP.store.getState())) {
|
||||
if (isPrejoinPageVisible(APP.store.getState())) {
|
||||
_connectionPromise = connect(roomName).then(c => {
|
||||
// we want to initialize it early, in case of errors to be able
|
||||
// to gather logs
|
||||
|
||||
96
config.js
96
config.js
@@ -490,6 +490,9 @@ var config = {
|
||||
// Default remote name to be displayed
|
||||
// defaultRemoteDisplayName: 'Fellow Jitster',
|
||||
|
||||
// Hides the display name from the participant thumbnail
|
||||
// hideDisplayName: false
|
||||
|
||||
// Default language for the user interface.
|
||||
// defaultLanguage: 'en',
|
||||
|
||||
@@ -543,6 +546,9 @@ var config = {
|
||||
// Document should be focused for this option to work
|
||||
// enableAutomaticUrlCopy: false,
|
||||
|
||||
// Array with avatar URL prefixes that need to use CORS.
|
||||
// corsAvatarURLs: [ 'https://www.gravatar.com/avatar/' ],
|
||||
|
||||
// Base URL for a Gravatar-compatible service. Defaults to libravatar.
|
||||
// gravatarBaseURL: 'https://seccdn.libravatar.org/avatar/',
|
||||
|
||||
@@ -609,41 +615,61 @@ var config = {
|
||||
// alwaysVisible: false
|
||||
// },
|
||||
|
||||
// Toolbar buttons which have their click event exposed through the API on
|
||||
// `toolbarButtonClicked` event instead of executing the normal click routine.
|
||||
// Toolbar buttons which have their click/tap event exposed through the API on
|
||||
// `toolbarButtonClicked`. Passing a string for the button key will
|
||||
// prevent execution of the click/tap routine; passing an object with `key` and
|
||||
// `preventExecution` flag on false will not prevent execution of the click/tap
|
||||
// routine. Below array with mixed mode for passing the buttons.
|
||||
// buttonsWithNotifyClick: [
|
||||
// 'camera',
|
||||
// 'chat',
|
||||
// 'closedcaptions',
|
||||
// 'desktop',
|
||||
// 'download',
|
||||
// 'embedmeeting',
|
||||
// 'etherpad',
|
||||
// 'feedback',
|
||||
// 'filmstrip',
|
||||
// 'fullscreen',
|
||||
// 'hangup',
|
||||
// 'help',
|
||||
// 'invite',
|
||||
// 'livestreaming',
|
||||
// 'microphone',
|
||||
// 'mute-everyone',
|
||||
// 'mute-video-everyone',
|
||||
// 'participants-pane',
|
||||
// 'profile',
|
||||
// 'raisehand',
|
||||
// 'recording',
|
||||
// 'security',
|
||||
// 'select-background',
|
||||
// 'settings',
|
||||
// 'shareaudio',
|
||||
// 'sharedvideo',
|
||||
// 'shortcuts',
|
||||
// 'stats',
|
||||
// 'tileview',
|
||||
// 'toggle-camera',
|
||||
// 'videoquality',
|
||||
// '__end'
|
||||
// 'camera',
|
||||
// {
|
||||
// key: 'chat',
|
||||
// preventExecution: false
|
||||
// },
|
||||
// {
|
||||
// key: 'closedcaptions',
|
||||
// preventExecution: true
|
||||
// },
|
||||
// 'desktop',
|
||||
// 'download',
|
||||
// 'embedmeeting',
|
||||
// 'etherpad',
|
||||
// 'feedback',
|
||||
// 'filmstrip',
|
||||
// 'fullscreen',
|
||||
// 'hangup',
|
||||
// 'help',
|
||||
// {
|
||||
// key: 'invite',
|
||||
// preventExecution: false
|
||||
// },
|
||||
// 'livestreaming',
|
||||
// 'microphone',
|
||||
// 'mute-everyone',
|
||||
// 'mute-video-everyone',
|
||||
// 'participants-pane',
|
||||
// 'profile',
|
||||
// {
|
||||
// key: 'raisehand',
|
||||
// preventExecution: true
|
||||
// },
|
||||
// 'recording',
|
||||
// 'security',
|
||||
// 'select-background',
|
||||
// 'settings',
|
||||
// 'shareaudio',
|
||||
// 'sharedvideo',
|
||||
// 'shortcuts',
|
||||
// 'stats',
|
||||
// 'tileview',
|
||||
// 'toggle-camera',
|
||||
// 'videoquality',
|
||||
// // The add passcode button from the security dialog.
|
||||
// {
|
||||
// key: 'add-passcode',
|
||||
// preventExecution: false
|
||||
// }
|
||||
// '__end'
|
||||
// ],
|
||||
|
||||
// List of pre meeting screens buttons to hide. The values must be one or more of the 5 allowed buttons:
|
||||
@@ -1026,7 +1052,7 @@ var config = {
|
||||
// If a label's id is not in any of the 2 arrays, it will not be visible at all on the header.
|
||||
// conferenceInfo: {
|
||||
// // those labels will not be hidden in tandem with the toolbox.
|
||||
// alwaysVisible: ['recording', 'local-recording'],
|
||||
// alwaysVisible: ['recording', 'local-recording', 'raised-hands-count'],
|
||||
// // those labels will be auto-hidden in tandem with the toolbox buttons.
|
||||
// autoHide: [
|
||||
// 'subject',
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
* {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
// Firefox only
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, .5) transparent;
|
||||
}
|
||||
|
||||
input,
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
.subject {
|
||||
color: #fff;
|
||||
margin-top: -120px;
|
||||
transition: margin-top .3s ease-in;
|
||||
transition: opacity .6s ease-in-out;
|
||||
z-index: $zindex3;
|
||||
margin-top: 20px;
|
||||
opacity: 0;
|
||||
|
||||
&.visible {
|
||||
margin-top: 20px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&#autoHide.with-always-on {
|
||||
overflow: hidden;
|
||||
animation: hideSubject forwards .6s ease-out;
|
||||
|
||||
& > .subject-info-container {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
animation: showSubject forwards .6s ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +50,7 @@
|
||||
line-height: 28px;
|
||||
padding: 0 16px;
|
||||
height: 28px;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
max-width: 100px;
|
||||
}
|
||||
max-width: 324px;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
display: none;
|
||||
@@ -74,8 +85,29 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 48px;
|
||||
max-width: calc(100% - 24px);
|
||||
}
|
||||
|
||||
.shift-right .details-container {
|
||||
margin-left: calc(#{$sidebarWidth} / 2);
|
||||
}
|
||||
|
||||
@keyframes hideSubject {
|
||||
0% {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
max-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes showSubject {
|
||||
0% {
|
||||
max-width: 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,4 +124,5 @@ Component "lobby.jitmeet.example.com" "muc"
|
||||
muc_room_default_public_jids = true
|
||||
modules_enabled = {
|
||||
"muc_rate_limit";
|
||||
"polls";
|
||||
}
|
||||
|
||||
@@ -32,4 +32,12 @@ denied-peer-ip=198.18.0.0-198.19.255.255
|
||||
denied-peer-ip=198.51.100.0-198.51.100.255
|
||||
denied-peer-ip=203.0.113.0-203.0.113.255
|
||||
denied-peer-ip=240.0.0.0-255.255.255.255
|
||||
denied-peer-ip=::1
|
||||
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
|
||||
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
|
||||
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
|
||||
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
|
||||
syslog
|
||||
|
||||
@@ -1,33 +1,55 @@
|
||||
{
|
||||
"en": "angielski",
|
||||
"af": "afrykanerski",
|
||||
"af": "Afrikaans",
|
||||
"ar": "arabski",
|
||||
"bg": "bułgarski",
|
||||
"ca": "kataloński",
|
||||
"cs": "czeski",
|
||||
"da": "duński",
|
||||
"de": "niemiecki",
|
||||
"el": "grecki",
|
||||
"enGB": "angielski (Zjednoczone Królestwo)",
|
||||
"enGB": "Angielski (Zjednoczone Królestwo)",
|
||||
"eo": "esperanto",
|
||||
"es": "hiszpański",
|
||||
"esUS": "hiszpański (Ameryka Łacińska)",
|
||||
"et": "estoński",
|
||||
"eu": "baskijski",
|
||||
"fi": "fiński",
|
||||
"fr": "francuski",
|
||||
"frCA": "francuski (kanadyjski)",
|
||||
"he": "hebrajski",
|
||||
"hi": "hindi",
|
||||
"mr":"Marathi",
|
||||
"hr": "chorwacki",
|
||||
"hu": "węgierski",
|
||||
"hy": "ormiański",
|
||||
"id": "indonezyjski",
|
||||
"it": "włoski",
|
||||
"ja": "japoński",
|
||||
"kab": "Kabyle",
|
||||
"ko": "koreański",
|
||||
"lt": "litewski",
|
||||
"ml": "malajalam",
|
||||
"lv": "łotewski",
|
||||
"nl": "holenderski",
|
||||
"oc": "oksytański",
|
||||
"oc": "prowansalski",
|
||||
"fa": "perski",
|
||||
"pl": "polski",
|
||||
"ptBR": "portugalski (brazylijski)",
|
||||
"pt": "portugalski",
|
||||
"ptBR": "portugalski (Brazylia)",
|
||||
"ru": "rosyjski",
|
||||
"ro": "rumuński",
|
||||
"sc": "sardyński",
|
||||
"sk": "słowacki",
|
||||
"sl": "słoweński",
|
||||
"sr": "serbski",
|
||||
"sq": "albański",
|
||||
"sv": "szwedzki",
|
||||
"te": "telugu",
|
||||
"th": "tajski",
|
||||
"tr": "turecki",
|
||||
"uk": "ukraiński",
|
||||
"vi": "wietnamski",
|
||||
"zhCN": "chiński (Chiny)",
|
||||
"zhTW": "chiński (Tajwan)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "غرف جانبية",
|
||||
"joined": " الغرفة الجانبيةالانضمام إلى\"{{name}}\"",
|
||||
"joined": " الغرفة الجانبيةالانضمام إلى {{index}}",
|
||||
"joinedMainRoom": "الانضمام للغرفة الرئيسية"
|
||||
}
|
||||
},
|
||||
@@ -180,7 +180,8 @@
|
||||
"joinInApp": "انضم للاجتماع عبر تطبيق الجوال",
|
||||
"launchWebButton": "افتح تطبيق الويب",
|
||||
"title": "قيد عقد اجتماعك في {{app}}...",
|
||||
"tryAgainButton": "جرب مرة أخرى في تطبيق الحاسوب"
|
||||
"tryAgainButton": "جرب مرة أخرى في تطبيق الحاسوب",
|
||||
"unsupportedBrowser": "يبدو أنك تستخدم متصفحًا لا ندعمه."
|
||||
},
|
||||
"defaultLink": "{{url}} مثلًا",
|
||||
"defaultNickname": "محمد علي مثلًا",
|
||||
@@ -418,6 +419,7 @@
|
||||
"desktopShareError": "تعذر إنشاء مشاركة سطح المكتب",
|
||||
"desktopShare": "مشاركة سطح المكتب",
|
||||
"webAssemblyWarning": "WebAssembly غير مدعوم",
|
||||
"webAssemblyWarningDescription": "تم تعطيل WebAssembly أو عدم دعمه بواسطة هذا المستعرض",
|
||||
"backgroundEffectError": "فشل تطبيق تأثير الخلفية."
|
||||
},
|
||||
"feedback": {
|
||||
@@ -579,10 +581,12 @@
|
||||
"allowedUnmute": "يمكنك إعادة صوت الميكروفون و بدء تشغيل الكاميرا أو مشاركة شاشتك.",
|
||||
"audioUnmuteBlockedTitle": "تم حظر إعادة صوت الميكروفون!",
|
||||
"audioUnmuteBlockedDescription": "تم حظر عملية إلغاء كتم صوت الميكروفون مؤقتًا بسبب قيود النظام.",
|
||||
"chatMessages": "رسائل الدردشة",
|
||||
"connectedOneMember": "انضم {{name}} للاجتماع",
|
||||
"connectedThreePlusMembers": "انضم {{name}} وعدد {{count}} غيره إلى الاجتماع",
|
||||
"connectedTwoMembers": "انضم {{first}} و {{second}} إلى الاجتماع",
|
||||
"disconnected": "انقطع الاتصال",
|
||||
"displayNotifications": "عرض الإخطارات لـ",
|
||||
"focus": "التركيز على المؤتمر",
|
||||
"focusFail": "إنَّ {{component}} غير متاح. ستعاد المحاولة مرة أخرى خلال {{ms}} ثانية.",
|
||||
"hostAskedUnmute": "The moderator would like you to speak",
|
||||
@@ -607,6 +611,7 @@
|
||||
"raisedHands": "{{participantName}} و {{raisedHands}}المزيد من الناس",
|
||||
"screenShareNoAudio": "لم يتم تحديد مربع مشاركة الصوت في شاشة تحديد النافذة.",
|
||||
"screenShareNoAudioTitle": "تعذرت مشاركة صوت النظام!",
|
||||
"selfViewTitle": "يمكنك دائمًا إلغاء إخفاء العرض الذاتي من الإعدادات",
|
||||
"somebody": "شخص ما",
|
||||
"startSilentTitle": "انضممت دون مخرج للصوت!",
|
||||
"startSilentDescription": "أنضم مجدَّدًا للاجتماع لتفعيل الصوت",
|
||||
@@ -821,8 +826,8 @@
|
||||
"security": {
|
||||
"about": "يمكنك إضافة $t(lockRoomPassword) إلى الاجتماع. سيتوجب على المشاركين إدخال $t(lockRoomPassword) قبل السماح لهم بالانضمام إلى الاجتماع.",
|
||||
"aboutReadOnly": "المشاركون بصفة رئيس الجلسة يمكنهم إضافة $t(lockRoomPassword) إلى الاجتماع. سيتوجب على المشاركين إدخال $t(lockRoomPassword) قبل السماح لهم بالانضمام إلى الاجتماع.",
|
||||
"insecureRoomNameWarning": "اسم الغرفة غير آمن، فقد ينضم عبره مشاركون غرباء إلى الاجتماع. ننصحك بتأمين الاجتماع عبر وسائل الحماية التي يوفرها زر الحماية.",
|
||||
"securityOptions": "خيارات الحماية"
|
||||
"header": "خيارات الأمان",
|
||||
"insecureRoomNameWarning": "اسم الغرفة غير آمن، فقد ينضم عبره مشاركون غرباء إلى الاجتماع. ننصحك بتأمين الاجتماع عبر وسائل الحماية التي يوفرها زر الحماية."
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
@@ -854,6 +859,7 @@
|
||||
"selectAudioOutput": "خرج الصوت",
|
||||
"selectCamera": "الكاميرا",
|
||||
"selectMic": "المجهار (المايكروفون)",
|
||||
"selfView": "عرض ذاتي",
|
||||
"sounds": "اصوات",
|
||||
"speakers": "المذياع (مكبر الصوت)",
|
||||
"startAudioMuted": "بدء الجميع مكتومي الصوت",
|
||||
@@ -890,20 +896,20 @@
|
||||
},
|
||||
"speaker": "المتحدث",
|
||||
"speakerStats": {
|
||||
"search": "بحث",
|
||||
"angry": "غاضب",
|
||||
"disgusted": "مشمئز",
|
||||
"fearful": "خائف",
|
||||
"happy": "سعيد",
|
||||
"hours": "{{count}}س",
|
||||
"minutes": "{{count}}د",
|
||||
"name": "الاسم",
|
||||
"seconds": "{{count}}ثا",
|
||||
"speakerStats": "حالة المتحدث",
|
||||
"speakerTime": "وقت المتحدث",
|
||||
"happy": "سعيد",
|
||||
"neutral": "حيادي",
|
||||
"sad": "حزين",
|
||||
"surprised": "مندهش",
|
||||
"angry": "غاضب",
|
||||
"fearful": "خائف",
|
||||
"disgusted": "مشمئز"
|
||||
"search": "بحث",
|
||||
"seconds": "{{count}}ثا",
|
||||
"speakerTime": "وقت المتحدث",
|
||||
"speakerStats": "حالة المتحدث",
|
||||
"surprised": "مندهش"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -946,8 +952,8 @@
|
||||
"mute": "اظهِر/اخفِ كتم الصوت",
|
||||
"muteEveryone": "كتم الجميع",
|
||||
"muteEveryoneElse": "كتم صوت الآخرين",
|
||||
"muteEveryonesVideo": "تعطيل كاميرا الجميع",
|
||||
"muteEveryoneElsesVideo": "تعطيل كاميرا الآخرين",
|
||||
"muteEveryonesVideoStream": "وقّف فيديو الجميع",
|
||||
"muteEveryoneElsesVideoStream": "وقّف فيديو أي شخص آخر",
|
||||
"participants": "مشاركون",
|
||||
"pip": "استعمل/اخرج من وضع صورة-في-صورة",
|
||||
"privateMessage": "أرسل رسالة خاصة",
|
||||
@@ -1123,6 +1129,7 @@
|
||||
"domuteVideoOfOthers": "تعطيل الكاميرا للآخرين",
|
||||
"flip": "قلب",
|
||||
"grantModerator": "امنح صلاحيات رئيس الجلسة",
|
||||
"hideSelfView": "إخفاء المنظر الذاتي",
|
||||
"kick": "طرد خارجًا",
|
||||
"moderator": "رئيس الجلسة",
|
||||
"mute": "المشارك مكتوم الصوت",
|
||||
|
||||
@@ -581,10 +581,12 @@
|
||||
"allowedUnmute": "Sie können die Stummschaltung aufheben, Ihre Kamera einschalten oder Ihren Bildschirm teilen.",
|
||||
"audioUnmuteBlockedTitle": "Stummschaltung kann nicht aufgehoben werden!",
|
||||
"audioUnmuteBlockedDescription": "Díe Stummschaltung kann aus Überlastungsschutzgründen temporär nicht aufgehoben werden.",
|
||||
"chatMessages": "Chatnachrichten",
|
||||
"connectedOneMember": "{{name}} nimmt am Meeting teil",
|
||||
"connectedThreePlusMembers": "{{name}} und {{count}} andere Personen nehmen am Meeting teil",
|
||||
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
|
||||
"disconnected": "getrennt",
|
||||
"displayNotifications": "Benachrichtigungen anzeigen für",
|
||||
"focus": "Konferenzleitung",
|
||||
"focusFail": "{{component}} ist im Moment nicht verfügbar – wiederholen in {{ms}} Sekunden",
|
||||
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
|
||||
@@ -782,6 +784,7 @@
|
||||
"title": "Profil"
|
||||
},
|
||||
"raisedHand": "Ich möchte sprechen",
|
||||
"raisedHandsLabel": "Anzahl gehobener Hände",
|
||||
"recording": {
|
||||
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
|
||||
@@ -857,6 +860,7 @@
|
||||
"selectAudioOutput": "Audioausgabe",
|
||||
"selectCamera": "Kamera",
|
||||
"selectMic": "Mikrofon",
|
||||
"selfView": "Eigene Ansicht",
|
||||
"sounds": "Hinweistöne",
|
||||
"speakers": "Lautsprecher",
|
||||
"startAudioMuted": "Alle Personen treten stummgeschaltet bei",
|
||||
|
||||
@@ -40,25 +40,25 @@
|
||||
"audioOnly": "Bande passante faible"
|
||||
},
|
||||
"blankPage": {
|
||||
"meetingEnded": "Réunion terminée."
|
||||
"meetingEnded": "Réunion terminée."
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"defaultName": "Salle annexe #{{index}}",
|
||||
"mainRoom": "Salle principale",
|
||||
"actions": {
|
||||
"add": "Ajouter salle annexe",
|
||||
"autoAssign": "Assigner automatiquement aux salles annexes",
|
||||
"close": "Fermer",
|
||||
"join": "Rejoindre",
|
||||
"leaveBreakoutRoom": "Quitter la salle annexe",
|
||||
"more": "Plus",
|
||||
"remove": "Supprimer",
|
||||
"sendToBreakoutRoom": "Envoyer le participant dans:"
|
||||
"add": "Ajouter salle annexe",
|
||||
"autoAssign": "Assigner automatiquement aux salles annexes",
|
||||
"close": "Fermer",
|
||||
"join": "Rejoindre",
|
||||
"leaveBreakoutRoom": "Quitter la salle annexe",
|
||||
"more": "Plus",
|
||||
"remove": "Supprimer",
|
||||
"sendToBreakoutRoom": "Envoyer le participant dans:"
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "Salles annexes",
|
||||
"joined": "Entrée en salle annexe \"{{name}}\"",
|
||||
"joinedMainRoom": "Retour à la salle principalem"
|
||||
"joinedTitle": "Salles annexes",
|
||||
"joined": "Entrée en salle annexe \"{{name}}\"",
|
||||
"joinedMainRoom": "Retour à la salle principalem"
|
||||
}
|
||||
},
|
||||
"calendarSync": {
|
||||
@@ -180,7 +180,8 @@
|
||||
"joinInApp": "Rejoindre la réunion en utilisant l'application",
|
||||
"launchWebButton": "Lancer dans le navigateur",
|
||||
"title": "Lancement de votre réunion dans {{app}} en cours ...",
|
||||
"tryAgainButton": "Réessayez sur le bureau"
|
||||
"tryAgainButton": "Réessayez sur le bureau",
|
||||
"unsupportedBrowser": "Il semble que vous utilisez un navigateur non supporté."
|
||||
},
|
||||
"defaultLink": "ex. {{url}}",
|
||||
"defaultNickname": "ex. Jean Dupont",
|
||||
@@ -244,8 +245,8 @@
|
||||
"gracefulShutdown": "Notre service est actuellement en maintenance. Veuillez réessayer plus tard.",
|
||||
"grantModeratorDialog": "Êtes-vous sûr de vouloir rendre ce participant modérateur ?",
|
||||
"grantModeratorTitle": "Nommer modérateur",
|
||||
"IamHost": "Je suis l'hôte",
|
||||
"hideShareAudioHelper": "Ne pas montrer ce dialogue à nouveau",
|
||||
"IamHost": "Je suis l'hôte",
|
||||
"incorrectRoomLockPassword": "Mot de passe incorrect",
|
||||
"incorrectPassword": "Nom d'utilisateur ou mot de passe incorrect",
|
||||
"internalError": "Oups ! Quelque chose s'est mal passée. L'erreur suivante s'est produite : {{error}}",
|
||||
@@ -261,8 +262,8 @@
|
||||
"lockMessage": "Impossible de verrouiller la conférence.",
|
||||
"lockRoom": "Ajouter un $t(lockRoomPassword) à la réunion ",
|
||||
"lockTitle": "Échec du verrouillage",
|
||||
"login": "Connexion",
|
||||
"logoutQuestion": "Voulez-vous vraiment vous déconnecter et arrêter la conférence ?",
|
||||
"login": "Connexion",
|
||||
"logoutTitle": "Déconnexion",
|
||||
"maxUsersLimitReached": "Le nombre maximal de participants est atteint. Le conférence est complète. Merci de contacter l'organisateur de la réunion ou réessayer plus tard !",
|
||||
"maxUsersLimitReachedTitle": "Le nombre maximal de participants est atteint",
|
||||
@@ -340,7 +341,7 @@
|
||||
"sessionRestarted": "L'appel est relancé par la passerelle",
|
||||
"Share": "Partager",
|
||||
"shareAudio": "Continuer",
|
||||
"shareAudioTitle" : "Comment partager le son",
|
||||
"shareAudioTitle": "Comment partager le son",
|
||||
"shareAudioWarningTitle": "Vous devez cesser de partager l'écran avant de partager l'audio",
|
||||
"shareAudioWarningH1": "Si vous voulez partager uniquement de l'audio:",
|
||||
"shareAudioWarningD1": "vous devez cesser le partage d'écran avant de partager votre son.",
|
||||
@@ -408,16 +409,17 @@
|
||||
"none": "Rien",
|
||||
"uploadedImage": "Image téléversée {{index}}",
|
||||
"deleteImage": "Supprimer l'image",
|
||||
"image1" : "Plage",
|
||||
"image2" : "Mur blanc neutre",
|
||||
"image3" : "Pièce vide blanche",
|
||||
"image4" : "Lampadaire noir",
|
||||
"image5" : "Montagne",
|
||||
"image6" : "Forêt ",
|
||||
"image7" : "Lever de soleil",
|
||||
"image1": "Plage",
|
||||
"image2": "Mur blanc neutre",
|
||||
"image3": "Pièce vide blanche",
|
||||
"image4": "Lampadaire noir",
|
||||
"image5": "Montagne",
|
||||
"image6": "Forêt ",
|
||||
"image7": "Lever de soleil",
|
||||
"desktopShareError": "Impossible de créer le partage de bureau",
|
||||
"desktopShare":"Partage de bureau",
|
||||
"desktopShare": "Partage de bureau",
|
||||
"webAssemblyWarning": "WebAssembly non supporté",
|
||||
"webAssemblyWarningDescription": "WebAssembly invalidé ou non supporté par ce navigateur",
|
||||
"backgroundEffectError": "Erreur dans l'application de l'effet d'arrière-plan."
|
||||
},
|
||||
"feedback": {
|
||||
@@ -445,7 +447,7 @@
|
||||
"country": "Pays",
|
||||
"dialANumber": "Pour rejoindre votre réunion, composez l'un de ces numéros, puis saisissez le code confidentiel.",
|
||||
"dialInConferenceID": "PIN :",
|
||||
"copyNumber":"Copier le numéro",
|
||||
"copyNumber": "Copier le numéro",
|
||||
"dialInNotSupported": "Désolé, l'accès par téléphone n'est pas pris en charge pour l'instant.",
|
||||
"dialInNumber": "Composer :",
|
||||
"dialInSummaryError": "Erreur lors de la récupération des informations de numérotation. Veuillez réessayer plus tard.",
|
||||
@@ -579,10 +581,12 @@
|
||||
"allowedUnmute": "Vous pouvez réactiver votre écran, votre caméra ou partager votre écran.",
|
||||
"audioUnmuteBlockedTitle": "Rétablissement du son bloqué !",
|
||||
"audioUnmuteBlockedDescription": "Le rétablissement du son a été bloqué temporairement en raison de limites système.",
|
||||
"chatMessages": "Messages de chat",
|
||||
"connectedOneMember": "{{name}} a rejoint la réunion",
|
||||
"connectedThreePlusMembers": "{{name}} et {{count}} autres personnes ont rejoint la réunion",
|
||||
"connectedTwoMembers": "{{first}} et {{second}} ont rejoint la réunion",
|
||||
"disconnected": "déconnecté",
|
||||
"displayNotifications": "Afficher les notifications pour",
|
||||
"focus": "Focus de conférence",
|
||||
"focusFail": "{{component}} n'est pas disponible - réessayez dans {{ms}} sec",
|
||||
"hostAskedUnmute": "Le modérateur souhaite vous donner la parole",
|
||||
@@ -604,9 +608,10 @@
|
||||
"passwordRemovedRemotely": "Le $t(lockRoomPassword) a été supprimé par un autre participant",
|
||||
"passwordSetRemotely": "Un $t(lockRoomPassword) a été défini par un autre participant",
|
||||
"raisedHand": "{{name}} aimerait prendre la parole.",
|
||||
"raisedHands": "{{participantName}} et {{raisedHands}} autres personnes",
|
||||
"raisedHands": "{{participantName}} et {{raisedHands}} autres personnes",
|
||||
"screenShareNoAudio": " La case Partager l'audio n'a pas été cochée dans l'écran de sélection de la fenêtre.",
|
||||
"screenShareNoAudioTitle": "La case Partager l'audio n'a pas été cochée",
|
||||
"selfViewTitle": "Vous pouvez toujours rétablir l'affichage de votre propre vidéo dans les paramètres",
|
||||
"somebody": "Quelqu'un",
|
||||
"startSilentTitle": "Vous avez rejoint sans sortie audio !",
|
||||
"startSilentDescription": "Rejoignez la réunion de nouveau pour activer l'audio",
|
||||
@@ -633,7 +638,7 @@
|
||||
"moderationToggleDescription": "par {{participantDisplayName}}",
|
||||
"raiseHandAction": "Lever la main",
|
||||
"reactionSounds": "Bloquer les réactions sonores",
|
||||
"reactionSoundsForAll": "Bloquer les réactions sonores pour tous",
|
||||
"reactionSoundsForAll": "Bloquer les réactions sonores pour tous",
|
||||
"groupTitle": "Notifications",
|
||||
"videoUnmuteBlockedTitle": "Rétablissement de la caméra bloqué !",
|
||||
"videoUnmuteBlockedDescription": "Le rétablissement de la vidéo a été bloqué temporairement en raison de limites système."
|
||||
@@ -649,7 +654,7 @@
|
||||
"actions": {
|
||||
"allow": "Autoriser les participants à:",
|
||||
"allowVideo": "permettre la vidéo",
|
||||
"audioModeration": "réouvrir leur micro",
|
||||
"audioModeration": "Rouvrir leur micro",
|
||||
"blockEveryoneMicCamera": "Bloquer tous les micros et caméras",
|
||||
"invite": "Inviter quelqu'un",
|
||||
"askUnmute": "Demander de réactiver le micro",
|
||||
@@ -662,7 +667,7 @@
|
||||
"stopVideo": "Couper la vidéo",
|
||||
"unblockEveryoneMicCamera": "Débloquer tous les micros et caméras",
|
||||
"videoModeration": "Démarrer leur vidéo",
|
||||
"moreModerationControls": "Options de modération supplémentaires"
|
||||
"moreModerationControls": "Options de modération supplémentaires"
|
||||
},
|
||||
"search": "Rechercher des participants"
|
||||
},
|
||||
@@ -675,8 +680,8 @@
|
||||
"answerPlaceholder": "Option {{index}}",
|
||||
"create": "Créer un sondage",
|
||||
"cancel": "Annuler",
|
||||
"pollOption" : "Option {{index}}",
|
||||
"pollQuestion" : "Question du sondage",
|
||||
"pollOption": "Option {{index}}",
|
||||
"pollQuestion": "Question du sondage",
|
||||
"questionPlaceholder": "Poser une question",
|
||||
"removeOption": "Supprimer l'option",
|
||||
"send": "Envoyer"
|
||||
@@ -750,8 +755,8 @@
|
||||
"or": "ou",
|
||||
"premeeting": "Pré-séance",
|
||||
"showScreen": "Activer l'écran de pré-séance",
|
||||
"keyboardShortcuts": "Activer les raccourcis clavier",
|
||||
"startWithPhone": "Commencez avec l'audio du téléphone",
|
||||
"keyboardShortcuts" : "Activer les raccourcis clavier",
|
||||
"screenSharingError": "Erreur de partage d'écran:",
|
||||
"videoOnlyError": "Erreur vidéo:",
|
||||
"videoTrackError": "Impossible de créer une piste vidéo.",
|
||||
@@ -779,6 +784,7 @@
|
||||
"title": "Profil"
|
||||
},
|
||||
"raisedHand": "Aimerait prendre la parole",
|
||||
"raisedHandsLabel": "Nombre de mains levées",
|
||||
"recording": {
|
||||
"limitNotificationDescriptionWeb": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",
|
||||
"limitNotificationDescriptionNative": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <3> {{app}} </3>.",
|
||||
@@ -821,8 +827,8 @@
|
||||
"security": {
|
||||
"about": "Vous pouvez ajouter un mot de passe à votre réunion. Les participants devront fournir le mot de passe avant de pouvoir rejoindre la réunion.",
|
||||
"aboutReadOnly": "Les modérateurs peuvent ajouter un mot de passe à la réunion. Les participants devront fournir le mot de passe avant de pouvoir rejoindre la réunion.",
|
||||
"insecureRoomNameWarning": "Le nom de la salle est peu sûr. Des participants non désirés peuvent rejoindre votre réunion. Pensez à sécuriser votre réunion en cliquant sur le bouton de sécurité.",
|
||||
"securityOptions": "Options de sécurité"
|
||||
"header": "Options de sécurité",
|
||||
"insecureRoomNameWarning": "Le nom de la salle est peu sûr. Des participants non désirés peuvent rejoindre votre réunion. Pensez à sécuriser votre réunion en cliquant sur le bouton de sécurité."
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
@@ -854,10 +860,11 @@
|
||||
"selectAudioOutput": "Sortie audio",
|
||||
"selectCamera": "Caméra",
|
||||
"selectMic": "Microphone",
|
||||
"selfView": "Affichage de votre propre vidéo",
|
||||
"sounds": "Sons",
|
||||
"speakers": "Haut-parleurs",
|
||||
"startAudioMuted": "Tout le monde commence en muet",
|
||||
"startReactionsMuted": "Tout le monde commence avec les réactions sonores bloquées",
|
||||
"startReactionsMuted": "Tout le monde commence avec les réactions sonores bloquées",
|
||||
"startVideoMuted": "Tout le monde commence sans vidéo",
|
||||
"talkWhileMuted": "vous parlez en étant muet",
|
||||
"title": "Paramètres"
|
||||
@@ -890,20 +897,20 @@
|
||||
},
|
||||
"speaker": "Haut-parleur",
|
||||
"speakerStats": {
|
||||
"search": "Recherche",
|
||||
"angry": "En colère",
|
||||
"disgusted": "Dégoûté",
|
||||
"fearful": "Effrayé",
|
||||
"happy": "Content",
|
||||
"hours": "{{count}}h",
|
||||
"minutes": "{{count}}m",
|
||||
"name": "Nom",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Statistiques de l'interlocuteur",
|
||||
"speakerTime": "Temps de l'interlocuteur",
|
||||
"happy": "Content",
|
||||
"neutral": "Indifférent",
|
||||
"sad": "Triste",
|
||||
"surprised": "Surpris",
|
||||
"angry": "En colère",
|
||||
"fearful": "Effrayé",
|
||||
"disgusted": "Dégoûté"
|
||||
"search": "Recherche",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerTime": "Temps de l'interlocuteur",
|
||||
"speakerStats": "Statistiques de l'interlocuteur",
|
||||
"surprised": "Surpris"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -946,8 +953,8 @@
|
||||
"mute": "Activer / Désactiver l'audio",
|
||||
"muteEveryone": "Couper le micro de tout le monde",
|
||||
"muteEveryoneElse": "Couper le micro de tous les autres",
|
||||
"muteEveryonesVideo": "Couper la caméra de tout le monde",
|
||||
"muteEveryoneElsesVideo": "Couper la caméra de tout les autres",
|
||||
"muteEveryonesVideoStream": "Couper la caméra de tout le monde",
|
||||
"muteEveryoneElsesVideoStream": "Couper la caméra de tous les autres",
|
||||
"participants": "Participants",
|
||||
"pip": "Activer / Désactiver le mode Picture in Picture",
|
||||
"privateMessage": "Envoyer un message privé",
|
||||
@@ -1123,6 +1130,7 @@
|
||||
"domuteVideoOfOthers": "Couper la caméra des autres",
|
||||
"flip": "Balancer",
|
||||
"grantModerator": "Donner des droits de modérateur",
|
||||
"hideSelfView": "Cacher l'affichage de votre propre vidéo",
|
||||
"kick": "Exclure",
|
||||
"moderator": "Modérateur",
|
||||
"mute": "Un participant a coupé son micro",
|
||||
@@ -1171,13 +1179,13 @@
|
||||
"startMeeting": "Démarrer la conférence",
|
||||
"terms": "Termes",
|
||||
"title": "Système de vidéoconférence sécurisé, riche en fonctionnalités et gratuit",
|
||||
"logo":{
|
||||
"calendar":"Logo Calendar",
|
||||
"microsoftLogo":"Logo Microsoft",
|
||||
"logoDeepLinking":"Logo Jitsi meet",
|
||||
"desktopPreviewThumbnail":"Miniature d'aperçu du bureau",
|
||||
"googleLogo":"Logo Google",
|
||||
"policyLogo":"Logo de la politique"
|
||||
"logo": {
|
||||
"calendar": "Logo Calendar",
|
||||
"microsoftLogo": "Logo Microsoft",
|
||||
"logoDeepLinking": "Logo Jitsi meet",
|
||||
"desktopPreviewThumbnail": "Miniature d'aperçu du bureau",
|
||||
"googleLogo": "Logo Google",
|
||||
"policyLogo": "Logo de la politique"
|
||||
}
|
||||
},
|
||||
"lonelyMeetingExperience": {
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
"copyStream": "Link naar livestream kopiëren",
|
||||
"countryNotSupported": "Deze bestemming wordt nog niet ondersteund.",
|
||||
"countryReminder": "Belt u buiten de Verenigde Staten? Vergeet dan niet met de landcode te beginnen.",
|
||||
"defaultEmail": "Uw Standaard Email",
|
||||
"defaultEmail": "Uw standaard e-mail",
|
||||
"disabled": "U kunt geen personen uitnodigen.",
|
||||
"failedToAdd": "Het toevoegen van deelnemers is mislukt",
|
||||
"footerText": "Uitgaande oproepen zijn uitgeschakeld.",
|
||||
"googleEmail": "Google Email",
|
||||
"googleEmail": "Google e-mail",
|
||||
"inviteMoreHeader": "U bent de enige in de vergadering",
|
||||
"inviteMoreMailSubject": "Deelnemen aan {{appName}}-vergadering",
|
||||
"inviteMorePrompt": "Nodig meer personen uit",
|
||||
@@ -21,7 +21,7 @@
|
||||
"loadingPeople": "Personen om uit te nodigen aan het zoeken",
|
||||
"noResults": "Geen resultaten die overeenkomen met de zoekopdracht",
|
||||
"noValidNumbers": "Voer een telefoonnummer in",
|
||||
"outlookEmail": "Outlook Email",
|
||||
"outlookEmail": "Outlook e-mail",
|
||||
"searchNumbers": "Telefoonnummers toevoegen",
|
||||
"searchPeople": "Personen zoeken",
|
||||
"searchPeopleAndNumbers": "Personen zoeken of hun telefoonnummers toevoegen",
|
||||
@@ -30,7 +30,7 @@
|
||||
"shareStream": "Deel de link naar de livestream",
|
||||
"telephone": "Telefoon: {{number}}",
|
||||
"title": "Personen uitnodigen voor deze vergadering",
|
||||
"yahooEmail": "Yahoo Email"
|
||||
"yahooEmail": "Yahoo e-mail"
|
||||
},
|
||||
"audioDevices": {
|
||||
"bluetooth": "Bluetooth",
|
||||
@@ -73,9 +73,17 @@
|
||||
"titleWithPolls": "Voer een bijnaam in om chat te gebruiken"
|
||||
},
|
||||
"privateNotice": "Privébericht aan {{recipient}}",
|
||||
"title": "Chat",
|
||||
"titleWithPolls": "Chat",
|
||||
"you": "u"
|
||||
"message": "Bericht",
|
||||
"messageAccessibleTitle": "{{user}} zegt:",
|
||||
"messageAccessibleTitleMe": "ik zeg:",
|
||||
"smileysPanel": "Smiley paneel",
|
||||
"tabs": {
|
||||
"chat": "Gesprek",
|
||||
"polls": "Peilingen"
|
||||
},
|
||||
"title": "Gesprek",
|
||||
"titleWithPolls": "Gesprek",
|
||||
"you": "U"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"installExtensionText": "Installeer de extensie voor Google Calendar en Office 365 integratie",
|
||||
@@ -526,6 +534,35 @@
|
||||
},
|
||||
"passwordSetRemotely": "ingesteld door een andere deelnemer",
|
||||
"passwordDigitsOnly": "Maximaal {{number}} cijfers",
|
||||
"polls": {
|
||||
"by": "Door {{ name }}",
|
||||
"create": {
|
||||
"addOption": "Voeg optie toe",
|
||||
"answerPlaceholder": "Optie {{index}}",
|
||||
"create": "Creeër een peiling",
|
||||
"cancel": "Annuleer",
|
||||
"pollOption" : "Peiling optie {{index}}",
|
||||
"pollQuestion" : "Peiling vraag",
|
||||
"questionPlaceholder": "Stel een vraag",
|
||||
"removeOption": "Verwijder optie",
|
||||
"send": "Verstuur"
|
||||
},
|
||||
"answer": {
|
||||
"skip": "Sla over",
|
||||
"submit": "Verzend"
|
||||
},
|
||||
"results": {
|
||||
"vote": "Stem",
|
||||
"changeVote": "Verander stem",
|
||||
"empty": "Er zijn nog geen peilingen in deze vergadering. Start hier een peiling!",
|
||||
"hideDetailedResults": "Verberg details",
|
||||
"showDetailedResults": "Toon details"
|
||||
},
|
||||
"notification": {
|
||||
"title": "Een nieuwe peiling is aangemaakt in deze vergadering",
|
||||
"description": "Open het peilingen tabblad om te stemmen"
|
||||
}
|
||||
},
|
||||
"poweredby": "mogelijk gemaakt door",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Audio- en videofout:",
|
||||
@@ -586,8 +623,8 @@
|
||||
},
|
||||
"profile": {
|
||||
"setDisplayNameLabel": "Uw weergavenaam instellen",
|
||||
"setEmailInput": "Voer emailadres in",
|
||||
"setEmailLabel": "Uw Gravatar email instellen",
|
||||
"setEmailInput": "Voer e-mailadres in",
|
||||
"setEmailLabel": "Uw Gravatar e-mail instellen",
|
||||
"title": "Profiel"
|
||||
},
|
||||
"raisedHand": "Zou graag willen spreken",
|
||||
@@ -935,7 +972,7 @@
|
||||
"dialogTitle": "Lobby-modus",
|
||||
"disableDialogContent": "Lobby-modus is momenteel ingeschakeld. Deze functie zorgt ervoor dat ongewenste deelnemers niet aan uw vergadering kunnen deelnemen. Wilt u het uitschakelen?",
|
||||
"disableDialogSubmit": "Uitschakelen",
|
||||
"emailField": "Voer uw emailadres in",
|
||||
"emailField": "Voer uw e-mailadres in",
|
||||
"enableDialogPasswordField": "Stel wachtwoord in (optioneel)",
|
||||
"enableDialogSubmit": "Inschakelen",
|
||||
"enableDialogText": "Met de lobby-modus kunt u uw vergadering beveiligen, door deelnemers alleen toe te laten na een formele goedkeuring van een moderator.",
|
||||
|
||||
@@ -158,7 +158,8 @@
|
||||
"joinInApp": "Rejónher la conferéncia en utilizant l’aplicacion",
|
||||
"launchWebButton": "Lançar del navigador",
|
||||
"title": "Aviada de vòstra conferéncia dins {{app}}…",
|
||||
"tryAgainButton": "Tornar ensajar del burèu"
|
||||
"tryAgainButton": "Tornar ensajar del burèu",
|
||||
"unsupportedBrowser": "Sembla qu’utilizatz un navigador que prenèm pas en carga."
|
||||
},
|
||||
"defaultLink": "ex. {{url}}",
|
||||
"defaultNickname": "ex. Joan Delpuèch",
|
||||
@@ -421,7 +422,7 @@
|
||||
"noPassword": "Pas cap",
|
||||
"noRoom": "Cap de sala pas donada per la jónher.",
|
||||
"numbers": "Sonar de numèros",
|
||||
"password": "$t(lockRoomPasswordUppercase) :",
|
||||
"password": "$t(lockRoomPasswordUppercase): ",
|
||||
"title": "Partejar",
|
||||
"tooltip": "Partejar lo ligam e las informacions d’aquesta conferéncia",
|
||||
"copyNumber": "Copiar lo numèro",
|
||||
@@ -627,9 +628,17 @@
|
||||
"moderationInEffectCSDescription": "Volgatz levar la man se volètz partejar vòstre ecran.",
|
||||
"moderationInEffectVideoDescription": "Volgatz levar la man se volètz aviar vòstra camèra.",
|
||||
"audioUnmuteBlockedTitle": "Restabliment del son del microfòn blocat !",
|
||||
"videoUnmuteBlockedTitle": "Restabliment de la camèra blocat !",
|
||||
"videoUnmuteBlockedTitle": "Restabliment de la camèra e del partiment de burèu blocat !",
|
||||
"audioUnmuteBlockedDescription": "Las operacion de restabliment del son microfòn son estadas blocadas pel moment a causa de limitas sistèma.",
|
||||
"videoUnmuteBlockedDescription": "Las operacion de restabliment de la camèra son estadas blocadas pel moment a causa de limitas sistèma."
|
||||
"videoUnmuteBlockedDescription": "Las operacion de restabliment de la camèra e del partiment del burèu son estadas blocadas pel moment a causa de limitas sistèma.",
|
||||
"chatMessages": "Messatges del chat",
|
||||
"displayNotifications": "Afichar las notificacions per",
|
||||
"leftOneMember": "{{name}} a quitat la conferéncia",
|
||||
"leftThreePlusMembers": "{{name}} e un molon d’autres an quitat la conferéncia",
|
||||
"leftTwoMembers": "{{first}} e {{second}} an quitat la conferéncia",
|
||||
"raisedHands": "{{participantName}} e {{raisedHands}} autres",
|
||||
"selfViewTitle": "Podètz totjorn quitar d’amagar vòstra pròpria vista a partir dels paramètres",
|
||||
"reactionSoundsForAll": "Desactivar los sons per totes"
|
||||
},
|
||||
"passwordDigitsOnly": "Fins a {{number}} chifras",
|
||||
"passwordSetRemotely": "causit per qualqu'un mai",
|
||||
@@ -758,7 +767,7 @@
|
||||
"about": "Podètz ajustar un $t(lockRoomPassword) per rejónher una conferéncia. Los participants deuràn fornir lo $t(lockRoomPassword) abans d’obténer l’autorizacion de dintrar dins la conferéncia.",
|
||||
"aboutReadOnly": "Los participants que son moderators pòdon ajustar un $t(lockRoomPassword) a la conferéncia. Los participants deuràn fornir lo $t(lockRoomPassword) abans d’aver l’autorizacion de rejónher la conferéncia.",
|
||||
"insecureRoomNameWarning": "Lo nom de la sala es pas segur. De monde indesirables poirián rejónher vòstra conferéncia.",
|
||||
"securityOptions": "Opcions de seguretat"
|
||||
"header": "Opcions de seguretat"
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
@@ -795,7 +804,9 @@
|
||||
"talkWhileMuted": "Parlar en mut",
|
||||
"desktopShareHighFpsWarning": "Una frequéncia d’imatge mai nauta pel partiment burèu pòt afectar la benda passanta. Devètz reaviar lo partiment d’ecran per aplicar los paramètres novèls.",
|
||||
"desktopShareWarning": "Devètz reaviar lo partiment d’ecran per prendre en compte las modificacions.",
|
||||
"incomingMessage": "Messatge dintrant"
|
||||
"incomingMessage": "Messatge dintrant",
|
||||
"selfView": "Vista de se",
|
||||
"startReactionsMuted": "Començan totes amb las reaccions sonòras amudidas"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "Avançat",
|
||||
@@ -898,7 +909,6 @@
|
||||
"clap": "Picar de las mans",
|
||||
"laugh": "Rire",
|
||||
"like": "Levar lo det gròs",
|
||||
"muteEveryonesVideo": "Copar la vidèo del monde",
|
||||
"muteEveryoneElsesVideo": "Copar la vidèo de los demai",
|
||||
"participants": "Participants",
|
||||
"remoteVideoMute": "Copar la camèra del participant",
|
||||
@@ -910,7 +920,9 @@
|
||||
"collapse": "Plegar",
|
||||
"muteEveryoneElse": "Copar lo microfòn dels autres",
|
||||
"reactionsMenu": "Dobrir / Tampar lo menú de reaccions",
|
||||
"breakoutRoom": "Rejónher/quitar la sala de reünion"
|
||||
"breakoutRoom": "Rejónher/quitar la sala de reünion",
|
||||
"muteEveryoneElsesVideoStream": "Arrestar la vidèo de totes los autres",
|
||||
"muteEveryonesVideoStream": "Arrestar la vidèo de tot lo monde"
|
||||
},
|
||||
"addPeople": "Ajustar de monde a vòstra sonada",
|
||||
"audioOnlyOff": "Desactivar lo mòde connexion febla",
|
||||
@@ -1064,7 +1076,8 @@
|
||||
"videomute": "Lo participant a arrestat la camèra",
|
||||
"domuteVideo": "Desactivar la camèra",
|
||||
"domuteVideoOfOthers": "Desactivar la camèra dels demai",
|
||||
"videoMuted": "Camèra desactivada"
|
||||
"videoMuted": "Camèra desactivada",
|
||||
"hideSelfView": "Amagar pròpria vista"
|
||||
},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
@@ -1135,7 +1148,8 @@
|
||||
"image6": "Forèst ",
|
||||
"desktopShareError": "Creacion impossibla d’un partiment de burèu",
|
||||
"desktopShare": "Partiment de burèu",
|
||||
"webAssemblyWarning": "WebAssembly pas pres en carga"
|
||||
"webAssemblyWarning": "WebAssembly pas pres en carga",
|
||||
"webAssemblyWarningDescription": "WebAssembly es desactivat o pas pres en carga per aqueste navigador"
|
||||
},
|
||||
"participantsPane": {
|
||||
"headings": {
|
||||
@@ -1160,7 +1174,8 @@
|
||||
"videoModeration": "Aviar lor vidèo",
|
||||
"allowVideo": "Autorizar la vidèo",
|
||||
"moreModerationActions": "Mai d’opcions de moderacion",
|
||||
"moreParticipantOptions": "Mai d’opcions de participant"
|
||||
"moreParticipantOptions": "Mai d’opcions de participant",
|
||||
"moreModerationControls": "Opcions de moderacion suplementàrias"
|
||||
},
|
||||
"search": "Cercar participants"
|
||||
},
|
||||
@@ -1207,7 +1222,12 @@
|
||||
"autoAssign": "Atribucion auto a las salas de reünion"
|
||||
},
|
||||
"defaultName": "Sala de reünion #{{index}}",
|
||||
"mainRoom": "Sala principala"
|
||||
"mainRoom": "Sala principala",
|
||||
"notifications": {
|
||||
"joinedMainRoom": "Retorn a la sala principala",
|
||||
"joinedTitle": "Salas suplementàrias",
|
||||
"joined": "Dintrada a la sala suplementària « {{name}} »"
|
||||
}
|
||||
},
|
||||
"privacyView": {
|
||||
"header": "Confidencialitat"
|
||||
@@ -1217,5 +1237,6 @@
|
||||
},
|
||||
"blankPage": {
|
||||
"meetingEnded": "Conferéncia acabada."
|
||||
}
|
||||
},
|
||||
"raisedHandsLabel": "Nombre de mans levadas"
|
||||
}
|
||||
|
||||
@@ -39,6 +39,28 @@
|
||||
"audioOnly": {
|
||||
"audioOnly": "Niska przepustowość"
|
||||
},
|
||||
"blankPage": {
|
||||
"meetingEnded": "Spotkanie zakończone."
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"defaultName": "Pokój podgrupy #{{index}}",
|
||||
"mainRoom": "Główny pokój",
|
||||
"actions": {
|
||||
"add": "Dodaj pokój podgrupy",
|
||||
"autoAssign": "Automatycznie przypisuj do pokoi podgrup",
|
||||
"close": "Blisko",
|
||||
"join": "Dołącz",
|
||||
"leaveBreakoutRoom": "Opuść pokój spotkań",
|
||||
"more": "Więcej",
|
||||
"remove": "Usuń",
|
||||
"sendToBreakoutRoom": "Wyślij uczestnika do:"
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "Pokoje podgrup",
|
||||
"joined": "Dołączanie do pokoju podgrup \"{{name}}\" ",
|
||||
"joinedMainRoom": "Dołączanie do głównego pokoju"
|
||||
}
|
||||
},
|
||||
"calendarSync": {
|
||||
"addMeetingURL": "Dodaj odnośnik do spotkania",
|
||||
"confirmAddLink": "Czy chcesz dodać odnośnik Jitsi do tego wydarzenia?",
|
||||
@@ -158,7 +180,8 @@
|
||||
"joinInApp": "Dołącz do spotkania używając aplikacji",
|
||||
"launchWebButton": "Uruchom przez przeglądarkę",
|
||||
"title": "Trwa uruchamianie Twojego spotkania w {{app}}…",
|
||||
"tryAgainButton": "Spróbuj ponownie w aplikacji stacjonarnej"
|
||||
"tryAgainButton": "Spróbuj ponownie w aplikacji stacjonarnej",
|
||||
"unsupportedBrowser": "Wygląda na to, że używasz przeglądarki, której nie wspieramy."
|
||||
},
|
||||
"defaultLink": "np. {{url}}",
|
||||
"defaultNickname": "np. Jan Kowalski",
|
||||
@@ -206,14 +229,16 @@
|
||||
"connectErrorWithMsg": "Upsss! Coś poszło nie tak i nie możemy podłączyć się do tej konferencji: {{msg}}",
|
||||
"connecting": "Nawiązywanie połączenia",
|
||||
"contactSupport": "Skontaktuj się ze wsparciem",
|
||||
"copy": "Kopiuj",
|
||||
"copied": "Skopiowano",
|
||||
"copy": "Kopiuj",
|
||||
"dismiss": "Odrzuć",
|
||||
"displayNameRequired": "Cześć! Jak się nazywasz?",
|
||||
"done": "Zrobione",
|
||||
"e2eeDescription": "Szyfrowanie End-to-End jest aktualnie w fazie EKSPERYMENTALNEJ. Proszę mieć na uwadze fakt, że szyfrowanie end-to-end wyłączy oferowane przez serwer usługi takie jak: nagrywanie, streaming na żywo i dołączanie uczestników przez telefon. Proszę mieć również na uwadze fakt, że takie spotkanie będą działać tylko dla uczestników korzystających z przeglądarek wspierających wstawiane strumienie.",
|
||||
"e2eeLabel": "Klucz E2EE",
|
||||
"e2eeLabel": "Włącz szyfrowanie E2EE",
|
||||
"e2eeDisabledDueToMaxModeDescription": "Nie można włączyć szyfrowania End-to-End z powodu dużej liczby uczestników konferencji.",
|
||||
"e2eeWarning": "UWAGA: Niektórzy uczestnicy tego spotkania nie mają włączonej obsługi szyfrowania E2EE. Jeśli włączysz tą funkcję ci uczestnicy nie będą mieli z tobą kontaktu.",
|
||||
"e2eeWillDisableDueToMaxModeDescription": "UWAGA: Szyfrowanie typu end-to-end zostanie automatycznie wyłączone, jeśli do konferencji dołączy więcej uczestników.",
|
||||
"enterDisplayName": "Wpisz tutaj swoje imię",
|
||||
"embedMeeting": "Osadź spotkanie",
|
||||
"error": "Błąd",
|
||||
@@ -249,25 +274,29 @@
|
||||
"micPermissionDeniedError": "Nie udzieliłeś pozwolenia na użycie twojego mikrofonu. Nadal możesz uczestniczyc w konferencji ale inni nie będą cię słyszeli. Użyj przycisku kamera aby to naprawić.",
|
||||
"micTimeoutError": "Nie udało się uruchomić źródła dźwięku. Przekroczono limit czasu",
|
||||
"micUnknownError": "Z nieznanej przyczyny nie można użyć mikrofonu.",
|
||||
"moderationAudioLabel": "Zezwalaj uczestnikom na wyłączanie wyciszenia",
|
||||
"moderationVideoLabel": "Zezwalaj uczestnikom na rozpoczęcie wideo",
|
||||
"muteEveryoneElseDialog": "Gdy wyciszysz wszystkich nie będziesz miał możliwości wyłączyć ich wyciszenia, ale oni będą mogli samodzielnie wyłączyć wyciszenie w dowolnym momencie.",
|
||||
"muteEveryoneElseTitle": "Wyciszyć wszystkich za wyjątkiem {{whom}}?",
|
||||
"muteEveryoneElsesVideoDialog": "Po dezaktywacji kamery nie można jej ponownie aktywować, ale uczestnicy mogą to zmienić samodzielnie w dowolnym momencie.",
|
||||
"muteEveryoneDialog": "Uczestnicy mogą w dowolnym momencie wyłączyć wyciszenie.",
|
||||
"muteEveryoneDialogModerationOn": "Uczestnicy mogą w każdej chwili wysłać prośbę o zabranie głosu.",
|
||||
"muteEveryoneTitle": "Wyciszyć wszystkich?",
|
||||
"muteEveryoneElsesVideoDialog": "Po wyłączeniu kamery nie będzie można go ponownie włączyć, ale można go ponownie włączyć w dowolnym momencie.",
|
||||
"muteEveryoneElsesVideoTitle": "Wyłączyć kamerę wszystkim oprócz {{whom}}?",
|
||||
"muteEveryonesVideoDialog": "Czy na pewno chcesz wyłączyć kamery wszystkich uczestników? Nie możesz ponownie włączyć kamer, ale uczestnicy mogą to zmienić samodzielnie w dowolnym momencie.",
|
||||
"muteEveryonesVideoDialog": "Uczestnicy mogą w każdej chwili włączyć swoje wideo.",
|
||||
"muteEveryonesVideoDialogModerationOn": "Uczestnicy mogą w dowolnym momencie wysłać prośbę o włączenie ich wideo.",
|
||||
"muteEveryonesVideoDialogOk": "Wyłącz",
|
||||
"muteEveryonesVideoTitle": "Wyłączyć kamery pozostałych uczestników?",
|
||||
"muteEveryoneDialog": "Czy na pewno wyciszyć wszystkich? Nie będziesz miał możliwości wyłączyć ich wyciszenia, ale oni będą mogli samodzielnie wyłączyć wyciszenie w dowolnym momencie.",
|
||||
"muteEveryoneTitle": "Wyciszyć wszystkich?",
|
||||
"muteEveryoneSelf": "siebie",
|
||||
"muteEveryoneStartMuted": "Od tego momentu wszyscy są wyciszeni",
|
||||
"muteParticipantBody": "Nie możesz wyłączyć ich wyciszenia, ale oni mogą samodzielnie wyłączyć wyciszenie w dowolnym momencie.",
|
||||
"muteParticipantButton": "Wycisz",
|
||||
"muteParticipantDialog": "Czy na pewno wyciszyć tego uczestnika? Nie będziesz mógł wyłączyć wyciszenia uczestników, ale oni mogą samodzielnie wyłączyć wyciszenie w dowolnym momencie.",
|
||||
"muteParticipantsVideoDialog": "Czy na pewno chcesz wyłączyć kamerę tego uczestnika? Nie będziesz mógł ponownie włączyć jego kamery, ale będzie on mógł samodzielnie włączyć kamerę w dowolnym momencie.",
|
||||
"muteParticipantTitle": "Wyciszyć tego uczestnika?",
|
||||
"muteParticipantsVideoDialogModerationOn": "Czy na pewno chcesz wyłączyć kamerę tego uczestnika? Nie będziesz w stanie ponownie włączyć aparatu i oni też nie.",
|
||||
"muteParticipantsVideoButton": "Wyłącz kamerę",
|
||||
"muteParticipantsVideoTitle": "Wyłączyć kamerę tego uczestnika?",
|
||||
"muteParticipantsVideoBody": "Nie będziesz mógł włączyć jego kamery ponownie, ale uczestnik samodzielnie może włączyć kamerę w dowolnym momencie.",
|
||||
"muteParticipantsVideoBodyModerationOn": "Nie będziesz w stanie ponownie włączyć kamery i oni też nie.",
|
||||
"noDropboxToken": "Brak poprawnego tokenu Dropbox",
|
||||
"Ok": "OK",
|
||||
"password": "$t(lockRoomPasswordUppercase)",
|
||||
@@ -329,6 +358,7 @@
|
||||
"shareScreenWarningH1": "Kiedy chcesz udostępniać wyłącznie swój ekran:",
|
||||
"shareScreenWarningD1": "musisz zatrzymać udostępnianie dźwięku przed udostępnieniem ekranu.",
|
||||
"shareScreenWarningD2": "musisz zatrzymać udostępnianie dźwięku, rozpocząć udostępnianie ekranu i zaznaczyć opcję \"udostępnij dźwięk\".",
|
||||
"sharedVideoLinkPlaceholder": "Link do YouTube lub bezpośredni link do wideo",
|
||||
"stopLiveStreaming": "Zatrzymaj transmisję na żywo",
|
||||
"stopRecording": "Zatrzymaj nagrywanie",
|
||||
"stopRecordingWarning": "Naprawdę chcesz zatrzymać nagrywanie?",
|
||||
@@ -388,7 +418,9 @@
|
||||
"image7": "Wschód słońca",
|
||||
"desktopShareError": "Nie można udostępnić pulpitu",
|
||||
"desktopShare": "Udostępnianie pulpitu",
|
||||
"webAssemblyWarning": "WebAssembly nie jest obsługiwany"
|
||||
"webAssemblyWarning": "WebAssembly nie jest obsługiwany",
|
||||
"webAssemblyWarningDescription": "WebAssembly wyłączony lub nieobsługiwany przez tę przeglądarkę",
|
||||
"backgroundEffectError": "Nie udało się zastosować efektu tła."
|
||||
},
|
||||
"feedback": {
|
||||
"average": "Średnio",
|
||||
@@ -493,6 +525,7 @@
|
||||
"expandedPending": "Transmisja na żywo rozpoczyna się…",
|
||||
"failedToStart": "Transmitowanie na żywo nie uruchomiło się",
|
||||
"getStreamKeyManually": "Nie byliśmy w stanie pobrać żadnych transmisji na żywo. Spróbuj uzyskać klucz do transmisji na żywo z YouTube.",
|
||||
"inProgress": "Trwa nagrywanie lub transmisja na żywo",
|
||||
"invalidStreamKey": "Klucz transmisji na żywo może być nieprawidłowy.",
|
||||
"off": "Transmitowanie na żywo zostało zatrzymane",
|
||||
"offBy": "{{name}} zatrzymał transmisję na żywo",
|
||||
@@ -500,6 +533,7 @@
|
||||
"onBy": "{{name}} rozpoczął transmisję na żywo",
|
||||
"pending": "Start strumieniowania live…",
|
||||
"serviceName": "Usługa transmisji na żywo",
|
||||
"sessionAlreadyActive": "Ta sesja jest już nagrywana lub transmitowana na żywo.",
|
||||
"signedInAs": "Jesteś obecnie zalogowany jako:",
|
||||
"signIn": "Zaloguj się z Google",
|
||||
"signInCTA": "Zaloguj się lub wpisz swój klucz do transmisji na żywo YouTube.",
|
||||
@@ -543,18 +577,24 @@
|
||||
"lockRoomPasswordUppercase": "Hasło",
|
||||
"me": "to ja",
|
||||
"notify": {
|
||||
"allowAction": "Zezwól",
|
||||
"allowedUnmute": "Możesz wyłączyć wyciszenie mikrofonu, uruchomić aparat lub udostępnić ekran.",
|
||||
"audioUnmuteBlockedTitle": "Zablokowano wyciszenie mikrofonu!",
|
||||
"audioUnmuteBlockedDescription": "Operacja wyłączania wyciszenia mikrofonu została tymczasowo zablokowana z powodu ograniczeń systemu.",
|
||||
"connectedOneMember": "{{name}} dołączył do spotkania",
|
||||
"connectedThreePlusMembers": "{{name}} i {{count}} innych osób dołączyło do spotkania",
|
||||
"connectedTwoMembers": "{{first}} i {{second}} dołączyli do spotkania",
|
||||
"disconnected": "Rozłączono",
|
||||
"focus": "Fokus konferencji",
|
||||
"focusFail": "{{component}} jest niedostępny - ponowienie w ciągu {{ms}} sec",
|
||||
"grantedTo": "Prawa moderatora przyznane dla {{to}}!",
|
||||
"hostAskedUnmute": "Gospodarz prosi Cię o wyłączenie wyciszenia",
|
||||
"invitedOneMember": "{{name}} został zaproszony",
|
||||
"invitedThreePlusMembers": "{{name}} i {{count}} innych osób zostało zaproszone",
|
||||
"invitedTwoMembers": "{{first}} i {{second}} zostali zaproszeni",
|
||||
"kickParticipant": "{{kicked}} został usunięty przez {{kicker}}",
|
||||
"leftOneMember": "{{name}} opuścił spotkanie",
|
||||
"leftThreePlusMembers": "{{name}} i wielu innych opuściło spotkanie",
|
||||
"leftTwoMembers": "{{first}} i {{second}} opuścił spotkanie",
|
||||
"me": "To ja",
|
||||
"moderator": "Prawa moderatora przydzielone!",
|
||||
"muted": "Rozpoczęto wyciszenie konwersacji.",
|
||||
@@ -566,8 +606,10 @@
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) usunięte przez innego uczestnika",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) ustawiony przez innego uczestnika",
|
||||
"raisedHand": "{{name}} chce mówić.",
|
||||
"raisedHands": "{{participantName}} i {{raisedHands}} więcej osób",
|
||||
"screenShareNoAudio": "Opcja \"Udostępnij dźwięk\" nie została zaznaczona podczas wyboru okna.",
|
||||
"screenShareNoAudioTitle": "Nie można udostępnić dźwięku",
|
||||
"selfViewTitle": "Zawsze możesz odkryć własny podgląd w ustawieniach",
|
||||
"somebody": "Ktoś",
|
||||
"startSilentTitle": "Dołączyłeś bez wyjścia dźwiękowego!",
|
||||
"startSilentDescription": "Ponownie dołącz do spotkania, aby włączyć dźwięk",
|
||||
@@ -594,7 +636,10 @@
|
||||
"moderationToggleDescription": "przez {{participantDisplayName}}",
|
||||
"raiseHandAction": "Podnieś rękę",
|
||||
"reactionSounds": "Wyłącz dźwięki",
|
||||
"groupTitle": "Powiadomienia"
|
||||
"reactionSoundsForAll": "Wyłącz dźwięki dla wszystkich",
|
||||
"groupTitle": "Powiadomienia",
|
||||
"videoUnmuteBlockedTitle": "Wyłączenie wyciszenia kamery i udostępnianie pulpitu zablokowane!",
|
||||
"videoUnmuteBlockedDescription": "Operacje wyłączania wyciszenia kamery i udostępniania pulpitu zostały tymczasowo zablokowane z powodu ograniczeń systemu."
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "Zamknij",
|
||||
@@ -606,21 +651,28 @@
|
||||
},
|
||||
"actions": {
|
||||
"allow": "Zezwól uczestnikom na:",
|
||||
"allowVideo": "Zezwól na wideo",
|
||||
"audioModeration": "Wyłącz wyciszenie",
|
||||
"blockEveryoneMicCamera": "Zablokuj wszystkim kamerę i mikrofon",
|
||||
"invite": "Zaproś",
|
||||
"askUnmute": "Poproś o wyłączenie wyciszenia",
|
||||
"moreModerationActions": "Więcej opcji moderatora",
|
||||
"moreParticipantOptions": "Więcej opcji dla uczestników",
|
||||
"mute": "Wycisz",
|
||||
"muteAll": "Wycisz wszystkich",
|
||||
"muteEveryoneElse": "Wycisz pozostałych",
|
||||
"startModeration": "Wyłącz wyciszenie lub rozpocznij wideo",
|
||||
"stopEveryonesVideo": "Wyłącz wszystkie kamery",
|
||||
"stopVideo": "Wyłącz kamerę",
|
||||
"unblockEveryoneMicCamera": "Odblokuj wszystkim kamerę i mikrofon"
|
||||
}
|
||||
"unblockEveryoneMicCamera": "Odblokuj wszystkim kamerę i mikrofon",
|
||||
"videoModeration": "Włącz kamerę",
|
||||
"moreModerationControls": "Więcej kontroli moderacji"
|
||||
},
|
||||
"search": "Wyszukaj uczestników"
|
||||
},
|
||||
"passwordSetRemotely": "wybrane przez innego uczestnika",
|
||||
"passwordSetRemotely": "Ustawione przez innego uczestnika",
|
||||
"passwordDigitsOnly": "Do {{number}} cyfr",
|
||||
"polls": {
|
||||
"by": "Przez {{ name }}",
|
||||
"create": {
|
||||
"addOption": "Dodaj opcję",
|
||||
"answerPlaceholder": "Opcja {{index}}",
|
||||
@@ -688,6 +740,7 @@
|
||||
"errorDialOutFailed": "Nie udało się wybrać numeru. Połączenie nieudane",
|
||||
"errorDialOutStatus": "Błąd podczas uzyskiwania stanu połączenia",
|
||||
"errorMissingName": "Podaj imię, aby dołączyć do spotkania",
|
||||
"errorNoPermissions": "Musisz włączyć dostęp do mikrofonu i kamery",
|
||||
"errorStatusCode": "Błąd wybierania, kod statusu: {{status}}",
|
||||
"errorValidation": "Weryfikacja numeru zakończona niepowodzeniem",
|
||||
"iWantToDialIn": "Chcę się wdzwonić",
|
||||
@@ -745,6 +798,7 @@
|
||||
"expandedPending": "Nagrywanie się rozpoczyna…",
|
||||
"failedToStart": "Nagrywanie nie jest możliwe",
|
||||
"fileSharingdescription": "Udostępnij nagranie uczestnikom spotkania",
|
||||
"inProgress": "Trwa nagrywanie lub transmisja na żywo",
|
||||
"linkGenerated": "Wygenerowano link do nagrania.",
|
||||
"live": "NA ŻYWO",
|
||||
"loggedIn": "Zalogowano jako {{userName}}",
|
||||
@@ -757,6 +811,7 @@
|
||||
"serviceDescription": "Twoje nagranie zostanie zapisane przez usługę nagrywania",
|
||||
"serviceDescriptionCloud": "Nagrywanie w chmurze",
|
||||
"serviceName": "Usługa nagrywania",
|
||||
"sessionAlreadyActive": "Ta sesja jest już nagrywana lub transmitowana na żywo.",
|
||||
"signIn": "Zaloguj się",
|
||||
"signOut": "Wyloguj się",
|
||||
"unavailable": "Ups! {{serviceName}} w tej chwili niedostępny. Próbujemy rozwiązać ten problem. Spróbuj ponownie później.",
|
||||
@@ -769,8 +824,8 @@
|
||||
"security": {
|
||||
"about": "Możesz dodać $t(lockRoomPassword) do spotkania. Uczestnicy będą musieli wprowadzić $t(lockRoomPassword) przed dołączeniem do spotkania.",
|
||||
"aboutReadOnly": "Uczestnicy posiadający uprawnienia do moderacji mogą ustawić $t(lockRoomPassword) do spotkania. Uczestnicy będą musieli wprowadzić $t(lockRoomPassword) zanim zostaną dołączeni do spotkania.",
|
||||
"insecureRoomNameWarning": "Nazwa pokoju nie jest bezpieczna. Niepowołaniu uczestnicy mogą dołączyć do spotkania. Proszę rozważyć ustawienie hasła spotkania używając przycisku Opcje zabezpieczeń.",
|
||||
"securityOptions": "Opcje zabezpieczeń"
|
||||
"header": "Opcje zabezpieczeń",
|
||||
"insecureRoomNameWarning": "Nazwa pokoju nie jest bezpieczna. Niepowołaniu uczestnicy mogą dołączyć do spotkania. Proszę rozważyć ustawienie hasła spotkania używając przycisku Opcje zabezpieczeń."
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
@@ -790,7 +845,6 @@
|
||||
"language": "Język",
|
||||
"loggedIn": "Zalogowano jako {{name}}",
|
||||
"microphones": "Mikrofony",
|
||||
"sounds": "Dźwięki",
|
||||
"moderator": "Moderacja",
|
||||
"more": "Więcej",
|
||||
"name": "Nazwa",
|
||||
@@ -803,8 +857,11 @@
|
||||
"selectAudioOutput": "Wyjście audio",
|
||||
"selectCamera": "Kamera",
|
||||
"selectMic": "Mikrofon",
|
||||
"selfView": "Własnego podgląd",
|
||||
"sounds": "Dźwięki",
|
||||
"speakers": "Głośniki",
|
||||
"startAudioMuted": "Wycisz wszystkich dołączających",
|
||||
"startReactionsMuted": "Wycisz dźwięki reakcji dla wszystkich",
|
||||
"startVideoMuted": "Ukryj wszystkich dołączających",
|
||||
"talkWhileMuted": "Jesteś wyciszony",
|
||||
"title": "Ustawienia"
|
||||
@@ -837,13 +894,20 @@
|
||||
},
|
||||
"speaker": "Mówca",
|
||||
"speakerStats": {
|
||||
"search": "Wyszukaj",
|
||||
"angry": "Zły",
|
||||
"disgusted": "Oburzony",
|
||||
"fearful": "Przerażony",
|
||||
"happy": "Szczęśliwy",
|
||||
"hours": "{{count}} godz.",
|
||||
"minutes": "{{count}} min.",
|
||||
"name": "Nazwa",
|
||||
"neutral": "Neutralny",
|
||||
"sad": "Smutny",
|
||||
"search": "Wyszukaj",
|
||||
"seconds": "{{count}} sek.",
|
||||
"speakerTime": "Czas mówcy",
|
||||
"speakerStats": "Statystyki mówców",
|
||||
"speakerTime": "Czas mówcy"
|
||||
"surprised": "Zdziwiony"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -860,6 +924,7 @@
|
||||
"audioOnly": "Przełączanie tylko audio",
|
||||
"audioRoute": "Wybierz urządzenie dźwiękowe",
|
||||
"boo": "Buczenie",
|
||||
"breakoutRoom": "Dołącz/opuść pokój podgrupy",
|
||||
"callQuality": "Zarządzanie jakością obrazu",
|
||||
"cc": "Przełączanie napisów",
|
||||
"chat": "Przełączanie okna rozmowy",
|
||||
@@ -943,7 +1008,9 @@
|
||||
"hangup": "Opuść spotkanie",
|
||||
"help": "Pomoc",
|
||||
"invite": "Zaproś uczestników",
|
||||
"joinBreakoutRoom": "Dołącz do pokoju podgrupy",
|
||||
"laugh": "Śmiech",
|
||||
"leaveBreakoutRoom": "Opuść pokój spotkań",
|
||||
"like": "Kciuk w górę",
|
||||
"lobbyButtonDisable": "Wyłącz tryb lobby",
|
||||
"lobbyButtonEnable": "Włącz tryb lobby",
|
||||
@@ -1033,7 +1100,10 @@
|
||||
"pending": "{{displayName}} został zaproszony"
|
||||
},
|
||||
"videoStatus": {
|
||||
"adjustFor": "Dostosuj do:",
|
||||
"audioOnly": "DŹW",
|
||||
"bestPerformance": "Najlepsza wydajność",
|
||||
"highestQuality": "Najwyższa jakość",
|
||||
"audioOnlyExpanded": "Jesteś w trybie słabego łącza. W tym trybie będziesz otrzymywać tylko dźwięk i udostępnianie ekranu.",
|
||||
"callQuality": "Jakość obrazu",
|
||||
"hd": "HD",
|
||||
@@ -1044,6 +1114,7 @@
|
||||
"ld": "LD",
|
||||
"ldTooltip": "Podgląd obrazu w niskiej rozdzielczości",
|
||||
"lowDefinition": "Niska rozdzielczość",
|
||||
"performanceSettings": "Ustawienia wydajności",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Podgląd obrazu w standardowej rozdzielczości",
|
||||
"standardDefinition": "Standardowa rozdzielczość"
|
||||
@@ -1056,14 +1127,15 @@
|
||||
"domuteVideoOfOthers": "Wyłącz kamerę pozostałym",
|
||||
"flip": "Odwrócenie",
|
||||
"grantModerator": "Przyznaj prawa moderatora",
|
||||
"hideSelfView": "Ukryj widok własnego podglądu",
|
||||
"kick": "Wyrzuć",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Uczestnik ma wyciszone audio",
|
||||
"muted": "Wyciszony",
|
||||
"videoMuted": "Kamera wyłączona",
|
||||
"remoteControl": "Kontrola zdalna",
|
||||
"show": "Pokaż na scenie",
|
||||
"videomute": "Uczestnik zatrzymał kamerę",
|
||||
"videoMuted": "Kamera wyłączona"
|
||||
"videomute": "Uczestnik zatrzymał kamerę"
|
||||
},
|
||||
"welcomepage": {
|
||||
"addMeetingName": "Dodaj nazwę spotkania",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"failedToAdd": "Falha ao adicionar participantes",
|
||||
"footerText": "A marcação está desactivada.",
|
||||
"googleEmail": "Email do Google",
|
||||
"inviteMoreHeader": "Você é o único na reunião",
|
||||
"inviteMoreHeader": "É o único na reunião",
|
||||
"inviteMoreMailSubject": "Participar na reunião {{appName}}",
|
||||
"inviteMorePrompt": "Convidar mais pessoas",
|
||||
"linkCopied": "Link copiado para a área de transferência",
|
||||
@@ -94,7 +94,7 @@
|
||||
"privateNotice": "Mensagem privada para {{recipient}}",
|
||||
"message": "Mensagem",
|
||||
"messageAccessibleTitle": "{{user}} disse:",
|
||||
"messageAccessibleTitleMe": "Você disse:",
|
||||
"messageAccessibleTitleMe": "Eu disse:",
|
||||
"smileysPanel": "Painel de Emojis",
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
@@ -515,7 +515,7 @@
|
||||
"busyTitle": "Todas as transmissões estão atualmente ocupadas",
|
||||
"changeSignIn": "Alternar contas.",
|
||||
"choose": "Escolha uma transmissão em direto",
|
||||
"chooseCTA": "Escolha uma opção de transmissão. Você está conectado atualmente como {{email}}.",
|
||||
"chooseCTA": "Escolha uma opção de transmissão. Está conectado atualmente como {{email}}.",
|
||||
"enterStreamKey": "Insira sua chave de transmissão em direto do YouTube aqui.",
|
||||
"error": "Falha na transmissão em direto. Tente de novo.",
|
||||
"errorAPI": "Ocorreu um erro ao acessar suas transmissões do YouTube. Por favor tente logar novamente.",
|
||||
@@ -534,7 +534,7 @@
|
||||
"pending": "Iniciando Transmissão em Direto...",
|
||||
"serviceName": "Serviço de Transmissão em Direto",
|
||||
"sessionAlreadyActive": "Esta sessão já está a ser gravada ou transmitida em direto.",
|
||||
"signedInAs": "Você está conectado como:",
|
||||
"signedInAs": "Está conectado como:",
|
||||
"signIn": "Faça login no Google",
|
||||
"signInCTA": "Faça login ou insira sua chave de transmissão em Direto do YouTube.",
|
||||
"signOut": "Sair",
|
||||
@@ -562,7 +562,7 @@
|
||||
"engaged": "Gravação local iniciada.",
|
||||
"finished": "Sessão de gravação {{token}} terminada. Por favor, envie o arquivo gravado para o moderador.",
|
||||
"finishedModerator": "Sessão de gravação {{token}} terminada. A gravação da faixa local foi salva. Por favor, peça aos outros participantes para enviar suas gravações.",
|
||||
"notModerator": "Você não é o moderador. Você não pode iniciar ou parar a gravação local."
|
||||
"notModerator": "Não é o moderador. Não pode iniciar ou parar a gravação local."
|
||||
},
|
||||
"moderator": "Moderador",
|
||||
"no": "Não",
|
||||
@@ -581,10 +581,12 @@
|
||||
"allowedUnmute": "Pode ligar o seu microfone, ligar a sua câmara ou partilhar o seu ecrã.",
|
||||
"audioUnmuteBlockedTitle": "Ligar microfone bloqueado!",
|
||||
"audioUnmuteBlockedDescription": "A operação de ligar o microfone foi temporariamente bloqueada devido aos limites do sistema.",
|
||||
"chatMessages": "Mensagens de chat",
|
||||
"connectedOneMember": "{{name}} entrou na reunião",
|
||||
"connectedThreePlusMembers": "{{name}} e muitos outros entraram na reunião",
|
||||
"connectedTwoMembers": "{{first}} e {{second}} entraram na reunião",
|
||||
"disconnected": "desconectado",
|
||||
"displayNotifications": "Mostrar notificações para",
|
||||
"focus": "Foco da conferência",
|
||||
"focusFail": "{{component}} não disponĩvel - tente em {{ms}} seg.",
|
||||
"hostAskedUnmute": "O moderador gostaria que você falasse",
|
||||
@@ -597,8 +599,8 @@
|
||||
"leftTwoMembers": "{{first}} e {{second}} deixaram a reunião",
|
||||
"me": "Eu",
|
||||
"moderator": "É agora um moderador",
|
||||
"muted": "Você iniciou uma conversa com o microfone desativado.",
|
||||
"mutedTitle": "Você está silenciado!",
|
||||
"muted": "Iniciou uma conversa com o microfone desativado.",
|
||||
"mutedTitle": "Está silenciado!",
|
||||
"mutedRemotelyTitle": "Foi silenciado pelo {{participantDisplayName}}",
|
||||
"mutedRemotelyDescription": "Pode sempre voltar a ativar o microfone quando estiver pronto para falar. Silencie de volta quando estiver pronto para manter o barulho afastado da reunião.",
|
||||
"videoMutedRemotelyTitle": "A sua câmara foi desligada pelo {{participantDisplayName}}.",
|
||||
@@ -611,7 +613,7 @@
|
||||
"screenShareNoAudioTitle": "Não foi possível partilhar o áudio do sistema!",
|
||||
"selfViewTitle": "Pode sempre reexibir a autovisualização a partir das definições",
|
||||
"somebody": "Alguém",
|
||||
"startSilentTitle": "Você entrou sem saída de áudio!",
|
||||
"startSilentTitle": "Entrou sem saída de áudio!",
|
||||
"startSilentDescription": "Volte à reunião para habilitar o áudio",
|
||||
"suboptimalBrowserWarning": "Tememos que sua experiência de reunião não seja tão boa aqui. Estamos procurando maneiras de melhorar isso, mas até então, tente usar um dos <a href='{{recommendedBrowserPageLink}}' target='_blank'>navegadores completamente suportados</a>.",
|
||||
"suboptimalExperienceTitle": "Alerta do navegador",
|
||||
@@ -652,7 +654,7 @@
|
||||
"actions": {
|
||||
"allow": "Permitir aos participantes:",
|
||||
"allowVideo": "Permitir vídeo",
|
||||
"audioModeration": "Ativarem o microfone deles",
|
||||
"audioModeration": "Ativar o microfone deles",
|
||||
"blockEveryoneMicCamera": "Bloquear o microfone e a câmara de todos",
|
||||
"invite": "Convidar alguém",
|
||||
"askUnmute": "Pedir para ligar o microfone",
|
||||
@@ -664,7 +666,7 @@
|
||||
"stopEveryonesVideo": "Desligar a câmara de todos",
|
||||
"stopVideo": "Desligar a câmara",
|
||||
"unblockEveryoneMicCamera": "Desbloquear o microfone e a câmara de todos",
|
||||
"videoModeration": "Ligarem a câmara deles",
|
||||
"videoModeration": "Ligar a câmara deles",
|
||||
"moreModerationControls": "Mais controlos de moderação"
|
||||
},
|
||||
"search": "Pesquisar participantes"
|
||||
@@ -782,6 +784,7 @@
|
||||
"title": "Perfil"
|
||||
},
|
||||
"raisedHand": "Gostaria de falar",
|
||||
"raisedHandsLabel": "Número de mãos levantadas",
|
||||
"recording": {
|
||||
"limitNotificationDescriptionWeb": "Devido à grande procura, a sua gravação será limitada a {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. Para gravações ilimitadas tente <3>{{app}}</3>.",
|
||||
@@ -857,6 +860,7 @@
|
||||
"selectAudioOutput": "Saída de áudio",
|
||||
"selectCamera": "Câmara",
|
||||
"selectMic": "Microfone",
|
||||
"selfView": "Autovisualização",
|
||||
"sounds": "Sons",
|
||||
"speakers": "Participantes",
|
||||
"startAudioMuted": "Todos começam com microfone desligado",
|
||||
@@ -1142,7 +1146,7 @@
|
||||
"join": "Toque para entrar",
|
||||
"roomname": "Digite o nome da sala"
|
||||
},
|
||||
"appDescription": "Vá em frente, converse por vídeo com toda a equipa. Na verdade, convide todos os que conhece. {{app}} é uma solução de videoconferência totalmente criptografada e 100% de código aberto que você pode usar todos os dias, a cada dia, gratuitamente — sem necessidade de conta.",
|
||||
"appDescription": "Vá em frente, converse por vídeo com toda a equipa. Na verdade, convide todos os que conhece. {{app}} é uma solução de videoconferência totalmente criptografada e 100% de código aberto que pode usar todos os dias, a cada dia, gratuitamente — sem necessidade de conta.",
|
||||
"audioVideoSwitch": {
|
||||
"audio": "Voz",
|
||||
"video": "Vídeo"
|
||||
@@ -1166,7 +1170,7 @@
|
||||
"privacy": "Política de Privacidade",
|
||||
"recentList": "Recente",
|
||||
"recentListDelete": "Remover",
|
||||
"recentListEmpty": "Sua lista recente está vazia. As reuniões que você realizar serão exibidas aqui.",
|
||||
"recentListEmpty": "A sua lista recente está atualmente vazia. Converse com a sua equipa e encontrará aqui todas as suas reuniões recentes.",
|
||||
"reducedUIText": "Bem-vindo ao {{app}}!",
|
||||
"roomNameAllowedChars": "Nome da reunião não deve conter qualquer um destes caracteres: ?. &, :, ', \", %, #.",
|
||||
"roomname": "Digite o nome da sala",
|
||||
|
||||
@@ -102,10 +102,13 @@
|
||||
},
|
||||
"connectionindicator": {
|
||||
"address": "Адрес:",
|
||||
"audio_ssrc": "Аудио SSRC:",
|
||||
"bandwidth": "Средняя скорость:",
|
||||
"bitrate": "Битрейт:",
|
||||
"bridgeCount": "Количество серверов: ",
|
||||
"codecs": "Кодеки (A/V): ",
|
||||
"connectedTo": "Подключен к:",
|
||||
"e2e_rtt": "E2E RTT:",
|
||||
"framerate": "Частота кадров:",
|
||||
"less": "Краткая информация",
|
||||
"localaddress_0": "Локальный адрес:",
|
||||
@@ -114,6 +117,7 @@
|
||||
"localport_0": "Локальный порт:",
|
||||
"localport_1": "Локальных порта:",
|
||||
"localport_2": "Локальных портов:",
|
||||
"maxEnabledResolution": "Максимальное разрешение",
|
||||
"more": "Подробная информация",
|
||||
"packetloss": "Потери пакетов:",
|
||||
"quality": {
|
||||
@@ -130,10 +134,13 @@
|
||||
"remoteport_1": "Удаленных порта:",
|
||||
"remoteport_2": "Удаленных портов:",
|
||||
"resolution": "Разрешение:",
|
||||
"savelogs": "Сохранить логи",
|
||||
"participant_id": "id участника:",
|
||||
"status": "Связь:",
|
||||
"transport_0": "Метод отправки:",
|
||||
"transport_1": "Метода отправки:",
|
||||
"transport_2": "Методов отправки:"
|
||||
"transport_2": "Методов отправки:",
|
||||
"video_ssrc": "Видео SSRC:"
|
||||
},
|
||||
"dateUtils": {
|
||||
"earlier": "Ранее",
|
||||
@@ -559,6 +566,30 @@
|
||||
"suboptimalExperienceTitle": "Предупреждение браузера",
|
||||
"unmute": "Включить микрофон"
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "Закрыть",
|
||||
"header": "Участники",
|
||||
"headings": {
|
||||
"lobby": "Лобби ({{count}})",
|
||||
"participantsList": "Список участников ({{count}})",
|
||||
"waitingLobby": "Ожидают в лобби ({{count}})"
|
||||
},
|
||||
"actions": {
|
||||
"allow": "Разрешить",
|
||||
"allowVideo": "Разрешить видео",
|
||||
"audioModeration": "Разрешить выключить микрофон",
|
||||
"blockEveryoneMicCamera": "Заблокировать у всех микрофон и камеру",
|
||||
"invite": "Пригласить",
|
||||
"askUnmute": "Попросить разрешение включить микрофон",
|
||||
"mute": "Выключить звук",
|
||||
"muteAll": "Выключить звук у всех",
|
||||
"muteEveryoneElse": "Выключить микрофон у остальных",
|
||||
"stopEveryonesVideo": "Выключить у всех камеру",
|
||||
"stopVideo": "Остановить видео",
|
||||
"unblockEveryoneMicCamera": "Разблокировать у всех микрофон и камеру",
|
||||
"videoModeration": "Разрешить видео"
|
||||
}
|
||||
},
|
||||
"passwordDigitsOnly": "До {{number}} цифр",
|
||||
"passwordSetRemotely": "установлен другим участником",
|
||||
"poweredby": "работает на",
|
||||
@@ -744,7 +775,7 @@
|
||||
"hangup": "Завершить звонок",
|
||||
"help": "Справка",
|
||||
"invite": "Пригласить",
|
||||
"kick": "Выкинуть участника",
|
||||
"kick": "Отключить участника",
|
||||
"lobbyButton": "Вкл/Выкл режим лобби",
|
||||
"localRecording": "Вкл/Выкл кнопки записи",
|
||||
"lockRoom": "Установить пароль",
|
||||
@@ -774,13 +805,18 @@
|
||||
"videomute": "Вкл/Выкл видео"
|
||||
},
|
||||
"addPeople": "Добавить людей к вашему сеансу связи",
|
||||
"audioSettings": "Настройка звука",
|
||||
"videoSettings": "Настройка видео",
|
||||
"audioOnlyOff": "Отключить режим экономии пропускной способности",
|
||||
"audioOnlyOn": "Включить режим экономии пропускной способности",
|
||||
"audioRoute": "Выбрать аудиоустройство",
|
||||
"authenticate": "Аутентифицировать",
|
||||
"boo": "Освистывать",
|
||||
"callQuality": "Качество связи",
|
||||
"chat": "Чат",
|
||||
"clap": "Аплодисменты",
|
||||
"closeChat": "Закрыть чат",
|
||||
"closeReactionsMenu": "Закрыть меню реакций",
|
||||
"documentClose": "Закрыть общий документ",
|
||||
"documentOpen": "Открыть общий документ",
|
||||
"download": "Скачать приложение",
|
||||
@@ -794,6 +830,8 @@
|
||||
"hangup": "Выход",
|
||||
"help": "Справка",
|
||||
"invite": "Пригласить",
|
||||
"laugh": "Смеяться",
|
||||
"like": "Мне нравится",
|
||||
"lobbyButtonDisable": "Отключить режим лобби",
|
||||
"lobbyButtonEnable": "Включить режим лобби",
|
||||
"login": "Войти",
|
||||
@@ -812,6 +850,7 @@
|
||||
"noisyAudioInputTitle": "Похоже, ваш микрофон создает шум!",
|
||||
"noisyAudioInputDesc": "Возможно, ваш микрофон создает шум. Вы можете выключить его или смените устройство.",
|
||||
"openChat": "Открыть чат",
|
||||
"openReactionsMenu": "Открыть меню реакций",
|
||||
"participants": "Участники",
|
||||
"pip": "Вкл режим Картинка-в-картинке",
|
||||
"privateMessage": "Отправить личное сообщение",
|
||||
@@ -819,9 +858,12 @@
|
||||
"raiseHand": "Хочу говорить",
|
||||
"raiseYourHand": "Поднять руку",
|
||||
"security": "Настройки безопасности",
|
||||
"selectBackground": "Выбрать фоновое изображение",
|
||||
"shareRoom": "Отправить приглашение",
|
||||
"shareaudio": "Предоставить доступ к звуку",
|
||||
"sharedvideo": "Видео YouTube",
|
||||
"shortcuts": "Комбинации клавиш",
|
||||
"silence": "Молчание",
|
||||
"speakerStats": "Статистика",
|
||||
"startScreenSharing": "Начать трансляцию с экрана",
|
||||
"startSubtitles": "Включить субтитры",
|
||||
@@ -830,6 +872,7 @@
|
||||
"stopSharedVideo": "Остановить видео на YouTube",
|
||||
"stopSubtitles": "Отключить субтитры",
|
||||
"stopvideoblur": "Отключить размытие фона",
|
||||
"surprised": "Удивиться",
|
||||
"talkWhileMutedPopup": "Пытаетесь говорить? У вас отключен звук.",
|
||||
"tileViewToggle": "Вкл/выкл плитку",
|
||||
"toggleCamera": "Переключить камеру",
|
||||
@@ -887,17 +930,21 @@
|
||||
"standardDefinition": "Стандартное качество (SD)"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "Информация о соединении",
|
||||
"domute": "Выключить звук",
|
||||
"domuteOthers": "Выключить остальных",
|
||||
"domuteVideo": "Выключить видео",
|
||||
"domuteOthers": "Выключить звук остальным",
|
||||
"domuteVideoOfOthers": "Выключить видео остальным",
|
||||
"flip": "Отразить",
|
||||
"grantModerator": "Сделать модератором",
|
||||
"kick": "Выкинуть",
|
||||
"kick": "Отключить",
|
||||
"moderator": "Модератор",
|
||||
"mute": "Без звука",
|
||||
"muted": "Звук выключен",
|
||||
"videoMuted": "Камера выключена",
|
||||
"remoteControl": "Начать / Остановить дистанционный контроль",
|
||||
"show": "Показать крупным планом",
|
||||
"videomute": "Участник отключил камеру"
|
||||
"videomute": "Участник выключил камеру"
|
||||
},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
@@ -934,4 +981,4 @@
|
||||
"terms": "Условия",
|
||||
"title": "Защищенная, полнофункциональная и совершенно бесплатная система видеоконференций"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,6 +784,7 @@
|
||||
"title": "Profile"
|
||||
},
|
||||
"raisedHand": "Would like to speak",
|
||||
"raisedHandsLabel": "Number of raised hands",
|
||||
"recording": {
|
||||
"limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
"limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}</3>.",
|
||||
|
||||
@@ -1521,12 +1521,14 @@ class API {
|
||||
* Notify external application ( if API is enabled) that a toolbar button was clicked.
|
||||
*
|
||||
* @param {string} key - The key of the toolbar button.
|
||||
* @param {boolean} preventExecution - Whether execution of the button click was prevented or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyToolbarButtonClicked(key: string) {
|
||||
notifyToolbarButtonClicked(key: string, preventExecution: boolean) {
|
||||
this._sendEvent({
|
||||
name: 'toolbar-button-clicked',
|
||||
key
|
||||
key,
|
||||
preventExecution
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -66,7 +66,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#63c3e1f869c69c6a17d7dc881570a1731478ac8c",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#55a03ac1b52f85dcbd9bfe339690ad88436ac029",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@@ -12519,8 +12519,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#63c3e1f869c69c6a17d7dc881570a1731478ac8c",
|
||||
"integrity": "sha512-EqkKpdudZ4ZkRPpUbqXeX0pF5s0kh2UJ6iT6qLyYYWCFlOiE27cTbKb5E85oj0Zzj9NnDGTgYHhCLn2q6y7JrA==",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#55a03ac1b52f85dcbd9bfe339690ad88436ac029",
|
||||
"integrity": "sha512-0ZNhG4ZPzcH+2R7K5xa5tSNVK8CKrKVCGP/bjr07XtiV3pcY65OWI2mH+QzlMIMDOXqgqQtry9RHv4vmzy5pIg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -29993,9 +29993,9 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#63c3e1f869c69c6a17d7dc881570a1731478ac8c",
|
||||
"integrity": "sha512-EqkKpdudZ4ZkRPpUbqXeX0pF5s0kh2UJ6iT6qLyYYWCFlOiE27cTbKb5E85oj0Zzj9NnDGTgYHhCLn2q6y7JrA==",
|
||||
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#63c3e1f869c69c6a17d7dc881570a1731478ac8c",
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#55a03ac1b52f85dcbd9bfe339690ad88436ac029",
|
||||
"integrity": "sha512-0ZNhG4ZPzcH+2R7K5xa5tSNVK8CKrKVCGP/bjr07XtiV3pcY65OWI2mH+QzlMIMDOXqgqQtry9RHv4vmzy5pIg==",
|
||||
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#55a03ac1b52f85dcbd9bfe339690ad88436ac029",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#63c3e1f869c69c6a17d7dc881570a1731478ac8c",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#55a03ac1b52f85dcbd9bfe339690ad88436ac029",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
|
||||
@@ -23,6 +23,5 @@ export default class HangupButton extends AbstractHangupButton<Props, *> {
|
||||
*/
|
||||
_doHangup() {
|
||||
api.executeCommand('hangup');
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ export type Props = {
|
||||
*/
|
||||
onAvatarLoadError?: Function,
|
||||
|
||||
/**
|
||||
* Additional parameters to be passed to onAvatarLoadError function.
|
||||
*/
|
||||
onAvatarLoadErrorParams?: Object,
|
||||
|
||||
/**
|
||||
* Expected size of the avatar.
|
||||
*/
|
||||
|
||||
@@ -4,12 +4,17 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { getParticipantById } from '../../participants';
|
||||
import { connect } from '../../redux';
|
||||
import { getAvatarColor, getInitials } from '../functions';
|
||||
import { getAvatarColor, getInitials, isCORSAvatarURL } from '../functions';
|
||||
|
||||
import { StatelessAvatar } from '.';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The URL patterns for URLs that needs to be handled with CORS.
|
||||
*/
|
||||
_corsAvatarURLs: Array<string>,
|
||||
|
||||
/**
|
||||
* Custom avatar backgrounds from branding.
|
||||
*/
|
||||
@@ -25,6 +30,11 @@ export type Props = {
|
||||
*/
|
||||
_loadableAvatarUrl: ?string,
|
||||
|
||||
/**
|
||||
* Indicates whether _loadableAvatarUrl should use CORS or not.
|
||||
*/
|
||||
_loadableAvatarUrlUseCORS: ?boolean,
|
||||
|
||||
/**
|
||||
* A prop to maintain compatibility with web.
|
||||
*/
|
||||
@@ -76,10 +86,16 @@ export type Props = {
|
||||
* URL of the avatar, if any.
|
||||
*/
|
||||
url: ?string,
|
||||
|
||||
/**
|
||||
* Indicates whether to load the avatar using CORS or not.
|
||||
*/
|
||||
useCORS?: ?boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
avatarFailed: boolean
|
||||
avatarFailed: boolean,
|
||||
isUsingCORS: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_SIZE = 65;
|
||||
@@ -105,8 +121,15 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
const {
|
||||
_corsAvatarURLs,
|
||||
url,
|
||||
useCORS
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
avatarFailed: false
|
||||
avatarFailed: false,
|
||||
isUsingCORS: Boolean(useCORS) || Boolean(url && isCORSAvatarURL(url, _corsAvatarURLs))
|
||||
};
|
||||
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
@@ -118,7 +141,9 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps: P) {
|
||||
if (prevProps.url !== this.props.url) {
|
||||
const { _corsAvatarURLs, url } = this.props;
|
||||
|
||||
if (prevProps.url !== url) {
|
||||
|
||||
// URI changed, so we need to try to fetch it again.
|
||||
// Eslint doesn't like this statement, but based on the React doc, it's safe if it's
|
||||
@@ -126,7 +151,8 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
avatarFailed: false
|
||||
avatarFailed: false,
|
||||
isUsingCORS: Boolean(this.props.useCORS) || Boolean(url && isCORSAvatarURL(url, _corsAvatarURLs))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -141,6 +167,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
_customAvatarBackgrounds,
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl,
|
||||
_loadableAvatarUrlUseCORS,
|
||||
className,
|
||||
colorBase,
|
||||
dynamicColor,
|
||||
@@ -150,7 +177,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
testId,
|
||||
url
|
||||
} = this.props;
|
||||
const { avatarFailed } = this.state;
|
||||
const { avatarFailed, isUsingCORS } = this.state;
|
||||
|
||||
const avatarProps = {
|
||||
className,
|
||||
@@ -158,19 +185,26 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
id,
|
||||
initials: undefined,
|
||||
onAvatarLoadError: undefined,
|
||||
onAvatarLoadErrorParams: undefined,
|
||||
size,
|
||||
status,
|
||||
testId,
|
||||
url: undefined
|
||||
url: undefined,
|
||||
useCORS: isUsingCORS
|
||||
};
|
||||
|
||||
// _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so
|
||||
// we still need to do a check for that. And an explicitly provided URI is higher priority than
|
||||
// an avatar URL anyhow.
|
||||
const effectiveURL = (!avatarFailed && url) || _loadableAvatarUrl;
|
||||
const useReduxLoadableAvatarURL = avatarFailed || !url;
|
||||
const effectiveURL = useReduxLoadableAvatarURL ? _loadableAvatarUrl : url;
|
||||
|
||||
if (effectiveURL) {
|
||||
avatarProps.onAvatarLoadError = this._onAvatarLoadError;
|
||||
if (useReduxLoadableAvatarURL) {
|
||||
avatarProps.onAvatarLoadErrorParams = { dontRetry: true };
|
||||
avatarProps.useCORS = _loadableAvatarUrlUseCORS;
|
||||
}
|
||||
avatarProps.url = effectiveURL;
|
||||
}
|
||||
|
||||
@@ -195,12 +229,24 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||
/**
|
||||
* Callback to handle the error while loading of the avatar URI.
|
||||
*
|
||||
* @param {Object} params - An object with parameters.
|
||||
* @param {boolean} params.dontRetry - If false we will retry to load the Avatar with different CORS mode.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError() {
|
||||
this.setState({
|
||||
avatarFailed: true
|
||||
});
|
||||
_onAvatarLoadError(params = {}) {
|
||||
const { dontRetry = false } = params;
|
||||
|
||||
if (Boolean(this.props.useCORS) === this.state.isUsingCORS && !dontRetry) {
|
||||
// try different mode of loading the avatar.
|
||||
this.setState({
|
||||
isUsingCORS: !this.state.isUsingCORS
|
||||
});
|
||||
} else {
|
||||
// we already have tried loading the avatar with and without CORS and it failed.
|
||||
this.setState({
|
||||
avatarFailed: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,11 +261,14 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { colorBase, displayName, participantId } = ownProps;
|
||||
const _participant: ?Object = participantId && getParticipantById(state, participantId);
|
||||
const _initialsBase = _participant?.name ?? displayName;
|
||||
const { corsAvatarURLs } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_customAvatarBackgrounds: state['features/dynamic-branding'].avatarBackgrounds,
|
||||
_corsAvatarURLs: corsAvatarURLs,
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl: _participant?.loadableAvatarUrl,
|
||||
_loadableAvatarUrlUseCORS: _participant?.loadableAvatarUrlUseCORS,
|
||||
colorBase
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,18 @@ type Props = AbstractProps & {
|
||||
* props.
|
||||
*/
|
||||
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
@@ -164,4 +176,22 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
style = { styles.avatarContent(size) } />
|
||||
);
|
||||
}
|
||||
|
||||
_onAvatarLoadError: () => void;
|
||||
|
||||
/**
|
||||
* Handles avatar load errors.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError() {
|
||||
const { onAvatarLoadError, onAvatarLoadErrorParams = {} } = this.props;
|
||||
|
||||
if (onAvatarLoadError) {
|
||||
onAvatarLoadError({
|
||||
...onAvatarLoadErrorParams,
|
||||
dontRetry: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '../../../icons';
|
||||
import { isGravatarURL } from '../../functions';
|
||||
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
@@ -31,7 +30,12 @@ type Props = AbstractProps & {
|
||||
/**
|
||||
* TestId of the element, if any.
|
||||
*/
|
||||
testId?: string
|
||||
testId?: string,
|
||||
|
||||
/**
|
||||
* Indicates whether to load the avatar using CORS or not.
|
||||
*/
|
||||
useCORS?: ?boolean
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -39,13 +43,25 @@ type Props = AbstractProps & {
|
||||
* props.
|
||||
*/
|
||||
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { initials, url } = this.props;
|
||||
const { initials, url, useCORS } = this.props;
|
||||
|
||||
if (this._isIcon(url)) {
|
||||
return (
|
||||
@@ -67,10 +83,10 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
<img
|
||||
alt = 'avatar'
|
||||
className = { this._getAvatarClassName() }
|
||||
crossOrigin = { isGravatarURL(url) ? '' : undefined }
|
||||
crossOrigin = { useCORS ? '' : undefined }
|
||||
data-testid = { this.props.testId }
|
||||
id = { this.props.id }
|
||||
onError = { this.props.onAvatarLoadError }
|
||||
onError = { this._onAvatarLoadError }
|
||||
src = { url }
|
||||
style = { this._getAvatarStyle() } />
|
||||
</div>
|
||||
@@ -160,4 +176,19 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||
}
|
||||
|
||||
_isIcon: (?string | ?Object) => boolean;
|
||||
|
||||
_onAvatarLoadError: () => void;
|
||||
|
||||
/**
|
||||
* Handles avatar load errors.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError() {
|
||||
const { onAvatarLoadError, onAvatarLoadErrorParams } = this.props;
|
||||
|
||||
if (typeof onAvatarLoadError === 'function') {
|
||||
onAvatarLoadError(onAvatarLoadErrorParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import GraphemeSplitter from 'grapheme-splitter';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { GRAVATAR_BASE_URL } from './constants';
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'#6A50D3',
|
||||
'#FF9B42',
|
||||
@@ -74,11 +72,12 @@ export function getInitials(s: ?string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the passed URL is pointing to the gravatar service.
|
||||
* Checks if the passed URL should be loaded with CORS.
|
||||
*
|
||||
* @param {string} url - The URL.
|
||||
* @param {Array<string>} corsURLs - The URL pattern that matches a URL that needs to be handled with CORS.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function isGravatarURL(url: string = '') {
|
||||
return url.startsWith(GRAVATAR_BASE_URL);
|
||||
export function isCORSAvatarURL(url: string | any = '', corsURLs: Array<string> = []) {
|
||||
return corsURLs.some(pattern => url.startsWith(pattern));
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const styles = theme => {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 292,
|
||||
marginRight: 16,
|
||||
marginRight: theme.spacing(3),
|
||||
|
||||
'&.selected': {
|
||||
fontWeight: 600
|
||||
@@ -186,8 +186,8 @@ function CopyButton({ classes, className, displayedText, textToCopy, textOnHover
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className = { `${classes.copyButton}-content` }>
|
||||
{ isHovered ? textOnHover : displayedText }
|
||||
<div className = { clsx(classes.content) }>
|
||||
<span> { isHovered ? textOnHover : displayedText } </span>
|
||||
</div>
|
||||
<Icon src = { IconCopy } />
|
||||
</>
|
||||
|
||||
@@ -162,6 +162,7 @@ export default [
|
||||
'googleApiApplicationClientID',
|
||||
'hiddenPremeetingButtons',
|
||||
'hideConferenceSubject',
|
||||
'hideDisplayName',
|
||||
'hideRecordingLabel',
|
||||
'hideParticipantsStats',
|
||||
'hideConferenceTimer',
|
||||
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
/**
|
||||
* Color of the icon (if not provided by the style object).
|
||||
*/
|
||||
color?: string,
|
||||
color?: ?string,
|
||||
|
||||
/**
|
||||
* Id prop (mainly for autotests).
|
||||
@@ -35,7 +35,7 @@ type Props = {
|
||||
/**
|
||||
* The size of the icon (if not provided by the style object).
|
||||
*/
|
||||
size?: number | string,
|
||||
size?: ?number | string,
|
||||
|
||||
/**
|
||||
* The preloaded icon component to render.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9167 3.20834L11.9167 4.83334L11.9167 11L11.9167 11.9167C11.9167 12.4229 12.3271 12.8333 12.8333 12.8333C13.3396 12.8333 13.75 12.4229 13.75 11.9167V11V4.83334C13.75 4.73632 13.9183 4.58334 14.2083 4.58334C14.4984 4.58334 14.6667 4.73632 14.6667 4.83334L14.6667 6.875L14.6667 11.9167C14.6667 12.4229 15.0771 12.8333 15.5833 12.8333C16.0896 12.8333 16.5 12.4229 16.5 11.9167L16.5 6.875C16.5 6.62187 16.7052 6.41667 16.9583 6.41667C17.2115 6.41667 17.4167 6.62187 17.4167 6.875V13.788C17.1671 16.3395 14.9919 18.3333 12.375 18.3333C10.6569 18.3333 9.09297 17.4675 8.17032 16.0753L8.16703 16.0762L4.67206 10.8208C4.50437 10.5805 4.55476 10.2948 4.76211 10.1496C4.96947 10.0044 5.25526 10.0548 5.40045 10.2621L6.57295 12.015L6.57177 12.0158C6.85487 12.4355 7.42461 12.5463 7.84432 12.2632C8.09767 12.0923 8.23844 11.8169 8.24797 11.5332L8.25 11.5338V5.04167C8.25 4.78854 8.4552 4.58334 8.70833 4.58334C8.96124 4.58334 9.1663 4.78818 9.16667 5.041L9.16667 5.04167L9.16667 11L9.16667 11.9167C9.16667 12.4229 9.57707 12.8333 10.0833 12.8333C10.5896 12.8333 11 12.4229 11 11.9167V11V5.04167V3.20834C11 2.95521 11.2052 2.75001 11.4583 2.75001C11.7115 2.75001 11.9167 2.95521 11.9167 3.20834ZM19.2337 13.7686L19.25 13.7605V13.2917V6.875C19.25 5.60935 18.224 4.58334 16.9583 4.58334C16.7976 4.58334 16.6408 4.59988 16.4894 4.63136C16.3776 3.57554 15.399 2.75001 14.2083 2.75001C14.0384 2.75001 13.8729 2.76681 13.7135 2.7987C13.5204 1.72851 12.5842 0.916672 11.4583 0.916672C10.3309 0.916672 9.39356 1.73088 9.20233 2.80339C9.04323 2.76843 8.87793 2.75001 8.70833 2.75001C7.44268 2.75001 6.41667 3.77602 6.41667 5.04167V8.70352C5.64445 8.11321 4.54919 8.06056 3.71056 8.64778C2.67379 9.37373 2.42183 10.8027 3.14778 11.8395L6.44893 16.7791C7.64459 18.8065 9.85095 20.1667 12.375 20.1667C16.0117 20.1667 18.9888 17.3431 19.2337 13.7686Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -34,6 +34,11 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
id: string,
|
||||
|
||||
/**
|
||||
* Color for the icon.
|
||||
*/
|
||||
iconColor?: string,
|
||||
|
||||
/**
|
||||
* Click handler if any.
|
||||
*/
|
||||
@@ -103,6 +108,7 @@ class Label extends AbstractLabel<Props, *> {
|
||||
className,
|
||||
color,
|
||||
icon,
|
||||
iconColor,
|
||||
id,
|
||||
onClick,
|
||||
text
|
||||
@@ -120,6 +126,7 @@ class Label extends AbstractLabel<Props, *> {
|
||||
id = { id }
|
||||
onClick = { onClick }>
|
||||
{ icon && <Icon
|
||||
color = { iconColor }
|
||||
size = '16'
|
||||
src = { icon } /> }
|
||||
{ text && <span className = { icon && classes.withIcon }>{text}</span> }
|
||||
|
||||
@@ -540,20 +540,23 @@ export function pinParticipant(id) {
|
||||
*
|
||||
* @param {string} participantId - The ID of the participant.
|
||||
* @param {string} url - The new URL.
|
||||
* @param {boolean} useCORS - Indicates whether we need to use CORS for this URL.
|
||||
* @returns {{
|
||||
* type: SET_LOADABLE_AVATAR_URL,
|
||||
* participant: {
|
||||
* id: string,
|
||||
* loadableAvatarUrl: string
|
||||
* loadableAvatarUrl: string,
|
||||
* loadableAvatarUrlUseCORS: boolean
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function setLoadableAvatarUrl(participantId, url) {
|
||||
export function setLoadableAvatarUrl(participantId, url, useCORS) {
|
||||
return {
|
||||
type: SET_LOADABLE_AVATAR_URL,
|
||||
participant: {
|
||||
id: participantId,
|
||||
loadableAvatarUrl: url
|
||||
loadableAvatarUrl: url,
|
||||
loadableAvatarUrlUseCORS: useCORS
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
||||
import type { Store } from 'redux';
|
||||
|
||||
import { GRAVATAR_BASE_URL } from '../avatar';
|
||||
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
|
||||
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
|
||||
import { toState } from '../redux';
|
||||
@@ -56,7 +56,7 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any,
|
||||
const deferred = createDeferred();
|
||||
const fullPromise = deferred.promise
|
||||
.then(() => _getFirstLoadableAvatarUrl(participant, store))
|
||||
.then(src => {
|
||||
.then(result => {
|
||||
|
||||
if (AVATAR_QUEUE.length) {
|
||||
const next = AVATAR_QUEUE.shift();
|
||||
@@ -64,7 +64,7 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any,
|
||||
next.resolve();
|
||||
}
|
||||
|
||||
return src;
|
||||
return result;
|
||||
});
|
||||
|
||||
if (AVATAR_QUEUE.length) {
|
||||
@@ -432,18 +432,33 @@ async function _getFirstLoadableAvatarUrl(participant, store) {
|
||||
|
||||
if (url !== null) {
|
||||
if (AVATAR_CHECKED_URLS.has(url)) {
|
||||
if (AVATAR_CHECKED_URLS.get(url)) {
|
||||
return url;
|
||||
const { isLoadable, isUsingCORS } = AVATAR_CHECKED_URLS.get(url) || {};
|
||||
|
||||
if (isLoadable) {
|
||||
return {
|
||||
isUsingCORS,
|
||||
src: url
|
||||
};
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const finalUrl = await preloadImage(url);
|
||||
const { corsAvatarURLs } = store.getState()['features/base/config'];
|
||||
const { isUsingCORS, src } = await preloadImage(url, isCORSAvatarURL(url, corsAvatarURLs));
|
||||
|
||||
AVATAR_CHECKED_URLS.set(finalUrl, true);
|
||||
AVATAR_CHECKED_URLS.set(src, {
|
||||
isLoadable: true,
|
||||
isUsingCORS
|
||||
});
|
||||
|
||||
return finalUrl;
|
||||
return {
|
||||
isUsingCORS,
|
||||
src
|
||||
};
|
||||
} catch (e) {
|
||||
AVATAR_CHECKED_URLS.set(url, false);
|
||||
AVATAR_CHECKED_URLS.set(url, {
|
||||
isLoadable: false,
|
||||
isUsingCORS: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +289,12 @@ StateListenerRegistry.register(
|
||||
})),
|
||||
'raisedHand': (participant, value) =>
|
||||
_raiseHandUpdated(store, conference, participant.getId(), value),
|
||||
'region': (participant, value) =>
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
region: value
|
||||
})),
|
||||
'remoteControlSessionStatus': (participant, value) =>
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
@@ -488,8 +494,8 @@ function _participantJoinedOrUpdated(store, next, action) {
|
||||
const updatedParticipant = getParticipantById(getState(), participantId);
|
||||
|
||||
getFirstLoadableAvatarUrl(updatedParticipant, store)
|
||||
.then(url => {
|
||||
dispatch(setLoadableAvatarUrl(participantId, url));
|
||||
.then(urlData => {
|
||||
dispatch(setLoadableAvatarUrl(participantId, urlData?.src, urlData?.isUsingCORS));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,17 @@ import { isIconUrl } from './functions';
|
||||
* @param {string | Object} src - Source of the avatar.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function preloadImage(src: string | Object): Promise<string> {
|
||||
export function preloadImage(src: string | Object): Promise<Object> {
|
||||
if (isIconUrl(src)) {
|
||||
return Promise.resolve(src);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
Image.prefetch(src).then(() => resolve(src), reject);
|
||||
Image.prefetch(src).then(
|
||||
() => resolve({
|
||||
src,
|
||||
isUsingCORS: false
|
||||
}),
|
||||
reject);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,29 +1,45 @@
|
||||
|
||||
// @flow
|
||||
|
||||
import { isGravatarURL } from '../avatar';
|
||||
|
||||
import { isIconUrl } from './functions';
|
||||
|
||||
/**
|
||||
* Tries to preload an image.
|
||||
*
|
||||
* @param {string | Object} src - Source of the avatar.
|
||||
* @param {boolean} useCORS - Whether to use CORS or not.
|
||||
* @param {boolean} tryOnce - If true we try to load the image only using the specified CORS mode. Otherwise both modes
|
||||
* (CORS and no CORS) will be used to load the image if the first atempt fails.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function preloadImage(src: string | Object): Promise<string> {
|
||||
export function preloadImage(
|
||||
src: string | Object,
|
||||
useCORS: ?boolean = false,
|
||||
tryOnce: ?boolean = false
|
||||
): Promise<Object> {
|
||||
if (isIconUrl(src)) {
|
||||
return Promise.resolve(src);
|
||||
return Promise.resolve({ src });
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
if (isGravatarURL(src)) {
|
||||
if (useCORS) {
|
||||
image.setAttribute('crossOrigin', '');
|
||||
}
|
||||
image.onload = () => resolve(src);
|
||||
image.onerror = reject;
|
||||
image.onload = () => resolve({
|
||||
src,
|
||||
isUsingCORS: useCORS
|
||||
});
|
||||
image.onerror = error => {
|
||||
if (tryOnce) {
|
||||
reject(error);
|
||||
} else {
|
||||
preloadImage(src, !useCORS, true)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
};
|
||||
|
||||
// $FlowExpectedError
|
||||
image.referrerPolicy = 'no-referrer';
|
||||
|
||||
@@ -26,6 +26,11 @@ type Props = {
|
||||
*/
|
||||
icon: string,
|
||||
|
||||
/**
|
||||
* Size of icon.
|
||||
*/
|
||||
iconSize: ?number,
|
||||
|
||||
/**
|
||||
* Additional style to be applied to the icon element.
|
||||
*/
|
||||
@@ -43,7 +48,7 @@ export default class BaseIndicator extends Component<Props> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { highlight, icon, iconStyle, backgroundColor } = this.props;
|
||||
const { highlight, icon, iconStyle, backgroundColor, iconSize } = this.props;
|
||||
const highlightedIndicator = { ...styles.highlightedIndicator };
|
||||
|
||||
if (backgroundColor) {
|
||||
@@ -55,6 +60,7 @@ export default class BaseIndicator extends Component<Props> {
|
||||
style = { [ BASE_INDICATOR,
|
||||
highlight ? highlightedIndicator : null ] }>
|
||||
<Icon
|
||||
size = { iconSize }
|
||||
src = { icon }
|
||||
style = { [
|
||||
styles.indicator,
|
||||
|
||||
@@ -8,8 +8,8 @@ export default {
|
||||
*/
|
||||
highlightedIndicator: {
|
||||
backgroundColor: ColorPalette.blue,
|
||||
borderRadius: 16,
|
||||
padding: 4
|
||||
borderRadius: 4,
|
||||
padding: 2
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,11 @@ type Props = {
|
||||
*/
|
||||
iconClassName: string,
|
||||
|
||||
/**
|
||||
* The color of the icon.
|
||||
*/
|
||||
iconColor: ?string,
|
||||
|
||||
/**
|
||||
* Id of the icon to be rendered.
|
||||
*/
|
||||
@@ -81,6 +86,7 @@ const BaseIndicator = ({
|
||||
className = '',
|
||||
icon,
|
||||
iconClassName,
|
||||
iconColor,
|
||||
iconId,
|
||||
iconSize,
|
||||
id = '',
|
||||
@@ -105,6 +111,7 @@ const BaseIndicator = ({
|
||||
id = { id }>
|
||||
<Icon
|
||||
className = { iconClassName }
|
||||
color = { iconColor }
|
||||
id = { iconId }
|
||||
src = { icon }
|
||||
style = { style } />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { PREJOIN_INITIALIZED } from '../../prejoin/actionTypes';
|
||||
import { setPrejoinPageVisibility } from '../../prejoin/actions';
|
||||
import { APP_WILL_MOUNT } from '../app';
|
||||
import { setAudioOnly } from '../audio-only';
|
||||
import { SET_LOCATION_URL } from '../connection/actionTypes'; // minimize imports to avoid circular imports
|
||||
@@ -29,6 +30,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
_initializeCallIntegration(store);
|
||||
_initializeShowPrejoin(store);
|
||||
break;
|
||||
case PREJOIN_INITIALIZED: {
|
||||
_maybeUpdateDisplayName(store);
|
||||
@@ -48,6 +50,21 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Overwrites the showPrejoin flag based on cached used selection for showing prejoin screen.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _initializeShowPrejoin({ dispatch, getState }) {
|
||||
const { userSelectedSkipPrejoin } = getState()['features/base/settings'];
|
||||
|
||||
if (userSelectedSkipPrejoin) {
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the audio device handler based on the `disableCallIntegration` setting.
|
||||
*
|
||||
|
||||
@@ -23,14 +23,6 @@ export default class AbstractAudioMuteButton<P: Props, S: *>
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._setAudioMuted(!this._isAudioMuted());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { combineStyles } from '../../styles';
|
||||
|
||||
import type { Styles } from './AbstractToolboxItem';
|
||||
@@ -14,6 +15,11 @@ export type Props = {
|
||||
*/
|
||||
afterClick: ?Function,
|
||||
|
||||
/**
|
||||
* The button's key.
|
||||
*/
|
||||
buttonKey?: string,
|
||||
|
||||
/**
|
||||
* Extra styles which will be applied in conjunction with `styles` or
|
||||
* `toggledStyles` when the button is disabled;.
|
||||
@@ -25,6 +31,12 @@ export type Props = {
|
||||
*/
|
||||
handleClick?: Function,
|
||||
|
||||
/**
|
||||
* Notify mode for `toolbarButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string,
|
||||
|
||||
/**
|
||||
* Whether to show the label or not.
|
||||
*/
|
||||
@@ -51,6 +63,8 @@ export type Props = {
|
||||
visible: boolean
|
||||
};
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Default style for disabled buttons.
|
||||
*/
|
||||
@@ -134,6 +148,17 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to be implemented by subclasses, which should be used
|
||||
* to handle a key being down.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyDown() {
|
||||
// To be implemented by subclass.
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to be implemented by subclasses, which should be used
|
||||
* to handle the button being clicked / pressed.
|
||||
@@ -248,17 +273,29 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
|
||||
_onClick: (*) => void;
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and toggles the audio mute state
|
||||
* accordingly.
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @param {Object} e - Event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick(e) {
|
||||
const { afterClick } = this.props;
|
||||
const { afterClick, handleClick, notifyMode, buttonKey } = this.props;
|
||||
|
||||
if (typeof APP !== 'undefined' && notifyMode) {
|
||||
APP.API.notifyToolbarButtonClicked(
|
||||
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}
|
||||
|
||||
if (notifyMode !== NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
}
|
||||
|
||||
this._handleClick();
|
||||
}
|
||||
|
||||
this._handleClick();
|
||||
afterClick && afterClick(e);
|
||||
|
||||
// blur after click to release focus from button to allow PTT.
|
||||
@@ -288,6 +325,7 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
|
||||
<ToolboxItem
|
||||
disabled = { this._isDisabled() }
|
||||
onClick = { this._onClick }
|
||||
onKeyDown = { this._onKeyDown }
|
||||
{ ...props } />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,14 +22,6 @@ export default class AbstractVideoMuteButton<P : Props, S : *>
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._setVideoMuted(!this._isVideoMuted());
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,15 @@ import { Icon } from '../../icons';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
|
||||
import AbstractToolboxItem from './AbstractToolboxItem';
|
||||
import type { Props } from './AbstractToolboxItem';
|
||||
import type { Props as AbstractToolboxItemProps } from './AbstractToolboxItem';
|
||||
|
||||
type Props = AbstractToolboxItemProps & {
|
||||
|
||||
/**
|
||||
* On key down handler.
|
||||
*/
|
||||
onKeyDown: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Web implementation of {@code AbstractToolboxItem}.
|
||||
@@ -53,6 +61,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||
disabled,
|
||||
elementAfter,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
showLabel,
|
||||
tooltipPosition,
|
||||
toggled
|
||||
@@ -64,6 +73,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||
'aria-label': this.accessibilityLabel,
|
||||
className: className + (disabled ? ' disabled' : ''),
|
||||
onClick: disabled ? undefined : onClick,
|
||||
onKeyDown: disabled ? undefined : onKeyDown,
|
||||
onKeyPress: this._onKeyPress,
|
||||
tabIndex: 0,
|
||||
role: showLabel ? 'menuitem' : 'button'
|
||||
|
||||
@@ -2,11 +2,17 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { NOTIFY_CLICK_MODE } from '../../../../toolbox/constants';
|
||||
import { Icon } from '../../../icons';
|
||||
import { Tooltip } from '../../../tooltip';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The button's key.
|
||||
*/
|
||||
buttonKey?: string,
|
||||
|
||||
/**
|
||||
* The decorated component (ToolboxButton).
|
||||
*/
|
||||
@@ -22,6 +28,12 @@ type Props = {
|
||||
*/
|
||||
iconDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Notify mode for `toolbarButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string,
|
||||
|
||||
/**
|
||||
* Click handler for the small icon.
|
||||
*/
|
||||
@@ -68,6 +80,8 @@ type Props = {
|
||||
iconId: string
|
||||
};
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Displays the `ToolboxButtonWithIcon` component.
|
||||
*
|
||||
@@ -80,6 +94,8 @@ export default function ToolboxButtonWithIcon(props: Props) {
|
||||
icon,
|
||||
iconDisabled,
|
||||
iconTooltip,
|
||||
buttonKey,
|
||||
notifyMode,
|
||||
onIconClick,
|
||||
onIconKeyDown,
|
||||
styles,
|
||||
@@ -97,7 +113,17 @@ export default function ToolboxButtonWithIcon(props: Props) {
|
||||
= 'settings-button-small-icon settings-button-small-icon--disabled';
|
||||
} else {
|
||||
iconProps.className = 'settings-button-small-icon';
|
||||
iconProps.onClick = onIconClick;
|
||||
iconProps.onClick = () => {
|
||||
if (typeof APP !== 'undefined' && notifyMode) {
|
||||
APP.API.notifyToolbarButtonClicked(
|
||||
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}
|
||||
|
||||
if (notifyMode !== NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
onIconClick();
|
||||
}
|
||||
};
|
||||
iconProps.onKeyDown = onIconKeyDown;
|
||||
iconProps.role = 'button';
|
||||
iconProps.tabIndex = 0;
|
||||
|
||||
@@ -216,7 +216,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch, getState }, prevConference) => {
|
||||
if (prevConference && !conference) {
|
||||
|
||||
// conference keep flipping while we are authenticating, skip clearing while we are in that process
|
||||
if (prevConference && !conference && !getState()['features/base/conference'].authRequired) {
|
||||
|
||||
// Clear all tracks.
|
||||
const remoteTracks = getState()['features/base/tracks'].filter(t => !t.local);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useDispatch } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import { Icon, IconArrowDown, IconArrowUp } from '../../../base/icons';
|
||||
import { participantMatchesSearch } from '../../../participants-pane/functions';
|
||||
|
||||
import BreakoutRoomContextMenu from './BreakoutRoomContextMenu';
|
||||
import BreakoutRoomParticipantItem from './BreakoutRoomParticipantItem';
|
||||
@@ -17,7 +18,12 @@ type Props = {
|
||||
/**
|
||||
* Room to display.
|
||||
*/
|
||||
room: Object
|
||||
room: Object,
|
||||
|
||||
/**
|
||||
* Participants search string.
|
||||
*/
|
||||
searchString: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +37,7 @@ function _keyExtractor(item: Object) {
|
||||
}
|
||||
|
||||
|
||||
export const CollapsibleRoom = ({ room }: Props) => {
|
||||
export const CollapsibleRoom = ({ room, searchString }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const [ collapsed, setCollapsed ] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@@ -65,7 +71,8 @@ export const CollapsibleRoom = ({ room }: Props) => {
|
||||
horizontal = { false }
|
||||
keyExtractor = { _keyExtractor }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
renderItem = { ({ item: participant }) => <BreakoutRoomParticipantItem item = { participant } /> }
|
||||
renderItem = { ({ item: participant }) => participantMatchesSearch(participant, searchString)
|
||||
&& <BreakoutRoomParticipantItem item = { participant } /> }
|
||||
showsHorizontalScrollIndicator = { false }
|
||||
windowSize = { 2 } />}
|
||||
</View>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ListItem } from '../../../base/components';
|
||||
import { Icon, IconArrowDown, IconArrowUp } from '../../../base/icons';
|
||||
import ParticipantItem from '../../../participants-pane/components/web/ParticipantItem';
|
||||
import { ACTION_TRIGGER } from '../../../participants-pane/constants';
|
||||
import { participantMatchesSearch } from '../../../participants-pane/functions';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -42,6 +43,11 @@ type Props = {
|
||||
* Room reference.
|
||||
*/
|
||||
room: Object,
|
||||
|
||||
/**
|
||||
* Participants search string.
|
||||
*/
|
||||
searchString: string
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
@@ -78,7 +84,8 @@ export const CollapsibleRoom = ({
|
||||
isHighlighted,
|
||||
onRaiseMenu,
|
||||
onLeave,
|
||||
room
|
||||
room,
|
||||
searchString
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const styles = useStyles();
|
||||
@@ -116,13 +123,14 @@ export const CollapsibleRoom = ({
|
||||
textChildren = { roomName }
|
||||
trigger = { actionsTrigger } />
|
||||
{!collapsed && room?.participants
|
||||
&& Object.values(room?.participants || {}).map((p: Object) => (
|
||||
<ParticipantItem
|
||||
displayName = { p.displayName || defaultRemoteDisplayName }
|
||||
key = { p.jid }
|
||||
local = { false }
|
||||
participantID = { p.jid } />
|
||||
))
|
||||
&& Object.values(room?.participants || {}).map((p: Object) =>
|
||||
participantMatchesSearch(p, searchString) && (
|
||||
<ParticipantItem
|
||||
displayName = { p.displayName || defaultRemoteDisplayName }
|
||||
key = { p.jid }
|
||||
local = { false }
|
||||
participantID = { p.jid } />
|
||||
))
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,15 @@ import { LeaveButton } from './LeaveButton';
|
||||
import RoomActionEllipsis from './RoomActionEllipsis';
|
||||
import { RoomContextMenu } from './RoomContextMenu';
|
||||
|
||||
export const RoomList = () => {
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Participants search string.
|
||||
*/
|
||||
searchString: string
|
||||
}
|
||||
|
||||
export const RoomList = ({ searchString }: Props) => {
|
||||
const currentRoomId = useSelector(getCurrentRoomId);
|
||||
const rooms = Object.values(useSelector(getBreakoutRooms, equals))
|
||||
.filter((room: Object) => room.id !== currentRoomId)
|
||||
@@ -44,7 +52,8 @@ export const RoomList = () => {
|
||||
isHighlighted = { raiseContext.entity === room }
|
||||
onLeave = { lowerMenu }
|
||||
onRaiseMenu = { onRaiseMenu(room) }
|
||||
room = { room }>
|
||||
room = { room }
|
||||
searchString = { searchString }>
|
||||
{!_overflowDrawer && <>
|
||||
<JoinActionButton room = { room } />
|
||||
{isLocalModerator && !room.isMainRoom
|
||||
|
||||
@@ -4,19 +4,3 @@
|
||||
* Key for this feature.
|
||||
*/
|
||||
export const FEATURE_KEY = 'features/breakout-rooms';
|
||||
|
||||
/**
|
||||
* The type of json-message which indicates that json carries
|
||||
* a request for a participant to move to a specified room.
|
||||
*/
|
||||
export const JSON_TYPE_MOVE_TO_ROOM_REQUEST = `${FEATURE_KEY}/move-to-room`;
|
||||
|
||||
/**
|
||||
* The type of json-message which indicates that json carries a request to remove a specified breakout room.
|
||||
*/
|
||||
export const JSON_TYPE_REMOVE_BREAKOUT_ROOM = `${FEATURE_KEY}/remove`;
|
||||
|
||||
/**
|
||||
* The type of json-message which indicates that json carries breakout rooms data.
|
||||
*/
|
||||
export const JSON_TYPE_UPDATE_BREAKOUT_ROOMS = `${FEATURE_KEY}/update`;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import { getParticipantById } from '../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { editMessage, MESSAGE_TYPE_REMOTE } from '../chat';
|
||||
|
||||
import { UPDATE_BREAKOUT_ROOMS } from './actionTypes';
|
||||
import { moveToRoom } from './actions';
|
||||
import { getBreakoutRooms } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
@@ -30,3 +33,37 @@ StateListenerRegistry.register(
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const { type } = action;
|
||||
const result = next(action);
|
||||
|
||||
switch (type) {
|
||||
case UPDATE_BREAKOUT_ROOMS: {
|
||||
const { messages } = getState()['features/chat'];
|
||||
|
||||
// edit the chat history to match names for participants in breakout rooms
|
||||
messages && messages.forEach(m => {
|
||||
if (m.messageType === MESSAGE_TYPE_REMOTE && !getParticipantById(getState(), m.id)) {
|
||||
const rooms = getBreakoutRooms(getState);
|
||||
|
||||
for (const room of Object.values(rooms)) {
|
||||
// $FlowExpectedError
|
||||
const participants = room.participants || {};
|
||||
const matchedJid = Object.keys(participants).find(jid => jid.endsWith(m.id));
|
||||
|
||||
if (matchedJid) {
|
||||
m.displayName = participants[matchedJid].displayName;
|
||||
|
||||
dispatch(editMessage(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -33,6 +33,16 @@ export const CLEAR_MESSAGES = 'CLEAR_MESSAGES';
|
||||
*/
|
||||
export const CLOSE_CHAT = 'CLOSE_CHAT';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to edit chat message.
|
||||
*
|
||||
* {
|
||||
* type: EDIT_MESSAGE,
|
||||
* message: Object
|
||||
* }
|
||||
*/
|
||||
export const EDIT_MESSAGE = 'EDIT_MESSAGE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to display the chat panel.
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ADD_MESSAGE,
|
||||
CLEAR_MESSAGES,
|
||||
CLOSE_CHAT,
|
||||
EDIT_MESSAGE,
|
||||
SEND_MESSAGE,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
SET_IS_POLL_TAB_FOCUSED
|
||||
@@ -41,6 +42,23 @@ export function addMessage(messageDetails: Object) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits an existing chat message.
|
||||
*
|
||||
* @param {Object} message - The chat message to edit/override. The messages will be matched from the state
|
||||
* comparing the messageId.
|
||||
* @returns {{
|
||||
* type: EDIT_MESSAGE,
|
||||
* message: Object
|
||||
* }}
|
||||
*/
|
||||
export function editMessage(message: Object) {
|
||||
return {
|
||||
type: EDIT_MESSAGE,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the chat messages in Redux.
|
||||
*
|
||||
|
||||
@@ -49,22 +49,6 @@ class ChatButton extends AbstractButton<Props, *> {
|
||||
// Unused.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
|
||||
@@ -314,12 +314,16 @@ function _handleReceivedMessage({ dispatch, getState },
|
||||
// Provide a default for for the case when a message is being
|
||||
// backfilled for a participant that has left the conference.
|
||||
const participant = getParticipantById(state, id) || {};
|
||||
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
const displayName = getParticipantDisplayName(state, id);
|
||||
const hasRead = participant.local || isChatOpen;
|
||||
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
|
||||
const millisecondsTimestamp = timestampToDate.getTime();
|
||||
const shouldShowNotification = userSelectedNotifications['notify.chatMessages'] && !hasRead && !isReaction;
|
||||
|
||||
// skip message notifications on join (the messages having timestamp - coming from the history)
|
||||
const shouldShowNotification = userSelectedNotifications['notify.chatMessages']
|
||||
&& !hasRead && !isReaction && !timestamp;
|
||||
|
||||
dispatch(addMessage({
|
||||
displayName,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
CLEAR_MESSAGES,
|
||||
CLOSE_CHAT,
|
||||
EDIT_MESSAGE,
|
||||
OPEN_CHAT,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
SET_IS_POLL_TAB_FOCUSED
|
||||
@@ -29,6 +32,7 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
error: action.error,
|
||||
id: action.id,
|
||||
isReaction: action.isReaction,
|
||||
messageId: uuidv4(),
|
||||
messageType: action.messageType,
|
||||
message: action.message,
|
||||
privateMessage: action.privateMessage,
|
||||
@@ -63,6 +67,30 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
messages: []
|
||||
};
|
||||
|
||||
case EDIT_MESSAGE: {
|
||||
let found = false;
|
||||
const newMessage = action.message;
|
||||
const messages = state.messages.map(m => {
|
||||
if (m.messageId === newMessage.messageId) {
|
||||
found = true;
|
||||
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
return m;
|
||||
});
|
||||
|
||||
// no change
|
||||
if (!found) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
messages
|
||||
};
|
||||
}
|
||||
|
||||
case SET_PRIVATE_MESSAGE_RECIPIENT:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONFERENCE_INFO = {
|
||||
alwaysVisible: [ 'recording', 'local-recording' ],
|
||||
alwaysVisible: [ 'recording', 'local-recording', 'raised-hands-count' ],
|
||||
autoHide: [
|
||||
'subject',
|
||||
'conference-timer',
|
||||
|
||||
1
react/features/conference/components/functions.native.js
Normal file
1
react/features/conference/components/functions.native.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './functions.any';
|
||||
14
react/features/conference/components/functions.web.js
Normal file
14
react/features/conference/components/functions.web.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// @flow
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Whether or not there are always on labels.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAlwaysOnTitleBarEmpty() {
|
||||
const bar = document.querySelector('#alwaysVisible>div');
|
||||
|
||||
return bar?.childNodes.length === 0;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { getConferenceInfo } from '../functions';
|
||||
import ConferenceInfoContainer from './ConferenceInfoContainer';
|
||||
import InsecureRoomNameLabel from './InsecureRoomNameLabel';
|
||||
import ParticipantsCount from './ParticipantsCount';
|
||||
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
|
||||
import SubjectText from './SubjectText';
|
||||
|
||||
/**
|
||||
@@ -66,6 +67,10 @@ const COMPONENTS = [
|
||||
Component: LocalRecordingLabel,
|
||||
id: 'local-recording'
|
||||
},
|
||||
{
|
||||
Component: RaisedHandsCountLabel,
|
||||
id: 'raised-hands-count'
|
||||
},
|
||||
{
|
||||
Component: TranscribingLabel,
|
||||
id: 'transcribing'
|
||||
@@ -115,7 +120,9 @@ class ConferenceInfo extends Component<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<ConferenceInfoContainer visible = { this.props._visible } >
|
||||
<ConferenceInfoContainer
|
||||
id = 'autoHide'
|
||||
visible = { this.props._visible }>
|
||||
{
|
||||
COMPONENTS
|
||||
.filter(comp => autoHide.includes(comp.id))
|
||||
@@ -142,7 +149,9 @@ class ConferenceInfo extends Component<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<ConferenceInfoContainer visible = { true } >
|
||||
<ConferenceInfoContainer
|
||||
id = 'alwaysVisible'
|
||||
visible = { true } >
|
||||
{
|
||||
COMPONENTS
|
||||
.filter(comp => alwaysVisible.includes(comp.id))
|
||||
|
||||
@@ -2,21 +2,30 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { isAlwaysOnTitleBarEmpty } from '../functions.web';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The children components.
|
||||
*/
|
||||
children: React$Node,
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Whether this conference info container should be visible or not.
|
||||
*/
|
||||
visible: boolean
|
||||
/**
|
||||
* Id of the component.
|
||||
*/
|
||||
id?: string,
|
||||
|
||||
/**
|
||||
* Whether this conference info container should be visible or not.
|
||||
*/
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
export default ({ visible, children }: Props) => (
|
||||
<div className = { `subject${visible ? ' visible' : ''}` }>
|
||||
export default ({ visible, children, id }: Props) => (
|
||||
<div
|
||||
className = { `subject${isAlwaysOnTitleBarEmpty() ? '' : ' with-always-on'}${visible ? ' visible' : ''}` }
|
||||
id = { id }>
|
||||
<div className = { 'subject-info-container' }>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IconRaisedHand } from '../../../base/icons';
|
||||
import { Label } from '../../../base/label';
|
||||
import { Tooltip } from '../../../base/tooltip';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme';
|
||||
import { open as openParticipantsPane } from '../../../participants-pane/actions';
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
label: {
|
||||
backgroundColor: theme.palette.warning02,
|
||||
color: theme.palette.uiBackground,
|
||||
marginRight: theme.spacing(1)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const RaisedHandsCountLabel = () => {
|
||||
const styles = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const raisedHandsCount = useSelector(state =>
|
||||
(state['features/base/participants'].raisedHandsQueue || []).length);
|
||||
const { t } = useTranslation();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(openParticipantsPane());
|
||||
}, []);
|
||||
|
||||
return raisedHandsCount > 0 && (<Tooltip
|
||||
content = { t('raisedHandsLabel') }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
className = { styles.label }
|
||||
icon = { IconRaisedHand }
|
||||
iconColor = { BaseTheme.palette.uiBackground }
|
||||
id = 'raisedHandsCountLabel'
|
||||
onClick = { onClick }
|
||||
text = { raisedHandsCount } />
|
||||
</Tooltip>);
|
||||
};
|
||||
|
||||
export default RaisedHandsCountLabel;
|
||||
@@ -91,6 +91,11 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
_onSaveLogs: Function,
|
||||
|
||||
/**
|
||||
* The region reported by the participant.
|
||||
*/
|
||||
_region: String,
|
||||
|
||||
/**
|
||||
* The video SSRC of this client.
|
||||
*/
|
||||
@@ -171,7 +176,6 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
|
||||
framerate,
|
||||
maxEnabledResolution,
|
||||
packetLoss,
|
||||
region,
|
||||
resolution,
|
||||
serverRegion,
|
||||
transport
|
||||
@@ -195,7 +199,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
|
||||
onShowMore = { this._onToggleShowMore }
|
||||
packetLoss = { packetLoss }
|
||||
participantId = { this.props.participantId }
|
||||
region = { region }
|
||||
region = { this.props._region }
|
||||
resolution = { resolution }
|
||||
serverRegion = { serverRegion }
|
||||
shouldShowMore = { this.state.showMoreStats }
|
||||
@@ -310,7 +314,8 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
_connectionStatus: participant?.connectionStatus,
|
||||
_enableSaveLogs: state['features/base/config'].enableSaveLogs,
|
||||
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
|
||||
_isLocalVideo: participant?.local
|
||||
_isLocalVideo: participant?.local,
|
||||
_region: participant?.region
|
||||
};
|
||||
|
||||
if (conference) {
|
||||
|
||||
@@ -36,13 +36,7 @@ class EmbedMeetingButton extends AbstractButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('embed.meeting'));
|
||||
dispatch(openDialog(EmbedMeetingDialog));
|
||||
|
||||
@@ -9,7 +9,6 @@ import { connect } from '../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
|
||||
import { toggleDocument } from '../../etherpad/actions';
|
||||
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
@@ -59,13 +58,7 @@ class SharedDocumentButton extends AbstractButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { _editing, dispatch, handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
const { _editing, dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'toggle.etherpad',
|
||||
|
||||
@@ -39,3 +39,21 @@ export const START_FACIAL_RECOGNITION = 'START_FACIAL_RECOGNITION';
|
||||
* }
|
||||
*/
|
||||
export const STOP_FACIAL_RECOGNITION = 'STOP_FACIAL_RECOGNITION';
|
||||
|
||||
/**
|
||||
* Redux action type dispatched in order to clear the facial expressions buffer in the state.
|
||||
*
|
||||
* {
|
||||
* type: CLEAR_FACIAL_EXPRESSIONS_BUFFER
|
||||
* }
|
||||
*/
|
||||
export const CLEAR_FACIAL_EXPRESSIONS_BUFFER = 'CLEAR_FACIAL_EXPRESSIONS_BUFFER';
|
||||
|
||||
/**
|
||||
* Redux action type dispatched in order to add a expression to the facial expressions buffer.
|
||||
*
|
||||
* {
|
||||
* type: ADD_TO_FACIAL_EXPRESSIONS_BUFFER
|
||||
* }
|
||||
*/
|
||||
export const ADD_TO_FACIAL_EXPRESSIONS_BUFFER = 'ADD_TO_FACIAL_EXPRESSIONS_BUFFER ';
|
||||
|
||||
@@ -6,23 +6,20 @@ import './createImageBitmap';
|
||||
|
||||
import {
|
||||
ADD_FACIAL_EXPRESSION,
|
||||
ADD_TO_FACIAL_EXPRESSIONS_BUFFER,
|
||||
CLEAR_FACIAL_EXPRESSIONS_BUFFER,
|
||||
SET_DETECTION_TIME_INTERVAL,
|
||||
START_FACIAL_RECOGNITION,
|
||||
STOP_FACIAL_RECOGNITION
|
||||
} from './actionTypes';
|
||||
import { sendDataToWorker } from './functions';
|
||||
import {
|
||||
CPU_TIME_INTERVAL,
|
||||
WEBGL_TIME_INTERVAL,
|
||||
WEBHOOK_SEND_TIME_INTERVAL
|
||||
} from './constants';
|
||||
import { sendDataToWorker, sendFacialExpressionsWebhook } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Time used for detection interval when facial expressions worker uses webgl backend.
|
||||
*/
|
||||
const WEBGL_TIME_INTERVAL = 1000;
|
||||
|
||||
/**
|
||||
* Time used for detection interval when facial expression worker uses cpu backend.
|
||||
*/
|
||||
const CPU_TIME_INTERVAL = 6000;
|
||||
|
||||
/**
|
||||
* Object containing a image capture of the local track.
|
||||
*/
|
||||
@@ -38,12 +35,22 @@ let worker;
|
||||
*/
|
||||
let lastFacialExpression;
|
||||
|
||||
/**
|
||||
* The last facial expression timestamp.
|
||||
*/
|
||||
let lastFacialExpressionTimestamp;
|
||||
|
||||
/**
|
||||
* How many duplicate consecutive expression occurred.
|
||||
* If a expression that is not the same as the last one it is reset to 0.
|
||||
*/
|
||||
let duplicateConsecutiveExpressions = 0;
|
||||
|
||||
/**
|
||||
* Variable that keeps the interval for sending expressions to webhook.
|
||||
*/
|
||||
let sendInterval;
|
||||
|
||||
/**
|
||||
* Loads the worker that predicts the facial expression.
|
||||
*
|
||||
@@ -95,9 +102,17 @@ export function loadWorker() {
|
||||
if (value === lastFacialExpression) {
|
||||
duplicateConsecutiveExpressions++;
|
||||
} else {
|
||||
lastFacialExpression
|
||||
&& dispatch(addFacialExpression(lastFacialExpression, duplicateConsecutiveExpressions + 1));
|
||||
if (lastFacialExpression && lastFacialExpressionTimestamp) {
|
||||
dispatch(
|
||||
addFacialExpression(
|
||||
lastFacialExpression,
|
||||
duplicateConsecutiveExpressions + 1,
|
||||
lastFacialExpressionTimestamp
|
||||
)
|
||||
);
|
||||
}
|
||||
lastFacialExpression = value;
|
||||
lastFacialExpressionTimestamp = Date.now();
|
||||
duplicateConsecutiveExpressions = 0;
|
||||
}
|
||||
}
|
||||
@@ -143,9 +158,15 @@ export function startFacialRecognition() {
|
||||
|
||||
// $FlowFixMe
|
||||
imageCapture = new ImageCapture(firstVideoTrack);
|
||||
|
||||
sendDataToWorker(worker, imageCapture);
|
||||
sendInterval = setInterval(async () => {
|
||||
const result = await sendFacialExpressionsWebhook(getState());
|
||||
|
||||
if (result) {
|
||||
dispatch(clearFacialExpressionBuffer());
|
||||
}
|
||||
}
|
||||
, WEBHOOK_SEND_TIME_INTERVAL);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -169,9 +190,21 @@ export function stopFacialRecognition() {
|
||||
id: 'CLEAR_TIMEOUT'
|
||||
});
|
||||
|
||||
lastFacialExpression
|
||||
&& dispatch(addFacialExpression(lastFacialExpression, duplicateConsecutiveExpressions + 1));
|
||||
if (lastFacialExpression && lastFacialExpressionTimestamp) {
|
||||
dispatch(
|
||||
addFacialExpression(
|
||||
lastFacialExpression,
|
||||
duplicateConsecutiveExpressions + 1,
|
||||
lastFacialExpressionTimestamp
|
||||
)
|
||||
);
|
||||
}
|
||||
duplicateConsecutiveExpressions = 0;
|
||||
|
||||
if (sendInterval) {
|
||||
clearInterval(sendInterval);
|
||||
sendInterval = null;
|
||||
}
|
||||
dispatch({ type: STOP_FACIAL_RECOGNITION });
|
||||
logger.log('Stop face recognition');
|
||||
};
|
||||
@@ -215,9 +248,10 @@ export function changeTrack(track: Object) {
|
||||
*
|
||||
* @param {string} facialExpression - Facial expression to be added.
|
||||
* @param {number} duration - Duration in seconds of the facial expression.
|
||||
* @param {number} timestamp - Duration in seconds of the facial expression.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function addFacialExpression(facialExpression: string, duration: number) {
|
||||
function addFacialExpression(facialExpression: string, duration: number, timestamp: number) {
|
||||
return function(dispatch: Function, getState: Function) {
|
||||
const { detectionTimeInterval } = getState()['features/facial-recognition'];
|
||||
let finalDuration = duration;
|
||||
@@ -228,7 +262,8 @@ function addFacialExpression(facialExpression: string, duration: number) {
|
||||
dispatch({
|
||||
type: ADD_FACIAL_EXPRESSION,
|
||||
facialExpression,
|
||||
duration: finalDuration
|
||||
duration: finalDuration,
|
||||
timestamp
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -245,3 +280,27 @@ function setDetectionTimeInterval(time: number) {
|
||||
time
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a facial expression with its timestamp to the facial expression buffer.
|
||||
*
|
||||
* @param {Object} facialExpression - Object containing facial expression string and its timestamp.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function addToFacialExpressionsBuffer(facialExpression: Object) {
|
||||
return {
|
||||
type: ADD_TO_FACIAL_EXPRESSIONS_BUFFER,
|
||||
facialExpression
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the facial expressions array in the state.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function clearFacialExpressionBuffer() {
|
||||
return {
|
||||
type: CLEAR_FACIAL_EXPRESSIONS_BUFFER
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,3 +9,18 @@ export const FACIAL_EXPRESSION_EMOJIS = {
|
||||
fearful: '😨',
|
||||
disgusted: '🤢'
|
||||
};
|
||||
|
||||
/**
|
||||
* Time used for detection interval when facial expressions worker uses webgl backend.
|
||||
*/
|
||||
export const WEBGL_TIME_INTERVAL = 1000;
|
||||
|
||||
/**
|
||||
* Time used for detection interval when facial expression worker uses cpu backend.
|
||||
*/
|
||||
export const CPU_TIME_INTERVAL = 6000;
|
||||
|
||||
/**
|
||||
* Time is ms used for sending expression.
|
||||
*/
|
||||
export const WEBHOOK_SEND_TIME_INTERVAL = 15000;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// @flow
|
||||
import { getLocalParticipant } from '../base/participants';
|
||||
import { extractFqnFromPath } from '../dynamic-branding';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
@@ -49,6 +52,60 @@ export function sendFacialExpressionToServer(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends facial expression to backend.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean} - True if sent, false otherwise.
|
||||
*/
|
||||
export async function sendFacialExpressionsWebhook(state: Object) {
|
||||
const { webhookProxyUrl: url } = state['features/base/config'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
const { connection } = state['features/base/connection'];
|
||||
const jid = connection.getJid();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { facialExpressionsBuffer } = state['features/facial-recognition'];
|
||||
|
||||
if (facialExpressionsBuffer.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const reqBody = {
|
||||
meetingFqn: extractFqnFromPath(),
|
||||
sessionId: conference.sessionId,
|
||||
submitted: Date.now(),
|
||||
emotions: facialExpressionsBuffer,
|
||||
participantId: localParticipant.jwtId,
|
||||
participantName: localParticipant.name,
|
||||
participantJid: jid
|
||||
};
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
const res = await fetch(`${url}/emotions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(reqBody)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
return true;
|
||||
}
|
||||
logger.error('Status error:', res.status);
|
||||
} catch (err) {
|
||||
logger.error('Could not send request', err);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the image data a canvas from the track in the image capture to the facial expression worker.
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@ import { VIRTUAL_BACKGROUND_TRACK_CHANGED } from '../virtual-background/actionTy
|
||||
|
||||
import { ADD_FACIAL_EXPRESSION } from './actionTypes';
|
||||
import {
|
||||
addToFacialExpressionsBuffer,
|
||||
changeTrack,
|
||||
loadWorker,
|
||||
resetTrack,
|
||||
@@ -96,6 +97,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
sendFacialExpressionToParticipants(conference, action.facialExpression, action.duration);
|
||||
}
|
||||
sendFacialExpressionToServer(conference, action.facialExpression, action.duration);
|
||||
dispatch(addToFacialExpressionsBuffer({
|
||||
emotion: action.facialExpression,
|
||||
timestamp: action.timestamp
|
||||
}));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
ADD_FACIAL_EXPRESSION,
|
||||
ADD_TO_FACIAL_EXPRESSIONS_BUFFER,
|
||||
CLEAR_FACIAL_EXPRESSIONS_BUFFER,
|
||||
SET_DETECTION_TIME_INTERVAL,
|
||||
START_FACIAL_RECOGNITION,
|
||||
STOP_FACIAL_RECOGNITION
|
||||
@@ -19,6 +21,7 @@ const defaultState = {
|
||||
disgusted: 0,
|
||||
sad: 0
|
||||
},
|
||||
facialExpressionsBuffer: [],
|
||||
detectionTimeInterval: -1,
|
||||
recognitionActive: false
|
||||
};
|
||||
@@ -30,6 +33,18 @@ ReducerRegistry.register('features/facial-recognition', (state = defaultState, a
|
||||
|
||||
return state;
|
||||
}
|
||||
case ADD_TO_FACIAL_EXPRESSIONS_BUFFER: {
|
||||
return {
|
||||
...state,
|
||||
facialExpressionsBuffer: [ ...state.facialExpressionsBuffer, action.facialExpression ]
|
||||
};
|
||||
}
|
||||
case CLEAR_FACIAL_EXPRESSIONS_BUFFER: {
|
||||
return {
|
||||
...state,
|
||||
facialExpressionsBuffer: []
|
||||
};
|
||||
}
|
||||
case SET_DETECTION_TIME_INTERVAL: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -40,13 +40,7 @@ class FeedbackButton extends AbstractButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { _conference, dispatch, handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
const { _conference, dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('feedback'));
|
||||
dispatch(openFeedbackDialog(_conference));
|
||||
|
||||
@@ -39,7 +39,7 @@ export function setTileViewDimensions(dimensions: Object) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { disableResponsiveTiles } = state['features/base/config'];
|
||||
const { disableResponsiveTiles, disableTileEnlargement } = state['features/base/config'];
|
||||
const {
|
||||
height,
|
||||
width
|
||||
@@ -47,7 +47,8 @@ export function setTileViewDimensions(dimensions: Object) {
|
||||
...dimensions,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableResponsiveTiles
|
||||
disableResponsiveTiles,
|
||||
disableTileEnlargement
|
||||
});
|
||||
const { columns, rows } = dimensions;
|
||||
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
|
||||
|
||||
@@ -25,9 +25,11 @@ class RaisedHandIndicator extends AbstractRaisedHandIndicator<Props> {
|
||||
_renderIndicator() {
|
||||
return (
|
||||
<BaseIndicator
|
||||
backgroundColor = { BaseTheme.palette.warning01 }
|
||||
backgroundColor = { BaseTheme.palette.warning02 }
|
||||
highlight = { true }
|
||||
icon = { IconRaisedHand } />
|
||||
icon = { IconRaisedHand }
|
||||
iconSize = { 16 }
|
||||
iconStyle = {{ color: BaseTheme.palette.uiBackground }} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
isEveryoneModerator,
|
||||
pinParticipant,
|
||||
getParticipantByIdOrUndefined,
|
||||
getLocalParticipant
|
||||
getLocalParticipant,
|
||||
hasRaisedHand
|
||||
} from '../../../base/participants';
|
||||
import { Container } from '../../../base/react';
|
||||
import { connect } from '../../../base/redux';
|
||||
@@ -84,6 +85,11 @@ type Props = {
|
||||
*/
|
||||
_pinned: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the participant has the hand raised.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* Whether to show the dominant speaker indicator or not.
|
||||
*/
|
||||
@@ -267,6 +273,7 @@ class Thumbnail extends PureComponent<Props> {
|
||||
_participantId: participantId,
|
||||
_participantInLargeVideo: participantInLargeVideo,
|
||||
_pinned,
|
||||
_raisedHand,
|
||||
_styles,
|
||||
disableTint,
|
||||
height,
|
||||
@@ -289,7 +296,8 @@ class Thumbnail extends PureComponent<Props> {
|
||||
style = { [
|
||||
styles.thumbnail,
|
||||
_pinned && !tileView ? _styles.thumbnailPinned : null,
|
||||
styleOverrides
|
||||
styleOverrides,
|
||||
_raisedHand ? styles.thumbnailRaisedHand : null
|
||||
] }
|
||||
touchFeedback = { false }>
|
||||
<ParticipantView
|
||||
@@ -354,6 +362,7 @@ function _mapStateToProps(state, ownProps) {
|
||||
_participantInLargeVideo: participantInLargeVideo,
|
||||
_participantId: id,
|
||||
_pinned: participant?.pinned,
|
||||
_raisedHand: hasRaisedHand(participant),
|
||||
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
|
||||
_renderModeratorIndicator: renderModeratorIndicator,
|
||||
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
FlatList,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
SafeAreaView,
|
||||
TouchableWithoutFeedback
|
||||
} from 'react-native';
|
||||
import { withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
|
||||
@@ -71,6 +72,11 @@ type Props = {
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
|
||||
/**
|
||||
* Object containing the safe area insets.
|
||||
*/
|
||||
insets: Object,
|
||||
|
||||
/**
|
||||
* Callback to invoke when tile view is tapped.
|
||||
*/
|
||||
@@ -126,7 +132,8 @@ class TileView extends PureComponent<Props> {
|
||||
...styles.flatListTileView
|
||||
};
|
||||
this._contentContainerStyles = {
|
||||
...styles.contentContainer
|
||||
...styles.contentContainer,
|
||||
paddingBottom: this.props.insets?.bottom || 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -194,13 +201,14 @@ class TileView extends PureComponent<Props> {
|
||||
this._contentContainerStyles = {
|
||||
...styles.contentContainer,
|
||||
minHeight: _height,
|
||||
minWidth: _width
|
||||
minWidth: _width,
|
||||
paddingBottom: this.props.insets?.bottom || 0
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress = { onClick }>
|
||||
<View style = { styles.flatListContainer }>
|
||||
<SafeAreaView style = { styles.flatListContainer }>
|
||||
<FlatList
|
||||
bounces = { false }
|
||||
contentContainerStyle = { this._contentContainerStyles }
|
||||
@@ -217,7 +225,7 @@ class TileView extends PureComponent<Props> {
|
||||
style = { this._flatListStyles }
|
||||
viewabilityConfig = { this._viewabilityConfig }
|
||||
windowSize = { 2 } />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
@@ -269,10 +277,11 @@ class TileView extends PureComponent<Props> {
|
||||
* Maps (parts of) the redux state to the associated {@code TileView}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - Component props.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const responsiveUi = state['features/base/responsive-ui'];
|
||||
const { remoteParticipants, tileViewDimensions } = state['features/filmstrip'];
|
||||
const disableSelfView = shouldHideSelfView(state);
|
||||
@@ -283,13 +292,14 @@ function _mapStateToProps(state) {
|
||||
_aspectRatio: responsiveUi.aspectRatio,
|
||||
_columns: columns,
|
||||
_disableSelfView: disableSelfView,
|
||||
_height: responsiveUi.clientHeight,
|
||||
_height: responsiveUi.clientHeight - (ownProps.insets?.top || 0),
|
||||
_insets: ownProps.insets,
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_participantCount: getParticipantCountWithFake(state),
|
||||
_remoteParticipants: remoteParticipants,
|
||||
_thumbnailHeight: height,
|
||||
_width: responsiveUi.clientWidth
|
||||
_width: responsiveUi.clientWidth - (ownProps.insets?.right || 0) - (ownProps.insets?.left || 0)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(TileView);
|
||||
export default withSafeAreaInsets(connect(_mapStateToProps)(TileView));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
|
||||
import { ColorPalette } from '../../../base/styles';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import { SMALL_THUMBNAIL_SIZE } from '../../constants';
|
||||
|
||||
/**
|
||||
@@ -147,6 +148,11 @@ export default {
|
||||
|
||||
thumbnailTopRightIndicatorContainer: {
|
||||
right: 0
|
||||
},
|
||||
|
||||
thumbnailRaisedHand: {
|
||||
borderWidth: 2,
|
||||
borderColor: BaseTheme.palette.warning02
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useSelector } from 'react-redux';
|
||||
import { IconRaisedHand } from '../../../base/icons';
|
||||
import { getParticipantById, hasRaisedHand } from '../../../base/participants';
|
||||
import { BaseIndicator } from '../../../base/react';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RaisedHandIndicator}.
|
||||
@@ -33,7 +34,7 @@ type Props = {
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
raisedHandIndicator: {
|
||||
backgroundColor: theme.palette.warning01,
|
||||
backgroundColor: theme.palette.warning02,
|
||||
padding: '2px',
|
||||
zIndex: 3,
|
||||
display: 'inline-block',
|
||||
@@ -65,6 +66,7 @@ const RaisedHandIndicator = ({
|
||||
<div className = { styles.raisedHandIndicator }>
|
||||
<BaseIndicator
|
||||
icon = { IconRaisedHand }
|
||||
iconColor = { BaseTheme.palette.uiBackground }
|
||||
iconSize = { `${iconSize}px` }
|
||||
tooltipKey = 'raisedHand'
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
|
||||
@@ -10,6 +10,7 @@ import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
|
||||
import {
|
||||
getParticipantByIdOrUndefined,
|
||||
hasRaisedHand,
|
||||
pinParticipant
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
@@ -153,6 +154,11 @@ export type Props = {|
|
||||
*/
|
||||
_participant: Object,
|
||||
|
||||
/**
|
||||
* Whether or not the participant has the hand raised.
|
||||
*/
|
||||
_raisedHand: boolean,
|
||||
|
||||
/**
|
||||
* The video track that will be displayed in the thumbnail.
|
||||
*/
|
||||
@@ -230,7 +236,7 @@ const defaultStyles = theme => {
|
||||
marginRight: '4px'
|
||||
},
|
||||
|
||||
'&:not(.top-indicators) > *:last-child': {
|
||||
'&:not(.top-indicators) > span:last-child': {
|
||||
marginRight: '6px'
|
||||
}
|
||||
},
|
||||
@@ -245,14 +251,23 @@ const defaultStyles = theme => {
|
||||
backgroundColor: theme.palette.ui02
|
||||
},
|
||||
|
||||
borderIndicator: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: '9',
|
||||
borderRadius: '4px'
|
||||
},
|
||||
|
||||
activeSpeaker: {
|
||||
'& .active-speaker-indicator': {
|
||||
boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: '9',
|
||||
borderRadius: '4px'
|
||||
boxShadow: `inset 0px 0px 0px 4px ${theme.palette.link01Active} !important`
|
||||
}
|
||||
},
|
||||
|
||||
raisedHand: {
|
||||
'& .raised-hand-border': {
|
||||
boxShadow: `inset 0px 0px 0px 2px ${theme.palette.warning02} !important`
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -454,12 +469,17 @@ class Thumbnail extends Component<Props, State> {
|
||||
_isHidden,
|
||||
_isScreenSharing,
|
||||
_participant,
|
||||
_videoTrack,
|
||||
_width,
|
||||
horizontalOffset,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
|
||||
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
|
||||
const jitsiVideoTrack = _videoTrack?.jitsiTrack;
|
||||
const track = jitsiVideoTrack?.track;
|
||||
const isPortraitVideo = ((track && track.getSettings()?.aspectRatio) || 1) < 1;
|
||||
|
||||
let styles: {
|
||||
avatar: Object,
|
||||
@@ -479,7 +499,7 @@ class Thumbnail extends Component<Props, State> {
|
||||
}
|
||||
|
||||
let videoStyles = null;
|
||||
const doNotStretchVideo = (_height < 320 && tileViewActive)
|
||||
const doNotStretchVideo = (isPortraitVideo && tileViewActive)
|
||||
|| _disableTileEnlargement
|
||||
|| _isScreenSharing;
|
||||
|
||||
@@ -661,11 +681,16 @@ class Thumbnail extends Component<Props, State> {
|
||||
_participant,
|
||||
_currentLayout,
|
||||
_isAnyParticipantPinned,
|
||||
_raisedHand,
|
||||
classes
|
||||
} = this.props;
|
||||
|
||||
className += ` ${DISPLAY_MODE_TO_CLASS_NAME[displayMode]}`;
|
||||
|
||||
if (_raisedHand) {
|
||||
className += ` ${classes.raisedHand}`;
|
||||
}
|
||||
|
||||
if (_currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
if (!_isDominantSpeakerDisabled && _participant?.dominantSpeaker) {
|
||||
className += ` ${classes.activeSpeaker} dominant-speaker`;
|
||||
@@ -828,7 +853,8 @@ class Thumbnail extends Component<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
<ThumbnailAudioIndicator _audioTrack = { _audioTrack } />
|
||||
<div className = 'active-speaker-indicator' />
|
||||
<div className = { clsx(classes.borderIndicator, 'raised-hand-border') } />
|
||||
<div className = { clsx(classes.borderIndicator, 'active-speaker-indicator') } />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -892,7 +918,6 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
const _isMobile = isMobileBrowser();
|
||||
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
|
||||
@@ -948,6 +973,7 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
_isVideoPlayable: id && isVideoPlayable(state, id),
|
||||
_localFlipX: Boolean(localFlipX),
|
||||
_participant: participant,
|
||||
_raisedHand: hasRaisedHand(participant),
|
||||
_videoTrack,
|
||||
...size
|
||||
};
|
||||
|
||||
@@ -63,6 +63,7 @@ const ThumbnailBottomIndicators = ({
|
||||
const styles = useStyles();
|
||||
const _allowEditing = !useSelector(isNameReadOnly);
|
||||
const _defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
|
||||
const _showDisplayName = useSelector(state => !state['features/base/config'].hideDisplayName);
|
||||
|
||||
return (<div className = { className }>
|
||||
<StatusIndicators
|
||||
@@ -70,14 +71,18 @@ const ThumbnailBottomIndicators = ({
|
||||
moderator = { true }
|
||||
participantID = { participantId }
|
||||
screenshare = { currentLayout === LAYOUTS.TILE_VIEW } />
|
||||
<span className = { styles.nameContainer }>
|
||||
<DisplayName
|
||||
allowEditing = { local ? _allowEditing : false }
|
||||
currentLayout = { currentLayout }
|
||||
displayNameSuffix = { local ? _defaultLocalDisplayName : '' }
|
||||
elementID = { local ? 'localDisplayName' : `participant_${participantId}_name` }
|
||||
participantID = { participantId } />
|
||||
</span>
|
||||
{
|
||||
_showDisplayName && (
|
||||
<span className = { styles.nameContainer }>
|
||||
<DisplayName
|
||||
allowEditing = { local ? _allowEditing : false }
|
||||
currentLayout = { currentLayout }
|
||||
displayNameSuffix = { local ? _defaultLocalDisplayName : '' }
|
||||
elementID = { local ? 'localDisplayName' : `participant_${participantId}_name` }
|
||||
participantID = { participantId } />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>);
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,21 @@ export const TWO_COLUMN_BREAKPOINT = 1000;
|
||||
*/
|
||||
export const ASPECT_RATIO_BREAKPOINT = 500;
|
||||
|
||||
/**
|
||||
* Minimum height of tile for small screens.
|
||||
*/
|
||||
export const TILE_MIN_HEIGHT_SMALL = 150;
|
||||
|
||||
/**
|
||||
* Minimum height of tile for large screens.
|
||||
*/
|
||||
export const TILE_MIN_HEIGHT_LARGE = 200;
|
||||
|
||||
/**
|
||||
* Aspect ratio for portrait tiles. (height / width).
|
||||
*/
|
||||
export const TILE_PORTRAIT_ASPECT_RATIO = 1.3;
|
||||
|
||||
/**
|
||||
* The default number of columns for tile view.
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,10 @@ import {
|
||||
TILE_VERTICAL_MARGIN,
|
||||
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
|
||||
TILE_VIEW_GRID_VERTICAL_MARGIN,
|
||||
VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN
|
||||
VERTICAL_FILMSTRIP_MIN_HORIZONTAL_MARGIN,
|
||||
TILE_MIN_HEIGHT_LARGE,
|
||||
TILE_MIN_HEIGHT_SMALL,
|
||||
TILE_PORTRAIT_ASPECT_RATIO
|
||||
} from './constants';
|
||||
|
||||
export * from './functions.any';
|
||||
@@ -183,7 +186,8 @@ export function calculateThumbnailSizeForTileView({
|
||||
rows,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
disableResponsiveTiles
|
||||
disableResponsiveTiles,
|
||||
disableTileEnlargement
|
||||
}: Object) {
|
||||
let aspectRatio = TILE_ASPECT_RATIO;
|
||||
|
||||
@@ -191,6 +195,7 @@ export function calculateThumbnailSizeForTileView({
|
||||
aspectRatio = SQUARE_TILE_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
const minHeight = clientWidth < ASPECT_RATIO_BREAKPOINT ? TILE_MIN_HEIGHT_SMALL : TILE_MIN_HEIGHT_LARGE;
|
||||
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN) - TILE_VIEW_GRID_HORIZONTAL_MARGIN;
|
||||
const viewHeight = clientHeight - (minVisibleRows * TILE_VERTICAL_MARGIN) - TILE_VIEW_GRID_VERTICAL_MARGIN;
|
||||
const initialWidth = viewWidth / columns;
|
||||
@@ -212,13 +217,54 @@ export function calculateThumbnailSizeForTileView({
|
||||
// window.
|
||||
height = Math.floor(Math.max(Math.min(scrollAspectRatioHeight, initialHeight), noScrollHeight));
|
||||
width = Math.floor(aspectRatio * height);
|
||||
|
||||
return {
|
||||
height,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
if (disableTileEnlargement) {
|
||||
return {
|
||||
height,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
if (initialHeight > noScrollHeight) {
|
||||
height = Math.max(height, viewHeight / rows, minHeight);
|
||||
width = Math.max(width, initialWidth);
|
||||
} else {
|
||||
height = Math.max(initialHeight, minHeight);
|
||||
width = initialWidth;
|
||||
}
|
||||
|
||||
if (height > width) {
|
||||
const heightFromWidth = TILE_PORTRAIT_ASPECT_RATIO * width;
|
||||
|
||||
if (height > heightFromWidth && heightFromWidth < minHeight) {
|
||||
return {
|
||||
height,
|
||||
width: height / TILE_PORTRAIT_ASPECT_RATIO
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
height: Math.min(height, heightFromWidth),
|
||||
width
|
||||
};
|
||||
} else if (height < width) {
|
||||
return {
|
||||
height,
|
||||
width: Math.min(width, aspectRatio * height)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
height: initialHeight,
|
||||
width: initialWidth
|
||||
height,
|
||||
width
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,13 +34,7 @@ class InviteButton extends AbstractButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('invite'));
|
||||
dispatch(beginAddPeople());
|
||||
|
||||
@@ -34,13 +34,7 @@ class KeyboardShortcutsButton extends AbstractButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('shortcuts'));
|
||||
dispatch(openKeyboardShortcutsDialog());
|
||||
|
||||
@@ -193,12 +193,15 @@ function _findLoadableAvatarForKnockingParticipant(store, { id }) {
|
||||
const { disableThirdPartyRequests } = getState()['features/base/config'];
|
||||
|
||||
if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
|
||||
getFirstLoadableAvatarUrl(updatedParticipant, store).then(loadableAvatarUrl => {
|
||||
if (loadableAvatarUrl) {
|
||||
getFirstLoadableAvatarUrl(updatedParticipant, store).then(result => {
|
||||
if (result) {
|
||||
const { isUsingCORS, src } = result;
|
||||
|
||||
dispatch(
|
||||
participantIsKnockingOrUpdated({
|
||||
loadableAvatarUrl,
|
||||
id
|
||||
loadableAvatarUrl: src,
|
||||
id,
|
||||
isUsingCORS
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,13 +36,7 @@ class LocalRecording extends AbstractButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('local.recording'));
|
||||
dispatch(openDialog(LocalRecordingInfoDialog));
|
||||
|
||||
@@ -8,10 +8,9 @@ import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconInviteMore } from '../../../base/icons';
|
||||
import { getLocalParticipant, getParticipantCountWithFake, getRemoteParticipants } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { normalizeAccents } from '../../../base/util/strings';
|
||||
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
|
||||
import { doInvitePeople } from '../../../invite/actions.native';
|
||||
import { shouldRenderInviteButton } from '../../functions';
|
||||
import { participantMatchesSearch, shouldRenderInviteButton } from '../../functions';
|
||||
|
||||
import ClearableInput from './ClearableInput';
|
||||
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||
@@ -55,6 +54,16 @@ type Props = {
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Participants search string.
|
||||
*/
|
||||
searchString: string,
|
||||
|
||||
/**
|
||||
* Function to update the search string.
|
||||
*/
|
||||
setSearchString: Function,
|
||||
|
||||
/**
|
||||
* Translation function.
|
||||
*/
|
||||
@@ -66,14 +75,10 @@ type Props = {
|
||||
theme: Object
|
||||
}
|
||||
|
||||
type State = {
|
||||
searchString: string
|
||||
};
|
||||
|
||||
/**
|
||||
* The meeting participant list component.
|
||||
*/
|
||||
class MeetingParticipantList extends PureComponent<Props, State> {
|
||||
class MeetingParticipantList extends PureComponent<Props> {
|
||||
|
||||
/**
|
||||
* Creates new MeetingParticipantList instance.
|
||||
@@ -83,10 +88,6 @@ class MeetingParticipantList extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
searchString: ''
|
||||
};
|
||||
|
||||
this._keyExtractor = this._keyExtractor.bind(this);
|
||||
this._onInvite = this._onInvite.bind(this);
|
||||
this._renderParticipant = this._renderParticipant.bind(this);
|
||||
@@ -139,27 +140,10 @@ class MeetingParticipantList extends PureComponent<Props, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderParticipant({ item/* , index, separators */ }) {
|
||||
const { _localParticipant, _remoteParticipants } = this.props;
|
||||
const { searchString } = this.state;
|
||||
const { _localParticipant, _remoteParticipants, searchString } = this.props;
|
||||
const participant = item === _localParticipant?.id ? _localParticipant : _remoteParticipants.get(item);
|
||||
const displayName = participant?.name || '';
|
||||
|
||||
if (displayName) {
|
||||
const names = normalizeAccents(displayName)
|
||||
.toLowerCase()
|
||||
.split(' ');
|
||||
const lowerCaseSearch = normalizeAccents(searchString).toLowerCase();
|
||||
|
||||
for (const name of names) {
|
||||
if (lowerCaseSearch === '' || name.startsWith(lowerCaseSearch)) {
|
||||
return (
|
||||
<MeetingParticipantItem
|
||||
key = { item }
|
||||
participant = { participant } />
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (displayName === '' && searchString === '') {
|
||||
if (participantMatchesSearch(participant, searchString)) {
|
||||
return (
|
||||
<MeetingParticipantItem
|
||||
key = { item }
|
||||
@@ -179,9 +163,7 @@ class MeetingParticipantList extends PureComponent<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSearchStringChange(text: string) {
|
||||
this.setState({
|
||||
searchString: text
|
||||
});
|
||||
this.props.setSearchString(text);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
@@ -36,6 +36,7 @@ import styles from './styles';
|
||||
*/
|
||||
const ParticipantsPane = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [ searchString, setSearchString ] = useState('');
|
||||
const openMoreMenu = useCallback(() => dispatch(openDialog(ContextMenuMore)), [ dispatch ]);
|
||||
const isLocalModerator = useSelector(isLocalParticipantModerator);
|
||||
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
|
||||
@@ -59,7 +60,9 @@ const ParticipantsPane = () => {
|
||||
style = { styles.participantsPane }>
|
||||
<ScrollView bounces = { false }>
|
||||
<LobbyParticipantList />
|
||||
<MeetingParticipantList />
|
||||
<MeetingParticipantList
|
||||
searchString = { searchString }
|
||||
setSearchString = { setSearchString } />
|
||||
{!inBreakoutRoom
|
||||
&& isLocalModerator
|
||||
&& participantsCount > 2
|
||||
@@ -69,7 +72,8 @@ const ParticipantsPane = () => {
|
||||
{_isBreakoutRoomsSupported
|
||||
&& rooms.map(room => (<CollapsibleRoom
|
||||
key = { room.id }
|
||||
room = { room } />))}
|
||||
room = { room }
|
||||
searchString = { searchString } />))}
|
||||
{_isBreakoutRoomsSupported && !hideAddRoomButton && isLocalModerator
|
||||
&& <AddBreakoutRoomButton />}
|
||||
</ScrollView>
|
||||
|
||||
@@ -8,7 +8,7 @@ import styles from './styles';
|
||||
export const RaisedHandIndicator = () => (
|
||||
<View style = { styles.raisedHandIndicator }>
|
||||
<Icon
|
||||
size = { 15 }
|
||||
size = { 16 }
|
||||
src = { IconRaisedHandHollow }
|
||||
style = { styles.raisedHandIcon } />
|
||||
</View>
|
||||
|
||||
@@ -195,7 +195,8 @@ export default {
|
||||
|
||||
raisedHandIcon: {
|
||||
...flexContent,
|
||||
top: BaseTheme.spacing[1]
|
||||
top: BaseTheme.spacing[1],
|
||||
color: BaseTheme.palette.uiBackground
|
||||
},
|
||||
|
||||
lobbyListContainer: {
|
||||
|
||||
@@ -19,12 +19,12 @@ import {
|
||||
isParticipantAudioMuted,
|
||||
isParticipantVideoMuted
|
||||
} from '../../../base/tracks';
|
||||
import { normalizeAccents } from '../../../base/util/strings';
|
||||
import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
|
||||
import {
|
||||
getParticipantAudioMediaState,
|
||||
getParticipantVideoMediaState,
|
||||
getQuickActionButtonType
|
||||
getQuickActionButtonType,
|
||||
participantMatchesSearch
|
||||
} from '../../functions';
|
||||
|
||||
import ParticipantActionEllipsis from './ParticipantActionEllipsis';
|
||||
@@ -300,22 +300,7 @@ function _mapStateToProps(state, ownProps): Object {
|
||||
|
||||
const _displayName = getParticipantDisplayName(state, participant?.id);
|
||||
|
||||
let _matchesSearch = false;
|
||||
const names = normalizeAccents(_displayName)
|
||||
.toLowerCase()
|
||||
.split(' ');
|
||||
const lowerCaseSearchString = searchString.toLowerCase();
|
||||
|
||||
if (lowerCaseSearchString === '') {
|
||||
_matchesSearch = true;
|
||||
} else {
|
||||
for (const name of names) {
|
||||
if (name.startsWith(lowerCaseSearchString)) {
|
||||
_matchesSearch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const _matchesSearch = participantMatchesSearch(participant, searchString);
|
||||
|
||||
const _isAudioMuted = isParticipantAudioMuted(participant, state);
|
||||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
@@ -45,8 +45,10 @@ const useStyles = makeStyles(theme => {
|
||||
type Props = {
|
||||
currentRoom: ?Object,
|
||||
participantsCount: number,
|
||||
showInviteButton: boolean,
|
||||
overflowDrawer: boolean,
|
||||
searchString: string,
|
||||
setSearchString: Function,
|
||||
showInviteButton: boolean,
|
||||
sortedParticipantIds: Array<string>
|
||||
};
|
||||
|
||||
@@ -64,11 +66,12 @@ function MeetingParticipants({
|
||||
currentRoom,
|
||||
overflowDrawer,
|
||||
participantsCount,
|
||||
searchString,
|
||||
setSearchString,
|
||||
showInviteButton,
|
||||
sortedParticipantIds = []
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const [ searchString, setSearchString ] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [ lowerMenu, , toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu();
|
||||
@@ -104,7 +107,8 @@ function MeetingParticipants({
|
||||
{showInviteButton && <InviteButton />}
|
||||
<ClearableInput
|
||||
onChange = { setSearchString }
|
||||
placeholder = { t('participantsPane.search') } />
|
||||
placeholder = { t('participantsPane.search') }
|
||||
value = { searchString } />
|
||||
<div>
|
||||
<MeetingParticipantItems
|
||||
askUnmuteText = { askUnmuteText }
|
||||
|
||||
@@ -75,6 +75,11 @@ type State = {
|
||||
* Indicates if the footer context menu is open.
|
||||
*/
|
||||
contextOpen: boolean,
|
||||
|
||||
/**
|
||||
* Participants search string.
|
||||
*/
|
||||
searchString: string
|
||||
};
|
||||
|
||||
const styles = theme => {
|
||||
@@ -152,7 +157,8 @@ class ParticipantsPane extends Component<Props, State> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contextOpen: false
|
||||
contextOpen: false,
|
||||
searchString: ''
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
@@ -162,6 +168,7 @@ class ParticipantsPane extends Component<Props, State> {
|
||||
this._onMuteAll = this._onMuteAll.bind(this);
|
||||
this._onToggleContext = this._onToggleContext.bind(this);
|
||||
this._onWindowClickListener = this._onWindowClickListener.bind(this);
|
||||
this.setSearchString = this.setSearchString.bind(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +204,7 @@ class ParticipantsPane extends Component<Props, State> {
|
||||
classes,
|
||||
t
|
||||
} = this.props;
|
||||
const { contextOpen } = this.state;
|
||||
const { contextOpen, searchString } = this.state;
|
||||
|
||||
// when the pane is not open optimize to not
|
||||
// execute the MeetingParticipantList render for large list of participants
|
||||
@@ -224,8 +231,10 @@ class ParticipantsPane extends Component<Props, State> {
|
||||
<div className = { classes.container }>
|
||||
<LobbyParticipants />
|
||||
<br className = { classes.antiCollapse } />
|
||||
<MeetingParticipants />
|
||||
{_isBreakoutRoomsSupported && <RoomList />}
|
||||
<MeetingParticipants
|
||||
searchString = { searchString }
|
||||
setSearchString = { this.setSearchString } />
|
||||
{_isBreakoutRoomsSupported && <RoomList searchString = { searchString } />}
|
||||
{_showAddRoomButton && <AddBreakoutRoomButton />}
|
||||
</div>
|
||||
{_showFooter && (
|
||||
@@ -255,6 +264,20 @@ class ParticipantsPane extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
setSearchString: (string) => void;
|
||||
|
||||
/**
|
||||
* Sets the search string.
|
||||
*
|
||||
* @param {string} newSearchString - The new search string.
|
||||
* @returns {void}
|
||||
*/
|
||||
setSearchString(newSearchString) {
|
||||
this.setState({
|
||||
searchString: newSearchString
|
||||
});
|
||||
}
|
||||
|
||||
_onClosePane: () => void;
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,22 +25,6 @@ class ParticipantsPaneButton extends AbstractButton<Props, *> {
|
||||
label = 'toolbar.participants';
|
||||
tooltip = 'toolbar.participants';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
|
||||
@@ -4,11 +4,12 @@ import { makeStyles } from '@material-ui/styles';
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, IconRaisedHandHollow } from '../../../base/icons';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme';
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
return {
|
||||
indicator: {
|
||||
backgroundColor: theme.palette.warning01,
|
||||
backgroundColor: theme.palette.warning02,
|
||||
borderRadius: `${theme.shape.borderRadius / 2}px`,
|
||||
height: '24px',
|
||||
width: '24px'
|
||||
@@ -22,7 +23,8 @@ export const RaisedHandIndicator = () => {
|
||||
return (
|
||||
<div className = { styles.indicator }>
|
||||
<Icon
|
||||
size = { 15 }
|
||||
color = { BaseTheme.palette.uiBackground }
|
||||
size = { 16 }
|
||||
src = { IconRaisedHandHollow } />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getRaiseHandsQueue
|
||||
} from '../base/participants/functions';
|
||||
import { toState } from '../base/redux';
|
||||
import { normalizeAccents } from '../base/util/strings';
|
||||
import { isInBreakoutRoom } from '../breakout-rooms/functions';
|
||||
|
||||
import { QUICK_ACTION_BUTTON, REDUCER_KEY, MEDIA_STATE } from './constants';
|
||||
@@ -242,3 +243,28 @@ export function getSortedParticipantIds(stateful: Object | Function): Array<stri
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a participant matches the search string.
|
||||
*
|
||||
* @param {Object} participant - The participant to be checked.
|
||||
* @param {string} searchString - The participants search string.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function participantMatchesSearch(participant: Object, searchString: string) {
|
||||
if (searchString === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const names = normalizeAccents(participant?.name || participant?.displayName || '')
|
||||
.toLowerCase()
|
||||
.split(' ');
|
||||
const lowerCaseSearchString = searchString.toLowerCase();
|
||||
|
||||
for (const name of names) {
|
||||
if (name.startsWith(lowerCaseSearchString)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
*/
|
||||
export const CHANGE_VOTE = 'CHANGE_VOTE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals that we need to clear all polls from the state.
|
||||
* For example we are moving to another conference.
|
||||
*
|
||||
* {
|
||||
* type: CLEAR_POLLS
|
||||
* }
|
||||
*/
|
||||
export const CLEAR_POLLS = 'CLEAR_POLLS';
|
||||
|
||||
/**
|
||||
* The type of the action which signals that a new Poll was received.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
CHANGE_VOTE,
|
||||
CLEAR_POLLS,
|
||||
RESET_NB_UNREAD_POLLS,
|
||||
RECEIVE_ANSWER,
|
||||
RECEIVE_POLL,
|
||||
@@ -10,6 +11,17 @@ import {
|
||||
} from './actionTypes';
|
||||
import type { Answer, Poll } from './types';
|
||||
|
||||
/**
|
||||
* Action to signal that existing polls needs to be cleared from state.
|
||||
*
|
||||
* @returns {{
|
||||
* type: CLEAR_POLLS
|
||||
* }}
|
||||
*/
|
||||
export const clearPolls = () => {
|
||||
return { type: CLEAR_POLLS };
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to signal that a poll's vote will be changed.
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
CHANGE_VOTE,
|
||||
CLEAR_POLLS,
|
||||
RECEIVE_POLL,
|
||||
RECEIVE_ANSWER,
|
||||
REGISTER_VOTE,
|
||||
@@ -38,6 +39,13 @@ ReducerRegistry.register('features/polls', (state = INITIAL_STATE, action) => {
|
||||
};
|
||||
}
|
||||
|
||||
case CLEAR_POLLS: {
|
||||
return {
|
||||
...state,
|
||||
...INITIAL_STATE
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer triggered when a poll is received
|
||||
case RECEIVE_POLL: {
|
||||
const newState = {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
showNotification
|
||||
} from '../notifications';
|
||||
|
||||
import { receiveAnswer, receivePoll } from './actions';
|
||||
import { clearPolls, receiveAnswer, receivePoll } from './actions';
|
||||
import { COMMAND_NEW_POLL, COMMAND_ANSWER_POLL, COMMAND_OLD_POLLS } from './constants';
|
||||
import type { Answer, Poll } from './types';
|
||||
|
||||
@@ -83,7 +83,7 @@ StateListenerRegistry.register(
|
||||
appearance: NOTIFICATION_TYPE.NORMAL,
|
||||
titleKey: 'polls.notification.title',
|
||||
descriptionKey: 'polls.notification.description'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
break;
|
||||
|
||||
}
|
||||
@@ -122,6 +122,9 @@ StateListenerRegistry.register(
|
||||
|
||||
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, receiveMessage);
|
||||
conference.on(JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED, receiveMessage);
|
||||
|
||||
// clean old polls
|
||||
store.dispatch(clearPolls());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user