mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-14 13:27:49 +00:00
Compare commits
17 Commits
android-sd
...
3495
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8da0552541 | ||
|
|
7ce0def995 | ||
|
|
48285e8a2d | ||
|
|
21dcc41d31 | ||
|
|
625d268373 | ||
|
|
681782ed20 | ||
|
|
1baa85b649 | ||
|
|
72137a2811 | ||
|
|
0734ce7ae3 | ||
|
|
2dc06c28e3 | ||
|
|
5848669552 | ||
|
|
c0376d238a | ||
|
|
979b773c3c | ||
|
|
3195a449ca | ||
|
|
d7483f07e3 | ||
|
|
9c1b802997 | ||
|
|
bb3a10b0fc |
144
conference.js
144
conference.js
@@ -23,7 +23,8 @@ import {
|
||||
sendAnalytics
|
||||
} from './react/features/analytics';
|
||||
import {
|
||||
redirectWithStoredParams,
|
||||
maybeRedirectToWelcomePage,
|
||||
redirectToStaticPage,
|
||||
reloadWithStoredParams
|
||||
} from './react/features/app';
|
||||
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
conferenceWillJoin,
|
||||
conferenceWillLeave,
|
||||
dataChannelOpened,
|
||||
kickedOut,
|
||||
lockStateChanged,
|
||||
onStartMutedPolicyChanged,
|
||||
p2pStatusChanged,
|
||||
@@ -76,7 +78,10 @@ import {
|
||||
setVideoAvailable,
|
||||
setVideoMuted
|
||||
} from './react/features/base/media';
|
||||
import { showNotification } from './react/features/notifications';
|
||||
import {
|
||||
hideNotification,
|
||||
showNotification
|
||||
} from './react/features/notifications';
|
||||
import {
|
||||
dominantSpeakerChanged,
|
||||
getLocalParticipant,
|
||||
@@ -100,11 +105,7 @@ import {
|
||||
trackAdded,
|
||||
trackRemoved
|
||||
} from './react/features/base/tracks';
|
||||
import {
|
||||
getLocationContextRoot,
|
||||
getJitsiMeetGlobalNS
|
||||
} from './react/features/base/util';
|
||||
import { notifyKickedOut } from './react/features/conference';
|
||||
import { getJitsiMeetGlobalNS } from './react/features/base/util';
|
||||
import { addMessage } from './react/features/chat';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { appendSuffix } from './react/features/display-name';
|
||||
@@ -211,77 +212,6 @@ function muteLocalVideo(muted) {
|
||||
APP.store.dispatch(setVideoMuted(muted));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the welcome page is enabled and redirects to it.
|
||||
* If requested show a thank you dialog before that.
|
||||
* If we have a close page enabled, redirect to it without
|
||||
* showing any other dialog.
|
||||
*
|
||||
* @param {object} options used to decide which particular close page to show
|
||||
* or if close page is disabled, whether we should show the thankyou dialog
|
||||
* @param {boolean} options.showThankYou - whether we should
|
||||
* show thank you dialog
|
||||
* @param {boolean} options.feedbackSubmitted - whether feedback was submitted
|
||||
*/
|
||||
function maybeRedirectToWelcomePage(options) {
|
||||
// if close page is enabled redirect to it, without further action
|
||||
if (config.enableClosePage) {
|
||||
const { isGuest } = APP.store.getState()['features/base/jwt'];
|
||||
|
||||
// save whether current user is guest or not, before navigating
|
||||
// to close page
|
||||
window.sessionStorage.setItem('guest', isGuest);
|
||||
redirectToStaticPage(`static/${
|
||||
options.feedbackSubmitted ? 'close.html' : 'close2.html'}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// else: show thankYou dialog only if there is no feedback
|
||||
if (options.showThankYou) {
|
||||
APP.store.dispatch(showNotification({
|
||||
titleArguments: { appName: interfaceConfig.APP_NAME },
|
||||
titleKey: 'dialog.thankYou'
|
||||
}));
|
||||
}
|
||||
|
||||
// if Welcome page is enabled redirect to welcome page after 3 sec, if
|
||||
// there is a thank you message to be shown, 0.5s otherwise.
|
||||
if (config.enableWelcomePage) {
|
||||
setTimeout(
|
||||
() => {
|
||||
APP.store.dispatch(redirectWithStoredParams('/'));
|
||||
},
|
||||
options.showThankYou ? 3000 : 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a specific pathname to window.location.pathname taking into account
|
||||
* the context root of the Web app.
|
||||
*
|
||||
* @param {string} pathname - The pathname to assign to
|
||||
* window.location.pathname. If the specified pathname is relative, the context
|
||||
* root of the Web app will be prepended to the specified pathname before
|
||||
* assigning it to window.location.pathname.
|
||||
* @return {void}
|
||||
*/
|
||||
function redirectToStaticPage(pathname) {
|
||||
const windowLocation = window.location;
|
||||
let newPathname = pathname;
|
||||
|
||||
if (!newPathname.startsWith('/')) {
|
||||
// A pathname equal to ./ specifies the current directory. It will be
|
||||
// fine but pointless to include it because contextRoot is the current
|
||||
// directory.
|
||||
newPathname.startsWith('./')
|
||||
&& (newPathname = newPathname.substring(2));
|
||||
newPathname = getLocationContextRoot(windowLocation) + newPathname;
|
||||
}
|
||||
|
||||
windowLocation.pathname = newPathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* A queue for the async replaceLocalTrack action so that multiple audio
|
||||
* replacements cannot happen simultaneously. This solves the issue where
|
||||
@@ -347,7 +277,7 @@ class ConferenceConnector {
|
||||
|
||||
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
|
||||
// let's show some auth not allowed page
|
||||
redirectToStaticPage('static/authError.html');
|
||||
APP.store.dispatch(redirectToStaticPage('static/authError.html'));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1018,17 +948,15 @@ export default {
|
||||
* Returns the connection times stored in the library.
|
||||
*/
|
||||
getConnectionTimes() {
|
||||
return this._room.getConnectionTimes();
|
||||
return room.getConnectionTimes();
|
||||
},
|
||||
|
||||
// used by torture currently
|
||||
isJoined() {
|
||||
return this._room
|
||||
&& this._room.isJoined();
|
||||
return room && room.isJoined();
|
||||
},
|
||||
getConnectionState() {
|
||||
return this._room
|
||||
&& this._room.getConnectionState();
|
||||
return room && room.getConnectionState();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1037,8 +965,7 @@ export default {
|
||||
* P2P connection
|
||||
*/
|
||||
getP2PConnectionState() {
|
||||
return this._room
|
||||
&& this._room.getP2PConnectionState();
|
||||
return room && room.getP2PConnectionState();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1047,7 +974,7 @@ export default {
|
||||
*/
|
||||
_startP2P() {
|
||||
try {
|
||||
this._room && this._room.startP2PSession();
|
||||
room && room.startP2PSession();
|
||||
} catch (error) {
|
||||
logger.error('Start P2P failed', error);
|
||||
throw error;
|
||||
@@ -1060,7 +987,7 @@ export default {
|
||||
*/
|
||||
_stopP2P() {
|
||||
try {
|
||||
this._room && this._room.stopP2PSession();
|
||||
room && room.stopP2PSession();
|
||||
} catch (error) {
|
||||
logger.error('Stop P2P failed', error);
|
||||
throw error;
|
||||
@@ -1075,7 +1002,7 @@ export default {
|
||||
* false otherwise.
|
||||
*/
|
||||
isConnectionInterrupted() {
|
||||
return this._room.isConnectionInterrupted();
|
||||
return room.isConnectionInterrupted();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1136,7 +1063,7 @@ export default {
|
||||
},
|
||||
|
||||
getMyUserId() {
|
||||
return this._room && this._room.myUserId();
|
||||
return room && room.myUserId();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1159,7 +1086,7 @@ export default {
|
||||
* least one track.
|
||||
*/
|
||||
getNumberOfParticipantsWithTracks() {
|
||||
return this._room.getParticipants()
|
||||
return room.getParticipants()
|
||||
.filter(p => p.getTracks().length > 0)
|
||||
.length;
|
||||
},
|
||||
@@ -1323,7 +1250,7 @@ export default {
|
||||
this.localVideo = newStream;
|
||||
this._setSharingScreen(newStream);
|
||||
if (newStream) {
|
||||
APP.UI.addLocalStream(newStream);
|
||||
APP.UI.addLocalVideoStream(newStream);
|
||||
}
|
||||
this.setVideoMuteStatus(this.isLocalVideoMuted());
|
||||
})
|
||||
@@ -1374,9 +1301,6 @@ export default {
|
||||
replaceLocalTrack(this.localAudio, newStream, room))
|
||||
.then(() => {
|
||||
this.localAudio = newStream;
|
||||
if (newStream) {
|
||||
APP.UI.addLocalStream(newStream);
|
||||
}
|
||||
this.setAudioMuteStatus(this.isLocalAudioMuted());
|
||||
})
|
||||
.then(resolve)
|
||||
@@ -1781,11 +1705,6 @@ export default {
|
||||
|
||||
logger.log(`USER ${id} LEFT:`, user);
|
||||
APP.API.notifyUserLeft(id);
|
||||
APP.UI.messageHandler.participantNotification(
|
||||
user.getDisplayName(),
|
||||
'notify.somebody',
|
||||
'disconnected',
|
||||
'notify.disconnected');
|
||||
APP.UI.onSharedVideoStop(id);
|
||||
});
|
||||
|
||||
@@ -1854,14 +1773,29 @@ export default {
|
||||
APP.UI.setAudioLevel(id, newLvl);
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (_, participantThatMutedUs) => {
|
||||
// we store the last start muted notification id that we showed,
|
||||
// so we can hide it when unmuted mic is detected
|
||||
let lastNotificationId;
|
||||
|
||||
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
|
||||
if (participantThatMutedUs) {
|
||||
APP.store.dispatch(participantMutedUs(participantThatMutedUs));
|
||||
}
|
||||
|
||||
if (lastNotificationId && track.isAudioTrack() && track.isLocal() && !track.isMuted()) {
|
||||
APP.store.dispatch(hideNotification(lastNotificationId));
|
||||
lastNotificationId = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.TALK_WHILE_MUTED, () => {
|
||||
APP.UI.showToolbar(6000);
|
||||
const action = APP.store.dispatch(showNotification({
|
||||
titleKey: 'toolbar.talkWhileMutedPopup',
|
||||
customActionNameKey: 'notify.unmute',
|
||||
customActionHandler: muteLocalAudio.bind(this, false)
|
||||
}));
|
||||
|
||||
lastNotificationId = action.uid;
|
||||
});
|
||||
room.on(JitsiConferenceEvents.SUBJECT_CHANGED,
|
||||
subject => APP.store.dispatch(conferenceSubjectChanged(subject)));
|
||||
@@ -1962,7 +1896,7 @@ export default {
|
||||
|
||||
room.on(JitsiConferenceEvents.KICKED, participant => {
|
||||
APP.UI.hideStats();
|
||||
APP.store.dispatch(notifyKickedOut(participant));
|
||||
APP.store.dispatch(kickedOut(room, participant));
|
||||
|
||||
// FIXME close
|
||||
});
|
||||
@@ -2269,7 +2203,7 @@ export default {
|
||||
|
||||
if (config.requireDisplayName
|
||||
&& !APP.conference.getLocalDisplayName()
|
||||
&& !this._room.isHidden()) {
|
||||
&& !room.isHidden()) {
|
||||
APP.UI.promptDisplayName();
|
||||
}
|
||||
|
||||
@@ -2601,7 +2535,7 @@ export default {
|
||||
room = undefined;
|
||||
|
||||
APP.API.notifyReadyToClose();
|
||||
maybeRedirectToWelcomePage(values[0]);
|
||||
APP.store.dispatch(maybeRedirectToWelcomePage(values[0]));
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
14
css/_avatar.scss
Normal file
14
css/_avatar.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.avatar {
|
||||
align-items: center;
|
||||
background-color: #AAA;
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-weight: 100;
|
||||
justify-content: center;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.defaultAvatar {
|
||||
opacity: 0.6
|
||||
}
|
||||
@@ -493,7 +493,6 @@
|
||||
}
|
||||
|
||||
#dominantSpeakerAvatarContainer,
|
||||
#dominantSpeakerAvatar,
|
||||
.dynamic-shadow {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
@@ -503,14 +502,9 @@
|
||||
top: 50px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
border-radius: 100px;
|
||||
overflow: hidden;
|
||||
visibility: inherit;
|
||||
}
|
||||
#dominantSpeakerAvatar {
|
||||
background-color: #000000;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dynamic-shadow {
|
||||
border-radius: 50%;
|
||||
@@ -524,7 +518,6 @@
|
||||
.avatar-container {
|
||||
@include maxSize(60px);
|
||||
@include absoluteAligning();
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 50%;
|
||||
|
||||
@@ -86,5 +86,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'navigate_section_list';
|
||||
@import 'third-party-branding/google';
|
||||
@import 'third-party-branding/microsoft';
|
||||
@import 'avatar';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
13
doc/api.md
13
doc/api.md
@@ -387,6 +387,19 @@ changes. The listener will receive an object with the following structure:
|
||||
}
|
||||
```
|
||||
|
||||
* **participantKickedOut** - event notifications about a participants being removed from the room. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
kicked: {
|
||||
id: string, // the id of the participant removed from the room
|
||||
local: boolean // whether or not the participant is the local particiapnt
|
||||
},
|
||||
kicker: {
|
||||
id: string // the id of the participant who kicked out the other participant
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **participantLeft** - event notifications about participants that leave the room. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -4,7 +4,7 @@
|
||||
"countryNotSupported": "We do not support this destination yet.",
|
||||
"countryReminder": "Calling outside the US? Please make sure you start with the country code!",
|
||||
"disabled": "You can't invite people.",
|
||||
"failedToAdd": "Failed to add members",
|
||||
"failedToAdd": "Failed to add participants",
|
||||
"footerText": "Dialing out is disabled.",
|
||||
"invite": "Invite",
|
||||
"loading": "Searching for people and phone numbers",
|
||||
@@ -112,7 +112,6 @@
|
||||
"transport_plural": "Transports:",
|
||||
"turn": " (turn)"
|
||||
},
|
||||
"contactlist_plural": "__count__ Members",
|
||||
"dateUtils": {
|
||||
"earlier": "Earlier",
|
||||
"today": "Today",
|
||||
@@ -148,7 +147,7 @@
|
||||
"liveStreaming": "Live Stream"
|
||||
},
|
||||
"allow": "Allow",
|
||||
"alreadySharedVideoMsg": "Another member is already sharing a video. This conference allows only one shared video at a time.",
|
||||
"alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
|
||||
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
|
||||
"applicationWindow": "Application window",
|
||||
"Back": "Back",
|
||||
@@ -173,7 +172,6 @@
|
||||
"connecting": "Connecting",
|
||||
"contactSupport": "Contact support",
|
||||
"copy": "Copy",
|
||||
"currentPassword": "The current password is",
|
||||
"defaultError": "There was some kind of error",
|
||||
"detectext": "Error when trying to detect desktopsharing extension.",
|
||||
"dismiss": "Dismiss",
|
||||
@@ -200,18 +198,18 @@
|
||||
"kickMessage": "You can contact __participantDisplayName__ for more details.",
|
||||
"kickParticipantButton": "Kick",
|
||||
"kickParticipantDialog": "Are you sure you want to kick this participant?",
|
||||
"kickParticipantTitle": "Kick this member?",
|
||||
"kickParticipantTitle": "Kick this participant?",
|
||||
"kickTitle": "Ouch! __participantDisplayName__ kicked you out of the meeting",
|
||||
"liveStreaming": "Live Streaming",
|
||||
"liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.",
|
||||
"liveStreamingDisabledTooltip": "Start live stream disabled.",
|
||||
"lockMessage": "Failed to lock the conference.",
|
||||
"lockRoom": "Add meeting password",
|
||||
"lockRoom": "Add meeting $t(lockRoomPasswordUppercase)",
|
||||
"lockTitle": "Lock failed",
|
||||
"logoutQuestion": "Are you sure you want to logout and stop the conference?",
|
||||
"logoutTitle": "Logout",
|
||||
"maxUsersLimitReached": "The limit for maximum number of members has been reached. The conference is full. Please contact the meeting owner or try again later!",
|
||||
"maxUsersLimitReachedTitle": "Maximum members limit reached",
|
||||
"maxUsersLimitReached": "The limit for maximum number of participants has been reached. The conference is full. Please contact the meeting owner or try again later!",
|
||||
"maxUsersLimitReachedTitle": "Maximum participants limit reached",
|
||||
"micConstraintFailedError": "Your microphone does not satisfy some of the required constraints.",
|
||||
"micNotFoundError": "Microphone was not found.",
|
||||
"micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level",
|
||||
@@ -221,17 +219,13 @@
|
||||
"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 member?",
|
||||
"muteParticipantTitle": "Mute this participant?",
|
||||
"Ok": "Ok",
|
||||
"oops": "Oops!",
|
||||
"password": "Enter password",
|
||||
"passwordError": "This conversation is currently protected by a password. Only the owner of the conference can set a password.",
|
||||
"passwordError2": "This conversation isn't currently protected by a password. Only the owner of the conference can set a password.",
|
||||
"passwordErrorTitle": "Password Error",
|
||||
"passwordLabel": "Password",
|
||||
"passwordNotSupported": "Setting a meeting password is not supported.",
|
||||
"passwordNotSupportedTitle": "Password not supported",
|
||||
"passwordRequired": "Password required",
|
||||
"passwordLabel": "$t(lockRoomPasswordUppercase)",
|
||||
"passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
|
||||
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) not supported",
|
||||
"passwordRequired": "$t(lockRoomPasswordUppercase) required",
|
||||
"permissionDenied": "Permission Denied",
|
||||
"popupError": "Your browser is blocking pop-up windows from this site. Please enable pop-ups in your browser's security settings and try again.",
|
||||
"popupErrorTitle": "Pop-up blocked",
|
||||
@@ -248,7 +242,7 @@
|
||||
"remoteControlStopMessage": "The remote control session ended!",
|
||||
"remoteControlTitle": "Remote desktop control",
|
||||
"Remove": "Remove",
|
||||
"removePassword": "Remove password",
|
||||
"removePassword": "Remove $t(lockRoomPassword)",
|
||||
"removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
|
||||
"removeSharedVideoTitle": "Remove shared video",
|
||||
"reservationError": "Reservation system error",
|
||||
@@ -286,7 +280,7 @@
|
||||
"tokenAuthFailedTitle": "Authentication failed",
|
||||
"transcribing": "Transcribing",
|
||||
"unableToSwitch": "Unable to switch video stream.",
|
||||
"unlockRoom": "Remove meeting password",
|
||||
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
|
||||
"userPassword": "user password",
|
||||
"WaitForHostMsg": "The conference <b>__room__</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
|
||||
"WaitForHostMsgWOk": "The conference <b>__room__</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
|
||||
@@ -298,34 +292,6 @@
|
||||
"dialOut": {
|
||||
"statusMessage": "is now __status__"
|
||||
},
|
||||
"email": {
|
||||
"and": "and",
|
||||
"body": [
|
||||
" Note that __appName__ is currently only supported by __supportedBrowsers__, so you need to be using one of these browsers.",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Hey there, I%27d like to invite you to a __appName__ conference I%27ve just set up.",
|
||||
"Please click on the following link in order to join the conference.",
|
||||
"Talk to you in a sec!",
|
||||
"__roomUrl__",
|
||||
"__sharedKeyText__"
|
||||
],
|
||||
"sharedKey": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"This conference is password-protected. Please use the following pin when joining:",
|
||||
"__sharedKey__"
|
||||
],
|
||||
"subject": "Invitation to a __appName__ (__conferenceName__)"
|
||||
},
|
||||
"feedback": {
|
||||
"average": "Average",
|
||||
"bad": "Bad",
|
||||
@@ -344,8 +310,8 @@
|
||||
},
|
||||
"info": {
|
||||
"accessibilityLabel": "Show info",
|
||||
"addPassword": "Add password",
|
||||
"cancelPassword": "Cancel password",
|
||||
"addPassword": "Add $t(lockRoomPassword)",
|
||||
"cancelPassword": "Cancel $t(lockRoomPassword)",
|
||||
"conferenceURL": "Link:",
|
||||
"country": "Country",
|
||||
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
|
||||
@@ -367,7 +333,7 @@
|
||||
"noPassword": "None",
|
||||
"noRoom": "No room was specified to dial-in into.",
|
||||
"numbers": "Dial-in Numbers",
|
||||
"password": "Password:",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"title": "Share",
|
||||
"tooltip": "Share link and dial-in info for this meeting",
|
||||
"label": "Meeting info"
|
||||
@@ -463,6 +429,8 @@
|
||||
"stop": "Stop Recording",
|
||||
"yes": "Yes"
|
||||
},
|
||||
"lockRoomPassword": "password",
|
||||
"lockRoomPasswordUppercase": "Password",
|
||||
"me": "me",
|
||||
"notify": {
|
||||
"connectedOneMember": "__name__ joined the meeting",
|
||||
@@ -482,17 +450,20 @@
|
||||
"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.",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
|
||||
"raisedHand": "__name__ would like to speak.",
|
||||
"somebody": "Somebody",
|
||||
"startSilentTitle": "You joined with no audio output!",
|
||||
"startSilentDescription": "Rejoin the meeting to enable audio",
|
||||
"suboptimalExperienceDescription": "Eer... we are afraid your experience with __appName__ isn't going to be that great here. We are looking for ways to improve this but, until then, please try using one of the <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>.",
|
||||
"suboptimalExperienceTitle": "Browser Warning",
|
||||
"unmute": "Unmute",
|
||||
"newDeviceCameraTitle": "New camera detected",
|
||||
"newDeviceAudioTitle": "New audio device detected",
|
||||
"newDeviceAction": "Use"
|
||||
},
|
||||
"passwordSetRemotely": "set by another member",
|
||||
"passwordSetRemotely": "set by another participant",
|
||||
"passwordDigitsOnly": "Up to __number__ digits",
|
||||
"poweredby": "powered by",
|
||||
"presenceStatus": {
|
||||
@@ -567,7 +538,6 @@
|
||||
"more": "More",
|
||||
"name": "Name",
|
||||
"noDevice": "None",
|
||||
"password": "SET PASSWORD",
|
||||
"selectAudioOutput": "Audio output",
|
||||
"selectCamera": "Camera",
|
||||
"selectMic": "Microphone",
|
||||
@@ -684,7 +654,7 @@
|
||||
"raiseYourHand": "Raise your hand",
|
||||
"Settings": "Settings",
|
||||
"sharedvideo": "Share a YouTube video",
|
||||
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other members.",
|
||||
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other participants.",
|
||||
"shareRoom": "Invite someone",
|
||||
"shortcuts": "View shortcuts",
|
||||
"sip": "Call SIP number",
|
||||
@@ -764,11 +734,11 @@
|
||||
"flip": "Flip",
|
||||
"kick": "Kick out",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Member is muted",
|
||||
"mute": "Participant is muted",
|
||||
"muted": "Muted",
|
||||
"remoteControl": "Remote control",
|
||||
"show": "Show on stage",
|
||||
"videomute": "Member has stopped the camera"
|
||||
"videomute": "Participant has stopped the camera"
|
||||
},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
|
||||
@@ -658,6 +658,24 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application of a participant, remote or local, being
|
||||
* removed from the conference by another participant.
|
||||
*
|
||||
* @param {string} kicked - The ID of the participant removed from the
|
||||
* conference.
|
||||
* @param {string} kicker - The ID of the participant that removed the
|
||||
* other participant.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyKickedOut(kicked: Object, kicker: Object) {
|
||||
this._sendEvent({
|
||||
name: 'participant-kicked-out',
|
||||
kicked,
|
||||
kicker
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application of the current meeting requiring a password
|
||||
* to join.
|
||||
|
||||
3
modules/API/external/external_api.js
vendored
3
modules/API/external/external_api.js
vendored
@@ -63,6 +63,7 @@ const events = {
|
||||
'mic-error': 'micError',
|
||||
'outgoing-message': 'outgoingMessage',
|
||||
'participant-joined': 'participantJoined',
|
||||
'participant-kicked-out': 'participantKickedOut',
|
||||
'participant-left': 'participantLeft',
|
||||
'password-required': 'passwordRequired',
|
||||
'proxy-connection-event': 'proxyConnectionEvent',
|
||||
@@ -561,7 +562,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
this.emit('_willDispose');
|
||||
this._transport.dispose();
|
||||
this.removeAllListeners();
|
||||
if (this._frame) {
|
||||
if (this._frame && this._frame.parentNode) {
|
||||
this._frame.parentNode.removeChild(this._frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,22 +229,11 @@ UI.unbindEvents = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Show local stream on UI.
|
||||
* Show local video stream on UI.
|
||||
* @param {JitsiTrack} track stream to show
|
||||
*/
|
||||
UI.addLocalStream = track => {
|
||||
switch (track.getType()) {
|
||||
case 'audio':
|
||||
// Local audio is not rendered so no further action is needed at this
|
||||
// point.
|
||||
break;
|
||||
case 'video':
|
||||
VideoLayout.changeLocalVideo(track);
|
||||
break;
|
||||
default:
|
||||
logger.error(`Unknown stream type: ${track.getType()}`);
|
||||
break;
|
||||
}
|
||||
UI.addLocalVideoStream = track => {
|
||||
VideoLayout.changeLocalVideo(track);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -510,8 +499,8 @@ UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
|
||||
* @param {string} avatarURL - The URL to avatar image to display.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.refreshAvatarDisplay = function(id, avatarURL) {
|
||||
VideoLayout.changeUserAvatar(id, avatarURL);
|
||||
UI.refreshAvatarDisplay = function(id) {
|
||||
VideoLayout.changeUserAvatar(id);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,7 +33,7 @@ SharedVideoThumb.prototype.constructor = SharedVideoThumb;
|
||||
SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function() {};
|
||||
|
||||
// eslint-disable-next-line no-empty-function
|
||||
SharedVideoThumb.prototype.avatarChanged = function() {};
|
||||
SharedVideoThumb.prototype.initializeAvatar = function() {};
|
||||
|
||||
SharedVideoThumb.prototype.createContainer = function(spanId) {
|
||||
const container = document.createElement('span');
|
||||
|
||||
@@ -5,11 +5,8 @@ import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../../react/features/base/avatar';
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURLByParticipantId
|
||||
} from '../../../react/features/base/participants';
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
@@ -214,8 +211,7 @@ export default class LargeVideoManager {
|
||||
container.setStream(id, stream, videoType);
|
||||
|
||||
// change the avatar url on large
|
||||
this.updateAvatar(
|
||||
getAvatarURLByParticipantId(APP.store.getState(), id));
|
||||
this.updateAvatar();
|
||||
|
||||
// If the user's connection is disrupted then the avatar will be
|
||||
// displayed in case we have no video image cached. That is if
|
||||
@@ -406,18 +402,16 @@ export default class LargeVideoManager {
|
||||
/**
|
||||
* Updates the src of the dominant speaker avatar
|
||||
*/
|
||||
updateAvatar(avatarUrl) {
|
||||
if (avatarUrl) {
|
||||
ReactDOM.render(
|
||||
updateAvatar() {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<Avatar
|
||||
id = "dominantSpeakerAvatar"
|
||||
uri = { avatarUrl } />,
|
||||
this._dominantSpeakerAvatarContainer
|
||||
);
|
||||
} else {
|
||||
ReactDOM.unmountComponentAtNode(
|
||||
this._dominantSpeakerAvatarContainer);
|
||||
}
|
||||
participantId = { this.id }
|
||||
size = { 200 } />
|
||||
</Provider>,
|
||||
this._dominantSpeakerAvatarContainer
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,9 +7,6 @@ import { Provider } from 'react-redux';
|
||||
|
||||
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { VideoTrack } from '../../../react/features/base/media';
|
||||
import {
|
||||
getAvatarURLByParticipantId
|
||||
} from '../../../react/features/base/participants';
|
||||
import { updateSettings } from '../../../react/features/base/settings';
|
||||
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
|
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
@@ -55,8 +52,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
|
||||
// Initialize the avatar display with an avatar url selected from the redux
|
||||
// state. Redux stores the local user with a hardcoded participant id of
|
||||
// 'local' if no id has been assigned yet.
|
||||
this.avatarChanged(
|
||||
getAvatarURLByParticipantId(APP.store.getState(), this.id));
|
||||
this.initializeAvatar();
|
||||
|
||||
this.addAudioLevelIndicator();
|
||||
this.updateIndicators();
|
||||
|
||||
@@ -10,9 +10,8 @@ import { Provider } from 'react-redux';
|
||||
import { i18next } from '../../../react/features/base/i18n';
|
||||
import { AudioLevelIndicator }
|
||||
from '../../../react/features/audio-level-indicator';
|
||||
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
|
||||
import {
|
||||
Avatar as AvatarDisplay,
|
||||
getAvatarURLByParticipantId,
|
||||
getPinnedParticipant,
|
||||
pinParticipant
|
||||
} from '../../../react/features/base/participants';
|
||||
@@ -570,8 +569,7 @@ SmallVideo.prototype.updateView = function() {
|
||||
if (!this.hasAvatar) {
|
||||
if (this.id) {
|
||||
// Init avatar
|
||||
this.avatarChanged(
|
||||
getAvatarURLByParticipantId(APP.store.getState(), this.id));
|
||||
this.initializeAvatar();
|
||||
} else {
|
||||
logger.error('Unable to init avatar - no id', this);
|
||||
|
||||
@@ -609,19 +607,22 @@ SmallVideo.prototype.updateView = function() {
|
||||
* Updates the react component displaying the avatar with the passed in avatar
|
||||
* url.
|
||||
*
|
||||
* @param {string} avatarUrl - The uri to the avatar image.
|
||||
* @returns {void}
|
||||
*/
|
||||
SmallVideo.prototype.avatarChanged = function(avatarUrl) {
|
||||
SmallVideo.prototype.initializeAvatar = function() {
|
||||
const thumbnail = this.$avatar().get(0);
|
||||
|
||||
this.hasAvatar = true;
|
||||
|
||||
if (thumbnail) {
|
||||
// Maybe add a special case for local participant, as on init of
|
||||
// LocalVideo.js the id is set to "local" but will get updated later.
|
||||
ReactDOM.render(
|
||||
<AvatarDisplay
|
||||
className = 'userAvatar'
|
||||
uri = { avatarUrl } />,
|
||||
<Provider store = { APP.store }>
|
||||
<AvatarDisplay
|
||||
className = 'userAvatar'
|
||||
participantId = { this.id } />
|
||||
</Provider>,
|
||||
thumbnail
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,16 +53,6 @@ function onLocalFlipXChanged(val) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the redux representation of all known users.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array}
|
||||
*/
|
||||
function getAllParticipants() {
|
||||
return APP.store.getState()['features/base/participants'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all thumbnails in the filmstrip.
|
||||
*
|
||||
@@ -86,43 +76,6 @@ function getLocalParticipant() {
|
||||
return getLocalParticipantFromStore(APP.store.getState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user ID of the remote participant that is current the dominant
|
||||
* speaker.
|
||||
*
|
||||
* @private
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function getCurrentRemoteDominantSpeakerID() {
|
||||
const dominantSpeaker = getAllParticipants()
|
||||
.find(participant => participant.dominantSpeaker);
|
||||
|
||||
if (dominantSpeaker) {
|
||||
return dominantSpeaker.local ? null : dominantSpeaker.id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the corresponding resource id to the given peer container
|
||||
* DOM element.
|
||||
*
|
||||
* @return the corresponding resource id to the given peer container
|
||||
* DOM element
|
||||
*/
|
||||
function getPeerContainerResourceId(containerElement) {
|
||||
if (localVideoThumbnail.container === containerElement) {
|
||||
return localVideoThumbnail.id;
|
||||
}
|
||||
|
||||
const i = containerElement.id.indexOf('participant_');
|
||||
|
||||
if (i >= 0) {
|
||||
return containerElement.id.substring(i + 12);
|
||||
}
|
||||
}
|
||||
|
||||
const VideoLayout = {
|
||||
init(emitter) {
|
||||
eventEmitter = emitter;
|
||||
@@ -208,10 +161,6 @@ const VideoLayout = {
|
||||
* and setting them assume the id is already set.
|
||||
*/
|
||||
mucJoined() {
|
||||
if (largeVideo && !largeVideo.id) {
|
||||
this.updateLargeVideo(getLocalParticipant().id, true);
|
||||
}
|
||||
|
||||
// FIXME: replace this call with a generic update call once SmallVideo
|
||||
// only contains a ReactElement. Then remove this call once the
|
||||
// Filmstrip is fully in React.
|
||||
@@ -247,79 +196,6 @@ const VideoLayout = {
|
||||
localVideoThumbnail.setVisible(visible);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if removed video is currently displayed and tries to display
|
||||
* another one instead.
|
||||
* Uses focusedID if any or dominantSpeakerID if any,
|
||||
* otherwise elects new video, in this order.
|
||||
*/
|
||||
_updateAfterThumbRemoved(id) {
|
||||
// Always trigger an update if large video is empty.
|
||||
if (!largeVideo
|
||||
|| (this.getLargeVideoID() && !this.isCurrentlyOnLarge(id))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pinnedId = this.getPinnedId();
|
||||
let newId;
|
||||
|
||||
if (pinnedId) {
|
||||
newId = pinnedId;
|
||||
} else if (getCurrentRemoteDominantSpeakerID()) {
|
||||
newId = getCurrentRemoteDominantSpeakerID();
|
||||
} else { // Otherwise select last visible video
|
||||
newId = this.electLastVisibleVideo();
|
||||
}
|
||||
|
||||
this.updateLargeVideo(newId);
|
||||
},
|
||||
|
||||
electLastVisibleVideo() {
|
||||
// pick the last visible video in the row
|
||||
// if nobody else is left, this picks the local video
|
||||
const remoteThumbs = Filmstrip.getThumbs(true).remoteThumbs;
|
||||
let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
|
||||
|
||||
const lastVisible = thumbs.filter(':visible:last');
|
||||
|
||||
if (lastVisible.length) {
|
||||
const id = getPeerContainerResourceId(lastVisible[0]);
|
||||
|
||||
if (remoteVideos[id]) {
|
||||
logger.info(`electLastVisibleVideo: ${id}`);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// The RemoteVideo was removed (but the DOM elements may still
|
||||
// exist).
|
||||
}
|
||||
|
||||
logger.info('Last visible video no longer exists');
|
||||
thumbs = Filmstrip.getThumbs().remoteThumbs;
|
||||
if (thumbs.length) {
|
||||
const id = getPeerContainerResourceId(thumbs[0]);
|
||||
|
||||
if (remoteVideos[id]) {
|
||||
logger.info(`electLastVisibleVideo: ${id}`);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// The RemoteVideo was removed (but the DOM elements may
|
||||
// still exist).
|
||||
}
|
||||
|
||||
// Go with local video
|
||||
logger.info('Fallback to local video...');
|
||||
|
||||
const { id } = getLocalParticipant();
|
||||
|
||||
logger.info(`electLastVisibleVideo: ${id}`);
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
onRemoteStreamAdded(stream) {
|
||||
const id = stream.getParticipantId();
|
||||
const remoteVideo = remoteVideos[id];
|
||||
@@ -423,23 +299,6 @@ const VideoLayout = {
|
||||
|
||||
getAllThumbnails().forEach(thumbnail =>
|
||||
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
|
||||
|
||||
if (pinnedParticipantID) {
|
||||
this.updateLargeVideo(pinnedParticipantID);
|
||||
} else {
|
||||
const currentDominantSpeakerID
|
||||
= getCurrentRemoteDominantSpeakerID();
|
||||
|
||||
if (currentDominantSpeakerID) {
|
||||
this.updateLargeVideo(currentDominantSpeakerID);
|
||||
} else {
|
||||
// if there is no currentDominantSpeakerID, it can also be
|
||||
// that local participant is the dominant speaker
|
||||
// we should act as a participant has left and was on large
|
||||
// and we should choose somebody (electLastVisibleVideo)
|
||||
this.updateLargeVideo(this.electLastVisibleVideo());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -473,19 +332,6 @@ const VideoLayout = {
|
||||
|
||||
this.updateMutedForNoTracks(id, 'audio');
|
||||
this.updateMutedForNoTracks(id, 'video');
|
||||
|
||||
const remoteVideosCount = Object.keys(remoteVideos).length;
|
||||
|
||||
if (remoteVideosCount === 1) {
|
||||
window.setTimeout(() => {
|
||||
const updatedRemoteVideosCount
|
||||
= Object.keys(remoteVideos).length;
|
||||
|
||||
if (updatedRemoteVideosCount === 1 && remoteVideos[id]) {
|
||||
this._maybePlaceParticipantOnLargeVideo(id);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -512,43 +358,14 @@ const VideoLayout = {
|
||||
|
||||
// FIXME: what does this do???
|
||||
remoteVideoActive(videoElement, resourceJid) {
|
||||
|
||||
logger.info(`${resourceJid} video is now active`, videoElement);
|
||||
|
||||
VideoLayout.resizeThumbnails(
|
||||
false, () => {
|
||||
if (videoElement) {
|
||||
$(videoElement).show();
|
||||
}
|
||||
});
|
||||
|
||||
this._maybePlaceParticipantOnLargeVideo(resourceJid);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the large video to the last added video only if there's no current
|
||||
* dominant, focused speaker or update it to the current dominant speaker.
|
||||
*
|
||||
* @params {string} resourceJid - The id of the user to maybe display on
|
||||
* large video.
|
||||
* @returns {void}
|
||||
*/
|
||||
_maybePlaceParticipantOnLargeVideo(resourceJid) {
|
||||
const pinnedId = this.getPinnedId();
|
||||
|
||||
if ((!pinnedId
|
||||
&& !getCurrentRemoteDominantSpeakerID()
|
||||
&& this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE))
|
||||
|| pinnedId === resourceJid
|
||||
|| (!pinnedId && resourceJid
|
||||
&& getCurrentRemoteDominantSpeakerID() === resourceJid)
|
||||
|
||||
/* Playback started while we're on the stage - may need to update
|
||||
video source with the new stream */
|
||||
|| this.isCurrentlyOnLarge(resourceJid)) {
|
||||
|
||||
this.updateLargeVideo(resourceJid, true);
|
||||
}
|
||||
this._updateLargeVideoIfDisplayed(resourceJid, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -646,10 +463,8 @@ const VideoLayout = {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
// large video will show avatar instead of muted stream
|
||||
this.updateLargeVideo(id, true);
|
||||
}
|
||||
// large video will show avatar instead of muted stream
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -677,18 +492,6 @@ const VideoLayout = {
|
||||
onDominantSpeakerChanged(id) {
|
||||
getAllThumbnails().forEach(thumbnail =>
|
||||
thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
|
||||
|
||||
|
||||
if (!remoteVideos[id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Local video will not have container found, but that's ok
|
||||
// since we don't want to switch to local video.
|
||||
if (!interfaceConfig.filmStripOnly && !this.getPinnedId()
|
||||
&& !this.getCurrentlyOnLargeContainer().stayOnStage()) {
|
||||
this.updateLargeVideo(id);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -758,9 +561,7 @@ const VideoLayout = {
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
if (remoteVideo.isCurrentlyOnLargeVideo()) {
|
||||
this.updateLargeVideo(id);
|
||||
}
|
||||
this._updateLargeVideoIfDisplayed(id);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -809,7 +610,6 @@ const VideoLayout = {
|
||||
}
|
||||
|
||||
VideoLayout.resizeThumbnails();
|
||||
VideoLayout._updateAfterThumbRemoved(id);
|
||||
},
|
||||
|
||||
onVideoTypeChanged(id, newVideoType) {
|
||||
@@ -835,9 +635,7 @@ const VideoLayout = {
|
||||
}
|
||||
smallVideo.setVideoType(newVideoType);
|
||||
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
this.updateLargeVideo(id, true);
|
||||
}
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -880,7 +678,7 @@ const VideoLayout = {
|
||||
const smallVideo = VideoLayout.getSmallVideo(id);
|
||||
|
||||
if (smallVideo) {
|
||||
smallVideo.avatarChanged(avatarUrl);
|
||||
smallVideo.initializeAvatar();
|
||||
} else {
|
||||
logger.warn(
|
||||
`Missed avatar update - no small video yet for ${id}`
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -8738,8 +8738,8 @@
|
||||
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
|
||||
},
|
||||
"js-utils": {
|
||||
"version": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
|
||||
"from": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
|
||||
"version": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
|
||||
"from": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
|
||||
"requires": {
|
||||
"bowser": "1.9.1",
|
||||
"js-md5": "0.7.3"
|
||||
@@ -8946,8 +8946,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"from": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"version": "github:jitsi/lib-jitsi-meet#9bcc2a26cc94683b8ed302418695a331b450df97",
|
||||
"from": "github:jitsi/lib-jitsi-meet#9bcc2a26cc94683b8ed302418695a331b450df97",
|
||||
"requires": {
|
||||
"@jitsi/sdp-interop": "0.1.14",
|
||||
"@jitsi/sdp-simulcast": "0.2.1",
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
"jquery-contextmenu": "2.4.5",
|
||||
"jquery-i18next": "1.2.0",
|
||||
"js-md5": "0.6.1",
|
||||
"js-utils": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
|
||||
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
|
||||
"jsrsasign": "8.0.12",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9bcc2a26cc94683b8ed302418695a331b450df97",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.11",
|
||||
"moment": "2.19.4",
|
||||
@@ -85,6 +85,7 @@
|
||||
"redux-thunk": "2.2.0",
|
||||
"styled-components": "3.4.9",
|
||||
"uuid": "3.1.0",
|
||||
"windows-iana": "^3.1.0",
|
||||
"xmldom": "0.1.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -13,10 +13,18 @@ import {
|
||||
import { connect, disconnect, setLocationURL } from '../base/connection';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
import { createDesiredLocalTracks } from '../base/tracks';
|
||||
import { parseURIString, toURLString } from '../base/util';
|
||||
import {
|
||||
getLocationContextRoot,
|
||||
parseURIString,
|
||||
toURLString
|
||||
} from '../base/util';
|
||||
import { showNotification } from '../notifications';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
import { getDefaultURL } from './functions';
|
||||
import {
|
||||
getDefaultURL,
|
||||
getName
|
||||
} from './functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -136,6 +144,34 @@ export function redirectWithStoredParams(pathname: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a specific pathname to window.location.pathname taking into account
|
||||
* the context root of the Web app.
|
||||
*
|
||||
* @param {string} pathname - The pathname to assign to
|
||||
* window.location.pathname. If the specified pathname is relative, the context
|
||||
* root of the Web app will be prepended to the specified pathname before
|
||||
* assigning it to window.location.pathname.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function redirectToStaticPage(pathname: string) {
|
||||
return () => {
|
||||
const windowLocation = window.location;
|
||||
let newPathname = pathname;
|
||||
|
||||
if (!newPathname.startsWith('/')) {
|
||||
// A pathname equal to ./ specifies the current directory. It will be
|
||||
// fine but pointless to include it because contextRoot is the current
|
||||
// directory.
|
||||
newPathname.startsWith('./')
|
||||
&& (newPathname = newPathname.substring(2));
|
||||
newPathname = getLocationContextRoot(windowLocation) + newPathname;
|
||||
}
|
||||
|
||||
windowLocation.pathname = newPathname;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page.
|
||||
*
|
||||
@@ -182,3 +218,58 @@ export function reloadWithStoredParams() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the welcome page is enabled and redirects to it.
|
||||
* If requested show a thank you dialog before that.
|
||||
* If we have a close page enabled, redirect to it without
|
||||
* showing any other dialog.
|
||||
*
|
||||
* @param {Object} options - Used to decide which particular close page to show
|
||||
* or if close page is disabled, whether we should show the thankyou dialog.
|
||||
* @param {boolean} options.showThankYou - Whether we should
|
||||
* show thank you dialog.
|
||||
* @param {boolean} options.feedbackSubmitted - Whether feedback was submitted.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function maybeRedirectToWelcomePage(options: Object = {}) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
|
||||
const {
|
||||
enableClosePage
|
||||
} = getState()['features/base/config'];
|
||||
|
||||
// if close page is enabled redirect to it, without further action
|
||||
if (enableClosePage) {
|
||||
const { isGuest } = getState()['features/base/jwt'];
|
||||
|
||||
// save whether current user is guest or not, before navigating
|
||||
// to close page
|
||||
window.sessionStorage.setItem('guest', isGuest);
|
||||
|
||||
dispatch(redirectToStaticPage(`static/${
|
||||
options.feedbackSubmitted ? 'close.html' : 'close2.html'}`));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// else: show thankYou dialog only if there is no feedback
|
||||
if (options.showThankYou) {
|
||||
dispatch(showNotification({
|
||||
titleArguments: { appName: getName() },
|
||||
titleKey: 'dialog.thankYou'
|
||||
}));
|
||||
}
|
||||
|
||||
// if Welcome page is enabled redirect to welcome page after 3 sec, if
|
||||
// there is a thank you message to be shown, 0.5s otherwise.
|
||||
if (getState()['features/base/config'].enableWelcomePage) {
|
||||
setTimeout(
|
||||
() => {
|
||||
dispatch(redirectWithStoredParams('/'));
|
||||
},
|
||||
options.showThankYou ? 3000 : 500);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
177
react/features/base/avatar/components/AbstractAvatar.js
Normal file
177
react/features/base/avatar/components/AbstractAvatar.js
Normal file
@@ -0,0 +1,177 @@
|
||||
// @flow
|
||||
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { getParticipantById } from '../../participants';
|
||||
|
||||
import { getAvatarColor, getInitials } from '../functions';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The string we base the initials on (this is generated from a list of precendences).
|
||||
*/
|
||||
_initialsBase: ?string,
|
||||
|
||||
/**
|
||||
* An URL that we validated that it can be loaded.
|
||||
*/
|
||||
_loadableAvatarUrl: ?string,
|
||||
|
||||
/**
|
||||
* A string to override the initials to generate a color of. This is handy if you don't want to make
|
||||
* the background color match the string that the initials are generated from.
|
||||
*/
|
||||
colorBase?: string,
|
||||
|
||||
/**
|
||||
* Display name of the entity to render an avatar for (if any). This is handy when we need
|
||||
* an avatar for a non-participasnt entity (e.g. a recent list item).
|
||||
*/
|
||||
displayName?: string,
|
||||
|
||||
/**
|
||||
* The ID of the participant to render an avatar for (if it's a participant avatar).
|
||||
*/
|
||||
participantId?: string,
|
||||
|
||||
/**
|
||||
* The size of the avatar.
|
||||
*/
|
||||
size: number,
|
||||
|
||||
/**
|
||||
* URI of the avatar, if any.
|
||||
*/
|
||||
uri: ?string,
|
||||
}
|
||||
|
||||
type State = {
|
||||
avatarFailed: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_SIZE = 65;
|
||||
|
||||
/**
|
||||
* Implements an abstract class to render avatars in the app.
|
||||
*/
|
||||
export default class AbstractAvatar<P: Props> extends PureComponent<P, State> {
|
||||
/**
|
||||
* Instantiates a new {@code Component}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
avatarFailed: false
|
||||
};
|
||||
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps: P) {
|
||||
if (prevProps.uri !== this.props.uri) {
|
||||
|
||||
// URI changed, so we need to try to fetch it again.
|
||||
// Eslint doesn't like this statement, but based on the React doc, it's safe if it's
|
||||
// wrapped in a condition: https://reactjs.org/docs/react-component.html#componentdidupdate
|
||||
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
avatarFailed: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Componenr#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl,
|
||||
colorBase,
|
||||
uri
|
||||
} = this.props;
|
||||
const { avatarFailed } = this.state;
|
||||
|
||||
// _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so
|
||||
// we still need to do a check for that. And an explicitly provided URI is higher priority than
|
||||
// an avatar URL anyhow.
|
||||
if ((uri && !avatarFailed) || _loadableAvatarUrl) {
|
||||
return this._renderURLAvatar((!avatarFailed && uri) || _loadableAvatarUrl);
|
||||
}
|
||||
|
||||
const _initials = getInitials(_initialsBase);
|
||||
|
||||
if (_initials) {
|
||||
return this._renderInitialsAvatar(_initials, getAvatarColor(colorBase || _initialsBase));
|
||||
}
|
||||
|
||||
return this._renderDefaultAvatar();
|
||||
}
|
||||
|
||||
_onAvatarLoadError: () => void;
|
||||
|
||||
/**
|
||||
* Callback to handle the error while loading of the avatar URI.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError() {
|
||||
this.setState({
|
||||
avatarFailed: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to render the actual, platform specific default avatar component.
|
||||
*
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderDefaultAvatar: () => React$Element<*>
|
||||
|
||||
/**
|
||||
* Function to render the actual, platform specific initials-based avatar component.
|
||||
*
|
||||
* @param {string} initials - The initials to use.
|
||||
* @param {string} color - The color to use.
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderInitialsAvatar: (string, string) => React$Element<*>
|
||||
|
||||
/**
|
||||
* Function to render the actual, platform specific URL-based avatar component.
|
||||
*
|
||||
* @param {string} uri - The URI of the avatar.
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderURLAvatar: ?string => React$Element<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Props} ownProps - The own props of the component.
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { displayName, participantId } = ownProps;
|
||||
const _participant = participantId && getParticipantById(state, participantId);
|
||||
const _initialsBase = (_participant && (_participant.name || _participant.email)) || displayName;
|
||||
|
||||
return {
|
||||
_initialsBase,
|
||||
_loadableAvatarUrl: _participant && _participant.loadableAvatarUrl
|
||||
};
|
||||
}
|
||||
3
react/features/base/avatar/components/index.native.js
Normal file
3
react/features/base/avatar/components/index.native.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './native';
|
||||
3
react/features/base/avatar/components/index.web.js
Normal file
3
react/features/base/avatar/components/index.web.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './web';
|
||||
106
react/features/base/avatar/components/native/Avatar.js
Normal file
106
react/features/base/avatar/components/native/Avatar.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Image, Text, View } from 'react-native';
|
||||
|
||||
import { connect } from '../../../redux';
|
||||
import { type StyleType } from '../../../styles';
|
||||
|
||||
import AbstractAvatar, {
|
||||
_mapStateToProps,
|
||||
type Props as AbstractProps,
|
||||
DEFAULT_SIZE
|
||||
} from '../AbstractAvatar';
|
||||
|
||||
import RemoteAvatar, { DEFAULT_AVATAR } from './RemoteAvatar';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* External style of the component.
|
||||
*/
|
||||
style?: StyleType
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements an avatar component that has 4 ways to render an avatar:
|
||||
*
|
||||
* - Based on an explicit avatar URI, if provided
|
||||
* - Gravatar, if there is any
|
||||
* - Based on initials generated from name or email
|
||||
* - Default avatar icon, if any of the above fails
|
||||
*/
|
||||
class Avatar extends AbstractAvatar<Props> {
|
||||
|
||||
_onAvatarLoadError: () => void;
|
||||
|
||||
/**
|
||||
* Implements {@code AbstractAvatar#_renderDefaultAvatar}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderDefaultAvatar() {
|
||||
return this._wrapAvatar(
|
||||
<Image
|
||||
source = { DEFAULT_AVATAR }
|
||||
style = { [
|
||||
styles.avatarContent(this.props.size || DEFAULT_SIZE),
|
||||
styles.staticAvatar
|
||||
] } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code AbstractAvatar#_renderGravatar}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderInitialsAvatar(initials, color) {
|
||||
return this._wrapAvatar(
|
||||
<View
|
||||
style = { [
|
||||
styles.initialsContainer,
|
||||
{
|
||||
backgroundColor: color
|
||||
}
|
||||
] }>
|
||||
<Text style = { styles.initialsText(this.props.size || DEFAULT_SIZE) }> { initials } </Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code AbstractAvatar#_renderGravatar}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderURLAvatar(uri) {
|
||||
return this._wrapAvatar(
|
||||
<RemoteAvatar
|
||||
onError = { this._onAvatarLoadError }
|
||||
size = { this.props.size || DEFAULT_SIZE }
|
||||
uri = { uri } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an avatar into a common wrapper.
|
||||
*
|
||||
* @param {React#Component} avatar - The avatar component.
|
||||
* @returns {React#Component}
|
||||
*/
|
||||
_wrapAvatar(avatar) {
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.avatarContainer(this.props.size || DEFAULT_SIZE),
|
||||
this.props.style
|
||||
] }>
|
||||
{ avatar }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(Avatar);
|
||||
50
react/features/base/avatar/components/native/RemoteAvatar.js
Normal file
50
react/features/base/avatar/components/native/RemoteAvatar.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Image } from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
export const DEFAULT_AVATAR = require('../../../../../../images/avatar.png');
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Callback for load errors.
|
||||
*/
|
||||
onError: Function,
|
||||
|
||||
/**
|
||||
* Size of the avatar.
|
||||
*/
|
||||
size: number,
|
||||
|
||||
/**
|
||||
* URI of the avatar to load.
|
||||
*/
|
||||
uri: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a private class that is used to fetch and render remote avatars based on an URI.
|
||||
*/
|
||||
export default class RemoteAvatar extends PureComponent<Props> {
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { onError, size, uri } = this.props;
|
||||
|
||||
return (
|
||||
<Image
|
||||
defaultSource = { DEFAULT_AVATAR }
|
||||
onError = { onError }
|
||||
resizeMode = 'cover'
|
||||
source = {{ uri }}
|
||||
style = { styles.avatarContent(size) } />
|
||||
);
|
||||
}
|
||||
}
|
||||
3
react/features/base/avatar/components/native/index.js
Normal file
3
react/features/base/avatar/components/native/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export { default as Avatar } from './Avatar';
|
||||
47
react/features/base/avatar/components/native/styles.js
Normal file
47
react/features/base/avatar/components/native/styles.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// @flow
|
||||
|
||||
import { ColorPalette } from '../../../styles';
|
||||
|
||||
/**
|
||||
* The styles of the feature base/participants.
|
||||
*/
|
||||
export default {
|
||||
|
||||
avatarContainer: (size: number) => {
|
||||
return {
|
||||
alignItems: 'center',
|
||||
borderRadius: size / 2,
|
||||
height: size,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: size
|
||||
};
|
||||
},
|
||||
|
||||
avatarContent: (size: number) => {
|
||||
return {
|
||||
height: size,
|
||||
width: size
|
||||
};
|
||||
},
|
||||
|
||||
initialsContainer: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
initialsText: (size: number) => {
|
||||
return {
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontSize: size * 0.5,
|
||||
fontWeight: '100'
|
||||
};
|
||||
},
|
||||
|
||||
staticAvatar: {
|
||||
backgroundColor: ColorPalette.lightGrey,
|
||||
opacity: 0.4
|
||||
}
|
||||
};
|
||||
98
react/features/base/avatar/components/web/Avatar.js
Normal file
98
react/features/base/avatar/components/web/Avatar.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { connect } from '../../../redux';
|
||||
|
||||
import AbstractAvatar, {
|
||||
_mapStateToProps,
|
||||
type Props as AbstractProps
|
||||
} from '../AbstractAvatar';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
className?: string,
|
||||
id: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an avatar as a React/Web {@link Component}.
|
||||
*/
|
||||
class Avatar extends AbstractAvatar<Props> {
|
||||
/**
|
||||
* Constructs a style object to be used on the avatars.
|
||||
*
|
||||
* @param {string?} color - The desired background color.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getAvatarStyle(color) {
|
||||
const { size } = this.props;
|
||||
|
||||
return {
|
||||
backgroundColor: color || undefined,
|
||||
fontSize: size ? size * 0.5 : '180%',
|
||||
height: size || '100%',
|
||||
width: size || '100%'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a list of class names required for the avatar component.
|
||||
*
|
||||
* @param {string} additional - Any additional class to add.
|
||||
* @returns {string}
|
||||
*/
|
||||
_getAvatarClassName(additional) {
|
||||
return `avatar ${additional || ''} ${this.props.className || ''}`;
|
||||
}
|
||||
|
||||
_onAvatarLoadError: () => void;
|
||||
|
||||
/**
|
||||
* Implements {@code AbstractAvatar#_renderDefaultAvatar}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderDefaultAvatar() {
|
||||
return (
|
||||
<img
|
||||
className = { this._getAvatarClassName('defaultAvatar') }
|
||||
id = { this.props.id }
|
||||
src = '/images/avatar.png'
|
||||
style = { this._getAvatarStyle() } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code AbstractAvatar#_renderGravatar}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderInitialsAvatar(initials, color) {
|
||||
return (
|
||||
<div
|
||||
className = { this._getAvatarClassName() }
|
||||
id = { this.props.id }
|
||||
style = { this._getAvatarStyle(color) }>
|
||||
{ initials }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code AbstractAvatar#_renderGravatar}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderURLAvatar(uri) {
|
||||
return (
|
||||
<img
|
||||
className = { this._getAvatarClassName() }
|
||||
id = { this.props.id }
|
||||
onError = { this._onAvatarLoadError }
|
||||
src = { uri }
|
||||
style = { this._getAvatarStyle() } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(Avatar);
|
||||
3
react/features/base/avatar/components/web/index.js
Normal file
3
react/features/base/avatar/components/web/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export { default as Avatar } from './Avatar';
|
||||
54
react/features/base/avatar/functions.js
Normal file
54
react/features/base/avatar/functions.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// @flow
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'232, 105, 156',
|
||||
'255, 198, 115',
|
||||
'128, 128, 255',
|
||||
'105, 232, 194',
|
||||
'234, 255, 128'
|
||||
];
|
||||
|
||||
const AVATAR_OPACITY = 0.4;
|
||||
|
||||
/**
|
||||
* Generates the background color of an initials based avatar.
|
||||
*
|
||||
* @param {string?} initials - The initials of the avatar.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getAvatarColor(initials: ?string) {
|
||||
let colorIndex = 0;
|
||||
|
||||
if (initials) {
|
||||
let nameHash = 0;
|
||||
|
||||
for (const s of initials) {
|
||||
nameHash += s.codePointAt(0);
|
||||
}
|
||||
|
||||
colorIndex = nameHash % AVATAR_COLORS.length;
|
||||
}
|
||||
|
||||
return `rgba(${AVATAR_COLORS[colorIndex]}, ${AVATAR_OPACITY})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates initials for a simple string.
|
||||
*
|
||||
* @param {string?} s - The string to generate initials for.
|
||||
* @returns {string?}
|
||||
*/
|
||||
export function getInitials(s: ?string) {
|
||||
// We don't want to use the domain part of an email address, if it is one
|
||||
const initialsBasis = _.split(s, '@')[0];
|
||||
const words = _.words(initialsBasis);
|
||||
let initials = '';
|
||||
|
||||
for (const w of words) {
|
||||
(initials.length < 2) && (initials += w.substr(0, 1).toUpperCase());
|
||||
}
|
||||
|
||||
return initials;
|
||||
}
|
||||
3
react/features/base/avatar/index.js
Normal file
3
react/features/base/avatar/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './components';
|
||||
@@ -1,3 +1,5 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Create an action for when dominant speaker changes.
|
||||
*
|
||||
@@ -65,6 +67,18 @@ export const PARTICIPANT_ID_CHANGED = 'PARTICIPANT_ID_CHANGED';
|
||||
*/
|
||||
export const PARTICIPANT_JOINED = 'PARTICIPANT_JOINED';
|
||||
|
||||
/**
|
||||
* Action to signal that a participant has been removed from a conference by
|
||||
* another participant.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_KICKED,
|
||||
* kicked: Object,
|
||||
* kicker: Object
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_KICKED = 'PARTICIPANT_KICKED';
|
||||
|
||||
/**
|
||||
* Action to handle case when participant lefts.
|
||||
*
|
||||
@@ -120,3 +134,17 @@ export const HIDDEN_PARTICIPANT_JOINED = 'HIDDEN_PARTICIPANT_JOINED';
|
||||
* }
|
||||
*/
|
||||
export const HIDDEN_PARTICIPANT_LEFT = 'HIDDEN_PARTICIPANT_LEFT';
|
||||
|
||||
/**
|
||||
* The type of Redux action which notifies the app that the loadable avatar URL has changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_LOADABLE_AVATAR_URL,
|
||||
* participant: {
|
||||
* id: string,
|
||||
loadableAvatarUrl: string
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const SET_LOADABLE_AVATAR_URL = 'SET_LOADABLE_AVATAR_URL';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import { set } from '../redux';
|
||||
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
|
||||
|
||||
@@ -11,9 +9,11 @@ import {
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
PARTICIPANT_ID_CHANGED,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_KICKED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED,
|
||||
PIN_PARTICIPANT
|
||||
PIN_PARTICIPANT,
|
||||
SET_LOADABLE_AVATAR_URL
|
||||
} from './actionTypes';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -417,6 +417,12 @@ export function participantMutedUs(participant) {
|
||||
export function participantKicked(kicker, kicked) {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({
|
||||
type: PARTICIPANT_KICKED,
|
||||
kicked: kicked.getId(),
|
||||
kicker: kicker.getId()
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
titleArguments: {
|
||||
kicked:
|
||||
@@ -451,71 +457,24 @@ export function pinParticipant(id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of names of participants that have joined the conference. The array
|
||||
* is replaced with an empty array as notifications are displayed.
|
||||
* Creates an action which notifies the app that the loadable URL of the avatar of a participant got updated.
|
||||
*
|
||||
* @private
|
||||
* @type {string[]}
|
||||
*/
|
||||
let joinedParticipantsNames = [];
|
||||
|
||||
/**
|
||||
* A throttled internal function that takes the internal list of participant
|
||||
* names, {@code joinedParticipantsNames}, and triggers the display of a
|
||||
* notification informing of their joining.
|
||||
*
|
||||
* @private
|
||||
* @type {Function}
|
||||
*/
|
||||
const _throttledNotifyParticipantConnected = throttle(dispatch => {
|
||||
const joinedParticipantsCount = joinedParticipantsNames.length;
|
||||
|
||||
let notificationProps;
|
||||
|
||||
if (joinedParticipantsCount >= 3) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
name: joinedParticipantsNames[0],
|
||||
count: joinedParticipantsCount - 1
|
||||
},
|
||||
titleKey: 'notify.connectedThreePlusMembers'
|
||||
};
|
||||
} else if (joinedParticipantsCount === 2) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
first: joinedParticipantsNames[0],
|
||||
second: joinedParticipantsNames[1]
|
||||
},
|
||||
titleKey: 'notify.connectedTwoMembers'
|
||||
};
|
||||
} else if (joinedParticipantsCount) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
name: joinedParticipantsNames[0]
|
||||
},
|
||||
titleKey: 'notify.connectedOneMember'
|
||||
};
|
||||
}
|
||||
|
||||
if (notificationProps) {
|
||||
dispatch(
|
||||
showNotification(notificationProps, NOTIFICATION_TIMEOUT));
|
||||
}
|
||||
|
||||
joinedParticipantsNames = [];
|
||||
|
||||
}, 500, { leading: false });
|
||||
|
||||
/**
|
||||
* Queues the display of a notification of a participant having connected to
|
||||
* the meeting. The notifications are batched so that quick consecutive
|
||||
* connection events are shown in one notification.
|
||||
*
|
||||
* @param {string} displayName - The name of the participant that connected.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showParticipantJoinedNotification(displayName) {
|
||||
joinedParticipantsNames.push(displayName);
|
||||
|
||||
return dispatch => _throttledNotifyParticipantConnected(dispatch);
|
||||
* @param {string} participantId - The ID of the participant.
|
||||
* @param {string} url - The new URL.
|
||||
* @returns {{
|
||||
* type: SET_LOADABLE_AVATAR_URL,
|
||||
* participant: {
|
||||
* id: string,
|
||||
* loadableAvatarUrl: string
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function setLoadableAvatarUrl(participantId, url) {
|
||||
return {
|
||||
type: SET_LOADABLE_AVATAR_URL,
|
||||
participant: {
|
||||
id: participantId,
|
||||
loadableAvatarUrl: url
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component, Fragment, PureComponent } from 'react';
|
||||
import { Dimensions, Image, Platform, View } from 'react-native';
|
||||
import FastImage, {
|
||||
type CacheControls,
|
||||
type Priorities
|
||||
} from 'react-native-fast-image';
|
||||
|
||||
import { ColorPalette } from '../../styles';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The default image/source to be used in case none is specified or the
|
||||
* specified one fails to load.
|
||||
*
|
||||
* XXX The relative path to the default/stock (image) file is defined by the
|
||||
* {@code const} {@code DEFAULT_AVATAR_RELATIVE_PATH}. Unfortunately, the
|
||||
* packager of React Native cannot deal with it early enough for the following
|
||||
* {@code require} to succeed at runtime. Anyway, be sure to synchronize the
|
||||
* relative path on Web and mobile for the purposes of consistency.
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
const _DEFAULT_SOURCE = require('../../../../../images/avatar.png');
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link Avatar}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The size for the {@link Avatar}.
|
||||
*/
|
||||
size: number,
|
||||
|
||||
|
||||
/**
|
||||
* The URI of the {@link Avatar}.
|
||||
*/
|
||||
uri: string
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} state of {@link Avatar}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Background color for the locally generated avatar.
|
||||
*/
|
||||
backgroundColor: string,
|
||||
|
||||
/**
|
||||
* Error indicator for non-local avatars.
|
||||
*/
|
||||
error: boolean,
|
||||
|
||||
/**
|
||||
* Indicates if the non-local avatar was loaded or not.
|
||||
*/
|
||||
loaded: boolean,
|
||||
|
||||
/**
|
||||
* Source for the non-local avatar.
|
||||
*/
|
||||
source: {
|
||||
uri?: string,
|
||||
headers?: Object,
|
||||
priority?: Priorities,
|
||||
cache?: CacheControls,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React Native/mobile {@link Component} wich renders the content
|
||||
* of an Avatar.
|
||||
*/
|
||||
class AvatarContent extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new Avatar instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Set the image source. The logic for the character # below is as
|
||||
// follows:
|
||||
// - Technically, URI is supposed to start with a scheme and scheme
|
||||
// cannot contain the character #.
|
||||
// - Technically, the character # in a URI signals the start of the
|
||||
// fragment/hash.
|
||||
// - Technically, the fragment/hash does not imply a retrieval
|
||||
// action.
|
||||
// - Practically, the fragment/hash does not always mandate a
|
||||
// retrieval action. For example, an HTML anchor with an href that
|
||||
// starts with the character # does not cause a Web browser to
|
||||
// initiate a retrieval action.
|
||||
// So I'll use the character # at the start of URI to not initiate
|
||||
// an image retrieval action.
|
||||
const source = {};
|
||||
|
||||
if (props.uri && !props.uri.startsWith('#')) {
|
||||
source.uri = props.uri;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
backgroundColor: this._getBackgroundColor(props),
|
||||
error: false,
|
||||
loaded: false,
|
||||
source
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes if the default avatar (ie, locally generated) should be used
|
||||
* or not.
|
||||
*/
|
||||
get useDefaultAvatar() {
|
||||
const { error, loaded, source } = this.state;
|
||||
|
||||
return !source.uri || error || !loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash over the URI and returns a HSL background color. We use
|
||||
* 75% as lightness, for nice pastel style colors.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props from
|
||||
* which the background color is to be generated.
|
||||
* @private
|
||||
* @returns {string} - The HSL CSS property.
|
||||
*/
|
||||
_getBackgroundColor({ uri }) {
|
||||
if (!uri) {
|
||||
return ColorPalette.white;
|
||||
}
|
||||
|
||||
let hash = 0;
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
for (let i = 0; i < uri.length; i++) {
|
||||
hash = uri.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash |= 0; // Convert to 32-bit integer
|
||||
}
|
||||
|
||||
/* eslint-enable no-bitwise */
|
||||
|
||||
return `hsl(${hash % 360}, 100%, 75%)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper which computes the style for the {@code Image} / {@code FastImage}
|
||||
* component.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getImageStyle() {
|
||||
const { size } = this.props;
|
||||
|
||||
return {
|
||||
...styles.avatar,
|
||||
borderRadius: size / 2,
|
||||
height: size,
|
||||
width: size
|
||||
};
|
||||
}
|
||||
|
||||
_onAvatarLoaded: () => void;
|
||||
|
||||
/**
|
||||
* Handler called when the remote image loading finishes. This doesn't
|
||||
* necessarily mean the load was successful.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoaded() {
|
||||
this.setState({ loaded: true });
|
||||
}
|
||||
|
||||
_onAvatarLoadError: () => void;
|
||||
|
||||
/**
|
||||
* Handler called when the remote image loading failed.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError() {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a default, locally generated avatar image.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderDefaultAvatar() {
|
||||
// When using a local image, react-native-fastimage falls back to a
|
||||
// regular Image, so we need to wrap it in a view to make it round.
|
||||
// https://github.com/facebook/react-native/issues/3198
|
||||
|
||||
const { backgroundColor } = this.state;
|
||||
const imageStyle = this._getImageStyle();
|
||||
const viewStyle = {
|
||||
...imageStyle,
|
||||
|
||||
backgroundColor,
|
||||
|
||||
// FIXME @lyubomir: Without the opacity below I feel like the
|
||||
// avatar colors are too strong. Besides, we use opacity for the
|
||||
// ToolbarButtons. That's where I copied the value from and we
|
||||
// may want to think about "standardizing" the opacity in the
|
||||
// app in a way similar to ColorPalette.
|
||||
opacity: 0.1,
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
return (
|
||||
<View style = { viewStyle }>
|
||||
<Image
|
||||
|
||||
// The Image adds a fade effect without asking, so lets
|
||||
// explicitly disable it. More info here:
|
||||
// https://github.com/facebook/react-native/issues/10194
|
||||
fadeDuration = { 0 }
|
||||
resizeMode = 'contain'
|
||||
source = { _DEFAULT_SOURCE }
|
||||
style = { imageStyle } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an avatar using a remote image.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderAvatar() {
|
||||
const { source } = this.state;
|
||||
let extraStyle;
|
||||
|
||||
if (this.useDefaultAvatar) {
|
||||
// On Android, the image loading indicators don't work unless the
|
||||
// Glide image is actually created, so we cannot use display: none.
|
||||
// Instead, render it off-screen, which does the trick.
|
||||
if (Platform.OS === 'android') {
|
||||
const windowDimensions = Dimensions.get('window');
|
||||
|
||||
extraStyle = {
|
||||
bottom: -windowDimensions.height,
|
||||
right: -windowDimensions.width
|
||||
};
|
||||
} else {
|
||||
extraStyle = { display: 'none' };
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
onError = { this._onAvatarLoadError }
|
||||
onLoadEnd = { this._onAvatarLoaded }
|
||||
resizeMode = 'contain'
|
||||
source = { source }
|
||||
style = { [ this._getImageStyle(), extraStyle ] } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { source } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ source.uri && this._renderAvatar() }
|
||||
{ this.useDefaultAvatar && this._renderDefaultAvatar() }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
|
||||
/**
|
||||
* Implements an avatar as a React Native/mobile {@link Component}.
|
||||
*
|
||||
* Note: we use `key` in order to trigger a new component creation in case
|
||||
* the URI changes.
|
||||
*/
|
||||
export default class Avatar extends PureComponent<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<AvatarContent
|
||||
key = { this.props.uri }
|
||||
{ ...this.props } />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link Avatar}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The URI of the {@link Avatar}.
|
||||
*/
|
||||
uri: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an avatar as a React/Web {@link Component}.
|
||||
*/
|
||||
export default class Avatar extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
// Propagate all props of this Avatar but the ones consumed by this
|
||||
// Avatar to the img it renders.
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { uri, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<img
|
||||
{ ...props }
|
||||
src = { uri } />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { Avatar } from '../../avatar';
|
||||
import { translate } from '../../i18n';
|
||||
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
|
||||
import {
|
||||
@@ -16,13 +16,7 @@ import { StyleType } from '../../styles';
|
||||
import { TestHint } from '../../testing/components';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../tracks';
|
||||
|
||||
import Avatar from './Avatar';
|
||||
import {
|
||||
getAvatarURL,
|
||||
getParticipantById,
|
||||
getParticipantDisplayName,
|
||||
shouldRenderParticipantVideo
|
||||
} from '../functions';
|
||||
import { shouldRenderParticipantVideo } from '../functions';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
@@ -30,14 +24,6 @@ import styles from './styles';
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The source (e.g. URI, URL) of the avatar image of the participant with
|
||||
* {@link #participantId}.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_avatar: string,
|
||||
|
||||
/**
|
||||
* The connection status of the participant. Her video will only be rendered
|
||||
* if the connection status is 'active'; otherwise, the avatar will be
|
||||
@@ -192,7 +178,6 @@ class ParticipantView extends Component<Props> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_avatar: avatar,
|
||||
_connectionStatus: connectionStatus,
|
||||
_renderVideo: renderVideo,
|
||||
_videoTrack: videoTrack,
|
||||
@@ -202,9 +187,6 @@ class ParticipantView extends Component<Props> {
|
||||
|
||||
const waitForVideoStarted = false;
|
||||
|
||||
// Is the avatar to be rendered?
|
||||
const renderAvatar = Boolean(!renderVideo && avatar);
|
||||
|
||||
// If the connection has problems, we will "tint" the video / avatar.
|
||||
const connectionProblem
|
||||
= connectionStatus !== JitsiParticipantConnectionStatus.ACTIVE;
|
||||
@@ -238,10 +220,12 @@ class ParticipantView extends Component<Props> {
|
||||
zOrder = { this.props.zOrder }
|
||||
zoomEnabled = { this.props.zoomEnabled } /> }
|
||||
|
||||
{ renderAvatar
|
||||
&& <Avatar
|
||||
size = { this.props.avatarSize }
|
||||
uri = { avatar } /> }
|
||||
{ !renderVideo
|
||||
&& <View style = { styles.avatarContainer }>
|
||||
<Avatar
|
||||
participantId = { this.props.participantId }
|
||||
size = { this.props.avatarSize } />
|
||||
</View> }
|
||||
|
||||
{ useTint
|
||||
|
||||
@@ -265,45 +249,14 @@ class ParticipantView extends Component<Props> {
|
||||
* @param {Object} ownProps - The React {@code Component} props passed to the
|
||||
* associated (instance of) {@code ParticipantView}.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _avatar: string,
|
||||
* _connectionStatus: string,
|
||||
* _participantName: string,
|
||||
* _renderVideo: boolean,
|
||||
* _videoTrack: Track
|
||||
* }}
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { participantId } = ownProps;
|
||||
const participant = getParticipantById(state, participantId);
|
||||
let avatar;
|
||||
let connectionStatus;
|
||||
let participantName;
|
||||
|
||||
if (participant) {
|
||||
avatar = getAvatarURL(participant);
|
||||
connectionStatus = participant.connectionStatus;
|
||||
participantName = getParticipantDisplayName(state, participant.id);
|
||||
|
||||
// Avatar (on React Native) now has the ability to generate an
|
||||
// automatically-colored default image when no URI/URL is specified or
|
||||
// when it fails to load. In order to make the coloring permanent(ish)
|
||||
// per participant, Avatar will need something permanent(ish) per
|
||||
// perticipant, obviously. A participant's ID is such a piece of data.
|
||||
// But the local participant changes her ID as she joins, leaves.
|
||||
// TODO @lyubomir: The participants may change their avatar URLs at
|
||||
// runtime which means that, if their old and new avatar URLs fail to
|
||||
// download, Avatar will change their automatically-generated colors.
|
||||
avatar || participant.local || (avatar = `#${participant.id}`);
|
||||
|
||||
// ParticipantView knows before Avatar that an avatar URL will be used
|
||||
// so it's advisable to prefetch here.
|
||||
avatar && !avatar.startsWith('#')
|
||||
&& FastImage.preload([ { uri: avatar } ]);
|
||||
}
|
||||
|
||||
return {
|
||||
_avatar: avatar,
|
||||
_connectionStatus:
|
||||
connectionStatus
|
||||
|| JitsiParticipantConnectionStatus.ACTIVE,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as Avatar } from './Avatar';
|
||||
// @flow
|
||||
|
||||
export { default as ParticipantView } from './ParticipantView';
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { BoxModel, ColorPalette, createStyleSheet } from '../../styles';
|
||||
// @flow
|
||||
|
||||
import { BoxModel, ColorPalette } from '../../styles';
|
||||
|
||||
/**
|
||||
* The styles of the feature base/participants.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
export default {
|
||||
/**
|
||||
* The style of the avatar of the participant.
|
||||
* Container for the avatar in the view.
|
||||
*/
|
||||
avatar: {
|
||||
alignSelf: 'center',
|
||||
flex: 0
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -42,4 +44,4 @@ export default createStyleSheet({
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
// @flow
|
||||
import { getAvatarURL as _getAvatarURL } from 'js-utils/avatar';
|
||||
import {
|
||||
getAvatarURL as _getAvatarURL,
|
||||
getGravatarURL
|
||||
} from 'js-utils/avatar';
|
||||
|
||||
import { toState } from '../redux';
|
||||
|
||||
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../tracks';
|
||||
import { createDeferred } from '../util';
|
||||
|
||||
import {
|
||||
DEFAULT_AVATAR_RELATIVE_PATH,
|
||||
@@ -13,10 +17,27 @@ import {
|
||||
MAX_DISPLAY_NAME_LENGTH,
|
||||
PARTICIPANT_ROLE
|
||||
} from './constants';
|
||||
import { preloadImage } from './preloadImage';
|
||||
|
||||
declare var config: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Temp structures for avatar urls to be checked/preloaded.
|
||||
*/
|
||||
const AVATAR_QUEUE = [];
|
||||
const AVATAR_CHECKED_URLS = new Map();
|
||||
/* eslint-disable arrow-body-style */
|
||||
const AVATAR_CHECKER_FUNCTIONS = [
|
||||
participant => {
|
||||
return participant && participant.avatarURL ? participant.avatarURL : null;
|
||||
},
|
||||
participant => {
|
||||
return participant && participant.email ? getGravatarURL(participant.email) : null;
|
||||
}
|
||||
];
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
/**
|
||||
* Returns the URL of the image for the avatar of a specific participant.
|
||||
*
|
||||
@@ -84,6 +105,36 @@ export function getAvatarURLByParticipantId(
|
||||
return participant && getAvatarURL(participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
* @param {Object} participant - The participant to resolve avatars for.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getFirstLoadableAvatarUrl(participant: Object) {
|
||||
const deferred = createDeferred();
|
||||
const fullPromise = deferred.promise
|
||||
.then(() => _getFirstLoadableAvatarUrl(participant))
|
||||
.then(src => {
|
||||
|
||||
if (AVATAR_QUEUE.length) {
|
||||
const next = AVATAR_QUEUE.shift();
|
||||
|
||||
next.resolve();
|
||||
}
|
||||
|
||||
return src;
|
||||
});
|
||||
|
||||
if (AVATAR_QUEUE.length) {
|
||||
AVATAR_QUEUE.push(deferred);
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
return fullPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns local participant from Redux state.
|
||||
*
|
||||
@@ -169,7 +220,6 @@ export function getParticipantCountWithFake(stateful: Object | Function) {
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {string} id - The ID of the participant's display name to retrieve.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getParticipantDisplayName(
|
||||
@@ -346,3 +396,35 @@ export function shouldRenderParticipantVideo(
|
||||
&& shouldRenderVideoTrack(videoTrack, waitForVideoStarted);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
* @param {Object} participant - The participant to resolve avatars for.
|
||||
* @returns {?string}
|
||||
*/
|
||||
async function _getFirstLoadableAvatarUrl(participant) {
|
||||
for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
|
||||
const url = AVATAR_CHECKER_FUNCTIONS[i](participant);
|
||||
|
||||
if (url) {
|
||||
if (AVATAR_CHECKED_URLS.has(url)) {
|
||||
if (AVATAR_CHECKED_URLS.get(url)) {
|
||||
return url;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const finalUrl = await preloadImage(url);
|
||||
|
||||
AVATAR_CHECKED_URLS.set(finalUrl, true);
|
||||
|
||||
return finalUrl;
|
||||
} catch (e) {
|
||||
AVATAR_CHECKED_URLS.set(url, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
localParticipantLeft,
|
||||
participantLeft,
|
||||
participantUpdated,
|
||||
showParticipantJoinedNotification
|
||||
setLoadableAvatarUrl
|
||||
} from './actions';
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
@@ -38,8 +38,9 @@ import {
|
||||
PARTICIPANT_LEFT_SOUND_ID
|
||||
} from './constants';
|
||||
import {
|
||||
getAvatarURLByParticipantId,
|
||||
getFirstLoadableAvatarUrl,
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
getParticipantDisplayName
|
||||
} from './functions';
|
||||
@@ -118,15 +119,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case PARTICIPANT_JOINED: {
|
||||
_maybePlaySounds(store, action);
|
||||
|
||||
const result = _participantJoinedOrUpdated(store, next, action);
|
||||
|
||||
const { participant: p } = action;
|
||||
|
||||
if (!p.local) {
|
||||
store.dispatch(showParticipantJoinedNotification(getParticipantDisplayName(store.getState, p.id)));
|
||||
}
|
||||
|
||||
return result;
|
||||
return _participantJoinedOrUpdated(store, next, action);
|
||||
}
|
||||
|
||||
case PARTICIPANT_LEFT:
|
||||
@@ -323,8 +316,8 @@ function _maybePlaySounds({ getState, dispatch }, action) {
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _participantJoinedOrUpdated({ getState }, next, action) {
|
||||
const { participant: { id, local, raisedHand } } = action;
|
||||
function _participantJoinedOrUpdated({ dispatch, getState }, next, action) {
|
||||
const { participant: { avatarURL, email, id, local, name, raisedHand } } = action;
|
||||
|
||||
// Send an external update of the local participant's raised hand state
|
||||
// if a new raised hand state is defined in the action.
|
||||
@@ -339,26 +332,29 @@ function _participantJoinedOrUpdated({ getState }, next, action) {
|
||||
}
|
||||
}
|
||||
|
||||
// Notify external listeners of potential avatarURL changes.
|
||||
if (typeof APP === 'object') {
|
||||
const oldAvatarURL = getAvatarURLByParticipantId(getState(), id);
|
||||
// Allow the redux update to go through and compare the old avatar
|
||||
// to the new avatar and emit out change events if necessary.
|
||||
const result = next(action);
|
||||
|
||||
// Allow the redux update to go through and compare the old avatar
|
||||
// to the new avatar and emit out change events if necessary.
|
||||
const result = next(action);
|
||||
const newAvatarURL = getAvatarURLByParticipantId(getState(), id);
|
||||
if (avatarURL || email || id || name) {
|
||||
const participantId = !id && local ? getLocalParticipant(getState()).id : id;
|
||||
const updatedParticipant = getParticipantById(getState(), participantId);
|
||||
|
||||
if (oldAvatarURL !== newAvatarURL) {
|
||||
const currentKnownId = local ? APP.conference.getMyUserId() : id;
|
||||
|
||||
APP.UI.refreshAvatarDisplay(currentKnownId, newAvatarURL);
|
||||
APP.API.notifyAvatarChanged(currentKnownId, newAvatarURL);
|
||||
}
|
||||
|
||||
return result;
|
||||
getFirstLoadableAvatarUrl(updatedParticipant)
|
||||
.then(url => {
|
||||
dispatch(setLoadableAvatarUrl(participantId, url));
|
||||
});
|
||||
}
|
||||
|
||||
return next(action);
|
||||
// Notify external listeners of potential avatarURL changes.
|
||||
if (typeof APP === 'object') {
|
||||
const currentKnownId = local ? APP.conference.getMyUserId() : id;
|
||||
|
||||
// Force update of local video getting a new id.
|
||||
APP.UI.refreshAvatarDisplay(currentKnownId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
16
react/features/base/participants/preloadImage.native.js
Normal file
16
react/features/base/participants/preloadImage.native.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
// @flow
|
||||
|
||||
import { Image } from 'react-native';
|
||||
|
||||
/**
|
||||
* Tries to preload an image.
|
||||
*
|
||||
* @param {string} src - Source of the avatar.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function preloadImage(src: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Image.prefetch(src).then(() => resolve(src), reject);
|
||||
});
|
||||
}
|
||||
24
react/features/base/participants/preloadImage.web.js
Normal file
24
react/features/base/participants/preloadImage.web.js
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
// @flow
|
||||
|
||||
declare var config: Object;
|
||||
|
||||
/**
|
||||
* Tries to preload an image.
|
||||
*
|
||||
* @param {string} src - Source of the avatar.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function preloadImage(src: string): Promise<string> {
|
||||
if (typeof config === 'object' && config.disableThirdPartyRequests) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
image.onload = () => resolve(src);
|
||||
image.onerror = reject;
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED,
|
||||
PIN_PARTICIPANT
|
||||
PIN_PARTICIPANT,
|
||||
SET_LOADABLE_AVATAR_URL
|
||||
} from './actionTypes';
|
||||
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
|
||||
|
||||
@@ -65,6 +66,7 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
||||
*/
|
||||
ReducerRegistry.register('features/base/participants', (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case SET_LOADABLE_AVATAR_URL:
|
||||
case DOMINANT_SPEAKER_CHANGED:
|
||||
case PARTICIPANT_ID_CHANGED:
|
||||
case PARTICIPANT_UPDATED:
|
||||
@@ -133,6 +135,7 @@ function _participant(state: Object = {}, action) {
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_LOADABLE_AVATAR_URL:
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { participant } = action; // eslint-disable-line no-shadow
|
||||
let { id } = participant;
|
||||
@@ -186,6 +189,7 @@ function _participantJoined({ participant }) {
|
||||
dominantSpeaker,
|
||||
email,
|
||||
isFakeParticipant,
|
||||
loadableAvatarUrl,
|
||||
local,
|
||||
name,
|
||||
pinned,
|
||||
@@ -221,6 +225,7 @@ function _participantJoined({ participant }) {
|
||||
email,
|
||||
id,
|
||||
isFakeParticipant,
|
||||
loadableAvatarUrl,
|
||||
local: local || false,
|
||||
name,
|
||||
pinned: pinned || false,
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { Icon } from '../../../font-icons';
|
||||
import { Avatar } from '../../../participants';
|
||||
import { Avatar } from '../../../avatar';
|
||||
import { StyleType } from '../../../styles';
|
||||
|
||||
import { type Item } from '../../Types';
|
||||
@@ -70,44 +69,6 @@ export default class AvatarListItem extends Component<Props> {
|
||||
this._renderItemLine = this._renderItemLine.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to render the content in the avatar container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_getAvatarContent() {
|
||||
const {
|
||||
avatarSize = AVATAR_SIZE,
|
||||
avatarTextStyle
|
||||
} = this.props;
|
||||
const { avatar, title } = this.props.item;
|
||||
const isAvatarURL = Boolean(avatar && avatar.match(/^http[s]*:\/\//i));
|
||||
|
||||
if (isAvatarURL) {
|
||||
return (
|
||||
<Avatar
|
||||
size = { avatarSize }
|
||||
uri = { avatar } />
|
||||
);
|
||||
}
|
||||
|
||||
if (avatar && !isAvatarURL) {
|
||||
return (
|
||||
<Icon name = { avatar } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
style = { [
|
||||
styles.avatarContent,
|
||||
avatarTextStyle
|
||||
] }>
|
||||
{ title.substr(0, 1).toUpperCase() }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
@@ -118,26 +79,19 @@ export default class AvatarListItem extends Component<Props> {
|
||||
avatarSize = AVATAR_SIZE,
|
||||
avatarStyle
|
||||
} = this.props;
|
||||
const { colorBase, lines, title } = this.props.item;
|
||||
const avatarStyles = {
|
||||
...styles.avatar,
|
||||
...this._getAvatarColor(colorBase),
|
||||
...avatarStyle,
|
||||
borderRadius: avatarSize / 2,
|
||||
height: avatarSize,
|
||||
width: avatarSize
|
||||
};
|
||||
const { avatar, colorBase, lines, title } = this.props.item;
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { this.props.onPress }
|
||||
style = { styles.listItem }
|
||||
underlayColor = { UNDERLAY_COLOR }>
|
||||
<Container style = { styles.avatarContainer }>
|
||||
<Container style = { avatarStyles }>
|
||||
{ this._getAvatarContent() }
|
||||
</Container>
|
||||
</Container>
|
||||
<Avatar
|
||||
colorBase = { colorBase }
|
||||
displayName = { title }
|
||||
size = { avatarSize }
|
||||
style = { avatarStyle }
|
||||
uri = { avatar } />
|
||||
<Container style = { styles.listItemDetails }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
@@ -155,27 +109,6 @@ export default class AvatarListItem extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a style (color) based on the string that determines the color of
|
||||
* the avatar.
|
||||
*
|
||||
* @param {string} colorBase - The string that is the base of the color.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getAvatarColor(colorBase) {
|
||||
if (!colorBase) {
|
||||
return null;
|
||||
}
|
||||
let nameHash = 0;
|
||||
|
||||
for (let i = 0; i < colorBase.length; i++) {
|
||||
nameHash += colorBase.codePointAt(i);
|
||||
}
|
||||
|
||||
return styles[`avatarColor${(nameHash % 5) + 1}`];
|
||||
}
|
||||
|
||||
_renderItemLine: (string, number) => React$Node;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
|
||||
|
||||
const AVATAR_OPACITY = 0.4;
|
||||
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
||||
|
||||
export const AVATAR_SIZE = 65;
|
||||
@@ -92,40 +91,6 @@ const PAGED_LIST_STYLES = {
|
||||
};
|
||||
|
||||
const SECTION_LIST_STYLES = {
|
||||
/**
|
||||
* The style of the actual avatar.
|
||||
*/
|
||||
avatar: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* List of styles of the avatar of a remote meeting (not the default
|
||||
* server). The number of colors are limited because they should match
|
||||
* nicely.
|
||||
*/
|
||||
avatarColor1: {
|
||||
backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor2: {
|
||||
backgroundColor: `rgba(255, 198, 115, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor3: {
|
||||
backgroundColor: `rgba(128, 128, 255, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor4: {
|
||||
backgroundColor: `rgba(105, 232, 194, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor5: {
|
||||
backgroundColor: `rgba(234, 255, 128, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of the avatar container that makes the avatar rounded.
|
||||
*/
|
||||
|
||||
@@ -124,9 +124,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
} else {
|
||||
APP.UI.setVideoMuted(participantID, muted);
|
||||
}
|
||||
APP.UI.onPeerVideoTypeChanged(
|
||||
participantID,
|
||||
jitsiTrack.videoType);
|
||||
APP.UI.onPeerVideoTypeChanged(participantID, jitsiTrack.videoType);
|
||||
} else if (jitsiTrack.isLocal()) {
|
||||
APP.conference.setAudioMuteStatus(muted);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Creates a deferred object.
|
||||
*
|
||||
* @returns {{promise, resolve, reject}}
|
||||
*/
|
||||
export function createDeferred(): Object {
|
||||
const deferred = {};
|
||||
|
||||
deferred.promise = new Promise((resolve, reject) => {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
|
||||
return deferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace for all global variables, functions, etc that we need.
|
||||
*
|
||||
|
||||
@@ -12,6 +12,8 @@ import { getShareInfoText } from '../../invite';
|
||||
|
||||
import { setCalendarAPIAuthState } from '../actions';
|
||||
|
||||
import { findWindows } from 'windows-iana';
|
||||
|
||||
/**
|
||||
* Constants used for interacting with the Microsoft API.
|
||||
*
|
||||
@@ -560,9 +562,13 @@ function requestCalendarEvents( // eslint-disable-line max-params
|
||||
startDate.toISOString()}' and End/DateTime lt '${
|
||||
endDate.toISOString()}'`;
|
||||
|
||||
const ianaTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const windowsTimeZone = findWindows(ianaTimeZone);
|
||||
|
||||
return client
|
||||
.api(`/me/calendars/${calendarId}/events`)
|
||||
.filter(filter)
|
||||
.header('Prefer', `outlook.timezone="${windowsTimeZone}"`)
|
||||
.select('id,subject,start,end,location,body')
|
||||
.orderby('createdDateTime DESC')
|
||||
.get()
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { getLocalizedDateFormatter } from '../../base/i18n';
|
||||
import { getAvatarURLByParticipantId } from '../../base/participants';
|
||||
|
||||
/**
|
||||
* Formatter string to display the message timestamp.
|
||||
@@ -15,11 +14,6 @@ const TIMESTAMP_FORMAT = 'H:mm';
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The URL of the avatar of the participant.
|
||||
*/
|
||||
_avatarURL: string,
|
||||
|
||||
/**
|
||||
* The representation of a chat message.
|
||||
*/
|
||||
@@ -63,20 +57,3 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
|
||||
.format(TIMESTAMP_FORMAT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Props} ownProps - The own props of the component.
|
||||
* @returns {{
|
||||
* _avatarURL: string
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { message } = ownProps;
|
||||
|
||||
return {
|
||||
_avatarURL: getAvatarURLByParticipantId(state, message.id)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Avatar } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import AbstractChatMessage, {
|
||||
_mapStateToProps,
|
||||
type Props
|
||||
} from '../AbstractChatMessage';
|
||||
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
@@ -81,8 +77,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||
return (
|
||||
<View style = { styles.avatarWrapper }>
|
||||
{ this.props.showAvatar && <Avatar
|
||||
size = { styles.avatarWrapper.width }
|
||||
uri = { this.props._avatarURL } />
|
||||
participantId = { this.props.message.id }
|
||||
size = { styles.avatarWrapper.width } />
|
||||
}
|
||||
</View>
|
||||
);
|
||||
@@ -115,4 +111,4 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(ChatMessage));
|
||||
export default translate(ChatMessage);
|
||||
|
||||
@@ -372,7 +372,8 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
label: 'settings.selectCamera',
|
||||
onSelect: selectedVideoInputId =>
|
||||
super._onChange({ selectedVideoInputId }),
|
||||
selectedDeviceId: this.props.selectedVideoInputId
|
||||
selectedDeviceId: this.state.previewVideoTrack
|
||||
? this.state.previewVideoTrack.getDeviceId() : null
|
||||
},
|
||||
{
|
||||
devices: availableDevices.audioInput,
|
||||
@@ -384,7 +385,8 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
|
||||
label: 'settings.selectMic',
|
||||
onSelect: selectedAudioInputId =>
|
||||
super._onChange({ selectedAudioInputId }),
|
||||
selectedDeviceId: this.props.selectedAudioInputId
|
||||
selectedDeviceId: this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.getDeviceId() : null
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
KICKED_OUT
|
||||
} from '../base/conference';
|
||||
import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices';
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
PARTICIPANT_KICKED,
|
||||
SET_LOADABLE_AVATAR_URL,
|
||||
getAvatarURLByParticipantId,
|
||||
getLocalParticipant
|
||||
getLocalParticipant,
|
||||
getParticipantById
|
||||
} from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { appendSuffix } from '../display-name';
|
||||
@@ -21,8 +28,31 @@ declare var interfaceConfig: Object;
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
// We need to do these before executing the rest of the middelware chain
|
||||
switch (action.type) {
|
||||
case SET_LOADABLE_AVATAR_URL: {
|
||||
const { id, loadableAvatarUrl } = action.participant;
|
||||
const participant = getParticipantById(
|
||||
store.getState(),
|
||||
id
|
||||
);
|
||||
|
||||
const result = next(action);
|
||||
|
||||
if (participant.loadableAvatarUrl !== loadableAvatarUrl) {
|
||||
APP.API.notifyAvatarChanged(
|
||||
id,
|
||||
loadableAvatarUrl
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
// These should happen after the rest of the middleware chain ran
|
||||
switch (action.type) {
|
||||
case CONFERENCE_FAILED: {
|
||||
if (action.conference
|
||||
@@ -49,10 +79,19 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
avatarURL: getAvatarURLByParticipantId(state, id)
|
||||
}
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case KICKED_OUT:
|
||||
APP.API.notifyKickedOut(
|
||||
{
|
||||
id: getLocalParticipant(store.getState()).id,
|
||||
local: true
|
||||
},
|
||||
{ id: action.participant.getId() }
|
||||
);
|
||||
break;
|
||||
|
||||
case NOTIFY_CAMERA_ERROR:
|
||||
if (action.error) {
|
||||
APP.API.notifyOnCameraError(
|
||||
@@ -66,6 +105,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
break;
|
||||
|
||||
case PARTICIPANT_KICKED:
|
||||
APP.API.notifyKickedOut(
|
||||
{
|
||||
id: action.kicked,
|
||||
local: false
|
||||
},
|
||||
{ id: action.kicker });
|
||||
break;
|
||||
|
||||
case SET_FILMSTRIP_VISIBLE:
|
||||
APP.API.notifyFilmstripDisplayChanged(action.visible);
|
||||
break;
|
||||
|
||||
28
react/features/invite/_utils.js
Normal file
28
react/features/invite/_utils.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Utility class with no dependencies. Used in components that are stripped in separate bundles
|
||||
* and requires as less dependencies as possible.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Formats the conference pin in readable way for UI to display it.
|
||||
* Formats the pin in 3 groups of digits:
|
||||
* XXXX XXXX XX or XXXXX XXXXX XXX.
|
||||
* The length of first and second group is Math.ceil(pin.length / 3)
|
||||
*
|
||||
* @param {Object} conferenceID - The conference id to format, string or number.
|
||||
* @returns {string} - The formatted conference pin.
|
||||
* @private
|
||||
*/
|
||||
export function _formatConferenceIDPin(conferenceID: Object) {
|
||||
const conferenceIDStr = conferenceID.toString();
|
||||
|
||||
// let's split the conferenceID in 3 parts, to be easier to read
|
||||
const partLen = Math.ceil(conferenceIDStr.length / 3);
|
||||
|
||||
return `${
|
||||
conferenceIDStr.substring(0, partLen)} ${
|
||||
conferenceIDStr.substring(partLen, 2 * partLen)} ${
|
||||
conferenceIDStr.substring(2 * partLen, conferenceIDStr.length)}`;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Alert,
|
||||
FlatList,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
@@ -51,6 +52,11 @@ type Props = AbstractProps & {
|
||||
|
||||
type State = AbstractState & {
|
||||
|
||||
/**
|
||||
* State variable to keep track of the search field value.
|
||||
*/
|
||||
fieldValue: string,
|
||||
|
||||
/**
|
||||
* True if a search is in progress, false otherwise.
|
||||
*/
|
||||
@@ -73,6 +79,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
defaultState = {
|
||||
addToCallError: false,
|
||||
addToCallInProgress: false,
|
||||
fieldValue: '',
|
||||
inviteItems: [],
|
||||
searchInprogress: false,
|
||||
selectableItems: []
|
||||
@@ -101,6 +108,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
this._keyExtractor = this._keyExtractor.bind(this);
|
||||
this._renderItem = this._renderItem.bind(this);
|
||||
this._renderSeparator = this._renderSeparator.bind(this);
|
||||
this._onClearField = this._onClearField.bind(this);
|
||||
this._onCloseAddPeopleDialog = this._onCloseAddPeopleDialog.bind(this);
|
||||
this._onInvite = this._onInvite.bind(this);
|
||||
this._onPressItem = this._onPressItem.bind(this);
|
||||
@@ -168,12 +176,15 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
<TextInput
|
||||
autoCorrect = { false }
|
||||
autoFocus = { true }
|
||||
clearButtonMode = 'always' // iOS only
|
||||
onChangeText = { this._onTypeQuery }
|
||||
placeholder = {
|
||||
this.props.t(`inviteDialog.${placeholderKey}`)
|
||||
}
|
||||
ref = { this._setFieldRef }
|
||||
style = { styles.searchField } />
|
||||
style = { styles.searchField }
|
||||
value = { this.state.fieldValue } />
|
||||
{ this._renderAndroidClearButton() }
|
||||
</View>
|
||||
<FlatList
|
||||
ItemSeparatorComponent = { this._renderSeparator }
|
||||
@@ -215,6 +226,22 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
return item.type === 'user' ? item.id || item.user_id : item.number;
|
||||
}
|
||||
|
||||
_onClearField: () => void
|
||||
|
||||
/**
|
||||
* Callback to clear the text field.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClearField() {
|
||||
this.setState({
|
||||
fieldValue: ''
|
||||
});
|
||||
|
||||
// Clear search results
|
||||
this._onTypeQuery('');
|
||||
}
|
||||
|
||||
_onCloseAddPeopleDialog: () => void
|
||||
|
||||
/**
|
||||
@@ -288,6 +315,10 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTypeQuery(query) {
|
||||
this.setState({
|
||||
fieldValue: query
|
||||
});
|
||||
|
||||
clearTimeout(this.searchTimeout);
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.setState({
|
||||
@@ -342,6 +373,31 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
|
||||
_renderItem: Object => ?React$Element<*>
|
||||
|
||||
/**
|
||||
* Renders a button to clear the text field on Android.
|
||||
*
|
||||
* NOTE: For the best platform experience we use the native solution on iOS.
|
||||
*
|
||||
* @returns {React#Element<*>}
|
||||
*/
|
||||
_renderAndroidClearButton() {
|
||||
if (Platform.OS !== 'android' || !this.state.fieldValue.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress = { this._onClearField }
|
||||
style = { styles.clearButton }>
|
||||
<View style = { styles.clearIconContainer }>
|
||||
<Icon
|
||||
name = 'close'
|
||||
style = { styles.clearIcon } />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single item in the {@code FlatList}.
|
||||
*
|
||||
|
||||
@@ -12,12 +12,6 @@ import { isAddPeopleEnabled, isDialOutEnabled } from '../../../functions';
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Whether or not the feature to invite people to join the
|
||||
* conference is available.
|
||||
*/
|
||||
_addPeopleEnabled: boolean,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
@@ -42,16 +36,6 @@ class InviteButton extends AbstractButton<Props, *> {
|
||||
_handleClick() {
|
||||
this.props.dispatch(setAddPeopleDialogVisible(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if none of the invite methods are available.
|
||||
*
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isDisabled() {
|
||||
return !this.props._addPeopleEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,14 +43,17 @@ class InviteButton extends AbstractButton<Props, *> {
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The redux store/state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component
|
||||
* instance.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _addPeopleEnabled: boolean
|
||||
* }}
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
function _mapStateToProps(state: Object, ownProps: Object) {
|
||||
const addPeopleEnabled = isAddPeopleEnabled(state) || isDialOutEnabled(state);
|
||||
const { visible = Boolean(addPeopleEnabled) } = ownProps;
|
||||
|
||||
return {
|
||||
_addPeopleEnabled: isAddPeopleEnabled(state) || isDialOutEnabled(state)
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ export const DARK_GREY = 'rgb(28, 32, 37)';
|
||||
export const LIGHT_GREY = 'rgb(209, 219, 232)';
|
||||
export const ICON_SIZE = 15;
|
||||
|
||||
const FIELD_COLOR = 'rgb(240, 243, 247)';
|
||||
|
||||
export default {
|
||||
avatar: {
|
||||
backgroundColor: LIGHT_GREY
|
||||
@@ -21,6 +23,27 @@ export default {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
clearButton: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 5
|
||||
},
|
||||
|
||||
clearIcon: {
|
||||
color: DARK_GREY,
|
||||
fontSize: 18,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
clearIconContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: FIELD_COLOR,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
height: 24,
|
||||
width: 24
|
||||
},
|
||||
|
||||
dialogWrapper: {
|
||||
alignItems: 'stretch',
|
||||
backgroundColor: ColorPalette.white,
|
||||
@@ -56,7 +79,7 @@ export default {
|
||||
},
|
||||
|
||||
searchField: {
|
||||
backgroundColor: 'rgb(240, 243, 247)',
|
||||
backgroundColor: FIELD_COLOR,
|
||||
borderBottomRightRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
color: DARK_GREY,
|
||||
@@ -86,7 +109,7 @@ export default {
|
||||
|
||||
searchIconWrapper: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgb(240, 243, 247)',
|
||||
backgroundColor: FIELD_COLOR,
|
||||
borderBottomLeftRadius: 10,
|
||||
borderTopLeftRadius: 10,
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURL,
|
||||
getParticipants,
|
||||
getParticipantDisplayName,
|
||||
getParticipantPresenceStatus
|
||||
@@ -23,7 +22,7 @@ import styles from './styles';
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The callee's information such as avatar and display name.
|
||||
* The callee's information such as display name.
|
||||
*/
|
||||
_callee: Object,
|
||||
|
||||
@@ -46,7 +45,7 @@ class CalleeInfo extends Component<Props> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
avatar,
|
||||
id,
|
||||
name,
|
||||
status = CALLING
|
||||
} = this.props._callee;
|
||||
@@ -60,7 +59,7 @@ class CalleeInfo extends Component<Props> {
|
||||
{ ...this._style('ringing__content') }>
|
||||
<Avatar
|
||||
{ ...this._style('ringing__avatar') }
|
||||
uri = { avatar } />
|
||||
participantId = { id } />
|
||||
<Container { ...this._style('ringing__status') }>
|
||||
<PresenceLabel
|
||||
defaultPresence = { status }
|
||||
@@ -144,7 +143,7 @@ function _mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
_callee: {
|
||||
avatar: getAvatarURL(poltergeist),
|
||||
id,
|
||||
name: getParticipantDisplayName(state, id),
|
||||
status: getParticipantPresenceStatus(state, id)
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@ import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
|
||||
import { _formatConferenceIDPin } from '../../../_utils';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ConferenceID}.
|
||||
*/
|
||||
@@ -49,7 +51,7 @@ class ConferenceID extends Component<Props> {
|
||||
{ t('info.dialANumber') }
|
||||
</div>
|
||||
<div className = 'dial-in-conference-pin'>
|
||||
{ `${t('info.dialInConferenceID')} ${conferenceID}` }
|
||||
{ `${t('info.dialInConferenceID')} ${_formatConferenceIDPin(conferenceID)}` }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
|
||||
import { _formatConferenceIDPin } from '../../../_utils';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link DialInNumber}.
|
||||
*/
|
||||
@@ -61,7 +63,7 @@ class DialInNumber extends Component<Props> {
|
||||
</span>
|
||||
<span className = 'spacer'> </span>
|
||||
<span className = 'info-value'>
|
||||
{ `${conferenceID}#` }
|
||||
{ `${_formatConferenceIDPin(conferenceID)}#` }
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
import './subscriber';
|
||||
|
||||
@@ -38,7 +38,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
case PARTICIPANT_JOINED:
|
||||
case PARTICIPANT_LEFT:
|
||||
case PIN_PARTICIPANT:
|
||||
@@ -47,13 +47,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
store.dispatch(selectParticipantInLargeVideo());
|
||||
break;
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
// Ensure a participant is selected on conference join. This addresses
|
||||
// the case where video tracks were received before CONFERENCE_JOINED
|
||||
// fired; without the conference selection may not happen.
|
||||
store.dispatch(selectParticipant());
|
||||
break;
|
||||
|
||||
case TRACK_UPDATED:
|
||||
// In order to minimize re-calculations, we need to select participant
|
||||
// only if the videoType of the current participant rendered in
|
||||
|
||||
0
react/features/large-video/subscriber.native.js
Normal file
0
react/features/large-video/subscriber.native.js
Normal file
14
react/features/large-video/subscriber.web.js
Normal file
14
react/features/large-video/subscriber.web.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
|
||||
/**
|
||||
* Updates the on stage participant video.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/large-video'].participantId,
|
||||
/* listener */ participantId => {
|
||||
VideoLayout.updateLargeVideo(participantId, true);
|
||||
}
|
||||
);
|
||||
@@ -4,8 +4,8 @@ import React, { Component } from 'react';
|
||||
import { Image, Text, View } from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Avatar } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import AnswerButton from './AnswerButton';
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
CLEAR_NOTIFICATIONS,
|
||||
HIDE_NOTIFICATION,
|
||||
@@ -7,7 +11,7 @@ import {
|
||||
SHOW_NOTIFICATION
|
||||
} from './actionTypes';
|
||||
|
||||
import { NOTIFICATION_TYPE } from './constants';
|
||||
import { NOTIFICATION_TIMEOUT, NOTIFICATION_TYPE } from './constants';
|
||||
|
||||
/**
|
||||
* Clears (removes) all the notifications.
|
||||
@@ -102,3 +106,73 @@ export function showWarningNotification(props: Object) {
|
||||
appearance: NOTIFICATION_TYPE.WARNING
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of names of participants that have joined the conference. The array
|
||||
* is replaced with an empty array as notifications are displayed.
|
||||
*
|
||||
* @private
|
||||
* @type {string[]}
|
||||
*/
|
||||
let joinedParticipantsNames = [];
|
||||
|
||||
/**
|
||||
* A throttled internal function that takes the internal list of participant
|
||||
* names, {@code joinedParticipantsNames}, and triggers the display of a
|
||||
* notification informing of their joining.
|
||||
*
|
||||
* @private
|
||||
* @type {Function}
|
||||
*/
|
||||
const _throttledNotifyParticipantConnected = throttle((dispatch: Dispatch<any>) => {
|
||||
const joinedParticipantsCount = joinedParticipantsNames.length;
|
||||
|
||||
let notificationProps;
|
||||
|
||||
if (joinedParticipantsCount >= 3) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
name: joinedParticipantsNames[0],
|
||||
count: joinedParticipantsCount - 1
|
||||
},
|
||||
titleKey: 'notify.connectedThreePlusMembers'
|
||||
};
|
||||
} else if (joinedParticipantsCount === 2) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
first: joinedParticipantsNames[0],
|
||||
second: joinedParticipantsNames[1]
|
||||
},
|
||||
titleKey: 'notify.connectedTwoMembers'
|
||||
};
|
||||
} else if (joinedParticipantsCount) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
name: joinedParticipantsNames[0]
|
||||
},
|
||||
titleKey: 'notify.connectedOneMember'
|
||||
};
|
||||
}
|
||||
|
||||
if (notificationProps) {
|
||||
dispatch(
|
||||
showNotification(notificationProps, NOTIFICATION_TIMEOUT));
|
||||
}
|
||||
|
||||
joinedParticipantsNames = [];
|
||||
|
||||
}, 500, { leading: false });
|
||||
|
||||
/**
|
||||
* Queues the display of a notification of a participant having connected to
|
||||
* the meeting. The notifications are batched so that quick consecutive
|
||||
* connection events are shown in one notification.
|
||||
*
|
||||
* @param {string} displayName - The name of the participant that connected.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showParticipantJoinedNotification(displayName: string) {
|
||||
joinedParticipantsNames.push(displayName);
|
||||
|
||||
return (dispatch: Dispatch<any>) => _throttledNotifyParticipantConnected(dispatch);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,67 @@
|
||||
/* @flow */
|
||||
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
import {
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
getParticipantById,
|
||||
getParticipantDisplayName
|
||||
} from '../base/participants';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
|
||||
import { clearNotifications } from './actions';
|
||||
import {
|
||||
clearNotifications,
|
||||
showNotification,
|
||||
showParticipantJoinedNotification
|
||||
} from './actions';
|
||||
import { NOTIFICATION_TIMEOUT } from './constants';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Middleware that captures actions to display notifications.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case PARTICIPANT_JOINED: {
|
||||
const result = next(action);
|
||||
|
||||
const { participant: p } = action;
|
||||
|
||||
if (!p.local) {
|
||||
store.dispatch(showParticipantJoinedNotification(
|
||||
getParticipantDisplayName(store.getState, p.id)
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
const participant = getParticipantById(
|
||||
store.getState(),
|
||||
action.participant.id
|
||||
);
|
||||
|
||||
if (typeof interfaceConfig === 'object'
|
||||
&& participant
|
||||
&& !participant.local) {
|
||||
store.dispatch(showNotification({
|
||||
descriptionKey: 'notify.disconnected',
|
||||
titleKey: 'notify.somebody',
|
||||
title: participant.name
|
||||
},
|
||||
NOTIFICATION_TIMEOUT));
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* StateListenerRegistry provides a reliable way to detect the leaving of a
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURL,
|
||||
getLocalParticipant
|
||||
} from '../../../base/participants';
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import OverlayFrame from './OverlayFrame';
|
||||
@@ -18,9 +15,9 @@ import OverlayFrame from './OverlayFrame';
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The source (e.g. URI, URL) of the avatar image of the local participant.
|
||||
* The ID of the local participant.
|
||||
*/
|
||||
_avatar: string,
|
||||
_localParticipantId: string,
|
||||
|
||||
/**
|
||||
* The children components to be displayed into the overlay frame for
|
||||
@@ -85,7 +82,7 @@ class FilmstripOnlyOverlayFrame extends Component<Props> {
|
||||
}
|
||||
</div>
|
||||
<div className = 'inlay-filmstrip-only__avatar-container'>
|
||||
<Avatar uri = { this.props._avatar } />
|
||||
<Avatar participantId = { this.props._localParticipantId } />
|
||||
{
|
||||
this._renderIcon()
|
||||
}
|
||||
@@ -103,12 +100,12 @@ class FilmstripOnlyOverlayFrame extends Component<Props> {
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _avatar: string
|
||||
* _localParticipantId: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_avatar: getAvatarURL(getLocalParticipant(state) || {})
|
||||
_localParticipantId: (getLocalParticipant(state) || {}).id
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import {
|
||||
BottomSheet
|
||||
} from '../../../base/dialog';
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURL,
|
||||
getParticipantDisplayName
|
||||
} from '../../../base/participants';
|
||||
import { getParticipantDisplayName } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
|
||||
@@ -39,11 +36,6 @@ type Props = {
|
||||
*/
|
||||
participant: Object,
|
||||
|
||||
/**
|
||||
* URL of the avatar of the participant.
|
||||
*/
|
||||
_avatarURL: string,
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the BottomSheet.
|
||||
*/
|
||||
@@ -76,10 +68,11 @@ class RemoteVideoMenu extends Component<Props> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { participant } = this.props;
|
||||
const buttonProps = {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
participantID: this.props.participant.id,
|
||||
participantID: participant.id,
|
||||
styles: this.props._bottomSheetStyles
|
||||
};
|
||||
|
||||
@@ -87,8 +80,8 @@ class RemoteVideoMenu extends Component<Props> {
|
||||
<BottomSheet onCancel = { this._onCancel }>
|
||||
<View style = { styles.participantNameContainer }>
|
||||
<Avatar
|
||||
size = { AVATAR_SIZE }
|
||||
uri = { this.props._avatarURL } />
|
||||
participantId = { participant.id }
|
||||
size = { AVATAR_SIZE } />
|
||||
<Text style = { styles.participantNameLabel }>
|
||||
{ this.props._participantDisplayName }
|
||||
</Text>
|
||||
@@ -120,7 +113,6 @@ class RemoteVideoMenu extends Component<Props> {
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _avatarURL: string,
|
||||
* _bottomSheetStyles: StyleType,
|
||||
* _participantDisplayName: string
|
||||
* }}
|
||||
@@ -129,7 +121,6 @@ function _mapStateToProps(state, ownProps) {
|
||||
const { participant } = ownProps;
|
||||
|
||||
return {
|
||||
_avatarURL: getAvatarURL(participant),
|
||||
_bottomSheetStyles:
|
||||
ColorSchemeRegistry.get(state, 'BottomSheet'),
|
||||
_participantDisplayName: getParticipantDisplayName(
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { appNavigate } from '../app';
|
||||
import {
|
||||
appNavigate,
|
||||
maybeRedirectToWelcomePage
|
||||
} from '../app';
|
||||
import {
|
||||
conferenceLeft,
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
@@ -11,6 +14,8 @@ import {
|
||||
import { hideDialog, openDialog } from '../base/dialog';
|
||||
import { PasswordRequiredPrompt, RoomLockPrompt } from './components';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Begins a (user) request to lock a specific conference/room.
|
||||
*
|
||||
@@ -44,6 +49,16 @@ export function beginRoomLockRequest(conference: ?Object) {
|
||||
*/
|
||||
export function _cancelPasswordRequiredPrompt(conference: Object) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
// when we are redirecting the library should handle any
|
||||
// unload and clean of the connection.
|
||||
APP.API.notifyReadyToClose();
|
||||
dispatch(maybeRedirectToWelcomePage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Canceling PasswordRequiredPrompt is to navigate the app/user to
|
||||
// WelcomePage. In other words, the canceling invalidates the
|
||||
// locationURL. Make sure that the canceling indeed has the intent to
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Dialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
|
||||
import { _cancelPasswordRequiredPrompt } from '../actions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link PasswordRequiredPrompt}.
|
||||
@@ -63,6 +65,7 @@ class PasswordRequiredPrompt extends Component<Props, State> {
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onPasswordChanged = this._onPasswordChanged.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
@@ -75,7 +78,9 @@ class PasswordRequiredPrompt extends Component<Props, State> {
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
isModal = { true }
|
||||
disableBlanketClickDismiss = { true }
|
||||
isModal = { false }
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.passwordRequired'
|
||||
width = 'small'>
|
||||
@@ -121,6 +126,22 @@ class PasswordRequiredPrompt extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
_onCancel: () => boolean;
|
||||
|
||||
/**
|
||||
* Dispatches action to cancel and dismiss this dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onCancel() {
|
||||
|
||||
this.props.dispatch(
|
||||
_cancelPasswordRequiredPrompt(this.props.conference));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,10 +8,15 @@ import {
|
||||
import { hideDialog } from '../base/dialog';
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import {
|
||||
NOTIFICATION_TIMEOUT,
|
||||
showNotification
|
||||
} from '../notifications';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
import { _openPasswordRequiredPrompt } from './actions';
|
||||
import { PasswordRequiredPrompt, RoomLockPrompt } from './components';
|
||||
import { LOCKED_REMOTELY } from './constants';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -29,14 +34,33 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(store, next, action);
|
||||
|
||||
case LOCK_STATE_CHANGED:
|
||||
case LOCK_STATE_CHANGED: {
|
||||
// TODO Remove this logic when all components interested in the lock
|
||||
// state change event are moved into react/redux.
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, action.locked);
|
||||
}
|
||||
break;
|
||||
|
||||
const previousLockedState = store.getState()['features/base/conference'].locked;
|
||||
|
||||
const result = next(action);
|
||||
|
||||
const currentLockedState = store.getState()['features/base/conference'].locked;
|
||||
|
||||
if (currentLockedState === LOCKED_REMOTELY) {
|
||||
store.dispatch(
|
||||
showNotification({
|
||||
titleKey: 'notify.passwordSetRemotely'
|
||||
}, NOTIFICATION_TIMEOUT));
|
||||
} else if (previousLockedState === LOCKED_REMOTELY && !currentLockedState) {
|
||||
store.dispatch(
|
||||
showNotification({
|
||||
titleKey: 'notify.passwordRemovedRemotely'
|
||||
}, NOTIFICATION_TIMEOUT));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case SET_PASSWORD_FAILED:
|
||||
return _setPasswordFailed(store, next, action);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ export type Props = {
|
||||
* @extends Component
|
||||
*/
|
||||
class ProfileTab extends AbstractDialogTab<Props> {
|
||||
static defaultProps = {
|
||||
displayName: '',
|
||||
email: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ConnectedSettingsDialog} instance.
|
||||
*
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURL,
|
||||
getLocalParticipant
|
||||
} from '../../../base/participants';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
@@ -65,7 +61,6 @@ class OverflowMenuProfileItem extends Component<Props> {
|
||||
const { _localParticipant, _unclickable } = this.props;
|
||||
const classNames = `overflow-menu-item ${
|
||||
_unclickable ? 'unclickable' : ''}`;
|
||||
const avatarURL = getAvatarURL(_localParticipant);
|
||||
let displayName;
|
||||
|
||||
if (_localParticipant && _localParticipant.name) {
|
||||
@@ -80,7 +75,9 @@ class OverflowMenuProfileItem extends Component<Props> {
|
||||
className = { classNames }
|
||||
onClick = { this._onClick }>
|
||||
<span className = 'overflow-menu-item-icon'>
|
||||
<Avatar uri = { avatarURL } />
|
||||
<Avatar
|
||||
participantId = { _localParticipant.id }
|
||||
size = { 24 } />
|
||||
</span>
|
||||
<span className = 'profile-text'>
|
||||
{ displayName }
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import { SafeAreaView, ScrollView, Text } from 'react-native';
|
||||
|
||||
import { Avatar } from '../../base/avatar';
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURL,
|
||||
getLocalParticipant,
|
||||
getParticipantDisplayName
|
||||
} from '../../base/participants';
|
||||
@@ -42,16 +41,16 @@ type Props = {
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The avatar URL to be rendered.
|
||||
*/
|
||||
_avatarURL: string,
|
||||
|
||||
/**
|
||||
* Display name of the local participant.
|
||||
*/
|
||||
_displayName: string,
|
||||
|
||||
/**
|
||||
* ID of the local participant.
|
||||
*/
|
||||
_localParticipantId: string,
|
||||
|
||||
/**
|
||||
* Sets the side bar visible or hidden.
|
||||
*/
|
||||
@@ -90,9 +89,8 @@ class WelcomePageSideBar extends Component<Props> {
|
||||
style = { styles.sideBar } >
|
||||
<Header style = { styles.sideBarHeader }>
|
||||
<Avatar
|
||||
size = { SIDEBAR_AVATAR_SIZE }
|
||||
style = { styles.avatar }
|
||||
uri = { this.props._avatarURL } />
|
||||
participantId = { this.props._localParticipantId }
|
||||
size = { SIDEBAR_AVATAR_SIZE } />
|
||||
<Text style = { styles.displayName }>
|
||||
{ this.props._displayName }
|
||||
</Text>
|
||||
@@ -155,18 +153,14 @@ class WelcomePageSideBar extends Component<Props> {
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* _avatarURL: string,
|
||||
* _displayName: string,
|
||||
* _visible: boolean
|
||||
* }}
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state: Object) {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const _localParticipant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_avatarURL: getAvatarURL(localParticipant),
|
||||
_displayName: getParticipantDisplayName(state, localParticipant.id),
|
||||
_displayName: getParticipantDisplayName(state, _localParticipant.id),
|
||||
_localParticipantId: _localParticipant.id,
|
||||
_visible: state['features/welcome'].sideBarVisible
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,14 +38,6 @@ export default createStyleSheet({
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style of the avatar in te side bar.
|
||||
*/
|
||||
avatar: {
|
||||
alignSelf: 'center',
|
||||
flex: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Join button style.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user