Compare commits

..

1 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
9f4c036174 fix(blur) fix model paths 2021-02-18 15:36:37 +01:00
77 changed files with 485 additions and 1571 deletions

View File

@@ -5,7 +5,7 @@ build/*
# modify as little as possible.
flow-typed/*
libs/*
resources/*
react/features/stream-effects/blur/vendor/*
# ESLint will by default ignore its own configuration file. However, there does

View File

@@ -115,7 +115,7 @@ import {
submitFeedback
} from './react/features/feedback';
import { showNotification } from './react/features/notifications';
import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './react/features/overlay';
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor';
import {
initPrejoin,
@@ -502,11 +502,6 @@ export default {
);
}
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMediaDevicesEvents.SLOW_GET_USER_MEDIA,
() => APP.store.dispatch(toggleSlowGUMOverlay(true))
);
let tryCreateLocalTracks;
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
@@ -524,10 +519,8 @@ export default {
return createLocalTracksF({
devices: [ 'audio' ],
timeout,
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
timeout
}, true)
.then(([ audioStream ]) =>
[ desktopStream, audioStream ])
.catch(error => {
@@ -543,10 +536,8 @@ export default {
return requestedAudio
? createLocalTracksF({
devices: [ 'audio' ],
timeout,
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
timeout
}, true)
: [];
})
.catch(error => {
@@ -560,10 +551,8 @@ export default {
} else {
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
timeout,
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
timeout
}, true)
.catch(err => {
if (requestedAudio && requestedVideo) {
@@ -585,10 +574,8 @@ export default {
return (
createLocalTracksF({
devices: [ 'audio' ],
timeout,
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
}));
timeout
}, true));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
@@ -611,9 +598,8 @@ export default {
return requestedVideo
? createLocalTracksF({
devices: [ 'video' ],
firePermissionPromptIsShownEvent: true,
fireSlowPromiseEvent: true
})
timeout
}, true)
: [];
})
.catch(err => {
@@ -633,7 +619,6 @@ export default {
// the user inputs their credentials, but the dialog would be
// overshadowed by the overlay.
tryCreateLocalTracks.then(tracks => {
APP.store.dispatch(toggleSlowGUMOverlay(false));
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
return tracks;
@@ -897,7 +882,7 @@ export default {
showUI && APP.store.dispatch(notifyMicError(error));
};
createLocalTracksF({ devices: [ 'audio' ] })
createLocalTracksF({ devices: [ 'audio' ] }, false)
.then(([ audioTrack ]) => audioTrack)
.catch(error => {
maybeShowErrorDialog(error);
@@ -1011,7 +996,7 @@ export default {
//
// FIXME when local track creation is moved to react/redux
// it should take care of the use case described above
createLocalTracksF({ devices: [ 'video' ] })
createLocalTracksF({ devices: [ 'video' ] }, false)
.then(([ videoTrack ]) => videoTrack)
.catch(error => {
// FIXME should send some feedback to the API on error ?
@@ -2008,10 +1993,7 @@ export default {
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
if (participantThatMutedUs) {
APP.store.dispatch(participantMutedUs(participantThatMutedUs, track));
if (this.isSharingScreen && track.isVideoTrack()) {
this._turnScreenSharingOff(false);
}
APP.store.dispatch(participantMutedUs(participantThatMutedUs));
}
});

View File

@@ -240,12 +240,6 @@ var config = {
// 90: 2
// },
// Provides a way to translate the legacy bridge signaling messages, 'LastNChangedEvent',
// 'SelectedEndpointsChangedEvent' and 'ReceiverVideoConstraint' into the new 'ReceiverVideoConstraints' message
// that invokes the new bandwidth allocation algorithm in the bridge which is described here
// - https://github.com/jitsi/jitsi-videobridge/blob/master/doc/allocation.md.
// useNewBandwidthAllocationStrategy: false,
// Specify the settings for video quality optimizations on the client.
// videoQuality: {
// // Provides a way to prevent a video codec from being negotiated on the JVB connection. The codec specified

View File

@@ -1,21 +1,3 @@
/**
* Mixins that mimic the way Atlaskit fills the screen with modals at low screen widths.
*/
@mixin full-size-modal-positioner() {
height: 100%;
left: 0;
position: fixed;
top: 0;
max-width: 100%;
width: 100%;
}
@mixin full-size-modal-dialog() {
height: 100%;
max-height: 100%;
border-radius: 0;
}
/**
* Move the @atlaskit/flag container up a little bit so it does not cover the
* toolbar with the first notification.
@@ -74,43 +56,4 @@
.toolbox-button-wth-dialog > div:nth-child(2) {
max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 46px);
overflow-y: auto;
}
/**
* The following selectors keep the chat modal full-size anywhere between 100px
* and 580px for desktop or 680px for mobile.
*/
@media (min-width: 100px) and (max-width: 320px) {
.smiley-input {
display: none;
}
.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
@media (min-width: 480px) and (max-width: 580px) {
.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
@media (min-width: 580px) and (max-width: 680px) {
.mobile-browser {
&.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
&.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
}
}

View File

@@ -32,13 +32,6 @@
width: $sidebarWidth;
word-wrap: break-word;
display: flex;
flex-direction: column;
& > :first-child {
margin-top: auto;
}
a {
display: block;
}
@@ -129,61 +122,16 @@
}
}
.chat-input-container {
padding: 0 16px 24px;
&.populated {
#chat-input {
border: 1px solid #619CF4;
.send-button {
background: #1B67EC;
cursor: pointer;
path {
fill: #fff;
}
}
}
}
}
#chat-input {
border: 1px solid $chatInputSeparatorColor;
border-top: 1px solid $chatInputSeparatorColor;
display: flex;
padding: 5px 10px;
border-radius: 3px;
* {
background-color: transparent;
}
}
.send-button-container {
display: flex;
align-items: center;
}
.send-button {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
width: 40px;
border-radius: 3px;
path {
fill: $chatInputSeparatorColor;
}
}
.mobile-browser {
.send-button {
height: 48px;
width: 48px;
}
}
.remoteuser {
color: #B8C7E0;
}
@@ -213,47 +161,10 @@
#nickname {
text-align: center;
color: #9d9d9d;
font-size: 16px;
margin: auto 0;
padding: 0 16px;
input {
height: 40px;
}
label {
line-height: 24px;
}
.enter-chat {
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
height: 40px;
background: #1B67EC;
border-radius: 3px;
color: #fff;
cursor: pointer;
&.disabled {
color: #757575;
background: #11336E;
pointer-events: none;
}
}
}
.mobile-browser {
#nickname {
input {
height: 48px;
}
.enter-chat {
height: 48px;
}
}
font-size: 18px;
margin-top: 30px;
left: 5px;
right: 5px;
}
.sideToolbarContainer {
@@ -500,16 +411,6 @@
#chatconversation {
width: 100%;
}
.chat-input-container {
padding: 0 0 24px;
}
}
.touchmove-hack {
display: flex;
flex: 1;
overflow: auto;
}
/**

View File

@@ -6,6 +6,7 @@
min-width: 75px;
text-align: left;
padding: 0px;
width: 180px;
white-space: nowrap;
&__item {

View File

@@ -53,6 +53,21 @@
}
}
@mixin full-size-modal-positioner() {
height: 100%;
left: 0;
position: fixed;
top: 0;
max-width: 100%;
width: 100%;
}
@mixin full-size-modal-dialog() {
height: 100%;
max-height: 100%;
border-radius: 0;
}
@media only screen and (max-width: $verySmallScreen) {
.welcome {
display: block;
@@ -150,3 +165,25 @@
}
}
}
@media (min-width: 480px) and (max-width: 580px) {
.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
@media (min-width: 580px) and (max-width: 680px) {
.mobile-browser {
&.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
&.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
}

View File

@@ -33,12 +33,4 @@
bottom: 24px;
width: 100%;
}
&__spinner-container {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
}
}

View File

@@ -206,7 +206,7 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', 'security'
],
TOOLBAR_TIMEOUT: 4000,

View File

@@ -20,11 +20,11 @@
- (void)sendHangUp;
- (void)sendSetAudioMuted:(BOOL)muted;
- (void)sendEndpointTextMessage:(NSString*)message :(NSString*)to;
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
- (void)toggleScreenShare;
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completion;
- (void)openChat:(NSString*)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString*)message :(NSString*)to ;
- (void)sendChatMessage:(NSString*)to :(NSString*)message;
@end

View File

@@ -153,7 +153,7 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:setAudioMutedAction body:data];
}
- (void)sendEndpointTextMessage:(NSString*)message :(NSString*)to {
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
data[@"to"] = to;
data[@"message"] = message;
@@ -185,7 +185,7 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:closeChatAction body:nil];
}
- (void)sendChatMessage:(NSString*)message :(NSString*)to {
- (void)sendChatMessage:(NSString*)to :(NSString*)message {
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
data[@"to"] = to;
data[@"message"] = message;

View File

@@ -38,11 +38,11 @@
- (void)leave;
- (void)hangUp;
- (void)setAudioMuted:(BOOL)muted;
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
- (void)toggleScreenShare;
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler;
- (void)openChat:(NSString * _Nullable)to;
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler;
- (void)openChat:(NSString*)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)sendChatMessage:(NSString*)to :(NSString*)message;
@end

View File

@@ -125,9 +125,9 @@ static void initializeViewsMap() {
[externalAPI sendSetAudioMuted:muted];
}
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendEndpointTextMessage:message :to];
[externalAPI sendEndpointTextMessage:to :message];
}
- (void)toggleScreenShare {
@@ -135,7 +135,7 @@ static void initializeViewsMap() {
[externalAPI toggleScreenShare];
}
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler {
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI retrieveParticipantsInfo:completionHandler];
}
@@ -150,9 +150,9 @@ static void initializeViewsMap() {
[externalAPI closeChat];
}
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
- (void)sendChatMessage:(NSString*)to :(NSString*)message {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendChatMessage:message :to];
[externalAPI sendChatMessage:to :message];
}
#pragma mark Private methods

View File

@@ -239,19 +239,12 @@
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
"muteEveryoneDialog": "Wollen Sie wirklich alle stummschalten? Sie können deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
"muteEveryoneTitle": "Alle stummschalten?",
"muteEveryoneElsesVideoDialog": "Sobald die Kamera deaktiviert ist, können Sie sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"muteEveryoneElsesVideoTitle": "Die Kamera von allen außer {{whom}} ausschalten?",
"muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Teilnehmern deaktivieren möchten? Sie können sie nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?",
"muteEveryoneSelf": "sich selbst",
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten",
"muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
"muteParticipantTitle": "Person stummschalten?",
"muteParticipantsVideoButton": "Kamera ausschalten",
"muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"Ok": "OK",
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
@@ -491,8 +484,6 @@
"mutedTitle": "Stummschaltung aktiv!",
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
"mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche vom Meeting fernzuhalten.",
"videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet!",
"videoMutedRemotelyDescription": "Sie können sie jederzeit wieder einschalten.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
"raisedHand": "{{name}} möchte sprechen.",
@@ -723,16 +714,12 @@
"moreOptions": "Menü „Weitere Optionen“",
"mute": "„Audio stummschalten“ ein-/ausschalten",
"muteEveryone": "Alle stummschalten",
"muteEveryoneElse": "Alle anderen stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
"raiseHand": "„Melden“ ein-/ausschalten",
"recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Personen stummschalten",
"remoteVideoMute": "Kamera von dieser Person ausschalten",
"security": "Sicherheitsoptionen",
"Settings": "Einstellungen ein-/ausschalten",
"sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
@@ -777,7 +764,6 @@
"moreOptions": "Weitere Optionen",
"mute": "Stummschaltung aktivieren / deaktivieren",
"muteEveryone": "Alle stummschalten",
"muteEveryonesVideo": "Alle Kameras ausschalten",
"noAudioSignalTitle": "Es kommt kein Input von Ihrem Mikrofon!",
"noAudioSignalDesc": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stumm geschaltet haben, sollten Sie einen Wechsel des Geräts in Erwägung ziehen.",
"noAudioSignalDescSuggestion": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stummgeschaltet haben, sollten Sie einen Wechsel auf das vorgeschlagene Gerät in Erwägung ziehen.",
@@ -863,16 +849,13 @@
},
"videothumbnail": {
"domute": "Stummschalten",
"domuteVideo": "Kamera ausschalten",
"domuteOthers": "Alle anderen stummschalten",
"domuteVideoOfOthers": "Alle anderen Kameras auschalten",
"flip": "Spiegeln",
"grantModerator": "Moderationsrechte vergeben",
"kick": "Hinauswerfen",
"moderator": "Moderation",
"mute": "Person ist stumm geschaltet",
"muted": "Stummgeschaltet",
"videoMuted": "Kamera ausgeschaltet",
"remoteControl": "Fernsteuerung",
"show": "Im Vordergrund anzeigen",
"videomute": "Person hat die Kamera angehalten"

View File

@@ -61,7 +61,6 @@
"today": "Today"
},
"chat": {
"enter": "Enter chat room",
"error": "Error: your message was not sent. Reason: {{error}}",
"fieldPlaceHolder": "Type your message here",
"messagebox": "Type a message",
@@ -241,19 +240,12 @@
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
"muteEveryoneDialog": "Are you sure you want to mute everyone? You won't be able to unmute them, but they can unmute themselves at any time.",
"muteEveryoneTitle": "Mute everyone?",
"muteEveryoneElsesVideoDialog": "Once the camera is disabled, you won't be able to turn it back on, but they can turn it back on at any time.",
"muteEveryoneElsesVideoTitle": "Disable everyone's camera except {{whom}}?",
"muteEveryonesVideoDialog": "Are you sure you want to disable everyone's camera? You won't be able to turn it back on, but they can turn it back on at any time.",
"muteEveryonesVideoTitle": "Disable everyone's camera?",
"muteEveryoneSelf": "yourself",
"muteEveryoneStartMuted": "Everyone starts muted from now on",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute",
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantTitle": "Mute this participant?",
"muteParticipantsVideoButton": "Disable camera",
"muteParticipantsVideoTitle": "Disable camera of this participant?",
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
"Ok": "OK",
"passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.",
"passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
@@ -491,8 +483,6 @@
"mutedTitle": "You're muted!",
"mutedRemotelyTitle": "You have been muted by {{participantDisplayName}}!",
"mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
"videoMutedRemotelyTitle": "Your camera has been disabled by {{participantDisplayName}}!",
"videoMutedRemotelyDescription": "You can always turn it on again.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
"raisedHand": "{{name}} would like to speak.",
@@ -725,16 +715,12 @@
"moreOptions": "Show more options",
"mute": "Toggle mute audio",
"muteEveryone": "Mute everyone",
"muteEveryoneElse": "Mute everyone else",
"muteEveryonesVideo": "Disable everyone's camera",
"muteEveryoneElsesVideo": "Disable everyone else's camera",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Toggle raise hand",
"recording": "Toggle recording",
"remoteMute": "Mute participant",
"remoteVideoMute": "Disable camera of participant",
"security": "Security options",
"Settings": "Toggle settings",
"sharedvideo": "Toggle Youtube video sharing",
@@ -779,7 +765,6 @@
"moreOptions": "More options",
"mute": "Mute / Unmute",
"muteEveryone": "Mute everyone",
"muteEveryonesVideo": "Disable everyone's camera",
"noAudioSignalTitle": "There is no input coming from your mic!",
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
@@ -864,16 +849,13 @@
"videothumbnail": {
"connectionInfo": "Connection Info",
"domute": "Mute",
"domuteVideo": "Disable camera",
"domuteOthers": "Mute everyone else",
"domuteVideoOfOthers": "Disable camera of everyone else",
"flip": "Flip",
"grantModerator": "Grant Moderator",
"kick": "Kick out",
"moderator": "Moderator",
"mute": "Participant is muted",
"muted": "Muted",
"videoMuted": "Camera disabled",
"remoteControl": "Start / Stop remote control",
"show": "Show on stage",
"videomute": "Participant has stopped the camera"

View File

@@ -14,7 +14,6 @@ import {
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../react/features/base/media';
import { pinParticipant, getParticipantById, kickParticipant } from '../../react/features/base/participants';
import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
import { openChat } from '../../react/features/chat/actions.web';
@@ -80,9 +79,7 @@ function initCommands() {
sendAnalytics(createApiEvent('display.name.changed'));
APP.conference.changeLocalDisplayName(displayName);
},
'mute-everyone': mediaType => {
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
'mute-everyone': () => {
sendAnalytics(createApiEvent('muted-everyone'));
const participants = APP.store.getState()['features/base/participants'];
const localIds = participants
@@ -90,7 +87,7 @@ function initCommands() {
.filter(participant => participant.role === 'moderator')
.map(participant => participant.id);
APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
APP.store.dispatch(muteAllParticipants(localIds));
},
'toggle-lobby': isLobbyEnabled => {
APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));

108
package-lock.json generated
View File

@@ -3781,6 +3781,68 @@
}
}
},
"@tensorflow-models/body-pix": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@tensorflow-models/body-pix/-/body-pix-2.0.4.tgz",
"integrity": "sha512-wRoEZBv2BNORDZjNNRLu1W4Td4/LRbaqSJFVWryHeBcHr5m5xSSETwnDfFRtLLofFtVNHfi7VUZ7TFjkaktNug=="
},
"@tensorflow/tfjs": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-1.5.1.tgz",
"integrity": "sha512-WiE+JQ3ibr5LibGiBz6HWUqLJW8HiX6ywUSCA7ehZ67vFsw4mPuVjv0WEEUfD/l47PkXYVAmWd+RYOJiuZC7Eg==",
"requires": {
"@tensorflow/tfjs-converter": "1.5.1",
"@tensorflow/tfjs-core": "1.5.1",
"@tensorflow/tfjs-data": "1.5.1",
"@tensorflow/tfjs-layers": "1.5.1"
}
},
"@tensorflow/tfjs-converter": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-1.5.1.tgz",
"integrity": "sha512-M9tl2/ep8ntcZpmncHwKuvThsS7TaUWqJ9vJSgJmkazwTfAvlAJmZ8/p1miJ+m5sH1EJO4oTjiEmch6g8IA5IQ=="
},
"@tensorflow/tfjs-core": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.5.1.tgz",
"integrity": "sha512-N4fsi8mLsRwRs8UJN2cARB1rYFxyVXkLyZ4wOusiR976BwwZbCwQrTTSIPzPqYT3rwiexEUzm7sM6ZaDl5dpXA==",
"requires": {
"@types/offscreencanvas": "~2019.3.0",
"@types/seedrandom": "2.4.27",
"@types/webgl-ext": "0.0.30",
"@types/webgl2": "0.0.4",
"node-fetch": "~2.1.2",
"seedrandom": "2.4.3"
},
"dependencies": {
"node-fetch": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
"integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U="
}
}
},
"@tensorflow/tfjs-data": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-1.5.1.tgz",
"integrity": "sha512-eu4X0tHS1Tng+cvMO9gkMhUWX/UZQ//VpiaZfQJfa3zvUgxw6s1MHJFb0JC1T1FOnEgDVriZ8G758ysJZOybog==",
"requires": {
"@types/node-fetch": "^2.1.2",
"node-fetch": "~2.1.2"
},
"dependencies": {
"node-fetch": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
"integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U="
}
}
},
"@tensorflow/tfjs-layers": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-1.5.1.tgz",
"integrity": "sha512-DyuhifqflK+bdpBRLAj3RuWm1eTVe8yNX2+WH+W+wmhpjGg7Yagnar6/66JdS2h3WUFoiplCpZRAVMVw631E5g=="
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@@ -3840,8 +3902,20 @@
"@types/node": {
"version": "12.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz",
"integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==",
"dev": true
"integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w=="
},
"@types/node-fetch": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.4.tgz",
"integrity": "sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ==",
"requires": {
"@types/node": "*"
}
},
"@types/offscreencanvas": {
"version": "2019.3.0",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz",
"integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q=="
},
"@types/parse-json": {
"version": "4.0.0",
@@ -3853,11 +3927,26 @@
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
},
"@types/seedrandom": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz",
"integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE="
},
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
},
"@types/webgl-ext": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
"integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg=="
},
"@types/webgl2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.4.tgz",
"integrity": "sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw=="
},
"@types/yargs": {
"version": "13.0.8",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz",
@@ -10254,8 +10343,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#4c668023b38c92615cd06c4e90006246c3e4aa2c",
"from": "github:jitsi/lib-jitsi-meet#4c668023b38c92615cd06c4e90006246c3e4aa2c",
"version": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
"from": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",
@@ -10351,9 +10440,9 @@
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash.clonedeep": {
"version": "4.5.0",
@@ -14808,6 +14897,11 @@
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz",
"integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY="
},
"seedrandom": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz",
"integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",

View File

@@ -38,6 +38,8 @@
"@react-native-community/google-signin": "3.0.1",
"@react-native-community/netinfo": "4.1.5",
"@svgr/webpack": "4.3.2",
"@tensorflow-models/body-pix": "2.0.4",
"@tensorflow/tfjs": "1.5.1",
"amplitude-js": "7.3.3",
"base64-js": "1.3.1",
"bc-css-flags": "3.0.0",
@@ -54,9 +56,9 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#4c668023b38c92615cd06c4e90006246c3e4aa2c",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c534f748849a308d08b06e306f5a66709ccae056",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"lodash": "4.17.19",
"moment": "2.19.4",
"moment-duration-format": "2.2.2",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",

View File

@@ -504,17 +504,15 @@ export function createRejoinedEvent({ url, lastConferenceDuration, timeSinceLeft
*
* @param {string} participantId - The ID of the participant that was remotely
* muted.
* @param {string} mediaType - The media type of the channel to mute.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRemoteMuteConfirmedEvent(participantId, mediaType) {
export function createRemoteMuteConfirmedEvent(participantId) {
return {
action: 'clicked',
actionSubject: 'remote.mute.dialog.confirm.button',
attributes: {
'participant_id': participantId,
'media_type': mediaType
'participant_id': participantId
},
source: 'remote.mute.dialog',
type: TYPE_UI

View File

@@ -2,9 +2,9 @@
import React, { useState } from 'react';
import { translate } from '../../base/i18n';
import { Icon, IconCheck, IconCopy } from '../../base/icons';
import { translate } from '../i18n';
import { copyText } from '../util';
import { copyText } from '../../base/util';
type Props = {

View File

@@ -149,9 +149,9 @@ function _addConferenceListeners(conference, dispatch) {
conference.on(
JitsiConferenceEvents.TRACK_MUTE_CHANGED,
(track, participantThatMutedUs) => {
(_, participantThatMutedUs) => {
if (participantThatMutedUs) {
dispatch(participantMutedUs(participantThatMutedUs, track));
dispatch(participantMutedUs(participantThatMutedUs));
}
});

View File

@@ -53,11 +53,6 @@ type Props = {
*/
hideCancelButton: boolean,
/**
* If true, no footer will be displayed.
*/
disableFooter?: boolean,
i18n: Object,
/**
@@ -179,10 +174,6 @@ class StatelessDialog extends Component<Props> {
this._renderCancelButton()
].filter(Boolean);
if (this.props.disableFooter) {
return null;
}
return (
<ModalFooter showKeyline = { propsFromModalFooter.showKeyline } >
{

View File

@@ -11,16 +11,6 @@ export function isMobileBrowser() {
return Platform.OS === 'android' || Platform.OS === 'ios';
}
/**
* Returns whether or not the current environment is an ios mobile device.
*
* @returns {boolean}
*/
export function isIosMobileBrowser() {
return Platform.OS === 'ios';
}
/**
* Checks whether the chrome extensions defined in the config file are installed or not.
*

View File

@@ -68,14 +68,11 @@ export { default as IconMicrophoneEmpty } from './microphone-empty.svg';
export { default as IconModerator } from './star.svg';
export { default as IconMuteEveryone } from './mute-everyone.svg';
export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
export { default as IconMuteVideoEveryone } from './mute-video-everyone.svg';
export { default as IconMuteVideoEveryoneElse } from './mute-video-everyone-else.svg';
export { default as IconNotificationJoin } from './navigate_next.svg';
export { default as IconOpenInNew } from './open_in_new.svg';
export { default as IconOutlook } from './office365.svg';
export { default as IconPhone } from './phone.svg';
export { default as IconPin } from './enlarge.svg';
export { default as IconPlane } from './paper-plane.svg';
export { default as IconPresentation } from './presentation.svg';
export { default as IconRaisedHand } from './raised-hand.svg';
export { default as IconRec } from './rec.svg';

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path fill="#FFFFFF" d="M3.136,7.1l14.448,14.447l-1.033,1.032l-2.598-2.599c-0.115,0.077-0.307,0.153-0.459,0.153H3.709
c-0.458,0-0.803-0.346-0.803-0.804v-8.179c0-0.459,0.344-0.803,0.803-0.803h0.612L2.104,8.131L3.136,7.1z M17.584,10.769v8.714
l-9.135-9.134h5.045c0.459,0,0.84,0.344,0.84,0.803v2.866L17.584,10.769z"/>
<path fill="#FFFFFF" d="M14.688,0.818l8.164,8.165l-0.584,0.583L20.8,8.098c-0.065,0.043-0.174,0.086-0.26,0.086h-5.528
c-0.259,0-0.454-0.195-0.454-0.454V3.108c0-0.26,0.195-0.454,0.454-0.454h0.345l-1.253-1.253L14.688,0.818z M22.852,2.892v4.924
l-5.162-5.162h2.851c0.26,0,0.476,0.194,0.476,0.454v1.619L22.852,2.892z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path fill="#A4B8D1" d="M3.136,7.1l14.448,14.447l-1.033,1.032l-2.598-2.599c-0.115,0.077-0.307,0.153-0.459,0.153H3.709
c-0.458,0-0.803-0.346-0.803-0.804v-8.179c0-0.459,0.344-0.803,0.803-0.803h0.612L2.104,8.131L3.136,7.1z M17.584,10.769v8.714
l-9.135-9.134h5.045c0.459,0,0.84,0.344,0.84,0.803v2.866L17.584,10.769z"/>
<path fill="#A4B8D1" d="M14.688,0.818l8.164,8.165l-0.584,0.583L20.8,8.098c-0.065,0.043-0.174,0.086-0.26,0.086h-5.528
c-0.259,0-0.454-0.195-0.454-0.454V3.108c0-0.26,0.195-0.454,0.454-0.454h0.345l-1.253-1.253L14.688,0.818z M22.852,2.892v4.924
l-5.162-5.162h2.851c0.26,0,0.476,0.194,0.476,0.454v1.619L22.852,2.892z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6667 1.66663L1.66669 10.8333L7.6326 11.8276L8.33335 17.0833L10.644 13.2323L16.6667 17.9166V1.66663ZM8.73722 10.3221L6.35041 9.92426L15 4.63839V14.5089L11.3161 11.6436L12.5 7.49996L8.73722 10.3221Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@@ -16,9 +16,7 @@ import {
PIN_PARTICIPANT,
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import {
DISCO_REMOTE_CONTROL_FEATURE
} from './constants';
import { DISCO_REMOTE_CONTROL_FEATURE } from './constants';
import {
getLocalParticipant,
getNormalizedDisplayName,
@@ -194,18 +192,15 @@ export function localParticipantRoleChanged(role) {
* Create an action for muting another participant in the conference.
*
* @param {string} id - Participant's ID.
* @param {MEDIA_TYPE} mediaType - The media to mute.
* @returns {{
* type: MUTE_REMOTE_PARTICIPANT,
* id: string,
* mediaType: MEDIA_TYPE
* id: string
* }}
*/
export function muteRemoteParticipant(id, mediaType) {
export function muteRemoteParticipant(id) {
return {
type: MUTE_REMOTE_PARTICIPANT,
id,
mediaType
id
};
}
@@ -455,20 +450,17 @@ export function participantUpdated(participant = {}) {
* Action to signal that a participant has muted us.
*
* @param {JitsiParticipant} participant - Information about participant.
* @param {JitsiLocalTrack} track - Information about the track that has been muted.
* @returns {Promise}
*/
export function participantMutedUs(participant, track) {
export function participantMutedUs(participant) {
return (dispatch, getState) => {
if (!participant) {
return;
}
const isAudio = track.isAudioTrack();
dispatch(showNotification({
descriptionKey: isAudio ? 'notify.mutedRemotelyDescription' : 'notify.videoMutedRemotelyDescription',
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
descriptionKey: 'notify.mutedRemotelyDescription',
titleKey: 'notify.mutedRemotelyTitle',
titleArguments: {
participantDisplayName:
getParticipantDisplayName(getState, participant.getId())

View File

@@ -112,7 +112,7 @@ MiddlewareRegistry.register(store => next => action => {
case MUTE_REMOTE_PARTICIPANT: {
const { conference } = store.getState()['features/base/conference'];
conference.muteParticipant(action.id, action.mediaType);
conference.muteParticipant(action.id);
break;
}

View File

@@ -127,6 +127,7 @@ export function createLocalTracksA(options = {}) {
options.facingMode || CAMERA_FACING_MODE.USER,
micDeviceId: options.micDeviceId
},
/* firePermissionPromptIsShownEvent */ false,
store)
.then(
localTracks => {

View File

@@ -63,25 +63,16 @@ export async function createLocalPresenterTrack(options, desktopHeight) {
* @param {string|null} [options.micDeviceId] - Microphone device id or
* {@code undefined} to use app's settings.
* @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks.
* @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* @param {boolean} [firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet
* should check for a {@code getUserMedia} permission prompt and fire a
* corresponding event.
* @param {boolean} [options.fireSlowPromiseEvent] - Whether lib-jitsi-meet
* should check for a slow {@code getUserMedia} request and fire a
* corresponding event.
* @param {Object} store - The redux store in the context of which the function
* is to execute and from which state such as {@code config} is to be retrieved.
* @returns {Promise<JitsiLocalTrack[]>}
*/
export function createLocalTracksF(options = {}, store) {
export function createLocalTracksF(options = {}, firePermissionPromptIsShownEvent, store) {
let { cameraDeviceId, micDeviceId } = options;
const {
desktopSharingSourceDevice,
desktopSharingSources,
firePermissionPromptIsShownEvent,
fireSlowPromiseEvent,
timeout
} = options;
const { desktopSharingSourceDevice, desktopSharingSources, timeout } = options;
if (typeof APP !== 'undefined') {
// TODO The app's settings should go in the redux store and then the
@@ -123,12 +114,11 @@ export function createLocalTracksF(options = {}, store) {
devices: options.devices.slice(0),
effects,
firefox_fake_device, // eslint-disable-line camelcase
firePermissionPromptIsShownEvent,
fireSlowPromiseEvent,
micDeviceId,
resolution,
timeout
})
},
firePermissionPromptIsShownEvent)
.catch(err => {
logger.error('Failed to create local tracks', options.devices, err);
@@ -171,10 +161,7 @@ export function createPrejoinTracks() {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
firePermissionPromptIsShownEvent: true
})
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
.catch(err => {
if (requestedAudio && requestedVideo) {
@@ -182,10 +169,7 @@ export function createPrejoinTracks() {
errors.audioAndVideoError = err;
return (
createLocalTracksF({
devices: [ 'audio' ],
firePermissionPromptIsShownEvent: true
}));
createLocalTracksF({ devices: [ 'audio' ] }, true));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
@@ -206,10 +190,7 @@ export function createPrejoinTracks() {
// Try video only...
return requestedVideo
? createLocalTracksF({
devices: [ 'video' ],
firePermissionPromptIsShownEvent: true
})
? createLocalTracksF({ devices: [ 'video' ] }, true)
: [];
})
.catch(err => {

View File

@@ -31,15 +31,24 @@ export function assignIfDefined(target: Object, source: Object) {
* @param {string} textToCopy - Text to be copied.
* @returns {boolean}
*/
export async function copyText(textToCopy: string) {
export function copyText(textToCopy: string) {
const fakeTextArea = document.createElement('textarea');
let result;
// $FlowFixMe
document.body.appendChild(fakeTextArea);
fakeTextArea.value = textToCopy;
fakeTextArea.select();
try {
result = await navigator.clipboard.writeText(textToCopy);
result = document.execCommand('copy');
} catch (err) {
result = false;
}
// $FlowFixMe
document.body.removeChild(fakeTextArea);
return result;
}

View File

@@ -1,12 +1,13 @@
// @flow
import React from 'react';
import { createVideoBlurEvent, sendAnalytics } from '../../analytics';
import { translate } from '../../base/i18n';
import { IconBlurBackground } from '../../base/icons';
import { connect } from '../../base/redux';
import { AbstractButton } from '../../base/toolbox/components';
import { AbstractButton, BetaTag } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components';
import { isLocalCameraTrackMuted } from '../../base/tracks';
import { toggleBlurEffect } from '../actions';
/**
@@ -19,11 +20,6 @@ type Props = AbstractButtonProps & {
*/
_isVideoBlurred: boolean,
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
@@ -41,6 +37,18 @@ class VideoBlurButton extends AbstractButton<Props, *> {
tooltip = 'toolbar.startvideoblur';
toggledLabel = 'toolbar.stopvideoblur';
/**
* Helper function to be implemented by subclasses, which returns
* a React Element to display (a beta tag) at the end of the button.
*
* @override
* @protected
* @returns {ReactElement}
*/
_getElementAfter() {
return <BetaTag />;
}
/**
* Handles clicking / pressing the button, and toggles the blur effect
* state accordingly.
@@ -66,17 +74,6 @@ class VideoBlurButton extends AbstractButton<Props, *> {
_isToggled() {
return this.props._isVideoBlurred;
}
/**
* Returns {@code boolean} value indicating if disabled state is
* enabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._videoMuted;
}
}
/**
@@ -90,13 +87,9 @@ class VideoBlurButton extends AbstractButton<Props, *> {
* }}
*/
function _mapStateToProps(state): Object {
const tracks = state['features/base/tracks'];
return {
_isVideoBlurred: Boolean(state['features/blur'].blurEnabled),
_videoMuted: isLocalCameraTrackMuted(tracks)
_isVideoBlurred: Boolean(state['features/blur'].blurEnabled)
};
}
export default translate(connect(_mapStateToProps)(VideoBlurButton));

View File

@@ -2,8 +2,6 @@
import { getJitsiMeetGlobalNS, loadScript } from '../base/util';
let filterSupport;
/**
* Returns promise that resolves with the blur effect instance.
*
@@ -18,21 +16,3 @@ export function getBlurEffect() {
return loadScript('libs/video-blur-effect.min.js').then(() => ns.effects.createBlurEffect());
}
/**
* Checks context filter support.
*
* @returns {boolean} True if the filter is supported and false if the filter is not supported by the browser.
*/
export function checkBlurSupport() {
if (typeof filterSupport === 'undefined') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
filterSupport = typeof ctx.filter !== 'undefined';
canvas.remove();
}
return filterSupport;
}

View File

@@ -20,10 +20,9 @@ export * from './actions.any';
*/
export function openChat(participant: Object) {
return function(dispatch: (Object) => Object) {
dispatch({
participant,
type: OPEN_CHAT
});
dispatch({ participant,
type: OPEN_CHAT });
VideoLayout.onResize();
};
}
@@ -41,8 +40,5 @@ export function toggleChat() {
} else {
dispatch(openChat());
}
// Recompute the large video size whenever we toggle the chat, as it takes chat state into account.
VideoLayout.onResize();
};
}

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { toggleChat } from '../../actions.web';
import AbstractChat, {
_mapStateToProps,
type Props
@@ -13,10 +14,8 @@ import ChatDialog from './ChatDialog';
import Header from './ChatDialogHeader';
import ChatInput from './ChatInput';
import DisplayNameForm from './DisplayNameForm';
import KeyboardAvoider from './KeyboardAvoider';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import TouchmoveHack from './TouchmoveHack';
/**
* React Component for holding the chat feature in a side panel that slides in
@@ -51,6 +50,7 @@ class Chat extends AbstractChat<Props> {
// Bind event handlers so they are only bound once for every instance.
this._renderPanelContent = this._renderPanelContent.bind(this);
this._onChatInputResize = this._onChatInputResize.bind(this);
this._onToggleChat = this._onToggleChat.bind(this);
}
/**
@@ -112,16 +112,13 @@ class Chat extends AbstractChat<Props> {
_renderChat() {
return (
<>
<TouchmoveHack isModal = { this.props._isModal }>
<MessageContainer
messages = { this.props._messages }
ref = { this._messageContainerRef } />
</TouchmoveHack>
<MessageContainer
messages = { this.props._messages }
ref = { this._messageContainerRef } />
<MessageRecipient />
<ChatInput
onResize = { this._onChatInputResize }
onSend = { this._onSendMessage } />
<KeyboardAvoider />
</>
);
}
@@ -135,7 +132,9 @@ class Chat extends AbstractChat<Props> {
*/
_renderChatHeader() {
return (
<Header className = 'chat-header' />
<Header
className = 'chat-header'
onCancel = { this._onToggleChat } />
);
}
@@ -199,6 +198,18 @@ class Chat extends AbstractChat<Props> {
}
_onSendMessage: (string) => void;
_onToggleChat: () => void;
/**
* Toggles the chat window.
*
* @returns {Function}
*/
_onToggleChat() {
this.props.dispatch(toggleChat());
}
}
export default translate(connect(_mapStateToProps)(Chat));

View File

@@ -24,7 +24,6 @@ function ChatDialog({ children }: Props) {
<Dialog
customHeader = { Header }
disableEnter = { true }
disableFooter = { true }
hideCancelButton = { true }
submitDisabled = { true }
titleKey = 'chat.title'>

View File

@@ -5,7 +5,7 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { toggleChat } from '../../actions.web';
import { closeChat } from '../../actions.any';
type Props = {
@@ -42,6 +42,6 @@ function Header({ onCancel, className, t }: Props) {
);
}
const mapDispatchToProps = { onCancel: toggleChat };
const mapDispatchToProps = { onCancel: closeChat };
export default translate(connect(null, mapDispatchToProps)(Header));

View File

@@ -6,7 +6,6 @@ import TextareaAutosize from 'react-textarea-autosize';
import type { Dispatch } from 'redux';
import { translate } from '../../../base/i18n';
import { Icon, IconPlane } from '../../../base/icons';
import { connect } from '../../../base/redux';
import SmileysPanel from './SmileysPanel';
@@ -82,7 +81,6 @@ class ChatInput extends Component<Props, State> {
this._onDetectSubmit = this._onDetectSubmit.bind(this);
this._onMessageChange = this._onMessageChange.bind(this);
this._onSmileySelect = this._onSmileySelect.bind(this);
this._onSubmitMessage = this._onSubmitMessage.bind(this);
this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
this._setTextAreaRef = this._setTextAreaRef.bind(this);
}
@@ -111,40 +109,31 @@ class ChatInput extends Component<Props, State> {
? 'show-smileys' : 'hide-smileys'} smileys-panel`;
return (
<div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
<div id = 'chat-input' >
<div className = 'smiley-input'>
<div id = 'smileysarea'>
<div id = 'smileys'>
<Emoji
onClick = { this._onToggleSmileysPanel }
text = ':)' />
</div>
</div>
<div className = { smileysPanelClassName }>
<SmileysPanel
onSmileySelect = { this._onSmileySelect } />
<div id = 'chat-input' >
<div className = 'smiley-input'>
<div id = 'smileysarea'>
<div id = 'smileys'>
<Emoji
onClick = { this._onToggleSmileysPanel }
text = ':)' />
</div>
</div>
<div className = 'usrmsg-form'>
<TextareaAutosize
id = 'usermsg'
inputRef = { this._setTextAreaRef }
maxRows = { 5 }
onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
value = { this.state.message } />
</div>
<div className = 'send-button-container'>
<div
className = 'send-button'
onClick = { this._onSubmitMessage }>
<Icon src = { IconPlane } />
</div>
<div className = { smileysPanelClassName }>
<SmileysPanel
onSmileySelect = { this._onSmileySelect } />
</div>
</div>
<div className = 'usrmsg-form'>
<TextareaAutosize
id = 'usermsg'
inputRef = { this._setTextAreaRef }
maxRows = { 5 }
onChange = { this._onMessageChange }
onHeightChange = { this.props.onResize }
onKeyDown = { this._onDetectSubmit }
placeholder = { this.props.t('chat.messagebox') }
value = { this.state.message } />
</div>
</div>
);
}
@@ -159,24 +148,6 @@ class ChatInput extends Component<Props, State> {
this._textArea && this._textArea.focus();
}
_onSubmitMessage: () => void;
/**
* Submits the message to the chat window.
*
* @returns {void}
*/
_onSubmitMessage() {
const trimmed = this.state.message.trim();
if (trimmed) {
this.props.onSend(trimmed);
this.setState({ message: '' });
}
}
_onDetectSubmit: (Object) => void;
/**
@@ -192,7 +163,13 @@ class ChatInput extends Component<Props, State> {
&& event.shiftKey === false) {
event.preventDefault();
this._onSubmitMessage();
const trimmed = this.state.message.trim();
if (trimmed) {
this.props.onSend(trimmed);
this.setState({ message: '' });
}
}
}

View File

@@ -8,8 +8,6 @@ import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { updateSettings } from '../../../base/settings';
import KeyboardAvoider from './KeyboardAvoider';
/**
* The type of the React {@code Component} props of {@DisplayNameForm}.
*/
@@ -72,24 +70,16 @@ class DisplayNameForm extends Component<Props, State> {
return (
<div id = 'nickname'>
<span>{ this.props.t('chat.nickname.title') }</span>
<form onSubmit = { this._onSubmit }>
<FieldTextStateless
autoFocus = { true }
compact = { true }
id = 'nickinput'
label = { t('chat.nickname.title') }
onChange = { this._onDisplayNameChange }
placeholder = { t('chat.nickname.popover') }
shouldFitContainer = { true }
type = 'text'
value = { this.state.displayName } />
</form>
<div
className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
onClick = { this._onSubmit }>
{ t('chat.enter') }
</div>
<KeyboardAvoider />
</div>
);
}

View File

@@ -1,60 +0,0 @@
// @flow
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { isIosMobileBrowser } from '../../../base/environment/utils';
const Avoider = styled.div`
height: ${props => props.elementHeight}px;
`;
/**
* Component that renders an element to lift the chat input above the Safari keyboard,
* computing the appropriate height comparisons based on the {@code visualViewport}.
*
* @returns {ReactElement}
*/
function KeyboardAvoider() {
if (!isIosMobileBrowser()) {
return null;
}
const [ elementHeight, setElementHeight ] = useState(0);
const [ storedHeight, setStoredHeight ] = useState(window.innerHeight);
/**
* Handles the resizing of the visual viewport in order to compute
* the {@code KeyboardAvoider}'s height.
*
* @returns {void}
*/
function handleViewportResize() {
const { innerWidth, visualViewport: { width, height } } = window;
// Compare the widths to make sure the {@code visualViewport} didn't resize due to zooming.
if (width === innerWidth) {
if (height < storedHeight) {
setElementHeight(storedHeight - height);
} else {
setElementHeight(0);
}
setStoredHeight(height);
}
}
useEffect(() => {
// Call the handler in case the keyboard is open when the {@code KeyboardAvoider} is mounted.
handleViewportResize();
window.visualViewport.addEventListener('resize', handleViewportResize);
return () => {
window.visualViewport.removeEventListener('resize', handleViewportResize);
};
}, []);
return <Avoider elementHeight = { elementHeight } />;
}
export default KeyboardAvoider;

View File

@@ -1,67 +0,0 @@
// @flow
import React, { useEffect, useRef } from 'react';
import { isMobileBrowser } from '../../../base/environment/utils';
type Props = {
/**
* The component(s) that need to be scrollable on mobile.
*/
children: React$Node,
/**
* Whether the component is rendered within a modal.
*/
isModal: boolean
};
/**
* Component that disables {@code touchmove} propagation below it.
*
* @returns {ReactElement}
*/
function TouchmoveHack({ children, isModal }: Props) {
if (!isModal || !isMobileBrowser()) {
return children;
}
const touchMoveElementRef = useRef(null);
/**
* Atlaskit's {@code Modal} uses a third party library to disable touchmove events
* which makes scrolling inside dialogs impossible. We therefore employ this hack
* to intercept and stop the propagation of touchmove events from this wrapper that
* is placed around the chat conversation from the {@code ChatDialog}.
*
* @param {Event} event - The touchmove event fired within the component.
* @returns {void}
*/
function handleTouchMove(event: TouchEvent) {
event.stopImmediatePropagation();
}
useEffect(() => {
if (touchMoveElementRef && touchMoveElementRef.current) {
touchMoveElementRef.current.addEventListener('touchmove', handleTouchMove, true);
}
return () => {
if (touchMoveElementRef && touchMoveElementRef.current) {
touchMoveElementRef.current.removeEventListener('touchmove', handleTouchMove, true);
}
};
}, []);
return (
<div
className = 'touchmove-hack'
ref = { touchMoveElementRef }>
{children}
</div>
);
}
export default TouchmoveHack;

View File

@@ -58,14 +58,6 @@ const DEFAULT_STATE = {
*/
defaultBranding: true,
/**
* Url for a custom page for DID numbers list.
*
* @public
* @type {string}
*/
didPageUrl: '',
/**
* The custom invite domain.
*
@@ -109,7 +101,6 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
backgroundColor,
backgroundImageUrl,
defaultBranding,
didPageUrl,
inviteDomain,
logoClickUrl,
logoImageUrl
@@ -119,7 +110,6 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
backgroundColor,
backgroundImageUrl,
defaultBranding,
didPageUrl,
inviteDomain,
logoClickUrl,
logoImageUrl,

View File

@@ -1,12 +1,14 @@
// @flow
import React, { useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
import { getRoomName } from '../../../../base/conference';
import { getInviteURL } from '../../../../base/connection';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { getLocalParticipant } from '../../../../base/participants';
import { connect } from '../../../../base/redux';
import { isVpaasMeeting } from '../../../../billing-counter/functions';
import EmbedMeetingTrigger from '../../../../embed-meeting/components/EmbedMeetingTrigger';
@@ -24,6 +26,11 @@ declare var interfaceConfig: Object;
type Props = {
/**
* The name of the current conference. Used as part of inviting users.
*/
_conferenceName: string,
/**
* The object representing the dialIn feature.
*/
@@ -34,11 +41,6 @@ type Props = {
*/
_embedMeetingVisible: boolean,
/**
* The meeting invitation text.
*/
_invitationText: string,
/**
* Whether or not invite contacts should be visible.
*/
@@ -55,9 +57,14 @@ type Props = {
_liveStreamViewURL: string,
/**
* The default phone number.
* The redux representation of the local participant.
*/
_phoneNumber: ?string,
_localParticipantName: ?string,
/**
* The current location url of the conference.
*/
_locationUrl: Object,
/**
* Invoked to obtain translated strings.
@@ -76,15 +83,17 @@ type Props = {
* @returns {React$Element<any>}
*/
function AddPeopleDialog({
_conferenceName,
_dialIn,
_embedMeetingVisible,
_invitationText,
_inviteContactsVisible,
_inviteUrl,
_liveStreamViewURL,
_phoneNumber,
_localParticipantName,
_locationUrl,
t,
updateNumbers }: Props) {
const [ phoneNumber, setPhoneNumber ] = useState(undefined);
/**
* Updates the dial-in numbers.
@@ -110,6 +119,27 @@ function AddPeopleDialog({
};
}, []);
/**
* Updates the phone number in the state once the dial-in numbers are fetched.
*
* @returns {void}
*/
useEffect(() => {
if (!phoneNumber && _dialIn && _dialIn.numbers) {
setPhoneNumber(_getDefaultPhoneNumber(_dialIn.numbers));
}
}, [ _dialIn ]);
const invite = getInviteText({
_conferenceName,
_localParticipantName,
_inviteUrl,
_locationUrl,
_dialIn,
_liveStreamViewURL,
phoneNumber,
t
});
const inviteSubject = t('addPeople.inviteMoreMailSubject', {
appName: interfaceConfig.APP_NAME
});
@@ -126,7 +156,7 @@ function AddPeopleDialog({
<CopyMeetingLinkSection url = { _inviteUrl } />
<InviteByEmailSection
inviteSubject = { inviteSubject }
inviteText = { _invitationText } />
inviteText = { invite } />
{ _embedMeetingVisible && <EmbedMeetingTrigger /> }
<div className = 'invite-more-dialog separator' />
{
@@ -135,7 +165,11 @@ function AddPeopleDialog({
}
{
_dialIn.numbers
&& <DialInSection phoneNumber = { _phoneNumber } />
&& <DialInSection
conferenceName = { _conferenceName }
dialIn = { _dialIn }
locationUrl = { _locationUrl }
phoneNumber = { phoneNumber } />
}
</div>
</Dialog>
@@ -147,32 +181,29 @@ function AddPeopleDialog({
* {@code AddPeopleDialog} component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component.
* @private
* @returns {Props}
*/
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
const localParticipant = getLocalParticipant(state);
const currentLiveStreamingSession
= getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
const { iAmRecorder } = state['features/base/config'];
const addPeopleEnabled = isAddPeopleEnabled(state);
const dialOutEnabled = isDialOutEnabled(state);
const hideInviteContacts = iAmRecorder || (!addPeopleEnabled && !dialOutEnabled);
const dialIn = state['features/invite'];
const phoneNumber = dialIn && dialIn.numbers ? _getDefaultPhoneNumber(dialIn.numbers) : undefined;
return {
_dialIn: dialIn,
_conferenceName: getRoomName(state),
_dialIn: state['features/invite'],
_embedMeetingVisible: !isVpaasMeeting(state),
_invitationText: getInviteText({ state,
phoneNumber,
t: ownProps.t }),
_inviteContactsVisible: interfaceConfig.ENABLE_DIAL_OUT && !hideInviteContacts,
_inviteUrl: getInviteURL(state),
_liveStreamViewURL:
currentLiveStreamingSession
&& currentLiveStreamingSession.liveStreamViewURL,
_phoneNumber: phoneNumber
_localParticipantName: localParticipant?.name,
_locationUrl: state['features/base/connection'].locationURL
};
}

View File

@@ -3,7 +3,6 @@
import React from 'react';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { getDialInfoPageURL } from '../../../functions';
import DialInNumber from './DialInNumber';
@@ -11,14 +10,19 @@ import DialInNumber from './DialInNumber';
type Props = {
/**
* The object representing the dialIn feature.
* The name of the current conference. Used as part of inviting users.
*/
_dialIn: Object,
conferenceName: string,
/**
* The url of the page containing the dial-in numbers list.
* The object representing the dialIn feature.
*/
_dialInfoPageUrl: string,
dialIn: Object,
/**
* The current location url of the conference.
*/
locationUrl: Object,
/**
* The phone number to dial to begin the process of dialing into a
@@ -41,19 +45,25 @@ type Props = {
* @returns {null|ReactElement}
*/
function DialInSection({
_dialIn,
_dialInfoPageUrl,
conferenceName,
dialIn,
locationUrl,
phoneNumber,
t
}: Props) {
return (
<div className = 'invite-more-dialog dial-in-display'>
<DialInNumber
conferenceID = { _dialIn.conferenceID }
conferenceID = { dialIn.conferenceID }
phoneNumber = { phoneNumber } />
<a
className = 'more-numbers'
href = { _dialInfoPageUrl }
href = {
getDialInfoPageURL(
conferenceName,
locationUrl
)
}
rel = 'noopener noreferrer'
target = '_blank'>
{ t('info.moreNumbers') }
@@ -62,20 +72,4 @@ function DialInSection({
);
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code DialInLink} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_dialIn: state['features/invite'],
_dialInfoPageUrl: getDialInfoPageURL(state)
};
}
export default translate(connect(_mapStateToProps)(DialInSection));
export default translate(DialInSection);

View File

@@ -1,11 +1,7 @@
// @flow
import { getActiveSession } from '../../features/recording/functions';
import { getRoomName } from '../base/conference';
import { getInviteURL } from '../base/connection';
import { i18next } from '../base/i18n';
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants';
import { isLocalParticipantModerator } from '../base/participants';
import { toState } from '../base/redux';
import { doGetJSON, parseURIString } from '../base/util';
import { isVpaasMeeting } from '../billing-counter/functions';
@@ -243,44 +239,43 @@ export function getInviteResultsForQuery(
* @returns {string}
*/
export function getInviteText({
state,
_conferenceName,
_localParticipantName,
_inviteUrl,
_locationUrl,
_dialIn,
_liveStreamViewURL,
phoneNumber,
t
}: Object) {
const dialIn = state['features/invite'];
const inviteUrl = getInviteURL(state);
const currentLiveStreamingSession = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
const liveStreamViewURL
= currentLiveStreamingSession
&& currentLiveStreamingSession.liveStreamViewURL;
const localParticipant = getLocalParticipant(state);
const localParticipantName = localParticipant?.name;
const inviteURL = _decodeRoomURI(_inviteUrl);
const inviteURL = _decodeRoomURI(inviteUrl);
let invite = localParticipantName
? t('info.inviteURLFirstPartPersonal', { name: localParticipantName })
let invite = _localParticipantName
? t('info.inviteURLFirstPartPersonal', { name: _localParticipantName })
: t('info.inviteURLFirstPartGeneral');
invite += t('info.inviteURLSecondPart', {
url: inviteURL
});
if (liveStreamViewURL) {
if (_liveStreamViewURL) {
const liveStream = t('info.inviteLiveStream', {
url: liveStreamViewURL
url: _liveStreamViewURL
});
invite = `${invite}\n${liveStream}`;
}
if (shouldDisplayDialIn(dialIn)) {
if (shouldDisplayDialIn(_dialIn)) {
const dial = t('info.invitePhone', {
number: phoneNumber,
conferenceID: dialIn.conferenceID
conferenceID: _dialIn.conferenceID
});
const moreNumbers = t('info.invitePhoneAlternatives', {
url: getDialInfoPageURL(state),
url: getDialInfoPageURL(
_conferenceName,
_locationUrl
),
silentUrl: `${inviteURL}#config.startSilent=true`
});
@@ -519,7 +514,9 @@ export function getShareInfoText(
.catch(error =>
logger.error('Error fetching numbers or conferenceID', error))
.then(defaultDialInNumber => {
let dialInfoPageUrl = getDialInfoPageURL(state);
let dialInfoPageUrl = getDialInfoPageURL(
room,
state['features/base/connection'].locationURL);
if (useHtml) {
dialInfoPageUrl
@@ -540,19 +537,28 @@ export function getShareInfoText(
/**
* Generates the URL for the static dial in info page.
*
* @param {Object} state - The state from the Redux store.
* @param {string} conferenceName - The conference name.
* @param {Object} locationURL - The current location URL, the object coming
* from state ['features/base/connection'].locationURL.
* @returns {string}
*/
export function getDialInfoPageURL(state: Object) {
const { didPageUrl } = state['features/dynamic-branding'];
const conferenceName = getRoomName(state);
const { locationURL } = state['features/base/connection'];
const { href } = locationURL;
const room = _decodeRoomURI(conferenceName);
export function getDialInfoPageURL(
conferenceName: string,
locationURL: Object) {
const origin = locationURL.origin;
const pathParts = locationURL.pathname.split('/');
const url = didPageUrl || `${href.substring(0, href.lastIndexOf('/'))}/static/dialInInfo.html`;
pathParts.length = pathParts.length - 1;
return `${url}?room=${room}`;
const newPath = pathParts.reduce((accumulator, currentValue) => {
if (currentValue) {
return `${accumulator}/${currentValue}`;
}
return accumulator;
}, '');
return `${origin}${newPath}/static/dialInInfo.html?room=${_decodeRoomURI(conferenceName)}`;
}
/**

View File

@@ -6,7 +6,7 @@ import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/pa
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { isTestModeEnabled } from '../base/testing';
import { NOTIFICATION_TYPE, showNotification } from '../notifications';
import { shouldAutoKnock } from '../prejoin/functions';
import { isPrejoinPageEnabled } from '../prejoin/functions';
import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
import {
@@ -100,7 +100,8 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
dispatch(openLobbyScreen());
if (shouldAutoKnock(state)) {
if (isPrejoinPageEnabled(state) && !state['features/lobby'].knocking) {
// prejoin is enabled, so we knock automatically
dispatch(startKnocking());
}

View File

@@ -26,7 +26,6 @@ import {
getURLWithoutParams
} 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 { PARTICIPANT_JOINED, PARTICIPANT_LEFT, getParticipants, getParticipantById } from '../../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
@@ -271,7 +270,7 @@ function _registerForNativeEvents(store) {
});
eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => {
dispatch(muteLocal(muted === 'true', MEDIA_TYPE.AUDIO));
dispatch(muteLocal(muted === 'true'));
});
eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => {

View File

@@ -11,16 +11,22 @@ import { getDialInfoPageURL, shouldDisplayDialIn } from '../../invite';
*/
type Props = {
/**
* The name of the current conference.
*/
_room: string,
/**
* The current location url of the conference.
*/
_locationURL: string,
/**
* The redux state representing the dial-in numbers feature.
*/
_dialIn: Object,
/**
* The url of the page containing the dial-in numbers list.
*/
_dialInfoPageUrl: string,
/**
* Invoked to obtain translated strings.
*/
@@ -41,7 +47,7 @@ class DialInLink extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _dialIn, _dialInfoPageUrl, t } = this.props;
const { _room, _locationURL, _dialIn, t } = this.props;
if (!shouldDisplayDialIn(_dialIn)) {
return null;
@@ -50,7 +56,12 @@ class DialInLink extends Component<Props> {
return (
<div>{t('toolbar.noAudioSignalDialInDesc')}&nbsp;
<a
href = { _dialInfoPageUrl }
href = {
getDialInfoPageURL(
_room,
_locationURL
)
}
rel = 'noopener noreferrer'
target = '_blank'>
{t('toolbar.noAudioSignalDialInLinkDesc')}
@@ -66,12 +77,18 @@ class DialInLink extends Component<Props> {
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
* @returns {{
* _room: string,
* _locationURL: string,
* _dialIn: Object,
* }}
*/
function _mapStateToProps(state) {
return {
_dialIn: state['features/invite'],
_dialInfoPageUrl: getDialInfoPageURL(state)
_room: state['features/base/conference'].room,
_locationURL: state['features/base/connection'].locationURL,
_dialIn: state['features/invite']
};
}

View File

@@ -14,17 +14,6 @@
export const MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED
= 'MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED';
/**
* The type of the Redux action which signals that the overlay for slow gUM is visible or not.
*
* {
* type: TOGGLE_SLOW_GUM_OVERLAY,
* isVisible: {boolean},
* }
* @public
*/
export const TOGGLE_SLOW_GUM_OVERLAY = 'TOGGLE_SLOW_GUM_OVERLAY';
/**
* Adjust the state of the fatal error which shows/hides the reload screen. See
* action methods's description for more info about each of the fields.

View File

@@ -2,8 +2,7 @@
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR,
TOGGLE_SLOW_GUM_OVERLAY
SET_FATAL_ERROR
} from './actionTypes';
/**
@@ -27,24 +26,6 @@ export function mediaPermissionPromptVisibilityChanged(isVisible: boolean, brows
};
}
/**
* Signals that the prompt for media permission is visible or not.
*
* @param {boolean} isVisible - If the value is true - the prompt for media
* permission is visible otherwise the value is false/undefined.
* @public
* @returns {{
* type: SLOW_GET_USER_MEDIA_OVERLAY,
* isVisible: {boolean}
* }}
*/
export function toggleSlowGUMOverlay(isVisible: boolean) {
return {
type: TOGGLE_SLOW_GUM_OVERLAY,
isVisible
};
}
/**
* The action indicates that an unrecoverable error has occurred and the reload
* screen will be displayed or hidden.

View File

@@ -1,33 +0,0 @@
// @flow
import { Component } from 'react';
/**
* The type of the React {@code Component} props of
* {@link AbstractSlowGUMOverlay}.
*/
type Props = {
/**
* The function to translate human-readable text.
*/
t: Function
};
/**
* Implements a React {@link Component} for slow gUM overlay. Shown when
* a slow gUM promise resolution is detected
*/
export default class AbstractSlowGUMOverlay extends Component<Props> {
/**
* Determines whether this overlay needs to be rendered (according to a
* specific redux state). Called by {@link OverlayContainer}.
*
* @param {Object} state - The redux state.
* @returns {boolean} - If this overlay needs to be rendered, {@code true};
* {@code false}, otherwise.
*/
static needsRender(state: Object) {
return state['features/overlay'].isSlowGUMOverlayVisible;
}
}

View File

@@ -1,36 +0,0 @@
// @flow
import Spinner from '@atlaskit/spinner';
import React from 'react';
import { translate } from '../../../base/i18n';
import AbstractSlowGUMOverlay from './AbstractSlowGUMOverlay';
import OverlayFrame from './OverlayFrame';
/**
* Implements a React {@link Component} for slow gUM overlay. Shown when
* a slow gUM promise resolution is detected
*/
class SlowGUMOverlay extends AbstractSlowGUMOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
// const { t } = this.props;
return (
<OverlayFrame>
<div className = { 'overlay__spinner-container' }>
<Spinner
invertColor = { true }
size = { 'large' } />
</div>
</OverlayFrame>
);
}
}
export default translate(SlowGUMOverlay);

View File

@@ -5,4 +5,3 @@ export { default as OverlayFrame } from './OverlayFrame';
export { default as PageReloadOverlay } from './PageReloadOverlay';
export { default as SuspendedOverlay } from './SuspendedOverlay';
export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay';
export { default as SlowGUMOverlay } from './SlowGUMOverlay';

View File

@@ -2,7 +2,6 @@
import {
PageReloadOverlay,
SlowGUMOverlay,
SuspendedOverlay,
UserMediaPermissionsOverlay
} from './components/web';
@@ -18,7 +17,6 @@ export function getOverlays(): Array<Object> {
return [
PageReloadOverlay,
SuspendedOverlay,
UserMediaPermissionsOverlay,
SlowGUMOverlay
UserMediaPermissionsOverlay
];
}

View File

@@ -5,8 +5,7 @@ import { assign, ReducerRegistry, set } from '../base/redux';
import {
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED,
SET_FATAL_ERROR,
TOGGLE_SLOW_GUM_OVERLAY
SET_FATAL_ERROR
} from './actionTypes';
/**
@@ -29,8 +28,6 @@ ReducerRegistry.register('features/overlay', (state = { }, action) => {
case SET_FATAL_ERROR:
return _setFatalError(state, action);
case TOGGLE_SLOW_GUM_OVERLAY:
return _toggleSlowGUMOverlay(state, action);
}
return state;
@@ -55,24 +52,6 @@ function _mediaPermissionPromptVisibilityChanged(
});
}
/**
* Reduces a specific redux action TOGGLE_SLOW_GUM_OVERLAY of
* the feature overlay.
*
* @param {Object} state - The redux state of the feature overlay.
* @param {Action} action - The redux action to reduce.
* @private
* @returns {Object} The new state of the feature overlay after the reduction of
* the specified action.
*/
function _toggleSlowGUMOverlay(
state,
{ isVisible }) {
return assign(state, {
isSlowGUMOverlayVisible: isVisible
});
}
/**
* Sets the {@code LoadConfigOverlay} overlay visible or not.
*

View File

@@ -4,6 +4,7 @@ declare var JitsiMeetJS: Object;
import uuid from 'uuid';
import { getRoomName } from '../base/conference';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { createLocalTrack } from '../base/lib-jitsi-meet';
import {
@@ -261,7 +262,10 @@ export function makePrecallTest(conferenceOptions: Object) {
*/
export function openDialInPage() {
return function(dispatch: Function, getState: Function) {
const dialInPage = getDialInfoPageURL(getState());
const state = getState();
const locationURL = state['features/base/connection'].locationURL;
const roomName = getRoomName(state);
const dialInPage = getDialInfoPageURL(roomName, locationURL);
openURLInBrowser(dialInPage, true);
};

View File

@@ -162,16 +162,3 @@ export function isPrejoinPageEnabled(state: Object): boolean {
export function isPrejoinPageVisible(state: Object): boolean {
return isPrejoinPageEnabled(state) && state['features/prejoin']?.showPrejoin;
}
/**
* Returns true if we should auto-knock in case lobby is enabled for the room.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function shouldAutoKnock(state: Object): boolean {
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
return (isPrejoinPageEnabled(state) || (iAmRecorder && iAmSipGateway))
&& !state['features/lobby'].knocking;
}

View File

@@ -6,16 +6,10 @@ import {
AUDIO_MUTE,
createRemoteMuteConfirmedEvent,
createToolbarEvent,
sendAnalytics,
VIDEO_MUTE
sendAnalytics
} from '../analytics';
import { hideDialog } from '../base/dialog';
import {
MEDIA_TYPE,
setAudioMuted,
setVideoMuted,
VIDEO_MUTISM_AUTHORITY
} from '../base/media';
import { setAudioMuted } from '../base/media';
import {
getLocalParticipant,
muteRemoteParticipant
@@ -38,26 +32,17 @@ export function hideRemoteVideoMenu() {
* Mutes the local participant.
*
* @param {boolean} enable - Whether to mute or unmute.
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
* @returns {Function}
*/
export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
export function muteLocal(enable: boolean) {
return (dispatch: Dispatch<any>) => {
const isAudio = mediaType === MEDIA_TYPE.AUDIO;
if (!isAudio && mediaType !== MEDIA_TYPE.VIDEO) {
console.error(`Unsupported media type: ${mediaType}`);
return;
}
sendAnalytics(createToolbarEvent(isAudio ? AUDIO_MUTE : VIDEO_MUTE, { enable }));
dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true)
: setVideoMuted(enable, mediaType, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable }));
dispatch(setAudioMuted(enable, /* ensureTrack */ true));
// FIXME: The old conference logic as well as the shared video feature
// still rely on this event being emitted.
typeof APP === 'undefined'
|| APP.UI.emitEvent(isAudio ? UIEvents.AUDIO_MUTED : UIEvents.VIDEO_MUTED, enable, true);
|| APP.UI.emitEvent(UIEvents.AUDIO_MUTED, enable, true);
};
}
@@ -65,18 +50,12 @@ export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
* Mutes the remote participant with the given ID.
*
* @param {string} participantId - ID of the participant to mute.
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
* @returns {Function}
*/
export function muteRemote(participantId: string, mediaType: MEDIA_TYPE) {
export function muteRemote(participantId: string) {
return (dispatch: Dispatch<any>) => {
if (mediaType !== MEDIA_TYPE.AUDIO && mediaType !== MEDIA_TYPE.VIDEO) {
console.error(`Unsupported media type: ${mediaType}`);
return;
}
sendAnalytics(createRemoteMuteConfirmedEvent(participantId, mediaType));
dispatch(muteRemoteParticipant(participantId, mediaType));
sendAnalytics(createRemoteMuteConfirmedEvent(participantId));
dispatch(muteRemoteParticipant(participantId));
};
}
@@ -84,10 +63,9 @@ export function muteRemote(participantId: string, mediaType: MEDIA_TYPE) {
* Mutes all participants.
*
* @param {Array<string>} exclude - Array of participant IDs to not mute.
* @param {MEDIA_TYPE} mediaType - The media type to mute.
* @returns {Function}
*/
export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYPE) {
export function muteAllParticipants(exclude: Array<string>) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const localId = getLocalParticipant(state).id;
@@ -97,7 +75,7 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
/* eslint-disable no-confusing-arrow */
participantIds
.filter(id => !exclude.includes(id))
.map(id => id === localId ? muteLocal(true, mediaType) : muteRemote(id, mediaType))
.map(id => id === localId ? muteLocal(true) : muteRemote(id))
.map(dispatch);
/* eslint-enable no-confusing-arrow */
};

View File

@@ -3,7 +3,6 @@
import React from 'react';
import { Dialog } from '../../base/dialog';
import { MEDIA_TYPE } from '../../base/media';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
import { muteAllParticipants } from '../actions';
@@ -70,7 +69,7 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
exclude
} = this.props;
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
dispatch(muteAllParticipants(exclude));
return true;
}

View File

@@ -1,48 +0,0 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { IconMuteVideoEveryone } from '../../base/icons';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryonesVideoDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to keep unmuted.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which disables the camera of all the other participants.
*/
export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideo';
icon = IconMuteVideoEveryone;
label = 'videothumbnail.domuteVideoOfOthers';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed'));
dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] }));
}
}

View File

@@ -1,103 +0,0 @@
// @flow
import React from 'react';
import { Dialog } from '../../base/dialog';
import { MEDIA_TYPE } from '../../base/media';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
import { muteAllParticipants } from '../actions';
import AbstractMuteRemoteParticipantsVideoDialog, {
type Props as AbstractProps
} from './AbstractMuteRemoteParticipantsVideoDialog';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteEveryonesVideoDialog}.
*/
export type Props = AbstractProps & {
content: string,
exclude: Array<string>,
title: string
};
/**
*
* An abstract Component with the contents for a dialog that asks for confirmation
* from the user before disabling all remote participants cameras.
*
* @extends AbstractMuteRemoteParticipantsVideoDialog
*/
export default class AbstractMuteEveryonesVideoDialog<P: Props> extends AbstractMuteRemoteParticipantsVideoDialog<P> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { content, title } = this.props;
return (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleString = { title }
width = 'small'>
<div>
{ content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
return true;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryonesVideoDialog}'s props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The properties explicitly passed to the component.
* @returns {Props}
*/
export function abstractMapStateToProps(state: Object, ownProps: Props) {
const { exclude, t } = ownProps;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => id === getLocalParticipant(state).id
? t('dialog.muteEveryoneSelf')
: getParticipantDisplayName(state, id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElsesVideoDialog'),
title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
} : {
content: t('dialog.muteEveryonesVideoDialog'),
title: t('dialog.muteEveryonesVideoTitle')
};
}

View File

@@ -2,7 +2,6 @@
import { Component } from 'react';
import { MEDIA_TYPE } from '../../base/media';
import { muteRemote } from '../actions';
/**
@@ -58,7 +57,7 @@ export default class AbstractMuteRemoteParticipantDialog<P:Props = Props>
_onSubmit() {
const { dispatch, participantID } = this.props;
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
dispatch(muteRemote(participantID));
return true;
}

View File

@@ -1,65 +0,0 @@
// @flow
import { Component } from 'react';
import { MEDIA_TYPE } from '../../base/media';
import { muteRemote } from '../actions';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteRemoteParticipantsVideoDialog}.
*/
export type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The ID of the remote participant to be muted.
*/
participantID: string,
/**
* Function to translate i18n labels.
*/
t: Function
};
/**
* Abstract dialog to confirm a remote participant video ute action.
*
* @extends Component
*/
export default class AbstractMuteRemoteParticipantsVideoDialog<P:Props = Props>
extends Component<P> {
/**
* Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onSubmit = this._onSubmit.bind(this);
}
_onSubmit: () => boolean;
/**
* Handles the submit button action.
*
* @private
* @returns {boolean} - True (to note that the modal should be closed).
*/
_onSubmit() {
const { dispatch, participantID } = this.props;
dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
return true;
}
}

View File

@@ -1,103 +0,0 @@
// @flow
import {
createRemoteVideoMenuButtonEvent,
sendAnalytics
} from '../../analytics';
import { openDialog } from '../../base/dialog';
import { IconCameraDisabled } from '../../base/icons';
import { MEDIA_TYPE } from '../../base/media';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { isRemoteTrackMuted } from '../../base/tracks';
import { MuteRemoteParticipantsVideoDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* Boolean to indicate if the video track of the participant is muted or
* not.
*/
_videoTrackMuted: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to
* mute/unmute.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which mutes the remote participant.
*/
export default class AbstractMuteVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.remoteVideoMute';
icon = IconCameraDisabled;
label = 'videothumbnail.domuteVideo';
toggledLabel = 'videothumbnail.videoMuted';
/**
* Handles clicking / pressing the button, and mutes the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createRemoteVideoMenuButtonEvent(
'mute.button',
{
'participant_id': participantID
}));
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
}
/**
* Renders the item disabled if the participant is muted.
*
* @inheritdoc
*/
_isDisabled() {
return this.props._videoTrackMuted;
}
/**
* Renders the item toggled if the participant is muted.
*
* @inheritdoc
*/
_isToggled() {
return this.props._videoTrackMuted;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {{
* _videoTrackMuted: boolean
* }}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const tracks = state['features/base/tracks'];
return {
_videoTrackMuted: isRemoteTrackMuted(
tracks, MEDIA_TYPE.VIDEO, ownProps.participantID)
};
}

View File

@@ -1,54 +0,0 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { IconMuteVideoEveryoneElse } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneElsesVideoButton, {
type Props
} from '../AbstractMuteEveryoneElsesVideoButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for audio muting
* every participant in the conference except the one with the given
* participantID
*/
class MuteEveryoneElsesVideoButton extends AbstractMuteEveryoneElsesVideoButton {
/**
* Instantiates a new {@code Component}.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
return (
<RemoteVideoMenuButton
buttonText = { t('videothumbnail.domuteVideoOfOthers') }
displayClass = { 'mutelink' }
icon = { IconMuteVideoEveryoneElse }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_handleClick: () => void;
}
export default translate(connect()(MuteEveryoneElsesVideoButton));

View File

@@ -1,41 +0,0 @@
// @flow
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractMuteEveryonesVideoDialog, { abstractMapStateToProps, type Props }
from '../AbstractMuteEveryonesVideoDialog';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before disabling all remote participants cameras.
*
* @extends AbstractMuteEveryonesVideoDialog
*/
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleString = { this.props.title }
width = 'small'>
<div>
{ this.props.content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect(abstractMapStateToProps)(MuteEveryonesVideoDialog));

View File

@@ -1,41 +0,0 @@
/* @flow */
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractMuteRemoteParticipantsVideoDialog
from '../AbstractMuteRemoteParticipantsVideoDialog';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before disabling a remote participants camera.
*
* @extends Component
*/
class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okKey = 'dialog.muteParticipantsVideoButton'
onSubmit = { this._onSubmit }
titleKey = 'dialog.muteParticipantsVideoTitle'
width = 'small'>
<div>
{ this.props.t('dialog.muteParticipantsVideoBody') }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(MuteRemoteParticipantsVideoDialog));

View File

@@ -1,67 +0,0 @@
/* @flow */
import React from 'react';
import { translate } from '../../../base/i18n';
import { IconCameraDisabled } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteVideoButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteVideoButton';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
* Implements a React {@link Component} which displays a button for disabling
* the camera of a participant in the conference.
*
* NOTE: At the time of writing this is a button that doesn't use the
* {@code AbstractButton} base component, but is inherited from the same
* super class ({@code AbstractMuteVideoButton} that extends {@code AbstractButton})
* for the sake of code sharing between web and mobile. Once web uses the
* {@code AbstractButton} base component, this can be fully removed.
*/
class MuteVideoButton extends AbstractMuteVideoButton {
/**
* Instantiates a new {@code Component}.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._handleClick = this._handleClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _videoTrackMuted, participantID, t } = this.props;
const muteConfig = _videoTrackMuted ? {
translationKey: 'videothumbnail.videoMuted',
muteClassName: 'mutelink disabled'
} : {
translationKey: 'videothumbnail.domuteVideo',
muteClassName: 'mutelink'
};
return (
<RemoteVideoMenuButton
buttonText = { t(muteConfig.translationKey) }
displayClass = { muteConfig.muteClassName }
icon = { IconCameraDisabled }
id = { `mutelink_${participantID}` }
// eslint-disable-next-line react/jsx-handler-names
onClick = { this._handleClick } />
);
}
_handleClick: () => void
}
export default translate(connect(_mapStateToProps)(MuteVideoButton));

View File

@@ -3,20 +3,20 @@
import React, { Component } from 'react';
import { Icon, IconMenuThumb } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { isRemoteTrackMuted } from '../../../base/tracks';
import { requestRemoteControl, stopController } from '../../../remote-control';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
import {
GrantModeratorButton,
MuteButton,
MuteVideoButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,
@@ -43,6 +43,11 @@ type Props = {
*/
_disableRemoteMute: Boolean,
/**
* Whether or not the participant is currently muted.
*/
_isAudioMuted: boolean,
/**
* Whether or not the participant is a conference moderator.
*/
@@ -146,6 +151,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
const {
_disableKick,
_disableRemoteMute,
_isAudioMuted,
_isModerator,
dispatch,
initialVolumeValue,
@@ -160,6 +166,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
if (!_disableRemoteMute) {
buttons.push(
<MuteButton
isAudioMuted = { _isAudioMuted }
key = 'mute'
participantID = { participantID } />
);
@@ -168,16 +175,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
key = 'mute-others'
participantID = { participantID } />
);
buttons.push(
<MuteVideoButton
key = 'mute-video'
participantID = { participantID } />
);
buttons.push(
<MuteEveryoneElsesVideoButton
key = 'mute-others-video'
participantID = { participantID } />
);
}
buttons.push(
@@ -250,6 +247,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
const tracks = state['features/base/tracks'];
const localParticipant = getLocalParticipant(state);
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const { disableKick } = remoteVideoMenu;
@@ -288,6 +286,7 @@ function _mapStateToProps(state, ownProps) {
}
return {
_isAudioMuted: isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID) || false,
_isModerator: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR),
_disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute),

View File

@@ -5,13 +5,9 @@ export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export { default as KickButton } from './KickButton';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton';
export { default as MuteVideoButton } from './MuteVideoButton';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteEveryoneElsesVideoButton } from './MuteEveryoneElsesVideoButton';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';

View File

@@ -6,6 +6,9 @@ import {
timerWorkerScript
} from './TimerWorker';
const segmentationWidth = 256;
const segmentationHeight = 144;
const segmentationPixelCount = segmentationWidth * segmentationHeight;
const blurValue = '25px';
/**
@@ -15,8 +18,6 @@ const blurValue = '25px';
*/
export default class JitsiStreamBlurEffect {
_model: Object;
_options: Object;
_segmentationPixelCount: number;
_inputVideoElement: HTMLVideoElement;
_onMaskFrameTimer: Function;
_maskFrameTimerWorker: Worker;
@@ -34,13 +35,10 @@ export default class JitsiStreamBlurEffect {
* Represents a modified video MediaStream track.
*
* @class
* @param {Object} model - Meet model.
* @param {Object} options - Segmentation dimensions.
* @param {BodyPix} bpModel - BodyPix model.
*/
constructor(model: Object, options: Object) {
this._model = model;
this._options = options;
this._segmentationPixelCount = this._options.width * this._options.height;
constructor(bpModel: Object) {
this._model = bpModel;
// Bind event handler so it is only bound once for every instance.
this._onMaskFrameTimer = this._onMaskFrameTimer.bind(this);
@@ -78,8 +76,8 @@ export default class JitsiStreamBlurEffect {
this._segmentationMaskCanvas,
0,
0,
this._options.width,
this._options.height,
segmentationWidth,
segmentationHeight,
0,
0,
this._inputVideoElement.width,
@@ -91,7 +89,7 @@ export default class JitsiStreamBlurEffect {
this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);
this._outputCanvasCtx.globalCompositeOperation = 'destination-over';
this._outputCanvasCtx.filter = `blur(${blurValue})`;
this._outputCanvasCtx.filter = `blur(${blurValue})`; // FIXME Does not work on Safari.
this._outputCanvasCtx.drawImage(this._inputVideoElement, 0, 0);
}
@@ -104,7 +102,7 @@ export default class JitsiStreamBlurEffect {
this._model._runInference();
const outputMemoryOffset = this._model._getOutputMemoryOffset() / 4;
for (let i = 0; i < this._segmentationPixelCount; i++) {
for (let i = 0; i < segmentationPixelCount; i++) {
const background = this._model.HEAPF32[outputMemoryOffset + (i * 2)];
const person = this._model.HEAPF32[outputMemoryOffset + (i * 2) + 1];
const shift = Math.max(background, person);
@@ -148,19 +146,19 @@ export default class JitsiStreamBlurEffect {
this._inputVideoElement.height,
0,
0,
this._options.width,
this._options.height
segmentationWidth,
segmentationHeight
);
const imageData = this._segmentationMaskCtx.getImageData(
0,
0,
this._options.width,
this._options.height
segmentationWidth,
segmentationHeight
);
const inputMemoryOffset = this._model._getInputMemoryOffset() / 4;
for (let i = 0; i < this._segmentationPixelCount; i++) {
for (let i = 0; i < segmentationPixelCount; i++) {
this._model.HEAPF32[inputMemoryOffset + (i * 3)] = imageData.data[i * 4] / 255;
this._model.HEAPF32[inputMemoryOffset + (i * 3) + 1] = imageData.data[(i * 4) + 1] / 255;
this._model.HEAPF32[inputMemoryOffset + (i * 3) + 2] = imageData.data[(i * 4) + 2] / 255;
@@ -191,10 +189,10 @@ export default class JitsiStreamBlurEffect {
const { height, frameRate, width }
= firstVideoTrack.getSettings ? firstVideoTrack.getSettings() : firstVideoTrack.getConstraints();
this._segmentationMask = new ImageData(this._options.width, this._options.height);
this._segmentationMask = new ImageData(segmentationWidth, segmentationHeight);
this._segmentationMaskCanvas = document.createElement('canvas');
this._segmentationMaskCanvas.width = this._options.width;
this._segmentationMaskCanvas.height = this._options.height;
this._segmentationMaskCanvas.width = segmentationWidth;
this._segmentationMaskCanvas.height = segmentationHeight;
this._segmentationMaskCtx = this._segmentationMaskCanvas.getContext('2d');
this._outputCanvasElement.width = parseInt(width, 10);
this._outputCanvasElement.height = parseInt(height, 10);

View File

@@ -7,19 +7,8 @@ import createTFLiteModule from './vendor/tflite/tflite';
import createTFLiteSIMDModule from './vendor/tflite/tflite-simd';
const models = {
'model96': 'libs/segm_lite_v681.tflite',
'model144': 'libs/segm_full_v679.tflite'
};
const segmentationDimensions = {
'model96': {
'height': 96,
'width': 160
},
'model144': {
'height': 144,
'width': 256
}
'96': 'libs/segm_lite_v681.tflite',
'144': 'libs/segm_full_v679.tflite'
};
/**
@@ -42,20 +31,14 @@ export async function createBlurEffect() {
const modelBufferOffset = tflite._getModelBufferMemoryOffset();
const modelResponse = await fetch(
wasmCheck.feature.simd ? models.model144 : models.model96
models['144']
);
if (!modelResponse.ok) {
throw new Error('Failed to download tflite model!');
}
const model = await modelResponse.arrayBuffer();
tflite.HEAPU8.set(new Uint8Array(model), modelBufferOffset);
tflite._loadModel(model.byteLength);
const options = wasmCheck.feature.simd ? segmentationDimensions.model144 : segmentationDimensions.model96;
return new JitsiStreamBlurEffect(tflite, options);
return new JitsiStreamBlurEffect(tflite);
}

View File

@@ -125,7 +125,7 @@ class AudioMuteButton extends AbstractAudioMuteButton<Props, *> {
* @returns {void}
*/
_setAudioMuted(audioMuted: boolean) {
this.props.dispatch(muteLocal(audioMuted, MEDIA_TYPE.AUDIO));
this.props.dispatch(muteLocal(audioMuted));
}
/**

View File

@@ -1,76 +0,0 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { IconMuteVideoEveryone } from '../../base/icons';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
import { connect } from '../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryonesVideoDialog } from '../../remote-video-menu/components';
type Props = AbstractButtonProps & {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/*
** Whether the local participant is a moderator or not.
*/
isModerator: Boolean,
/**
* The ID of the local participant.
*/
localParticipantId: string
};
/**
* Implements a React {@link Component} which displays a button for disabling the camera of
* every participant (except the local one)
*/
class MuteEveryonesVideoButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryonesVideo';
icon = IconMuteVideoEveryone;
label = 'toolbar.muteEveryonesVideo';
tooltip = 'toolbar.muteVideoEveryone';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, localParticipantId } = this.props;
sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
dispatch(openDialog(MuteEveryonesVideoDialog, {
exclude: [ localParticipantId ]
}));
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @param {Props} ownProps - The component's own props.
* @returns {Object}
*/
function _mapStateToProps(state: Object, ownProps: Props) {
const localParticipant = getLocalParticipant(state);
const isModerator = localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
const { visible } = ownProps;
const { disableRemoteMute } = state['features/base/config'];
return {
isModerator,
localParticipantId: localParticipant.id,
visible: visible && isModerator && !disableRemoteMute
};
}
export default translate(connect(_mapStateToProps)(MuteEveryonesVideoButton));

View File

@@ -1,6 +1,7 @@
// @flow
import React, { Component } from 'react';
import * as wasmCheck from 'wasm-check';
import {
ACTION_SHORTCUT_TRIGGERED,
@@ -36,7 +37,6 @@ import { OverflowMenuItem } from '../../../base/toolbox/components';
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
import { isVpaasMeeting } from '../../../billing-counter/functions';
import { VideoBlurButton } from '../../../blur';
import { checkBlurSupport } from '../../../blur/functions';
import { CHAT_SIZE, ChatCounter, toggleChat } from '../../../chat';
import { EmbedMeetingDialog } from '../../../embed-meeting';
import { SharedDocumentButton } from '../../../etherpad';
@@ -82,7 +82,6 @@ import DownloadButton from '../DownloadButton';
import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';
import MuteEveryoneButton from '../MuteEveryoneButton';
import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
import AudioSettingsButton from './AudioSettingsButton';
import OverflowMenuButton from './OverflowMenuButton';
@@ -1071,7 +1070,7 @@ class Toolbox extends Component<Props, State> {
&& <VideoBlurButton
key = 'videobackgroundblur'
showLabel = { true }
visible = { !_screensharing && checkBlurSupport() } />,
visible = { !_screensharing && wasmCheck.feature.simd } />,
this._shouldShowButton('settings')
&& <SettingsButton
key = 'settings'
@@ -1080,10 +1079,6 @@ class Toolbox extends Component<Props, State> {
&& <MuteEveryoneButton
key = 'mute-everyone'
showLabel = { true } />,
this._shouldShowButton('mute-video-everyone')
&& <MuteEveryonesVideoButton
key = 'mute-video-everyone'
showLabel = { true } />,
this._shouldShowButton('stats')
&& <OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }

View File

@@ -216,7 +216,7 @@ function onConnectionSuccess() {
devices.push('video');
}
if (localAudio || autoCreateLocalAudio) {
if (autoCreateLocalAudio) {
devices.push('audio');
}