diff --git a/lang/main.json b/lang/main.json
index 084e7675c..fc5ae7ba9 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -214,7 +214,9 @@
"notify": {
"disconnected": "disconnected",
"moderator": "Moderator rights granted!",
- "connected": "connected",
+ "connectedOneMember": "__name__ connected",
+ "connectedTwoMembers": "__first__ and __second__ connected",
+ "connectedThreePlusMembers": "__name__ and __count__ others connected",
"somebody": "Somebody",
"me": "Me",
"focus": "Conference focus",
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 0d89cf202..9dce8f8b6 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -25,7 +25,10 @@ import {
} from '../../react/features/device-selection';
import { updateDeviceList } from '../../react/features/base/devices';
import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
-import { getLocalParticipant } from '../../react/features/base/participants';
+import {
+ getLocalParticipant,
+ showParticipantJoinedNotification
+} from '../../react/features/base/participants';
import { openDisplayNamePrompt } from '../../react/features/display-name';
import {
maybeShowNotificationWithDoNotDisplay,
@@ -473,8 +476,7 @@ UI.addUser = function(user) {
const id = user.getId();
const displayName = user.getDisplayName();
- messageHandler.participantNotification(
- displayName, 'notify.somebody', 'connected', 'notify.connected');
+ APP.store.dispatch(showParticipantJoinedNotification(displayName));
if (!config.startAudioMuted
|| config.startAudioMuted > APP.conference.membersCount) {
diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js
index bc5fc99f6..2e9a63fcb 100644
--- a/modules/UI/util/MessageHandler.js
+++ b/modules/UI/util/MessageHandler.js
@@ -362,7 +362,7 @@ const messageHandler = {
$titleString.attr('data-i18n', titleKey);
return $('
').append($titleString)
-.html();
+ .html();
},
/**
@@ -479,7 +479,7 @@ const messageHandler = {
* @param displayName the display name of the participant that is
* associated with the notification.
* @param displayNameKey the key from the language file for the display
- * name. Only used if displayName i not provided.
+ * name. Only used if displayName is not provided.
* @param cls css class for the notification
* @param messageKey the key from the language file for the text of the
* message.
diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js
index bf006d6fe..41b2d2bb2 100644
--- a/react/features/base/participants/actions.js
+++ b/react/features/base/participants/actions.js
@@ -1,3 +1,9 @@
+/* global interfaceConfig */
+
+import throttle from 'lodash/throttle';
+
+import { Notification, showNotification } from '../../notifications';
+
import {
DOMINANT_SPEAKER_CHANGED,
KICK_PARTICIPANT,
@@ -318,3 +324,79 @@ 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.
+ *
+ * @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(
+ Notification,
+ notificationProps,
+ 2500));
+ }
+
+ 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 || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
+
+ return dispatch => {
+ _throttledNotifyParticipantConnected(dispatch);
+ };
+}
diff --git a/react/features/notifications/components/Notification.web.js b/react/features/notifications/components/Notification.web.js
index 2f5308e85..45643e343 100644
--- a/react/features/notifications/components/Notification.web.js
+++ b/react/features/notifications/components/Notification.web.js
@@ -104,6 +104,11 @@ class Notification extends Component<*> {
*/
title: PropTypes.string,
+ /**
+ * The translation arguments that may be necessary for the title.
+ */
+ titleArguments: PropTypes.object,
+
/**
* The translation key to display as the title of the notification if
* no title is provided.
@@ -138,16 +143,17 @@ class Notification extends Component<*> {
*/
render() {
const {
- hideErrorSupportLink,
appearance,
- titleKey,
+ description,
descriptionArguments,
descriptionKey,
- description,
+ hideErrorSupportLink,
isDismissAllowed,
onDismissed,
t,
title,
+ titleArguments,
+ titleKey,
uid
} = this.props;
@@ -161,7 +167,7 @@ class Notification extends Component<*> {
id = { uid }
isDismissAllowed = { isDismissAllowed }
onDismissed = { onDismissed }
- title = { title || t(titleKey) } />
+ title = { title || t(titleKey, titleArguments) } />
);
}