Compare commits

...

20 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
18c6486c5b chore(rn,version) bump SDK version to 3.3.0 2021-04-01 11:41:37 +02:00
tmoldovan8x8
e4b34e1c89 feat(rn): makes InputDialog textInput autoFocus 2021-03-26 10:51:47 +02:00
damencho
0067f6b077 fix: Fixes lobby when allowners is enabled. 2021-03-25 15:20:49 -06:00
JohnProv
989044b3a9 fix(lang) update Dutch translation 2021-03-25 17:43:52 +01:00
Mihai-Andrei Uscat
a78ca5fcad feat(external_api): Add command for toggling localFlipX 2021-03-25 14:57:41 +02:00
Mihai-Andrei Uscat
1ad40de487 feat(external_api): Add command for toggling camera on mobile web 2021-03-25 13:48:49 +02:00
ggalperi
2c9078985f fix(lang) fix typo in Russian translation
Fixed typo
2021-03-24 15:59:45 -06:00
Tudor D. Pop
77ee4b13e1 feat(virtual-backgrounds) add ability to upload custom images 2021-03-24 17:32:45 +01:00
Jaya Allamsetty
a3a2ce3875 feat(rn,polyfill): Add a polyfill for Promise.allSettled.
Promise.allSettled is supported from RN 0.63 onwards and is not supported on the current version, use a polyfill for that shims Promise.allSettled if its unavailable or noncompliant.

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2021-03-24 11:59:52 -04:00
Saúl Ibarra Corretgé
e0c77dcd95 feat(tile-view) allow to toggle tile view while alone 2021-03-24 16:43:50 +01:00
Calinteodor
e035d33fa9 feat(authentication) refactor auth dialogs to use React 2021-03-24 15:09:40 +01:00
Kylian Kropf
11202595bd fix(lang) update Dutch translation 2021-03-24 11:16:32 +01:00
Jaya Allamsetty
415670e24b chore(deps) lib-jitsi-meet@latest
* fix(TPC): get ssrc info per ssrc and not per mline.
* feat: Consider absence of A/V muted from presence as muted.
* Feature: Moderator can revoke moderator role to others and himself (#1532)

4191198233...0e180efdfa
2021-03-23 18:11:23 -04:00
Izak Glasenčnik
05f3b4390d feat(iFrame): Emit event when recording status changes, including errors (#7973)
* feat(iFrame): Emit event when recording status changes, including errors

* Fix APP access on mobile
2021-03-23 11:35:46 -05:00
Saúl Ibarra Corretgé
cff0a619f5 fix(interfaceConfig) mark as deprecated 2021-03-23 16:59:46 +01:00
hmuresan
f7c0d4f1fe feat(background alpha) Set background transparency 2021-03-23 16:16:56 +02:00
TigiBoom
8fccb05519 fix(lang) fix typo in Russian translation 2021-03-23 14:47:25 +01:00
Vlad Piersec
b4155ab6d2 fix(toolbox): Add missing lang key for video settings 2021-03-23 15:38:15 +02:00
tmoldovan8x8
a1d3870634 feat(external_api): add videoMuted event and action (#8862) 2021-03-23 15:30:17 +02:00
hmuresan
07f16a7a51 feat (external-api) Add command for setting tile view mode 2021-03-23 15:21:57 +02:00
64 changed files with 1840 additions and 464 deletions

View File

@@ -26,4 +26,4 @@ android.useAndroidX=true
android.enableJetifier=true
appVersion=21.0.0
sdkVersion=3.2.0
sdkVersion=3.3.0

View File

@@ -36,8 +36,17 @@ public class BroadcastAction {
for (String key : this.data.keySet()) {
try {
// TODO add support for different types of objects
nativeMap.putString(key, this.data.get(key).toString());
if (this.data.get(key) instanceof Boolean) {
nativeMap.putBoolean(key, (Boolean) this.data.get(key));
} else if (this.data.get(key) instanceof Integer) {
nativeMap.putInt(key, (Integer) this.data.get(key));
} else if (this.data.get(key) instanceof Double) {
nativeMap.putDouble(key, (Double) this.data.get(key));
} else if (this.data.get(key) instanceof String) {
nativeMap.putString(key, (String) this.data.get(key));
} else {
throw new Exception("Unsupported extra data type");
}
} catch (Exception e) {
JitsiMeetLogger.w(TAG + " invalid extra data in event", e);
}
@@ -66,7 +75,8 @@ public class BroadcastAction {
RETRIEVE_PARTICIPANTS_INFO("org.jitsi.meet.RETRIEVE_PARTICIPANTS_INFO"),
OPEN_CHAT("org.jitsi.meet.OPEN_CHAT"),
CLOSE_CHAT("org.jitsi.meet.CLOSE_CHAT"),
SEND_CHAT_MESSAGE("org.jitsi.meet.SEND_CHAT_MESSAGE");
SEND_CHAT_MESSAGE("org.jitsi.meet.SEND_CHAT_MESSAGE"),
SET_VIDEO_MUTED("org.jitsi.meet.SET_VIDEO_MUTED");
private final String action;

View File

@@ -85,7 +85,9 @@ public class BroadcastEvent {
SCREEN_SHARE_TOGGLED("org.jitsi.meet.SCREEN_SHARE_TOGGLED"),
PARTICIPANTS_INFO_RETRIEVED("org.jitsi.meet.PARTICIPANTS_INFO_RETRIEVED"),
CHAT_MESSAGE_RECEIVED("org.jitsi.meet.CHAT_MESSAGE_RECEIVED"),
CHAT_TOGGLED("org.jitsi.meet.CHAT_TOGGLED");
CHAT_TOGGLED("org.jitsi.meet.CHAT_TOGGLED"),
VIDEO_MUTED_CHANGED("org.jitsi.meet.VIDEO_MUTED_CHANGED");
private static final String CONFERENCE_WILL_JOIN_NAME = "CONFERENCE_WILL_JOIN";
private static final String CONFERENCE_JOINED_NAME = "CONFERENCE_JOINED";
@@ -98,6 +100,7 @@ public class BroadcastEvent {
private static final String PARTICIPANTS_INFO_RETRIEVED_NAME = "PARTICIPANTS_INFO_RETRIEVED";
private static final String CHAT_MESSAGE_RECEIVED_NAME = "CHAT_MESSAGE_RECEIVED";
private static final String CHAT_TOGGLED_NAME = "CHAT_TOGGLED";
private static final String VIDEO_MUTED_CHANGED_NAME = "VIDEO_MUTED_CHANGED";
private final String action;
@@ -142,6 +145,8 @@ public class BroadcastEvent {
return CHAT_MESSAGE_RECEIVED;
case CHAT_TOGGLED_NAME:
return CHAT_TOGGLED;
case VIDEO_MUTED_CHANGED_NAME:
return VIDEO_MUTED_CHANGED;
}
return null;

View File

@@ -40,4 +40,10 @@ public class BroadcastIntentHelper {
intent.putExtra("message", message);
return intent;
}
public static Intent buildSetVideoMutedIntent(boolean muted) {
Intent intent = new Intent(BroadcastAction.Type.SET_VIDEO_MUTED.getAction());
intent.putExtra("muted", muted);
return intent;
}
}

View File

@@ -85,6 +85,7 @@ class ExternalAPIModule
constants.put("OPEN_CHAT", BroadcastAction.Type.OPEN_CHAT.getAction());
constants.put("CLOSE_CHAT", BroadcastAction.Type.CLOSE_CHAT.getAction());
constants.put("SEND_CHAT_MESSAGE", BroadcastAction.Type.SEND_CHAT_MESSAGE.getAction());
constants.put("SET_VIDEO_MUTED", BroadcastAction.Type.SET_VIDEO_MUTED.getAction());
return constants;
}

View File

@@ -309,11 +309,6 @@ class ConferenceConnector {
room.join();
}, 5000);
const { password }
= APP.store.getState()['features/base/conference'];
AuthHandler.requireAuth(room, password);
break;
}
@@ -378,7 +373,6 @@ class ConferenceConnector {
if (this.reconnectTimeout !== null) {
clearTimeout(this.reconnectTimeout);
}
AuthHandler.closeAuth();
}
/**
@@ -2242,7 +2236,7 @@ export default {
});
APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
AuthHandler.authenticate(room);
AuthHandler.authenticateExternal(room);
});
APP.UI.addListener(

View File

@@ -677,6 +677,9 @@ var config = {
*/
// dynamicBrandingUrl: '',
// Sets the background transparency level. '0' is fully transparent, '1' is opaque.
// backgroundAlpha: 1,
// The URL of the moderated rooms microservice, if available. If it
// is present, a link to the service will be rendered on the welcome page,
// otherwise the app doesn't render it.

View File

@@ -3,18 +3,21 @@
import { jitsiLocalStorage } from '@jitsi/js-utils';
import Logger from 'jitsi-meet-logger';
import AuthHandler from './modules/UI/authentication/AuthHandler';
import { redirectToTokenAuthService } from './modules/UI/authentication/AuthHandler';
import { hideLoginDialog } from './react/features/authentication/actions.web';
import { LoginDialog } from './react/features/authentication/components';
import { isTokenAuthEnabled } from './react/features/authentication/functions';
import {
connectionEstablished,
connectionFailed
} from './react/features/base/connection/actions';
import { openDialog } from './react/features/base/dialog/actions';
import {
isFatalJitsiConnectionError,
JitsiConnectionErrors,
JitsiConnectionEvents
} from './react/features/base/lib-jitsi-meet';
import { setPrejoinDisplayNameRequired } from './react/features/prejoin/actions';
const logger = Logger.getLogger(__filename);
/**
@@ -80,7 +83,7 @@ function checkForAttachParametersAndConnect(id, password, connection) {
* @returns {Promise<JitsiConnection>} connection if
* everything is ok, else error.
*/
function connect(id, password, roomName) {
export function connect(id, password, roomName) {
const connectionConfig = Object.assign({}, config);
const { jwt } = APP.store.getState()['features/base/jwt'];
@@ -214,10 +217,39 @@ export function openConnection({ id, password, retry, roomName }) {
const { jwt } = APP.store.getState()['features/base/jwt'];
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED && !jwt) {
return AuthHandler.requestAuth(roomName, connect);
return requestAuth(roomName);
}
}
throw err;
});
}
/**
* Show Authentication Dialog and try to connect with new credentials.
* If failed to connect because of PASSWORD_REQUIRED error
* then ask for password again.
* @param {string} [roomName] name of the conference room
*
* @returns {Promise<JitsiConnection>}
*/
function requestAuth(roomName) {
const config = APP.store.getState()['features/base/config'];
if (isTokenAuthEnabled(config)) {
// This Promise never resolves as user gets redirected to another URL
return new Promise(() => redirectToTokenAuthService(roomName));
}
return new Promise(resolve => {
const onSuccess = connection => {
APP.store.dispatch(hideLoginDialog());
resolve(connection);
};
APP.store.dispatch(
openDialog(LoginDialog, { onSuccess,
roomName })
);
});
}

View File

@@ -1,25 +1,64 @@
.virtual-background-dialog{
display: inline-flex;
cursor: pointer;
.thumbnail{
object-fit: cover;
padding: 5px;
height: 40px;
width: 40px;
}
.thumbnail-selected{
object-fit: cover;
padding: 5px;
height: 40px;
width: 40px;
border: 2px solid #a4b8d1;
}
.blur-selected{
border: 2px solid #a4b8d1;
}
.virtual-background-none{
.virtual-background-dialog {
display: inline-grid;
grid-template-columns: auto auto auto auto auto auto auto;
max-width: 370px;
cursor: pointer;
.thumbnail {
border-radius: 10px;
object-fit: cover;
padding: 5px;
height: 40px;
width: 40px;
}
.thumbnail:hover ~ .delete-image-icon {
display: block;
}
.thumbnail-selected {
border-radius: 10px;
object-fit: cover;
padding: 5px;
height: 40px;
width: 40px;
border: 2px solid #a4b8d1;
}
.blur-selected {
border-radius: 10px;
border: 2px solid #a4b8d1;
}
.virtual-background-none {
font-weight: bold;
padding: 5px;
height: 34px;
width: 34px;
border-radius: 10px;
border: 1px solid #a4b8d1;
text-align: center;
vertical-align: middle;
line-height: 35px;
margin-right: 5px;
}
.none-selected {
font-weight: bold;
padding: 5px;
height: 34px;
width: 34px;
border-radius: 10px;
border: 2px solid #a4b8d1;
text-align: center;
vertical-align: middle;
line-height: 35px;
margin-right: 5px;
}
}
.file-upload-btn {
display: none;
}
.custom-file-upload {
font-size: x-large;
font-weight: bold;
padding: 5px;
display: inline-block;
padding: 4px;
height: 35px;
width: 35px;
border-radius: 10px;
@@ -27,18 +66,24 @@
text-align: center;
vertical-align: middle;
line-height: 35px;
margin-right: 5px;
}
.none-selected{
font-weight: bold;
padding: 5px;
height: 35px;
width: 35px;
border-radius: 10px;
border: 2px solid #a4b8d1;
text-align: center;
vertical-align: middle;
line-height: 35px;
margin-right: 5px;
}
}
margin-left: 5px;
cursor: pointer;
}
.delete-image-icon {
position: absolute;
display: none;
left: 36;
bottom: 36;
}
.delete-image-icon:hover {
display: block;
}
.thumbnail-container {
position: relative;
}
.loading-content-text{
margin-right: 15px;
}

View File

@@ -1,6 +1,13 @@
/* eslint-disable no-unused-vars, no-var, max-len */
/* eslint sort-keys: ["error", "asc", {"caseSensitive": false}] */
/**
* !!!IMPORTANT!!!
*
* This file is considered deprecated. All options will eventually be moved to
* config.js, and no new options should be added here.
*/
var interfaceConfig = {
APP_NAME: 'Jitsi Meet',
AUDIO_LEVEL_PRIMARY_COLOR: 'rgba(255,255,255,0.4)',

View File

@@ -131,6 +131,10 @@
NSLog(@"%@%@", @"Chat toggled: ", data);
}
- (void)videoMutedChanged:(NSDictionary *)data {
NSLog(@"%@%@", @"Video muted changed: ", data[@"muted"]);
}
#pragma mark - Helpers
- (void)terminate {

View File

@@ -26,5 +26,6 @@
- (void)openChat:(NSString*)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString*)message :(NSString*)to ;
- (void)sendSetVideoMuted:(BOOL)muted;
@end

View File

@@ -26,6 +26,7 @@ static NSString * const retrieveParticipantsInfoAction = @"org.jitsi.meet.RETRIE
static NSString * const openChatAction = @"org.jitsi.meet.OPEN_CHAT";
static NSString * const closeChatAction = @"org.jitsi.meet.CLOSE_CHAT";
static NSString * const sendChatMessageAction = @"org.jitsi.meet.SEND_CHAT_MESSAGE";
static NSString * const setVideoMutedAction = @"org.jitsi.meet.SET_VIDEO_MUTED";
@implementation ExternalAPI
@@ -47,7 +48,8 @@ RCT_EXPORT_MODULE();
@"RETRIEVE_PARTICIPANTS_INFO": retrieveParticipantsInfoAction,
@"OPEN_CHAT": openChatAction,
@"CLOSE_CHAT": closeChatAction,
@"SEND_CHAT_MESSAGE": sendChatMessageAction
@"SEND_CHAT_MESSAGE": sendChatMessageAction,
@"SET_VIDEO_MUTED" : setVideoMutedAction
};
};
@@ -70,7 +72,8 @@ RCT_EXPORT_MODULE();
retrieveParticipantsInfoAction,
openChatAction,
closeChatAction,
sendChatMessageAction
sendChatMessageAction,
setVideoMutedAction
];
}
@@ -193,4 +196,11 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:sendChatMessageAction body:data];
}
- (void)sendSetVideoMuted:(BOOL)muted {
NSDictionary *data = @{ @"muted": [NSNumber numberWithBool:muted]};
[self sendEventWithName:setVideoMutedAction body:data];
}
@end

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>3.2.0</string>
<string>3.3.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -44,5 +44,6 @@
- (void)openChat:(NSString * _Nullable)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)setVideoMuted:(BOOL)muted;
@end

View File

@@ -155,6 +155,11 @@ static void initializeViewsMap() {
[externalAPI sendChatMessage:message :to];
}
- (void)setVideoMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetVideoMuted:muted];
}
#pragma mark Private methods
/**

View File

@@ -104,4 +104,11 @@
*/
- (void)chatToggled:(NSDictionary *)data;
/**
* Called when videoMuted state changed.
*
* The `data` dictionary contains a `muted` key with state of the videoMuted for the localParticipant.
*/
- (void)videoMutedChanged:(NSDictionary *)data;
@end

View File

@@ -61,6 +61,7 @@
"today": "Vandaag"
},
"chat": {
"enter": "Chat openen",
"error": "Fout: uw bericht \"{{originalText}}\" is niet verzonden. Reden: {{error}}",
"fieldPlaceHolder": "Typ hier uw bericht",
"messagebox": "Typ een bericht",
@@ -101,6 +102,8 @@
"address": "Adres:",
"bandwidth": "Geschatte bandbreedte:",
"bitrate": "Bitrate:",
"audio_ssrc": "Audio SSRC:",
"codecs": "Codecs (A/V): ",
"bridgeCount": "Aantal servers: ",
"connectedTo": "Verbonden met:",
"e2e_rtt": "E2E RTT:",
@@ -125,9 +128,12 @@
"remoteport": "Externe poort:",
"remoteport_plural": "Externe poorten:",
"resolution": "Resolutie:",
"savelogs": "Logs opslaan",
"participant_id": "Deelnemer id:",
"status": "Verbinding:",
"transport": "Transport:",
"transport_plural": "Transporten:"
"transport_plural": "Transporten:",
"video_ssrc": "Video SSRC:"
},
"dateUtils": {
"earlier": "Eerder",
@@ -174,6 +180,7 @@
"cameraNotFoundError": "Camera niet gevonden.",
"cameraNotSendingData": "Er is geen toegang tot uw camera verkregen. Controleer of dit apparaat wordt gebruikt door een andere toepassing, selecteer een ander apparaat vanuit de instellingen of probeer de toepassing te herladen.",
"cameraNotSendingDataTitle": "Geen toegang tot camera",
"cameraTimeoutError": "Er heeft een camera timeout opgetreden.",
"cameraPermissionDeniedError": "U hebt geen toestemming verleend om uw camera te gebruiken. U kunt wel deelnemen aan de vergadering, maar anderen kunnen u niet zien. Gebruik de cameraknop in de adresbalk om dit op te lossen.",
"cameraUnknownError": "Kan de camera om een onbekende reden niet gebruiken.",
"cameraUnsupportedResolutionError": "Uw camera ondersteunt de vereiste videoresolutie niet.",
@@ -190,6 +197,7 @@
"connectErrorWithMsg": "Oeps! Er is iets misgegaan en er kon geen verbinding met de vergadering worden gemaakt: {{msg}}",
"connecting": "Verbinding maken",
"contactSupport": "Contact opnemen met ondersteuning",
"copied": "Gekopieerd",
"copy": "Kopiëren",
"dismiss": "Negeren",
"displayNameRequired": "Hallo! Wat is uw naam?",
@@ -232,6 +240,7 @@
"micNotSendingDataTitle": "Uw microfoon is gedempt door uw systeeminstellingen",
"micPermissionDeniedError": "U hebt geen toestemming verleend om uw microfoon te gebruiken. U kunt wel deelnemen aan de vergadering, maar anderen kunnen u niet horen. Gebruik de cameraknop in de adresbalk om dit op te lossen.",
"micUnknownError": "Kan de microfoon om een onbekende reden niet gebruiken.",
"micTimeoutError": "Kan de microfoon niet gebruiken vanwege een timeout fout.",
"muteEveryoneElseDialog": "Eenmaal gedempt kunt u het dempen niet opheffen, maar zij kunnen dit wel ieder moment zelf doen.",
"muteEveryoneElseTitle": "Iedereen dempen behalve {{whom}}?",
"muteEveryoneDialog": "Weet u zeker dat u iedereen wilt dempen? U kunt het dempen niet opheffen, maar zij kunnen dit wel ieder moment zelf doen.",
@@ -242,6 +251,13 @@
"muteParticipantButton": "Dempen",
"muteParticipantDialog": "Weet u zeker dat u deze deelnemer wilt dempen? U kunt het dempen niet opheffen, maar deze deelnemer kan dit wel ieder moment zelf doen.",
"muteParticipantTitle": "Deze deelnemer dempen?",
"muteEveryoneElsesVideoDialog": "Als u de camera's uitzet kan u hem niet meer aanzetten, maar de andere deelnemers kunnen dit wel ieder moment zelf doen.",
"muteEveryoneElsesVideoTitle": "De camera van iedereen behalve {{whom}} uitzetten?",
"muteEveryonesVideoDialog": "Weet u zeker dat u iedereen zijn camera uit wilt zetten? Als u de camera's uitzet kan u hem niet meer aanzetten, maar de andere deelnemers kunnen dit wel ieder moment zelf doen.",
"muteEveryonesVideoTitle": "Iedereen zijn camera uitzetten?",
"muteParticipantsVideoButton": "Camera uitzetten",
"muteParticipantsVideoTitle": "Camera van deze deelnemer uitzetten?",
"muteParticipantsVideoBody": "Het is niet mogelijk voor u om de camera weer aan te zetten, de deelnemer kan de camera wel weer aanzetten.",
"Ok": "OK",
"passwordLabel": "De vergadering is vergrendeld door een deelnemer. Voer het $t(lockRoomPassword) in om deel te nemen.",
"passwordNotSupported": "Instellen van een $t(lockRoomPassword) voor de vergadering wordt niet ondersteund.",
@@ -279,6 +295,7 @@
"sendPrivateMessageTitle": "Privé versturen?",
"serviceUnavailable": "Service niet beschikbaar",
"sessTerminated": "Gesprek beëindigd",
"sessionRestarted": "Gesprek herstart door de server",
"Share": "Delen",
"shareVideoLinkError": "Geef een juiste YouTube-link op",
"shareVideoTitle": "Een video delen",
@@ -300,7 +317,9 @@
"tokenAuthFailedTitle": "Authenticering mislukt",
"transcribing": "Transcriberen",
"unlockRoom": "$t(lockRoomPasswordUppercase) voor vergadering verwijderen",
"user": "gebruiker",
"userPassword": "gebruikerswachtwoord",
"videoLink": "Video link",
"WaitForHostMsg": "De vergadering <b>{{room}}</b> is nog niet gestart. Authenticeer uzelf als u de host bent. Anders wacht u tot de host aanwezig is.",
"WaitForHostMsgWOk": "De vergadering <b>{{room}}</b> is nog niet gestart. Als u de host bent, drukt u op 'OK' om uzelf te authenticeren. Anders wacht u tot de host aanwezig is.",
"WaitingForHost": "Wachten op de host...",
@@ -316,6 +335,9 @@
"e2ee": {
"labelToolTip": "Audio- en Videocommunicatie in dit gesprek is eind-tot-eind-versleuteld"
},
"embedMeeting": {
"title": "Deze vergadering embedden"
},
"feedback": {
"average": "Gemiddeld",
"bad": "Slecht",
@@ -477,6 +499,8 @@
"mutedTitle": "U bent gedempt!",
"mutedRemotelyTitle": "U bent gedempt door {{participantDisplayName}}!",
"mutedRemotelyDescription": "U kunt het dempen altijd opheffen wanneer u klaar bent om te spreken. Demp opnieuw wanneer u klaar bent, om ruis buiten de vergadering te houden.",
"videoMutedRemotelyTitle": "Uw camera is uitgezet door {{participantDisplayName}}!",
"videoMutedRemotelyDescription": "U kan hem ten alle tijden weer aanzetten.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) verwijderd door een andere deelnemer",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) ingesteld door een ander deelnemer",
"raisedHand": "{{name}} zou graag willen spreken.",
@@ -501,6 +525,7 @@
"audioAndVideoError": "Audio- en videofout:",
"audioOnlyError": "Audiofout:",
"audioTrackError": "Kon audiotrack niet aanmaken.",
"audioDeviceProblem": "Er is een probleem met uw microfoon",
"callMe": "Bel me",
"callMeAtNumber": "Bel me op dit nummer:",
"configuringDevices": "Apparaten instellen...",
@@ -746,7 +771,7 @@
"profile": "Uw profiel bewerken",
"raiseHand": "Uw hand opsteken / laten zakken",
"raiseYourHand": "Uw hand opsteken",
"security": "Beveiligingsoptions",
"security": "Beveiligingsopties",
"Settings": "Instellingen",
"sharedvideo": "Een YouTube-video delen",
"shareRoom": "Iemand uitnodigen",

View File

@@ -4,7 +4,7 @@
"addContacts": "Пригласите других людей",
"copyInvite": "Скопировать приглашение на встречу",
"copyLink": "Скопировать ссылку на встречу",
"copyStream": "Скопировать ссылку на прямую транасляцию",
"copyStream": "Скопировать ссылку на прямую трансляцию",
"countryNotSupported": "Эта страна пока не поддерживается.",
"countryReminder": "Вызов не в США? Пожалуйста, убедитесь, что указали код страны!",
"defaultEmail": "Ваш адрес электронной почты",
@@ -472,7 +472,7 @@
"knockingParticipantList": "Список ожидающих участников",
"nameField": "Введите ваше имя",
"notificationLobbyAccessDenied": "{{originParticipantName}} запретил присоединиться {{targetParticipantName}}",
"notificationLobbyAccessGranted": "{{originParticipantName}}разрешил присоединиться {{targetParticipantName}} ",
"notificationLobbyAccessGranted": "{{originParticipantName}} разрешил присоединиться {{targetParticipantName}} ",
"notificationLobbyDisabled": "Лобби отключено пользователем {{originParticipantName}}",
"notificationLobbyEnabled": "Лобби включено пользователем {{originParticipantName}}",
"notificationTitle": "Лобби",

View File

@@ -175,6 +175,7 @@
"alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
"applicationWindow": "Application window",
"authenticationRequired": "Authentication required",
"Back": "Back",
"cameraConstraintFailedError": "Your camera does not satisfy some of the required constraints.",
"cameraNotFoundError": "Camera was not found.",
@@ -227,6 +228,7 @@
"lockRoom": "Add meeting $t(lockRoomPasswordUppercase)",
"lockTitle": "Lock failed",
"logoutQuestion": "Are you sure you want to logout and stop the conference?",
"login": "Login",
"logoutTitle": "Logout",
"maxUsersLimitReached": "The limit for maximum number of participants has been reached. The conference is full. Please contact the meeting owner or try again later!",
"maxUsersLimitReachedTitle": "Maximum participants limit reached",
@@ -312,12 +314,13 @@
"tokenAuthFailedTitle": "Authentication failed",
"transcribing": "Transcribing",
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
"user": "user",
"userPassword": "user password",
"user": "User",
"userIdentifier": "User identifier",
"userPassword": "User password",
"videoLink": "Video link",
"WaitForHostMsg": "The conference <b>{{room}}</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
"WaitingForHost": "Waiting for the host ...",
"WaitingForHostTitle": "Waiting for the host ...",
"Yes": "Yes",
"yourEntireScreen": "Your entire screen"
},
@@ -336,7 +339,10 @@
"virtualBackground": {
"title": "Backgrounds",
"enableBlur": "Enable blur",
"removeBackground": "Remove background"
"removeBackground": "Remove background",
"uploadImage": "Upload image",
"pleaseWait": "Please wait...",
"none": "None"
},
"feedback": {
"average": "Average",
@@ -816,6 +822,7 @@
"tileViewToggle": "Toggle tile view",
"toggleCamera": "Toggle camera",
"videomute": "Start / Stop camera",
"videoSettings": "Video settings",
"selectBackground": "Select background"
},
"transcribing": {

View File

@@ -23,6 +23,8 @@ import {
pinParticipant,
kickParticipant
} from '../../react/features/base/participants';
import { updateSettings } from '../../react/features/base/settings';
import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
import { openChat } from '../../react/features/chat/actions.web';
import {
@@ -39,7 +41,7 @@ import {
import { toggleLobbyMode } from '../../react/features/lobby/actions.web';
import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession } from '../../react/features/recording/functions';
import { toggleTileView } from '../../react/features/video-layout';
import { toggleTileView, setTileView } from '../../react/features/video-layout';
import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality';
import { getJitsiMeetTransport } from '../transport';
@@ -169,6 +171,19 @@ function initCommands() {
sendAnalytics(createApiEvent('film.strip.toggled'));
APP.UI.toggleFilmstrip();
},
'toggle-camera': () => {
if (!isToggleCameraEnabled(APP.store.getState())) {
return;
}
APP.store.dispatch(toggleCamera());
},
'toggle-camera-mirror': () => {
const state = APP.store.getState();
const { localFlipX: currentFlipX } = state['features/base/settings'];
APP.store.dispatch(updateSettings({ localFlipX: !currentFlipX }));
},
'toggle-chat': () => {
sendAnalytics(createApiEvent('chat.toggled'));
APP.UI.toggleChat();
@@ -209,6 +224,9 @@ function initCommands() {
APP.store.dispatch(toggleTileView());
},
'set-tile-view': enabled => {
APP.store.dispatch(setTileView(enabled));
},
'video-hangup': (showFeedbackDialog = true) => {
sendAnalytics(createApiEvent('video.hangup'));
APP.conference.hangup(showFeedbackDialog);
@@ -1147,6 +1165,23 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that recording has started or stopped.
*
* @param {boolean} on - True if recording is on, false otherwise.
* @param {string} mode - Stream or file.
* @param {string} error - Error type or null if success.
* @returns {void}
*/
notifyRecordingStatusChanged(on: boolean, mode: string, error?: string) {
this._sendEvent({
name: 'recording-status-changed',
on,
mode,
error
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -44,12 +44,15 @@ const commands = {
sendEndpointTextMessage: 'send-endpoint-text-message',
sendTones: 'send-tones',
setLargeVideoParticipant: 'set-large-video-participant',
setTileView: 'set-tile-view',
setVideoQuality: 'set-video-quality',
startRecording: 'start-recording',
stopRecording: 'stop-recording',
subject: 'subject',
submitFeedback: 'submit-feedback',
toggleAudio: 'toggle-audio',
toggleCamera: 'toggle-camera',
toggleCameraMirror: 'toggle-camera-mirror',
toggleChat: 'toggle-chat',
toggleFilmStrip: 'toggle-film-strip',
toggleRaiseHand: 'toggle-raise-hand',
@@ -87,6 +90,7 @@ const events = {
'password-required': 'passwordRequired',
'proxy-connection-event': 'proxyConnectionEvent',
'raise-hand-updated': 'raiseHandUpdated',
'recording-status-changed': 'recordingStatusChanged',
'video-ready-to-close': 'readyToClose',
'video-conference-joined': 'videoConferenceJoined',
'video-conference-left': 'videoConferenceLeft',

View File

@@ -7,6 +7,7 @@ import EventEmitter from 'events';
import Logger from 'jitsi-meet-logger';
import { isMobileBrowser } from '../../react/features/base/environment/utils';
import { setColorAlpha } from '../../react/features/base/util';
import { toggleChat } from '../../react/features/chat';
import { setDocumentUrl } from '../../react/features/etherpad';
import { setFilmstripVisible } from '../../react/features/filmstrip';
@@ -129,6 +130,13 @@ UI.start = function() {
$('body').addClass('mobile-browser');
} else {
$('body').addClass('desktop-browser');
if (config.backgroundAlpha !== undefined) {
const backgroundColor = $('body').css('background-color');
const alphaColor = setColorAlpha(backgroundColor, config.backgroundAlpha);
$('body').css('background-color', alphaColor);
}
}
if (config.iAmRecorder) {
@@ -138,7 +146,6 @@ UI.start = function() {
}
APP.store.dispatch(setToolboxEnabled(false));
UI.messageHandler.enablePopups(false);
}
};

View File

@@ -1,25 +1,22 @@
/* global APP, config, JitsiMeetJS, Promise */
// @flow
import Logger from 'jitsi-meet-logger';
import { openConnection } from '../../../connection';
import { setJWT } from '../../../react/features/base/jwt';
import {
JitsiConnectionErrors
} from '../../../react/features/base/lib-jitsi-meet';
isTokenAuthEnabled,
getTokenAuthUrl
} from '../../../react/features/authentication/functions';
import { setJWT } from '../../../react/features/base/jwt';
import UIUtil from '../util/UIUtil';
import LoginDialog from './LoginDialog';
let externalAuthWindow;
declare var APP: Object;
const logger = Logger.getLogger(__filename);
let externalAuthWindow;
let authRequiredDialog;
const isTokenAuthEnabled
= typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length;
const getTokenAuthUrl
= JitsiMeetJS.util.AuthUtil.getTokenAuthUrl.bind(null, config.tokenAuthUrl);
/**
* Authenticate using external service or just focus
@@ -29,6 +26,8 @@ const getTokenAuthUrl
* @param {string} [lockPassword] password to use if the conference is locked
*/
function doExternalAuth(room, lockPassword) {
const config = APP.store.getState()['features/base/config'];
if (externalAuthWindow) {
externalAuthWindow.focus();
@@ -37,8 +36,8 @@ function doExternalAuth(room, lockPassword) {
if (room.isJoined()) {
let getUrl;
if (isTokenAuthEnabled) {
getUrl = Promise.resolve(getTokenAuthUrl(room.getName(), true));
if (isTokenAuthEnabled(config)) {
getUrl = Promise.resolve(getTokenAuthUrl(config)(room.getName(), true));
initJWTTokenListener(room);
} else {
getUrl = room.getExternalAuthUrl(true);
@@ -48,13 +47,13 @@ function doExternalAuth(room, lockPassword) {
url,
() => {
externalAuthWindow = null;
if (!isTokenAuthEnabled) {
if (!isTokenAuthEnabled(config)) {
room.join(lockPassword);
}
}
);
});
} else if (isTokenAuthEnabled) {
} else if (isTokenAuthEnabled(config)) {
redirectToTokenAuthService(room.getName());
} else {
room.getExternalAuthUrl().then(UIUtil.redirect);
@@ -67,10 +66,12 @@ function doExternalAuth(room, lockPassword) {
* back with "?jwt={the JWT token}" query parameter added.
* @param {string} [roomName] the name of the conference room.
*/
function redirectToTokenAuthService(roomName) {
export function redirectToTokenAuthService(roomName: string) {
const config = APP.store.getState()['features/base/config'];
// FIXME: This method will not preserve the other URL params that were
// originally passed.
UIUtil.redirect(getTokenAuthUrl(roomName, false));
UIUtil.redirect(getTokenAuthUrl(config)(roomName, false));
}
/**
@@ -157,58 +158,15 @@ function initJWTTokenListener(room) {
}
/**
* Authenticate on the server.
* @param {JitsiConference} room
* @param {string} [lockPassword] password to use if the conference is locked
*/
function doXmppAuth(room, lockPassword) {
const loginDialog = LoginDialog.showAuthDialog(
/* successCallback */ (id, password) => {
room.authenticateAndUpgradeRole({
id,
password,
roomPassword: lockPassword,
/** Called when the XMPP login succeeds. */
onLoginSuccessful() {
loginDialog.displayConnectionStatus(
'connection.FETCH_SESSION_ID');
}
})
.then(
/* onFulfilled */ () => {
loginDialog.displayConnectionStatus(
'connection.GOT_SESSION_ID');
loginDialog.close();
},
/* onRejected */ error => {
logger.error('authenticateAndUpgradeRole failed', error);
const { authenticationError, connectionError } = error;
if (authenticationError) {
loginDialog.displayError(
'connection.GET_SESSION_ID_ERROR',
{ msg: authenticationError });
} else if (connectionError) {
loginDialog.displayError(connectionError);
}
});
},
/* cancelCallback */ () => loginDialog.close());
}
/**
* Authenticate for the conference.
* Uses external service for auth if conference supports that.
* @param {JitsiConference} room
* @param {string} [lockPassword] password to use if the conference is locked
*/
function authenticate(room, lockPassword) {
if (isTokenAuthEnabled || room.isExternalAuthEnabled()) {
function authenticateExternal(room: Object, lockPassword: string) {
const config = APP.store.getState()['features/base/config'];
if (isTokenAuthEnabled(config) || room.isExternalAuthEnabled()) {
doExternalAuth(room, lockPassword);
} else {
doXmppAuth(room, lockPassword);
}
}
@@ -219,7 +177,7 @@ function authenticate(room, lockPassword) {
* @param {string} [lockPassword] password to use if the conference is locked
* @returns {Promise}
*/
function logout(room) {
function logout(room: Object) {
return new Promise(resolve => {
room.room.moderator.logout(resolve);
}).then(url => {
@@ -232,83 +190,7 @@ function logout(room) {
});
}
/**
* Notify user that authentication is required to create the conference.
* @param {JitsiConference} room
* @param {string} [lockPassword] password to use if the conference is locked
*/
function requireAuth(room, lockPassword) {
if (authRequiredDialog) {
return;
}
authRequiredDialog = LoginDialog.showAuthRequiredDialog(
room.getName(), authenticate.bind(null, room, lockPassword)
);
}
/**
* Close auth-related dialogs if there are any.
*/
function closeAuth() {
if (externalAuthWindow) {
externalAuthWindow.close();
externalAuthWindow = null;
}
if (authRequiredDialog) {
authRequiredDialog.close();
authRequiredDialog = null;
}
}
/**
*
*/
function showXmppPasswordPrompt(roomName, connect) {
return new Promise((resolve, reject) => {
const authDialog = LoginDialog.showAuthDialog(
(id, password) => {
connect(id, password, roomName).then(connection => {
authDialog.close();
resolve(connection);
}, err => {
if (err === JitsiConnectionErrors.PASSWORD_REQUIRED) {
authDialog.displayError(err);
} else {
authDialog.close();
reject(err);
}
});
}
);
});
}
/**
* Show Authentication Dialog and try to connect with new credentials.
* If failed to connect because of PASSWORD_REQUIRED error
* then ask for password again.
* @param {string} [roomName] name of the conference room
* @param {function(id, password, roomName)} [connect] function that returns
* a Promise which resolves with JitsiConnection or fails with one of
* JitsiConnectionErrors.
* @returns {Promise<JitsiConnection>}
*/
function requestAuth(roomName, connect) {
if (isTokenAuthEnabled) {
// This Promise never resolves as user gets redirected to another URL
return new Promise(() => redirectToTokenAuthService(roomName));
}
return showXmppPasswordPrompt(roomName, connect);
}
export default {
authenticate,
requireAuth,
requestAuth,
closeAuth,
authenticateExternal,
logout
};

View File

@@ -212,45 +212,5 @@ export default {
}
return dialog;
},
/**
* Shows a notification that authentication is required to create the
* conference, so the local participant should authenticate or wait for a
* host.
*
* @param {string} room - The name of the conference.
* @param {function} onAuthNow - The callback to invoke if the local
* participant wants to authenticate.
* @returns dialog
*/
showAuthRequiredDialog(room, onAuthNow) {
const msg = APP.translation.generateTranslationHTML(
'[html]dialog.WaitForHostMsg',
{ room }
);
const buttonTxt = APP.translation.generateTranslationHTML(
'dialog.IamHost'
);
const buttons = [ {
title: buttonTxt,
value: 'authNow'
} ];
return APP.UI.messageHandler.openDialog(
'dialog.WaitingForHost',
msg,
true,
buttons,
(e, submitValue) => {
// Do not close the dialog yet.
e.preventDefault();
// Open login popup.
if (submitValue === 'authNow') {
onAuthNow();
}
}
);
}
};

View File

@@ -12,12 +12,6 @@ import {
const logger = Logger.getLogger(__filename);
/**
* Flag for enabling/disabling popups.
* @type {boolean}
*/
let popupEnabled = true;
/**
* Currently displayed two button dialog.
* @type {null}
@@ -167,7 +161,7 @@ const messageHandler = {
let { classes } = options;
if (!popupEnabled || twoButtonDialog) {
if (twoButtonDialog) {
return null;
}
@@ -233,88 +227,6 @@ const messageHandler = {
return $.prompt.getApi();
},
/**
* Shows a message to the user with two buttons: first is given as a
* parameter and the second is Cancel.
*
* @param titleKey the key for the title of the message
* @param msgString the text of the message
* @param persistent boolean value which determines whether the message is
* persistent or not
* @param buttons object with the buttons. The keys must be the name of the
* button and value is the value that will be passed to
* submitFunction
* @param submitFunction function to be called on submit
* @param loadedFunction function to be called after the prompt is fully
* loaded
* @param closeFunction function to be called on dialog close
* @param {object} dontShowAgain - options for dont show again checkbox.
* @param {string} dontShowAgain.id the id of the checkbox.
* @param {string} dontShowAgain.textKey the key for the text displayed
* next to checkbox
* @param {boolean} dontShowAgain.checked if true the checkbox is foing to
* be checked
* @param {Array} dontShowAgain.buttonValues The button values that will
* trigger storing the checkbox value
* @param {string} dontShowAgain.localStorageKey the key for the local
* storage. if not provided dontShowAgain.id will be used.
*/
openDialog(// eslint-disable-line max-params
titleKey,
msgString,
persistent,
buttons,
submitFunction,
loadedFunction,
closeFunction,
dontShowAgain) {
if (!popupEnabled) {
return;
}
if (dontShowTheDialog(dontShowAgain)) {
// Maybe we should pass some parameters here? I'm not sure
// and currently we don't need any parameters.
submitFunction();
return;
}
const args = {
title: this._getFormattedTitleString(titleKey),
persistent,
buttons,
defaultButton: 1,
promptspeed: 0,
loaded() {
if (loadedFunction) {
// eslint-disable-next-line prefer-rest-params
loadedFunction.apply(this, arguments);
}
// Hide the close button
if (persistent) {
$('.jqiclose', this).hide();
}
},
submit: dontShowAgainSubmitFunctionWrapper(
dontShowAgain, submitFunction),
close: closeFunction,
classes: this._getDialogClasses()
};
if (persistent) {
args.closeText = '';
}
const dialog = $.prompt(
msgString + generateDontShowCheckbox(dontShowAgain), args);
APP.translation.translateElement(dialog);
return $.prompt.getApi();
},
/**
* Returns the formatted title string.
*
@@ -358,9 +270,6 @@ const messageHandler = {
* @param translateOptions options passed to translation
*/
openDialogWithStates(statesObject, options, translateOptions) {
if (!popupEnabled) {
return;
}
const { classes, size } = options;
const defaultClasses = this._getDialogClasses(size);
@@ -397,10 +306,6 @@ const messageHandler = {
*/
// eslint-disable-next-line max-params
openCenteredPopup(url, w, h, onPopupClosed) {
if (!popupEnabled) {
return;
}
const l = window.screenX + (window.innerWidth / 2) - (w / 2);
const t = window.screenY + (window.innerHeight / 2) - (h / 2);
const popup = window.open(
@@ -481,19 +386,6 @@ const messageHandler = {
notify(titleKey, messageKey, messageArguments) {
this.participantNotification(
null, titleKey, null, messageKey, messageArguments);
},
enablePopups(enable) {
popupEnabled = enable;
},
/**
* Returns true if dialog is opened
* false otherwise
* @returns {boolean} isOpened
*/
isDialogOpened() {
return Boolean($.prompt.getCurrentStateName());
}
};

432
package-lock.json generated
View File

@@ -4708,6 +4708,135 @@
}
}
},
"array.prototype.map": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.3.tgz",
"integrity": "sha512-nNcb30v0wfDyIe26Yif3PcV1JXQp4zEeEfupG7L4SRjnD6HLbO5b2a7eVSba53bOx4YCHYMBHt+Fp4vYstneRA==",
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1",
"es-array-method-boxes-properly": "^1.0.0",
"is-string": "^1.0.5"
},
"dependencies": {
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"requires": {
"object-keys": "^1.0.12"
}
},
"es-abstract": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz",
"integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==",
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.2",
"is-callable": "^1.2.3",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.2",
"is-string": "^1.0.5",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.4",
"string.prototype.trimstart": "^1.0.4",
"unbox-primitive": "^1.0.0"
}
},
"es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
"is-symbol": "^1.0.2"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"is-callable": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
"integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
},
"is-regex": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
"integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
"requires": {
"call-bind": "^1.0.2",
"has-symbols": "^1.0.1"
}
},
"is-symbol": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw=="
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
},
"string.prototype.trimend": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
"integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"string.prototype.trimstart": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
"integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
}
}
},
"arrify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
@@ -5566,6 +5695,15 @@
"unset-value": "^1.0.0"
}
},
"call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"requires": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
}
},
"caller-callsite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
@@ -7144,6 +7282,46 @@
"is-regex": "^1.0.4"
}
},
"es-array-method-boxes-properly": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
"integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="
},
"es-get-iterator": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz",
"integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==",
"requires": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.0",
"has-symbols": "^1.0.1",
"is-arguments": "^1.1.0",
"is-map": "^2.0.2",
"is-set": "^2.0.2",
"is-string": "^1.0.5",
"isarray": "^2.0.5"
},
"dependencies": {
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"is-arguments": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz",
"integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==",
"requires": {
"call-bind": "^1.0.0"
}
},
"isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
}
}
},
"es-to-primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz",
@@ -8802,6 +8980,31 @@
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U="
},
"get-intrinsic": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1"
},
"dependencies": {
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
}
}
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
@@ -8945,6 +9148,11 @@
"function-bind": "^1.0.2"
}
},
"has-bigints": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
"integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA=="
},
"has-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
@@ -9570,6 +9778,11 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-bigint": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz",
"integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg=="
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -9579,6 +9792,14 @@
"binary-extensions": "^2.0.0"
}
},
"is-boolean-object": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz",
"integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==",
"requires": {
"call-bind": "^1.0.0"
}
},
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
@@ -9701,6 +9922,16 @@
"is-extglob": "^2.1.1"
}
},
"is-map": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
"integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg=="
},
"is-negative-zero": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
"integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w=="
},
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -9709,6 +9940,11 @@
"kind-of": "^3.0.2"
}
},
"is-number-object": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz",
"integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw=="
},
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
@@ -9765,11 +10001,21 @@
"integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
"dev": true
},
"is-set": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
"integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g=="
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"is-string": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
"integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ=="
},
"is-svg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz",
@@ -9823,6 +10069,20 @@
"whatwg-fetch": ">=0.10.0"
}
},
"iterate-iterator": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz",
"integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw=="
},
"iterate-value": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz",
"integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==",
"requires": {
"es-get-iterator": "^1.0.2",
"iterate-iterator": "^1.0.1"
}
},
"jQuery-Impromptu": {
"version": "github:trentrichardson/jQuery-Impromptu#753c2833f62f9c00301dd8b75af03599dc4f2ee8",
"from": "github:trentrichardson/jQuery-Impromptu#v6.0.0"
@@ -10253,8 +10513,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#4191198233ae64e3cb12349069c582724d09537c",
"from": "github:jitsi/lib-jitsi-meet#4191198233ae64e3cb12349069c582724d09537c",
"version": "github:jitsi/lib-jitsi-meet#0e180efdfa46a048ae1276cfbdf9cf8e051405c7",
"from": "github:jitsi/lib-jitsi-meet#0e180efdfa46a048ae1276cfbdf9cf8e051405c7",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",
@@ -13199,6 +13459,136 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
"promise.allsettled": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.4.tgz",
"integrity": "sha512-o73CbvQh/OnPFShxHcHxk0baXR2a1m4ozb85ha0H14VEoi/EJJLa9mnPfEWJx9RjA9MLfhdjZ8I6HhWtBa64Ag==",
"requires": {
"array.prototype.map": "^1.0.3",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.2",
"get-intrinsic": "^1.0.2",
"iterate-value": "^1.0.2"
},
"dependencies": {
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
"requires": {
"object-keys": "^1.0.12"
}
},
"es-abstract": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz",
"integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==",
"requires": {
"call-bind": "^1.0.2",
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.2",
"is-callable": "^1.2.3",
"is-negative-zero": "^2.0.1",
"is-regex": "^1.1.2",
"is-string": "^1.0.5",
"object-inspect": "^1.9.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.2",
"string.prototype.trimend": "^1.0.4",
"string.prototype.trimstart": "^1.0.4",
"unbox-primitive": "^1.0.0"
}
},
"es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
"is-symbol": "^1.0.2"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"requires": {
"function-bind": "^1.1.1"
}
},
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"is-callable": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
"integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ=="
},
"is-regex": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
"integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==",
"requires": {
"call-bind": "^1.0.2",
"has-symbols": "^1.0.1"
}
},
"is-symbol": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw=="
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
},
"string.prototype.trimend": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
"integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
},
"string.prototype.trimstart": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
"integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
}
}
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
@@ -16674,6 +17064,17 @@
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
"integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po="
},
"unbox-primitive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.0.tgz",
"integrity": "sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA==",
"requires": {
"function-bind": "^1.1.1",
"has-bigints": "^1.0.0",
"has-symbols": "^1.0.0",
"which-boxed-primitive": "^1.0.1"
}
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -18444,6 +18845,33 @@
"isexe": "^2.0.0"
}
},
"which-boxed-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
"integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
"requires": {
"is-bigint": "^1.0.1",
"is-boolean-object": "^1.1.0",
"is-number-object": "^1.0.4",
"is-string": "^1.0.5",
"is-symbol": "^1.0.3"
},
"dependencies": {
"has-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"is-symbol": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
"requires": {
"has-symbols": "^1.0.1"
}
}
}
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",

View File

@@ -48,19 +48,20 @@
"i18next": "17.0.6",
"i18next-browser-languagedetector": "3.0.1",
"i18next-xhr-backend": "3.0.0",
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#v1.0.0",
"jquery": "3.5.1",
"jquery-i18next": "1.2.1",
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#4191198233ae64e3cb12349069c582724d09537c",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0e180efdfa46a048ae1276cfbdf9cf8e051405c7",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",
"moment-duration-format": "2.2.2",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"pixelmatch": "5.1.0",
"promise.allsettled": "1.0.4",
"punycode": "2.1.1",
"react": "16.12",
"react-dom": "16.12",

View File

@@ -1,5 +1,6 @@
// @flow
import '../authentication/middleware';
import '../base/devices/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';

View File

@@ -1,6 +1,7 @@
// @flow
import '../analytics/reducer';
import '../authentication/reducer';
import '../base/app/reducer';
import '../base/audio-only/reducer';
import '../base/color-scheme/reducer';

View File

@@ -1,6 +1,5 @@
// @flow
import '../authentication/reducer';
import '../mobile/audio-mode/reducer';
import '../mobile/background/reducer';
import '../mobile/call-integration/reducer';

View File

@@ -3,9 +3,9 @@
import type { Dispatch } from 'redux';
import { appNavigate } from '../app/actions';
import { checkIfCanJoin, conferenceLeft } from '../base/conference';
import { connectionFailed } from '../base/connection';
import { openDialog } from '../base/dialog';
import { checkIfCanJoin, conferenceLeft } from '../base/conference/actions';
import { connectionFailed } from '../base/connection/actions.native';
import { openDialog } from '../base/dialog/actions';
import { set } from '../base/redux';
import {

View File

@@ -0,0 +1,67 @@
// @flow
import { maybeRedirectToWelcomePage } from '../app/actions';
import { hideDialog, openDialog } from '../base/dialog/actions';
import {
CANCEL_LOGIN
} from './actionTypes';
import { WaitForOwnerDialog, LoginDialog } from './components';
/**
* Cancels {@ink LoginDialog}.
*
* @returns {{
* type: CANCEL_LOGIN
* }}
*/
export function cancelLogin() {
return {
type: CANCEL_LOGIN
};
}
/**
* Cancels authentication, closes {@link WaitForOwnerDialog}
* and navigates back to the welcome page.
*
* @returns {Function}
*/
export function cancelWaitForOwner() {
return (dispatch: Function) => {
dispatch(maybeRedirectToWelcomePage());
};
}
/**
* Hides a authentication dialog where the local participant
* should authenticate.
*
* @returns {Function}.
*/
export function hideLoginDialog() {
return hideDialog(LoginDialog);
}
/**
* Shows a authentication dialog where the local participant
* should authenticate.
*
* @returns {Function}.
*/
export function openLoginDialog() {
return openDialog(LoginDialog);
}
/**
* Shows a notification dialog that authentication is required to create the.
* Conference, so the local participant should authenticate or wait for a
* host.
*
* @returns {Function}.
*/
export function openWaitForOwnerDialog() {
return openDialog(WaitForOwnerDialog);
}

View File

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

View File

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

View File

@@ -1,2 +1 @@
export { default as LoginDialog } from './LoginDialog';
export { default as WaitForOwnerDialog } from './WaitForOwnerDialog';
export * from './_';

View File

@@ -5,20 +5,20 @@ import { Text, TextInput, View } from 'react-native';
import { connect as reduxConnect } from 'react-redux';
import type { Dispatch } from 'redux';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { toJid } from '../../base/connection';
import { connect } from '../../base/connection/actions.native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { toJid } from '../../../base/connection';
import { connect } from '../../../base/connection/actions.native';
import {
CustomSubmitDialog,
FIELD_UNDERLINE,
PLACEHOLDER_COLOR,
_abstractMapStateToProps,
inputDialog as inputDialogStyle
} from '../../base/dialog';
import { translate } from '../../base/i18n';
import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet';
import type { StyleType } from '../../base/styles';
import { authenticateAndUpgradeRole, cancelLogin } from '../actions';
} from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet';
import type { StyleType } from '../../../base/styles';
import { authenticateAndUpgradeRole, cancelLogin } from '../../actions.native';
// Register styles.
import './styles';

View File

@@ -3,10 +3,10 @@
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { ConfirmDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import { cancelWaitForOwner, _openLoginDialog } from '../actions';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { cancelWaitForOwner, _openLoginDialog } from '../../actions.native';
/**
* The type of the React {@code Component} props of {@link WaitForOwnerDialog}.
@@ -107,9 +107,7 @@ class WaitForOwnerDialog extends Component<Props> {
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _room: string
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const { authRequired } = state['features/base/conference'];

View File

@@ -0,0 +1,2 @@
export { default as LoginDialog } from './LoginDialog';
export { default as WaitForOwnerDialog } from './WaitForOwnerDialog';

View File

@@ -1,5 +1,5 @@
import { ColorSchemeRegistry, schemeColor } from '../../base/color-scheme';
import { BoxModel } from '../../base/styles';
import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
import { BoxModel } from '../../../base/styles';
/**
* The styles of the authentication feature.

View File

@@ -0,0 +1,313 @@
// @flow
import { FieldTextStateless as TextField } from '@atlaskit/field-text';
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { connect } from '../../../../../connection';
import { toJid } from '../../../base/connection/functions';
import { Dialog } from '../../../base/dialog';
import { translate, translateToHTML } from '../../../base/i18n';
import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet';
import { connect as reduxConnect } from '../../../base/redux';
import { authenticateAndUpgradeRole } from '../../actions.native';
import { cancelLogin } from '../../actions.web';
/**
* The type of the React {@code Component} props of {@link LoginDialog}.
*/
type Props = {
/**
* {@link JitsiConference} that needs authentication - will hold a valid
* value in XMPP login + guest access mode.
*/
_conference: Object,
/**
* The server hosts specified in the global config.
*/
_configHosts: Object,
/**
* Indicates if the dialog should display "connecting" status message.
*/
_connecting: boolean,
/**
* The error which occurred during login/authentication.
*/
_error: Object,
/**
* The progress in the floating range between 0 and 1 of the authenticating
* and upgrading the role of the local participant/user.
*/
_progress: number,
/**
* Redux store dispatch method.
*/
dispatch: Dispatch<any>,
/**
* Invoked when username and password are submitted.
*/
onSuccess: Function,
/**
* Conference room name.
*/
roomName: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
}
/**
* The type of the React {@code Component} state of {@link LoginDialog}.
*/
type State = {
/**
* The user entered password for the conference.
*/
password: string,
/**
* The user entered local participant name.
*/
username: string,
/**
* Authentication process starts before joining the conference room.
*/
loginStarted: boolean
}
/**
* Component that renders the login in conference dialog.
*
* @returns {React$Element<any>}
*/
class LoginDialog extends Component<Props, State> {
/**
* Initializes a new {@code LoginDialog} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
username: '',
password: '',
loginStarted: false
};
this._onCancelLogin = this._onCancelLogin.bind(this);
this._onLogin = this._onLogin.bind(this);
this._onChange = this._onChange.bind(this);
}
_onCancelLogin: () => void;
/**
* Called when the cancel button is clicked.
*
* @private
* @returns {void}
*/
_onCancelLogin() {
const { dispatch } = this.props;
dispatch(cancelLogin());
}
_onLogin: () => void;
/**
* Notifies this LoginDialog that the login button (OK) has been pressed by
* the user.
*
* @private
* @returns {void}
*/
_onLogin() {
const {
_conference: conference,
_configHosts: configHosts,
roomName,
onSuccess,
dispatch
} = this.props;
const { password, username } = this.state;
const jid = toJid(username, configHosts);
if (conference) {
dispatch(authenticateAndUpgradeRole(jid, password, conference));
} else {
this.setState({
loginStarted: true
});
connect(jid, password, roomName)
.then(connection => {
onSuccess && onSuccess(connection);
})
.catch(() => {
this.setState({
loginStarted: false
});
});
}
}
_onChange: Object => void;
/**
* Callback for the onChange event of the field.
*
* @param {Object} evt - The static event.
* @returns {void}
*/
_onChange(evt: Object) {
this.setState({
[evt.target.name]: evt.target.value
});
}
/**
* Renders an optional message, if applicable.
*
* @returns {ReactElement}
* @private
*/
renderMessage() {
const {
_configHosts: configHosts,
_connecting: connecting,
_error: error,
_progress: progress,
t
} = this.props;
const { username, password } = this.state;
const messageOptions = {};
let messageKey;
if (progress && progress >= 0.5) {
messageKey = t('connection.FETCH_SESSION_ID');
} else if (error) {
const { name } = error;
if (name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
const { credentials } = error;
if (credentials
&& credentials.jid === toJid(username, configHosts)
&& credentials.password === password) {
messageKey = t('dialog.incorrectPassword');
}
} else if (name) {
messageKey = t('dialog.connectErrorWithMsg');
messageOptions.msg = `${name} ${error.message}`;
}
} else if (connecting) {
messageKey = t('connection.CONNECTING');
}
if (messageKey) {
return (
<span>
{ translateToHTML(t, messageKey, messageOptions) }
</span>
);
}
return null;
}
/**
* Implements {@Component#render}.
*
* @inheritdoc
*/
render() {
const {
_connecting: connecting,
t
} = this.props;
const { password, loginStarted, username } = this.state;
return (
<Dialog
okDisabled = {
connecting
|| loginStarted
|| !password
|| !username
}
okKey = { t('dialog.login') }
onCancel = { this._onCancelLogin }
onSubmit = { this._onLogin }
titleKey = { t('dialog.authenticationRequired') }
width = { 'small' }>
<TextField
autoFocus = { true }
className = 'input-control'
compact = { false }
label = { t('dialog.user') }
name = 'username'
onChange = { this._onChange }
placeholder = { t('dialog.userIdentifier') }
shouldFitContainer = { true }
type = 'text'
value = { username } />
<TextField
className = 'input-control'
compact = { false }
label = { t('dialog.userPassword') }
name = 'password'
onChange = { this._onChange }
shouldFitContainer = { true }
type = 'password'
value = { password } />
{ this.renderMessage() }
</Dialog>
);
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code LoginDialog} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state) {
const {
error: authenticateAndUpgradeRoleError,
progress,
thenableWithCancel
} = state['features/authentication'];
const { authRequired } = state['features/base/conference'];
const { hosts: configHosts } = state['features/base/config'];
const {
connecting,
error: connectionError
} = state['features/base/connection'];
return {
_conference: authRequired,
_configHosts: configHosts,
_connecting: connecting || thenableWithCancel,
_error: connectionError || authenticateAndUpgradeRoleError,
_progress: progress
};
}
export default translate(reduxConnect(mapStateToProps)(LoginDialog));

View File

@@ -0,0 +1,129 @@
// @flow
import React, { PureComponent } from 'react';
import type { Dispatch } from 'redux';
import { Dialog } from '../../../base/dialog';
import { translate, translateToHTML } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { openLoginDialog, cancelWaitForOwner } from '../../actions.web';
/**
* The type of the React {@code Component} props of {@link WaitForOwnerDialog}.
*/
type Props = {
/**
* The name of the conference room (without the domain part).
*/
_room: string,
/**
* Redux store dispatch method.
*/
dispatch: Dispatch<any>,
/**
* Function to be invoked after click.
*/
onAuthNow: ?Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
}
/**
* Authentication message dialog for host confirmation.
*
* @returns {React$Element<any>}
*/
class WaitForOwnerDialog extends PureComponent<Props> {
/**
* Instantiates a new component.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this._onCancelWaitForOwner = this._onCancelWaitForOwner.bind(this);
this._onIAmHost = this._onIAmHost.bind(this);
}
_onCancelWaitForOwner: () => void;
/**
* Called when the cancel button is clicked.
*
* @private
* @returns {void}
*/
_onCancelWaitForOwner() {
const { dispatch } = this.props;
dispatch(cancelWaitForOwner());
}
_onIAmHost: () => void;
/**
* Called when the OK button is clicked.
*
* @private
* @returns {void}
*/
_onIAmHost() {
const { dispatch } = this.props;
dispatch(openLoginDialog());
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const {
_room,
t
} = this.props;
return (
<Dialog
okKey = { t('dialog.IamHost') }
onCancel = { this._onCancelWaitForOwner }
onSubmit = { this._onIAmHost }
titleKey = { t('dialog.WaitingForHostTitle') }
width = { 'small' }>
<span>
{
translateToHTML(
t, 'dialog.WaitForHostMsg', { room: _room })
}
</span>
</Dialog>
);
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code WaitForOwnerDialog} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state) {
const { authRequired } = state['features/base/conference'];
return {
_room: authRequired && authRequired.getName()
};
}
export default translate(connect(mapStateToProps)(WaitForOwnerDialog));

View File

@@ -0,0 +1,4 @@
// @flow
export { default as WaitForOwnerDialog } from './WaitForOwnerDialog';
export { default as LoginDialog } from './LoginDialog';

View File

@@ -0,0 +1,25 @@
// @flow
import JitsiMeetJS from '../../../react/features/base/lib-jitsi-meet';
/**
* Checks if the token for authentication is available.
*
* @param {Object} config - Configuration state object from store.
* @returns {boolean}
*/
export const isTokenAuthEnabled = (config: Object) =>
typeof config.tokenAuthUrl === 'string'
&& config.tokenAuthUrl.length;
/**
* Token url.
*
* @param {Object} config - Configuration state object from store.
* @returns {string}
*/
export const getTokenAuthUrl = (config: Object) =>
JitsiMeetJS.util.AuthUtil.getTokenAuthUrl.bind(null,
config.tokenAuthUrl);

View File

@@ -1,3 +0,0 @@
export * from './actions';
export * from './actionTypes';
export * from './components';

View File

@@ -26,7 +26,7 @@ import {
_openWaitForOwnerDialog,
stopWaitForOwner,
waitForOwner
} from './actions';
} from './actions.native';
import { LoginDialog, WaitForOwnerDialog } from './components';
/**

View File

@@ -0,0 +1,134 @@
// @flow
import { maybeRedirectToWelcomePage } from '../app/actions';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference';
import { CONNECTION_ESTABLISHED } from '../base/connection';
import { hideDialog, isDialogOpen } from '../base/dialog';
import {
JitsiConferenceErrors
} from '../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../base/redux';
import {
CANCEL_LOGIN,
STOP_WAIT_FOR_OWNER,
WAIT_FOR_OWNER
} from './actionTypes';
import {
stopWaitForOwner,
waitForOwner
} from './actions.native';
import {
hideLoginDialog,
openWaitForOwnerDialog
} from './actions.web';
import { LoginDialog, WaitForOwnerDialog } from './components';
/**
* Middleware that captures connection or conference failed errors and controls
* {@link WaitForOwnerDialog} and {@link LoginDialog}.
*
* FIXME Some of the complexity was introduced by the lack of dialog stacking.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CANCEL_LOGIN: {
if (!isDialogOpen(store, WaitForOwnerDialog)) {
if (isWaitingForOwner(store)) {
store.dispatch(openWaitForOwnerDialog());
return next(action);
}
store.dispatch(hideLoginDialog());
store.dispatch(maybeRedirectToWelcomePage());
}
break;
}
case CONFERENCE_FAILED: {
const { error } = action;
let recoverable;
if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) {
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
recoverable = error.recoverable;
}
if (recoverable) {
store.dispatch(waitForOwner());
} else {
store.dispatch(stopWaitForOwner());
}
break;
}
case CONFERENCE_JOINED:
if (isWaitingForOwner(store)) {
store.dispatch(stopWaitForOwner());
}
store.dispatch(hideLoginDialog);
break;
case CONFERENCE_LEFT:
store.dispatch(stopWaitForOwner());
break;
case CONNECTION_ESTABLISHED:
store.dispatch(hideLoginDialog);
break;
case STOP_WAIT_FOR_OWNER:
clearExistingWaitForOwnerTimeout(store);
store.dispatch(hideDialog(WaitForOwnerDialog));
break;
case WAIT_FOR_OWNER: {
clearExistingWaitForOwnerTimeout(store);
const { handler, timeoutMs } = action;
action.waitForOwnerTimeoutID = setTimeout(handler, timeoutMs);
isDialogOpen(store, LoginDialog)
|| store.dispatch(openWaitForOwnerDialog());
break;
}
}
return next(action);
});
/**
* Will clear the wait for conference owner timeout handler if any is currently
* set.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function clearExistingWaitForOwnerTimeout(
{ getState }: { getState: Function }) {
const { waitForOwnerTimeoutID } = getState()['features/authentication'];
waitForOwnerTimeoutID && clearTimeout(waitForOwnerTimeoutID);
}
/**
* Checks if the cyclic "wait for conference owner" task is currently scheduled.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function isWaitingForOwner({ getState }: { getState: Function }) {
return getState()['features/authentication'].waitForOwnerTimeoutID;
}

View File

@@ -1,4 +1,4 @@
/* @flow */
// @flow
import { assign, ReducerRegistry } from '../base/redux';
@@ -10,6 +10,14 @@ import {
WAIT_FOR_OWNER
} from './actionTypes';
/**
* Listens for actions which change the state of the authentication feature.
*
* @param {Object} state - The Redux state of the authentication feature.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @returns {Object}
*/
ReducerRegistry.register('features/authentication', (state = {}, action) => {
switch (action.type) {
case CANCEL_LOGIN:

View File

@@ -17,6 +17,7 @@ export default [
'audioLevelsInterval',
'apiLogLevels',
'avgRtpStatsN',
'backgroundAlpha',
/**
* The display name of the CallKit call representing the conference/meeting

View File

@@ -96,6 +96,7 @@ class InputDialog extends BaseDialog<Props, State> {
{ t(this.props.contentKey) }
</Text>
<TextInput
autoFocus = { true }
onChangeText = { this._onChangeText }
style = { _dialogStyles.field }
underlineColorAndroid = { FIELD_UNDERLINE }

View File

@@ -1,7 +1,9 @@
/* global APP */
import { isMobileBrowser } from '../environment/utils';
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media';
import { toState } from '../redux';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
@@ -475,3 +477,16 @@ export function setTrackMuted(track, muted) {
}
});
}
/**
* Determines whether toggle camera should be enabled or not.
*
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {boolean} - Whether toggle camera should be enabled.
*/
export function isToggleCameraEnabled(stateful) {
const state = toState(stateful);
const { videoInput } = state['features/base/devices'].availableDevices;
return isMobileBrowser() && videoInput.length > 1;
}

View File

@@ -126,3 +126,62 @@ export function reportError(e: Object, msg: string = '') {
console.error(msg, e);
window.onerror && window.onerror(msg, null, null, null, e);
}
/**
* Adds alpha to a color css string.
*
* @param {string} color - The color string either in rgb... Or #... Format.
* @param {number} opacity -The opacity(alpha) to apply to the color. Can take a value between 0 and 1, including.
* @returns {string} - The color with applied alpha.
*/
export function setColorAlpha(color: string, opacity: number) {
if (!color) {
return `rgba(0, 0, 0, ${opacity})`;
}
let b, g, r;
try {
if (color.startsWith('rgb')) {
[ r, g, b ] = color.split('(')[1].split(')')[0].split(',').map(c => c.trim());
} else if (color.startsWith('#')) {
if (color.length === 4) {
[ r, g, b ] = parseShorthandColor(color);
} else {
r = parseInt(color.substring(1, 3), 16);
g = parseInt(color.substring(3, 5), 16);
b = parseInt(color.substring(5, 7), 16);
}
} else {
return color;
}
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
} catch {
return color;
}
}
/**
* Gets the hexa rgb values for a shorthand css color.
*
* @param {string} color -
* @returns {Array<number>} - Array containing parsed r, g, b values of the color.
*/
function parseShorthandColor(color) {
let b, g, r;
r = color.substring(1, 2);
r += r;
r = parseInt(r, 16);
g = color.substring(2, 3);
g += g;
g = parseInt(g, 16);
b = color.substring(3, 4);
b += b;
b = parseInt(b, 16);
return [ r, g, b ];
}

View File

@@ -8,6 +8,7 @@ import { getConferenceNameForTitle } from '../../../base/conference';
import { connect, disconnect } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
import { setColorAlpha } from '../../../base/util';
import { Chat } from '../../../chat';
import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
@@ -61,6 +62,11 @@ const LAYOUT_CLASSNAMES = {
*/
type Props = AbstractProps & {
/**
* The alpha(opacity) of the background
*/
_backgroundAlpha: number,
/**
* Whether the local participant is recording the conference.
*/
@@ -98,6 +104,7 @@ class Conference extends AbstractConference<Props, *> {
_onFullScreenChange: Function;
_onShowToolbar: Function;
_originalOnShowToolbar: Function;
_setBackground: Function;
/**
* Initializes a new Conference instance.
@@ -121,6 +128,7 @@ class Conference extends AbstractConference<Props, *> {
// Bind event handler so it is only bound once for every instance.
this._onFullScreenChange = this._onFullScreenChange.bind(this);
this._setBackground = this._setBackground.bind(this);
}
/**
@@ -186,7 +194,8 @@ class Conference extends AbstractConference<Props, *> {
<div
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { this._onShowToolbar }>
onMouseMove = { this._onShowToolbar }
ref = { this._setBackground }>
<Notice />
<div id = 'videospace'>
@@ -208,6 +217,35 @@ class Conference extends AbstractConference<Props, *> {
);
}
/**
* Sets custom background opacity based on config. It also applies the
* opacity on parent element, as the parent element is not accessible directly,
* only though it's child.
*
* @param {Object} element - The DOM element for which to apply opacity.
*
* @private
* @returns {void}
*/
_setBackground(element) {
if (!element) {
return;
}
if (this.props._backgroundAlpha !== undefined) {
const elemColor = element.style.background;
const alphaElemColor = setColorAlpha(elemColor, this.props._backgroundAlpha);
element.style.background = alphaElemColor;
if (element.parentElement) {
const parentColor = element.parentElement.style.background;
const alphaParentColor = setColorAlpha(parentColor, this.props._backgroundAlpha);
element.parentElement.style.background = alphaParentColor;
}
}
}
/**
* Updates the Redux state when full screen mode has been enabled or
* disabled.
@@ -265,6 +303,7 @@ function _mapStateToProps(state) {
return {
...abstractMapStateToProps(state),
_iAmRecorder: state['features/base/config'].iAmRecorder,
_backgroundAlpha: state['features/base/config'].backgroundAlpha,
_isLobbyScreenVisible: state['features/base/dialog']?.component === LobbyScreen,
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
_roomName: getConferenceNameForTitle(state),

View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
import { Watermarks } from '../../base/react';
import { connect } from '../../base/redux';
import { setColorAlpha } from '../../base/util';
import { Subject } from '../../conference';
import { fetchCustomBrandingData } from '../../dynamic-branding';
import { Captions } from '../../subtitles/';
@@ -12,6 +13,11 @@ declare var interfaceConfig: Object;
type Props = {
/**
* The alpha(opacity) of the background
*/
_backgroundAlpha: number,
/**
* The user selected background color.
*/
@@ -121,6 +127,12 @@ class LargeVideo extends Component<Props> {
styles.backgroundColor = _customBackgroundColor || interfaceConfig.DEFAULT_BACKGROUND;
if (this.props._backgroundAlpha !== undefined) {
const alphaColor = setColorAlpha(styles.backgroundColor, this.props._backgroundAlpha);
styles.backgroundColor = alphaColor;
}
if (_customBackgroundImageUrl) {
styles.backgroundImage = `url(${_customBackgroundImageUrl})`;
styles.backgroundSize = 'cover';
@@ -144,6 +156,7 @@ function _mapStateToProps(state) {
const { isOpen: isChatOpen } = state['features/chat'];
return {
_backgroundAlpha: state['features/base/config'].backgroundAlpha,
_customBackgroundColor: backgroundColor,
_customBackgroundImageUrl: backgroundImageUrl,
_isChatOpen: isChatOpen,

View File

@@ -27,7 +27,7 @@ import {
} from '../../base/connection';
import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../base/media';
import { SET_AUDIO_MUTED } from '../../base/media/actionTypes';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
import { toggleScreensharing } from '../../base/tracks';
@@ -209,6 +209,15 @@ MiddlewareRegistry.register(store => next => action => {
muted: action.muted
});
break;
case SET_VIDEO_MUTED:
sendEvent(
store,
'VIDEO_MUTED_CHANGED',
/* data */ {
muted: action.muted
});
break;
}
return result;
@@ -271,7 +280,11 @@ function _registerForNativeEvents(store) {
});
eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => {
dispatch(muteLocal(muted === 'true', MEDIA_TYPE.AUDIO));
dispatch(muteLocal(muted, MEDIA_TYPE.AUDIO));
});
eventEmitter.addListener(ExternalAPI.SET_VIDEO_MUTED, ({ muted }) => {
dispatch(muteLocal(muted, MEDIA_TYPE.VIDEO));
});
eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => {

View File

@@ -108,6 +108,13 @@ function _visitNode(node, callback) {
global.addEventListener = () => {};
}
// Promise.allSettled is supported from RN 0.63 onwards, use a polyfill for that.
// Invokes its shim method to shim Promise.allSettled if it is unavailable or noncompliant.
//
// Required by:
// lib-jitsi-meet/JitsiConference.js
require('promise.allsettled').shim();
// removeEventListener
//
// Required by:

View File

@@ -45,6 +45,7 @@ import {
RECORDING_ON_SOUND_FILE
} from './sounds';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
@@ -181,6 +182,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
if (soundID) {
dispatch(playSound(soundID));
}
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(true, mode);
}
} else if (updatedSessionData.status === OFF
&& (!oldSessionData || oldSessionData.status !== OFF)) {
dispatch(showStoppedRecordingNotification(
@@ -209,6 +214,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
dispatch(stopSound(soundOn));
dispatch(playSound(soundOff));
}
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(false, mode);
}
}
}
@@ -231,11 +240,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
* @returns {void}
*/
function _showRecordingErrorNotification(recorderSession, dispatch) {
const isStreamMode
= recorderSession.getMode()
=== JitsiMeetJS.constants.recording.mode.STREAM;
const mode = recorderSession.getMode();
const error = recorderSession.getError();
const isStreamMode = mode === JitsiMeetJS.constants.recording.mode.STREAM;
switch (recorderSession.getError()) {
switch (error) {
case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
dispatch(showRecordingError({
descriptionKey: 'recording.unavailable',
@@ -270,4 +279,8 @@ function _showRecordingErrorNotification(recorderSession, dispatch) {
}));
break;
}
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(false, mode, error);
}
}

View File

@@ -5,7 +5,6 @@ import {
SET_TIMEOUT,
timerWorkerScript
} from './TimerWorker';
const blurValue = '25px';
/**
@@ -114,7 +113,13 @@ export default class JitsiStreamBackgroundEffect {
this._outputCanvasCtx.globalCompositeOperation = 'destination-over';
if (this._options.virtualBackground.isVirtualBackground) {
this._outputCanvasCtx.drawImage(this._virtualImage, 0, 0);
this._outputCanvasCtx.drawImage(
this._virtualImage,
0,
0,
this._inputVideoElement.width,
this._inputVideoElement.height
);
} else {
this._outputCanvasCtx.filter = `blur(${blurValue})`;
this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);

View File

@@ -1,11 +1,10 @@
// @flow
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { IconCameraRefresh } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { isLocalCameraTrackMuted, toggleCamera } from '../../../base/tracks';
import { isLocalCameraTrackMuted, isToggleCameraEnabled, toggleCamera } from '../../../base/tracks';
/**
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
@@ -65,12 +64,11 @@ class ToggleCameraButton extends AbstractButton<Props, any> {
function mapStateToProps(state): Object {
const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks'];
const { videoInput } = state['features/base/devices'].availableDevices;
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalCameraTrackMuted(tracks),
visible: isMobileBrowser() && videoInput.length > 1
visible: isToggleCameraEnabled(state)
};
}

View File

@@ -9,7 +9,6 @@ import {
import { TILE_VIEW_ENABLED, getFeatureFlag } from '../../base/flags';
import { translate } from '../../base/i18n';
import { IconTileView } from '../../base/icons';
import { getParticipantCount } from '../../base/participants';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { setTileView } from '../actions';
@@ -87,8 +86,7 @@ class TileViewButton<P: Props> extends AbstractButton<P, *> {
*/
function _mapStateToProps(state, ownProps) {
const enabled = getFeatureFlag(state, TILE_VIEW_ENABLED, true);
const lonelyMeeting = getParticipantCount(state) < 2;
const { visible = enabled && !lonelyMeeting } = ownProps;
const { visible = enabled } = ownProps;
return {
_tileViewEnabled: shouldDisplayTileView(state),

View File

@@ -107,13 +107,6 @@ export function getTileViewGridDimensions(state: Object) {
export function shouldDisplayTileView(state: Object = {}) {
const participantCount = getParticipantCount(state);
// In case of a lonely meeting, we don't allow tile view.
// But it's a special case too, as we don't even render the button,
// see TileViewButton component.
if (participantCount < 2) {
return false;
}
const tileViewEnabledFeatureFlag = getFeatureFlag(state, TILE_VIEW_ENABLED, true);
const { disableTileView } = state['features/base/config'];

View File

@@ -1,36 +1,37 @@
// @flow
/* eslint-disable react/jsx-no-bind, no-return-assign */
import React, { useState } from 'react';
import Spinner from '@atlaskit/spinner';
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import React, { useState, useEffect } from 'react';
import uuid from 'uuid';
import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { Icon, IconBlurBackground } from '../../base/icons';
import { Icon, IconBlurBackground, IconCancelSelection } from '../../base/icons';
import { connect } from '../../base/redux';
import { Tooltip } from '../../base/tooltip';
import { toggleBackgroundEffect, setVirtualBackground } from '../actions';
import { resizeImage, toDataURL } from '../functions';
import logger from '../logger';
// The limit of virtual background uploads is 21. When the number
// of uploads is 22 we trigger the deleteStoredImage function to delete
// the first/oldest uploaded background.
const backgroundsLimit = 22;
const images = [
{
tooltip: 'Image 1',
name: 'background-1.jpg',
id: 1,
src: 'images/virtual-background/background-1.jpg'
},
{
tooltip: 'Image 2',
name: 'background-2.jpg',
id: 2,
src: 'images/virtual-background/background-2.jpg'
},
{
tooltip: 'Image 3',
name: 'background-3.jpg',
id: 3,
src: 'images/virtual-background/background-3.jpg'
},
{
tooltip: 'Image 4',
name: 'background-4.jpg',
id: 4,
src: 'images/virtual-background/background-4.jpg'
}
@@ -54,23 +55,81 @@ type Props = {
* @returns {ReactElement}
*/
function VirtualBackground({ dispatch, t }: Props) {
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState((localImages && JSON.parse(localImages)) || []);
const [ loading, isloading ] = useState(false);
const deleteStoredImage = image => {
setStoredImages(storedImages.filter(item => item !== image));
};
/**
* Updates stored images on local storage.
*/
useEffect(() => {
jitsiLocalStorage.setItem('virtualBackgrounds', JSON.stringify(storedImages));
if (storedImages.length === backgroundsLimit) {
deleteStoredImage(storedImages[0]);
}
}, [ storedImages ]);
const [ selected, setSelected ] = useState('');
const enableBlur = () => {
const enableBlur = async () => {
isloading(true);
setSelected('blur');
dispatch(setVirtualBackground('', false));
dispatch(toggleBackgroundEffect(true));
await dispatch(setVirtualBackground('', false));
await dispatch(toggleBackgroundEffect(true));
isloading(false);
};
const removeBackground = () => {
const removeBackground = async () => {
isloading(true);
setSelected('none');
dispatch(setVirtualBackground('', false));
dispatch(toggleBackgroundEffect(false));
await dispatch(setVirtualBackground('', false));
await dispatch(toggleBackgroundEffect(false));
isloading(false);
};
const addImageBackground = image => {
const setUploadedImageBackground = async image => {
isloading(true);
setSelected(image.id);
dispatch(setVirtualBackground(image.src, true));
dispatch(toggleBackgroundEffect(true));
await dispatch(setVirtualBackground(image.src, true));
await dispatch(toggleBackgroundEffect(true));
isloading(false);
};
const setImageBackground = async image => {
isloading(true);
setSelected(image.id);
await dispatch(setVirtualBackground(await toDataURL(image.src), true));
await dispatch(toggleBackgroundEffect(true));
isloading(false);
};
const uploadImage = async imageFile => {
const reader = new FileReader();
reader.readAsDataURL(imageFile[0]);
reader.onload = async () => {
const resizedImage = await resizeImage(reader.result);
isloading(true);
setStoredImages([
...storedImages,
{
id: uuid.v4(),
src: resizedImage
}
]);
await dispatch(setVirtualBackground(resizedImage, true));
await dispatch(toggleBackgroundEffect(true));
isloading(false);
};
reader.onerror = () => {
isloading(false);
logger.error('Failed to upload virtual image!');
};
};
return (
@@ -79,38 +138,79 @@ function VirtualBackground({ dispatch, t }: Props) {
submitDisabled = { false }
titleKey = { 'virtualBackground.title' }
width = 'small'>
<div className = 'virtual-background-dialog'>
<Tooltip
content = { t('virtualBackground.removeBackground') }
position = { 'top' }>
<div
className = { selected === 'none' ? 'none-selected' : 'virtual-background-none' }
onClick = { () => removeBackground() }>
None
{loading ? (
<div>
<span className = 'loading-content-text'>{t('virtualBackground.pleaseWait')}</span>
<Spinner
isCompleting = { false }
size = 'medium' />
</div>
) : (
<div>
<div className = 'virtual-background-dialog'>
<Tooltip
content = { t('virtualBackground.removeBackground') }
position = { 'top' }>
<div
className = { selected === 'none' ? 'none-selected' : 'virtual-background-none' }
onClick = { removeBackground }>
{t('virtualBackground.none')}
</div>
</Tooltip>
<Tooltip
content = { t('virtualBackground.enableBlur') }
position = { 'top' }>
<Icon
className = { selected === 'blur' ? 'blur-selected' : '' }
onClick = { () => enableBlur() }
size = { 50 }
src = { IconBlurBackground } />
</Tooltip>
{images.map((image, index) => (
<img
className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
key = { index }
onClick = { () => setImageBackground(image) }
onError = { event => event.target.style.display = 'none' }
src = { image.src } />
))}
<Tooltip
content = { t('virtualBackground.uploadImage') }
position = { 'top' }>
<label
className = 'custom-file-upload'
htmlFor = 'file-upload'>
+
</label>
<input
accept = 'image/*'
className = 'file-upload-btn'
id = 'file-upload'
onChange = { e => uploadImage(e.target.files) }
type = 'file' />
</Tooltip>
</div>
</Tooltip>
<Tooltip
content = { t('virtualBackground.enableBlur') }
position = { 'top' }>
<Icon
className = { selected === 'blur' ? 'blur-selected' : '' }
onClick = { () => enableBlur() }
size = { 50 }
src = { IconBlurBackground } />
</Tooltip>
{images.map((image, index) => (
<Tooltip
content = { image.tooltip }
key = { index }
position = { 'top' }>
<img
className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
onClick = { () => addImageBackground(image) }
onError = { event => event.target.style.display = 'none' }
src = { image.src } />
</Tooltip>
))}
</div>
<div className = 'virtual-background-dialog'>
{storedImages.map((image, index) => (
<div
className = { 'thumbnail-container' }
key = { index }>
<img
className = { selected === image.id ? 'thumbnail-selected' : 'thumbnail' }
onClick = { () => setUploadedImageBackground(image) }
onError = { event => event.target.style.display = 'none' }
src = { image.src } />
<Icon
className = { 'delete-image-icon' }
onClick = { () => deleteStoredImage(image) }
size = { 15 }
src = { IconCancelSelection } />
</div>
))}
</div>
</div>
)}
</Dialog>
);
}

View File

@@ -1,6 +1,4 @@
// @flow
let filterSupport;
/**
@@ -20,3 +18,62 @@ export function checkBlurSupport() {
return filterSupport;
}
/**
* Convert blob to base64.
*
* @param {Blob} blob - The link to add info with.
* @returns {Promise<string>}
*/
export const blobToData = (blob: Blob): Promise<string> => new Promise(resolve => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.toString());
reader.readAsDataURL(blob);
});
/**
* Convert blob to base64.
*
* @param {string} url - The image url.
* @returns {Object} - Returns the converted blob to base64.
*/
export const toDataURL = async (url: string) => {
const response = await fetch(url);
const blob = await response.blob();
const resData = await blobToData(blob);
return resData;
};
/**
* Resize image and adjust original aspect ratio.
*
* @param {Object} base64image - Base64 image extraction.
* @param {number} width - Value for resizing the image width.
* @param {number} height - Value for resizing the image height.
* @returns {Object} Returns the canvas output.
*
*/
export async function resizeImage(base64image: any, width: number = 1920, height: number = 1080) {
const img = document.createElement('img');
img.src = base64image;
/* eslint-disable no-empty-function */
img.onload = await function() {};
// Create an off-screen canvas.
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set its dimension to target size.
canvas.width = width;
canvas.height = height;
// Draw source image into the off-screen canvas.
// TODO: keep aspect ratio and implement object-fit: cover.
ctx.drawImage(img, 0, 0, width, height);
// Encode image to data-uri with base64 version of compressed image.
return canvas.toDataURL('image/jpeg', 0.5);
}

View File

@@ -1,6 +1,7 @@
local filters = require 'util.filters';
local jid = require "util.jid";
local jid_bare = require "util.jid".bare;
local jid_host = require "util.jid".host;
local um_is_admin = require "core.usermanager".is_admin;
local util = module:require "util";
local is_healthcheck_room = util.is_healthcheck_room;
@@ -116,12 +117,24 @@ function filter_stanza(stanza)
return stanza;
end
-- we want to filter presences only on this host for allowners and skip anything like lobby etc.
local host_from = jid_host(stanza.attr.from);
if host_from ~= module.host then
return stanza;
end
local bare_to = jid_bare(stanza.attr.to);
if stanza:get_error() and joining_moderator_participants[bare_to] then
-- pre-join succeeded but joined did not so we need to clear cache
joining_moderator_participants[bare_to] = nil;
return stanza;
end
local muc_x = stanza:get_child('x', MUC_NS..'#user');
if not muc_x then
return stanza;
end
local bare_to = jid_bare(stanza.attr.to);
if joining_moderator_participants[bare_to] and presence_check_status(muc_x, '110') then
-- skip the local presence for participant
return nil;