diff --git a/config.js b/config.js
index a0656bd87..2bfb82ac4 100644
--- a/config.js
+++ b/config.js
@@ -217,6 +217,21 @@ var config = {
// Default value for the channel "last N" attribute. -1 for unlimited.
channelLastN: -1,
+ // // Options for the recording limit notification.
+ // recordingLimit: {
+ //
+ // // The recording limit in minutes. Note: This number appears in the notification text
+ // // but doesn't enforce the actual recording time limit. This should be configured in
+ // // jibri!
+ // limit: 60,
+ //
+ // // The name of the app with unlimited recordings.
+ // appName: 'Unlimited recordings APP',
+ //
+ // // The URL of the app with unlimited recordings.
+ // appURL: 'https://unlimited.recordings.app.com/'
+ // },
+
// Disables or enables RTX (RFC 4588) (defaults to false).
// disableRtx: false,
diff --git a/lang/main.json b/lang/main.json
index 515c4a2f4..076ee147c 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -395,6 +395,8 @@
"videoQuality": "Manage call quality"
},
"liveStreaming": {
+ "limitNotificationDescriptionWeb": "Due to high demand your streaming will be limited to {{limit}} min. For unlimited streaming try {{app}}.",
+ "limitNotificationDescriptionNative": "Your streaming will be limited to {{limit}} min. For unlimited streaming try {{app}}.",
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
"busyTitle": "All streamers are currently busy",
"changeSignIn": "Switch accounts.",
@@ -552,6 +554,8 @@
},
"raisedHand": "Would like to speak",
"recording": {
+ "limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try {{app}}.",
+ "limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}3>.",
"authDropboxText": "Upload to Dropbox",
"availableSpace": "Available space: {{spaceLeft}} MB (approximately {{duration}} minutes of recording)",
"beta": "BETA",
diff --git a/react/features/notifications/components/AbstractNotification.js b/react/features/notifications/components/AbstractNotification.js
index ae25749b2..8b0709295 100644
--- a/react/features/notifications/components/AbstractNotification.js
+++ b/react/features/notifications/components/AbstractNotification.js
@@ -55,6 +55,11 @@ export type Props = {
*/
isDismissAllowed: boolean,
+ /**
+ * Maximum lines of the description.
+ */
+ maxLines: ?number,
+
/**
* Callback invoked when the user clicks to dismiss the notification.
*/
diff --git a/react/features/notifications/components/native/Notification.js b/react/features/notifications/components/native/Notification.js
index 9f09be3ce..4e12cab5c 100644
--- a/react/features/notifications/components/native/Notification.js
+++ b/react/features/notifications/components/native/Notification.js
@@ -11,6 +11,13 @@ import AbstractNotification, {
import styles from './styles';
+/**
+ * Default value for the maxLines prop.
+ *
+ * @type {number}
+ */
+const DEFAULT_MAX_LINES = 1;
+
/**
* Implements a React {@link Component} to display a notification.
*
@@ -24,9 +31,7 @@ class Notification extends AbstractNotification {
* @returns {ReactElement}
*/
render() {
- const {
- isDismissAllowed
- } = this.props;
+ const { isDismissAllowed } = this.props;
return (
{
* @private
*/
_renderContent() {
- const { t, title, titleArguments, titleKey } = this.props;
+ const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey } = this.props;
const titleText = title || (titleKey && t(titleKey, titleArguments));
const description = this._getDescription();
@@ -69,7 +74,7 @@ class Notification extends AbstractNotification {
return description.map((line, index) => (
{ line }
@@ -78,7 +83,7 @@ class Notification extends AbstractNotification {
return (
{ titleText }
diff --git a/react/features/notifications/components/native/styles.js b/react/features/notifications/components/native/styles.js
index 07101a821..3719c0a3f 100644
--- a/react/features/notifications/components/native/styles.js
+++ b/react/features/notifications/components/native/styles.js
@@ -40,7 +40,7 @@ export default {
notification: {
backgroundColor: '#768898',
flexDirection: 'row',
- height: 48,
+ minHeight: 48,
marginTop: 0.5 * BoxModel.margin
},
diff --git a/react/features/recording/actions.js b/react/features/recording/actions.any.js
similarity index 100%
rename from react/features/recording/actions.js
rename to react/features/recording/actions.any.js
diff --git a/react/features/recording/actions.native.js b/react/features/recording/actions.native.js
new file mode 100644
index 000000000..d8de2e328
--- /dev/null
+++ b/react/features/recording/actions.native.js
@@ -0,0 +1,42 @@
+// @flow
+
+import JitsiMeetJS from '../base/lib-jitsi-meet';
+import { showNotification } from '../notifications';
+
+export * from './actions.any';
+
+/**
+ * Signals that a started recording notification should be shown on the
+ * screen for a given period.
+ *
+ * @param {string} streamType - The type of the stream ({@code file} or
+ * {@code stream}).
+ * @returns {showNotification}
+ */
+export function showRecordingLimitNotification(streamType: string) {
+ return (dispatch: Function, getState: Function) => {
+ const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
+ let descriptionKey, titleKey;
+
+ if (isLiveStreaming) {
+ descriptionKey = 'liveStreaming.limitNotificationDescriptionNative';
+ titleKey = 'dialog.liveStreaming';
+ } else {
+ descriptionKey = 'recording.limitNotificationDescriptionNative';
+ titleKey = 'dialog.recording';
+ }
+
+ const { recordingLimit = {} } = getState()['features/base/config'];
+ const { limit, appName } = recordingLimit;
+
+ return dispatch(showNotification({
+ descriptionArguments: {
+ limit,
+ app: appName
+ },
+ descriptionKey,
+ titleKey,
+ maxLines: 2
+ }, 10000));
+ };
+}
diff --git a/react/features/recording/actions.web.js b/react/features/recording/actions.web.js
new file mode 100644
index 000000000..3baef9d12
--- /dev/null
+++ b/react/features/recording/actions.web.js
@@ -0,0 +1,27 @@
+// @flow
+
+import React from 'react';
+
+import JitsiMeetJS from '../base/lib-jitsi-meet';
+import { showNotification } from '../notifications';
+
+import { RecordingLimitNotificationDescription } from './components';
+
+export * from './actions.any';
+
+/**
+ * Signals that a started recording notification should be shown on the
+ * screen for a given period.
+ *
+ * @param {string} streamType - The type of the stream ({@code file} or
+ * {@code stream}).
+ * @returns {showNotification}
+ */
+export function showRecordingLimitNotification(streamType: string) {
+ const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
+
+ return showNotification({
+ description: ,
+ titleKey: isLiveStreaming ? 'dialog.liveStreaming' : 'dialog.recording'
+ }, 10000);
+}
diff --git a/react/features/recording/components/web/RecordingLimitNotificationDescription.js b/react/features/recording/components/web/RecordingLimitNotificationDescription.js
new file mode 100644
index 000000000..1c37da42e
--- /dev/null
+++ b/react/features/recording/components/web/RecordingLimitNotificationDescription.js
@@ -0,0 +1,81 @@
+// @flow
+
+import React from 'react';
+
+import { translate, translateToHTML } from '../../../base/i18n';
+import { connect } from '../../../base/redux';
+
+/**
+ * The type of the React {@code Component} props of {@link RecordingLimitNotificationDescription}.
+ */
+type Props = {
+
+ /**
+ * The limit of time in minutes for the recording.
+ */
+ _limit: number,
+
+ /**
+ * The name of the app with unlimited recordings.
+ */
+ _appName: string,
+
+ /**
+ * The URL to the app with unlimited recordings.
+ */
+ _appURL: string,
+
+ /**
+ * True if the notification is related to the livestreaming and false if not.
+ */
+ isLiveStreaming: Boolean,
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: Function
+};
+
+/**
+ * A component that renders the description of the notification for the recording initiator.
+ *
+ * @param {Props} props - The props of the component.
+ * @returns {Component}
+ */
+function RecordingLimitNotificationDescription(props: Props) {
+ const { _limit, _appName, _appURL, isLiveStreaming, t } = props;
+
+ return (
+
+ {
+ translateToHTML(
+ t,
+ `${isLiveStreaming ? 'liveStreaming' : 'recording'}.limitNotificationDescriptionWeb`, {
+ limit: _limit,
+ app: _appName,
+ url: _appURL
+ })
+ }
+
+ );
+}
+
+
+/**
+ * Maps part of the Redix state to the props of this component.
+ *
+ * @param {Object} state - The Redux state.
+ * @returns {Props}
+ */
+function _mapStateToProps(state): $Shape {
+ const { recordingLimit = {} } = state['features/base/config'];
+ const { limit: _limit, appName: _appName, appURL: _appURL } = recordingLimit;
+
+ return {
+ _limit,
+ _appName,
+ _appURL
+ };
+}
+
+export default translate(connect(_mapStateToProps)(RecordingLimitNotificationDescription));
diff --git a/react/features/recording/components/web/index.js b/react/features/recording/components/web/index.js
index 2004cf90b..df93a236a 100644
--- a/react/features/recording/components/web/index.js
+++ b/react/features/recording/components/web/index.js
@@ -1,3 +1,4 @@
// @flow
export { default as RecordingLabel } from './RecordingLabel';
+export { default as RecordingLimitNotificationDescription } from './RecordingLimitNotificationDescription';
diff --git a/react/features/recording/middleware.js b/react/features/recording/middleware.js
index c680c3364..9c6a41534 100644
--- a/react/features/recording/middleware.js
+++ b/react/features/recording/middleware.js
@@ -26,6 +26,7 @@ import {
hidePendingRecordingNotification,
showPendingRecordingNotification,
showRecordingError,
+ showRecordingLimitNotification,
showStartedRecordingNotification,
showStoppedRecordingNotification,
updateRecordingSessionData
@@ -44,6 +45,8 @@ import {
RECORDING_ON_SOUND_FILE
} from './sounds';
+declare var interfaceConfig: Object;
+
/**
* StateListenerRegistry provides a reliable way to detect the leaving of a
* conference, where we need to clean up the recording sessions.
@@ -131,7 +134,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const {
iAmRecorder,
iAmSipGateway,
- disableRecordAudioNotification
+ disableRecordAudioNotification,
+ recordingLimit
} = getState()['features/base/config'];
if (iAmRecorder && !iAmSipGateway) {
@@ -151,9 +155,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
if (updatedSessionData.status === ON
&& (!oldSessionData || oldSessionData.status !== ON)) {
- const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
+ if (initiator) {
+ const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
+
+ initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
+ } else if (typeof recordingLimit === 'object') {
+ // Show notification with additional information to the initiator.
+ dispatch(showRecordingLimitNotification(mode));
+ }
+
- initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {