Преглед на файлове

[RN] Implement Recording on mobile

master
Bettenbuk Zoltan преди 7 години
родител
ревизия
7164cd49e4
променени са 20 файла, в които са добавени 770 реда и са изтрити 540 реда
  1. 0
    77
      conference.js
  2. 26
    0
      react/features/recording/actionTypes.js
  3. 107
    2
      react/features/recording/actions.js
  4. 40
    3
      react/features/recording/components/AbstractRecordingLabel.js
  5. 103
    0
      react/features/recording/components/Recording/AbstractStartRecordingDialog.js
  6. 120
    0
      react/features/recording/components/Recording/AbstractStopRecordingDialog.js
  7. 108
    0
      react/features/recording/components/Recording/RecordButton.native.js
  8. 0
    0
      react/features/recording/components/Recording/RecordButton.web.js
  9. 40
    0
      react/features/recording/components/Recording/StartRecordingDialog.native.js
  10. 11
    81
      react/features/recording/components/Recording/StartRecordingDialog.web.js
  11. 42
    0
      react/features/recording/components/Recording/StopRecordingDialog.native.js
  12. 12
    86
      react/features/recording/components/Recording/StopRecordingDialog.web.js
  13. 1
    0
      react/features/recording/components/Recording/index.js
  14. 9
    46
      react/features/recording/components/RecordingLabel.native.js
  15. 10
    218
      react/features/recording/components/RecordingLabel.web.js
  16. 5
    1
      react/features/recording/components/index.js
  17. 6
    1
      react/features/recording/components/styles.js
  18. 110
    24
      react/features/recording/middleware.js
  19. 18
    1
      react/features/recording/reducer.js
  20. 2
    0
      react/features/toolbox/components/native/OverflowMenu.js

+ 0
- 77
conference.js Целия файл

@@ -1906,31 +1906,6 @@ export default {
1906 1906
                 }
1907 1907
             });
1908 1908
 
1909
-        /* eslint-enable max-params */
1910
-        room.on(
1911
-            JitsiConferenceEvents.RECORDER_STATE_CHANGED,
1912
-            recorderSession => {
1913
-                if (!recorderSession) {
1914
-                    logger.error(
1915
-                        'Received invalid recorder status update',
1916
-                        recorderSession);
1917
-
1918
-                    return;
1919
-                }
1920
-
1921
-                // These errors fire when the local participant has requested a
1922
-                // recording but the request itself failed, hence the missing
1923
-                // session ID because the recorder never started.
1924
-                if (recorderSession.getError()) {
1925
-                    this._showRecordingErrorNotification(recorderSession);
1926
-
1927
-                    return;
1928
-                }
1929
-
1930
-                logger.error(
1931
-                    'Received a recorder status update with no ID or error');
1932
-            });
1933
-
1934 1909
         room.on(JitsiConferenceEvents.KICKED, () => {
1935 1910
             APP.UI.hideStats();
1936 1911
             APP.UI.notifyKicked();
@@ -2728,57 +2703,5 @@ export default {
2728 2703
         if (score === -1 || (score >= 1 && score <= 5)) {
2729 2704
             APP.store.dispatch(submitFeedback(score, message, room));
2730 2705
         }
2731
-    },
2732
-
2733
-    /**
2734
-     * Shows a notification about an error in the recording session. A
2735
-     * default notification will display if no error is specified in the passed
2736
-     * in recording session.
2737
-     *
2738
-     * @param {Object} recorderSession - The recorder session model from the
2739
-     * lib.
2740
-     * @private
2741
-     * @returns {void}
2742
-     */
2743
-    _showRecordingErrorNotification(recorderSession) {
2744
-        const isStreamMode
2745
-            = recorderSession.getMode()
2746
-                === JitsiMeetJS.constants.recording.mode.STREAM;
2747
-
2748
-        switch (recorderSession.getError()) {
2749
-        case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
2750
-            APP.UI.messageHandler.showError({
2751
-                descriptionKey: 'recording.unavailable',
2752
-                descriptionArguments: {
2753
-                    serviceName: isStreamMode
2754
-                        ? 'Live Streaming service'
2755
-                        : 'Recording service'
2756
-                },
2757
-                titleKey: isStreamMode
2758
-                    ? 'liveStreaming.unavailableTitle'
2759
-                    : 'recording.unavailableTitle'
2760
-            });
2761
-            break;
2762
-        case JitsiMeetJS.constants.recording.error.RESOURCE_CONSTRAINT:
2763
-            APP.UI.messageHandler.showError({
2764
-                descriptionKey: isStreamMode
2765
-                    ? 'liveStreaming.busy'
2766
-                    : 'recording.busy',
2767
-                titleKey: isStreamMode
2768
-                    ? 'liveStreaming.busyTitle'
2769
-                    : 'recording.busyTitle'
2770
-            });
2771
-            break;
2772
-        default:
2773
-            APP.UI.messageHandler.showError({
2774
-                descriptionKey: isStreamMode
2775
-                    ? 'liveStreaming.error'
2776
-                    : 'recording.error',
2777
-                titleKey: isStreamMode
2778
-                    ? 'liveStreaming.failedToStart'
2779
-                    : 'recording.failedToStart'
2780
-            });
2781
-            break;
2782
-        }
2783 2706
     }
2784 2707
 };

+ 26
- 0
react/features/recording/actionTypes.js Целия файл

@@ -1,3 +1,15 @@
1
+// @flow
2
+
3
+/**
4
+ * The type of Redux action which clears all the data of every sessions.
5
+ *
6
+ * {
7
+ *     type: CLEAR_RECORDING_SESSIONS
8
+ * }
9
+ * @public
10
+ */
11
+export const CLEAR_RECORDING_SESSIONS = Symbol('CLEAR_RECORDING_SESSIONS');
12
+
1 13
 /**
2 14
  * The type of Redux action which updates the current known state of a recording
3 15
  * session.
@@ -9,3 +21,17 @@
9 21
  * @public
10 22
  */
11 23
 export const RECORDING_SESSION_UPDATED = Symbol('RECORDING_SESSION_UPDATED');
24
+
25
+/**
26
+ * The type of Redux action which sets the pending recording notification UID to
27
+ * use it for when hiding the notification is necessary, or unsets it when
28
+ * undefined (or no param) is passed.
29
+ *
30
+ * {
31
+ *     type: SET_PENDING_RECORDING_NOTIFICATION_UID,
32
+ *     uid: ?number
33
+ * }
34
+ * @public
35
+ */
36
+export const SET_PENDING_RECORDING_NOTIFICATION_UID
37
+    = Symbol('SET_PENDING_RECORDING_NOTIFICATION_UID');

+ 107
- 2
react/features/recording/actions.js Целия файл

@@ -1,4 +1,109 @@
1
-import { RECORDING_SESSION_UPDATED } from './actionTypes';
1
+// @flow
2
+
3
+import {
4
+    hideNotification,
5
+    showErrorNotification,
6
+    showNotification
7
+} from '../notifications';
8
+
9
+import {
10
+    CLEAR_RECORDING_SESSIONS,
11
+    RECORDING_SESSION_UPDATED,
12
+    SET_PENDING_RECORDING_NOTIFICATION_UID
13
+} from './actionTypes';
14
+
15
+/**
16
+ * Clears the data of every recording sessions.
17
+ *
18
+ * @returns {{
19
+ *     type: CLEAR_RECORDING_SESSIONS
20
+ * }}
21
+ */
22
+export function clearRecordingSessions() {
23
+    return {
24
+        type: CLEAR_RECORDING_SESSIONS
25
+    };
26
+}
27
+
28
+/**
29
+ * Signals that the pending recording notification should be removed from the
30
+ * screen.
31
+ *
32
+ * @returns {Function}
33
+ */
34
+export function hidePendingRecordingNotification() {
35
+    return (dispatch: Function, getState: Function) => {
36
+        const { pendingNotificationUid } = getState()['features/recording'];
37
+
38
+        if (pendingNotificationUid) {
39
+            dispatch(hideNotification(pendingNotificationUid));
40
+            dispatch(setPendingRecordingNotificationUid());
41
+        }
42
+    };
43
+}
44
+
45
+/**
46
+ * Sets UID of the the pending recording notification to use it when hinding
47
+ * the notification is necessary, or unsets it when
48
+ * undefined (or no param) is passed.
49
+ *
50
+ * @param {?number} uid - The UID of the notification.
51
+ * redux.
52
+ * @returns {{
53
+ *     type: SET_PENDING_RECORDING_NOTIFICATION_UID,
54
+ *     uid: number
55
+ * }}
56
+ */
57
+export function setPendingRecordingNotificationUid(uid: ?number) {
58
+    return {
59
+        type: SET_PENDING_RECORDING_NOTIFICATION_UID,
60
+        uid
61
+    };
62
+}
63
+
64
+/**
65
+ * Signals that the pending recording notification should be shown on the
66
+ * screen.
67
+ *
68
+ * @returns {Function}
69
+ */
70
+export function showPendingRecordingNotification() {
71
+    return (dispatch: Function) => {
72
+        const showNotificationAction = showNotification({
73
+            descriptionKey: 'recording.pending',
74
+            isDismissAllowed: false,
75
+            titleKey: 'dialog.recording'
76
+        });
77
+
78
+        dispatch(showNotificationAction);
79
+
80
+        dispatch(setPendingRecordingNotificationUid(
81
+            showNotificationAction.uid));
82
+    };
83
+}
84
+
85
+/**
86
+ * Signals that the recording error notification should be shown.
87
+ *
88
+ * @param {Object} props - The Props needed to render the notification.
89
+ * @returns {showErrorNotification}
90
+ */
91
+export function showRecordingError(props: Object) {
92
+    return showErrorNotification(props);
93
+}
94
+
95
+/**
96
+ * Signals that the stopped recording notification should be shown on the
97
+ * screen for a given period.
98
+ *
99
+ * @returns {showNotification}
100
+ */
101
+export function showStoppedRecordingNotification() {
102
+    return showNotification({
103
+        descriptionKey: 'recording.off',
104
+        titleKey: 'dialog.recording'
105
+    }, 2500);
106
+}
2 107
 
3 108
 /**
4 109
  * Updates the known state for a given recording session.
@@ -10,7 +115,7 @@ import { RECORDING_SESSION_UPDATED } from './actionTypes';
10 115
  *     sessionData: Object
11 116
  * }}
12 117
  */
13
-export function updateRecordingSessionData(session) {
118
+export function updateRecordingSessionData(session: Object) {
14 119
     return {
15 120
         type: RECORDING_SESSION_UPDATED,
16 121
         sessionData: {

+ 40
- 3
react/features/recording/components/AbstractRecordingLabel.js Целия файл

@@ -34,8 +34,45 @@ export type Props = {
34 34
 /**
35 35
  * Abstract class for the {@code RecordingLabel} component.
36 36
  */
37
-export default class AbstractRecordingLabel<P: Props, S: *>
38
-    extends Component<P, S> {
37
+export default class AbstractRecordingLabel<P: Props>
38
+    extends Component<P> {
39
+
40
+    /**
41
+     * Implements React {@code Component}'s render.
42
+     *
43
+     * @inheritdoc
44
+     */
45
+    render() {
46
+        return this.props._visible ? this._renderLabel() : null;
47
+    }
48
+
49
+    _getLabelKey: () => ?string
50
+
51
+    /**
52
+     * Returns the label key that this indicator should render.
53
+     *
54
+     * @protected
55
+     * @returns {?string}
56
+     */
57
+    _getLabelKey() {
58
+        switch (this.props.mode) {
59
+        case JitsiRecordingConstants.mode.STREAM:
60
+            return 'recording.live';
61
+        case JitsiRecordingConstants.mode.FILE:
62
+            return 'recording.rec';
63
+        default:
64
+            // Invalid mode is passed to the component.
65
+            return undefined;
66
+        }
67
+    }
68
+
69
+    /**
70
+     * Renders the platform specific label component.
71
+     *
72
+     * @protected
73
+     * @returns {React$Element}
74
+     */
75
+    _renderLabel: () => React$Element<*>
39 76
 
40 77
 }
41 78
 
@@ -50,7 +87,7 @@ export default class AbstractRecordingLabel<P: Props, S: *>
50 87
  *     _visible: boolean
51 88
  * }}
52 89
  */
53
-export function _abstractMapStateToProps(state: Object, ownProps: Props) {
90
+export function _mapStateToProps(state: Object, ownProps: Props) {
54 91
     const { mode } = ownProps;
55 92
     const _recordingSessions = state['features/recording'].sessionDatas;
56 93
     const _visible

+ 103
- 0
react/features/recording/components/Recording/AbstractStartRecordingDialog.js Целия файл

@@ -0,0 +1,103 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+import {
6
+    createRecordingDialogEvent,
7
+    sendAnalytics
8
+} from '../../../analytics';
9
+import { Dialog } from '../../../base/dialog';
10
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
11
+
12
+export type Props = {
13
+
14
+    /**
15
+     * The {@code JitsiConference} for the current conference.
16
+     */
17
+    _conference: Object,
18
+
19
+    /**
20
+     * Invoked to obtain translated strings.
21
+     */
22
+    t: Function
23
+}
24
+
25
+/**
26
+ * Abstract class for {@code StartRecordingDialog} components.
27
+ */
28
+export default class AbstractStartRecordingDialog<P: Props>
29
+    extends Component<P> {
30
+    /**
31
+     * Initializes a new {@code StartRecordingDialog} instance.
32
+     *
33
+     * @inheritdoc
34
+     */
35
+    constructor(props: P) {
36
+        super(props);
37
+
38
+        // Bind event handler so it is only bound once for every instance.
39
+        this._onSubmit = this._onSubmit.bind(this);
40
+    }
41
+
42
+    /**
43
+     * Implements React's {@link Component#render()}.
44
+     *
45
+     * @inheritdoc
46
+     * @returns {ReactElement}
47
+     */
48
+    render() {
49
+        return (
50
+            <Dialog
51
+                okTitleKey = 'dialog.confirm'
52
+                onSubmit = { this._onSubmit }
53
+                titleKey = 'dialog.recording'
54
+                width = 'small'>
55
+                { this._renderDialogContent() }
56
+            </Dialog>
57
+        );
58
+    }
59
+
60
+    _onSubmit: () => boolean;
61
+
62
+    /**
63
+     * Starts a file recording session.
64
+     *
65
+     * @private
66
+     * @returns {boolean} - True (to note that the modal should be closed).
67
+     */
68
+    _onSubmit() {
69
+        sendAnalytics(
70
+            createRecordingDialogEvent('start', 'confirm.button')
71
+        );
72
+
73
+        this.props._conference.startRecording({
74
+            mode: JitsiRecordingConstants.mode.FILE
75
+        });
76
+
77
+        return true;
78
+    }
79
+
80
+    /**
81
+     * Renders the platform specific dialog content.
82
+     *
83
+     * @protected
84
+     * @returns {React$Component}
85
+     */
86
+    _renderDialogContent: () => React$Component<*>
87
+}
88
+
89
+/**
90
+ * Maps (parts of) the Redux state to the associated props for the
91
+ * {@code StartRecordingDialog} component.
92
+ *
93
+ * @param {Object} state - The Redux state.
94
+ * @private
95
+ * @returns {{
96
+ *     _conference: JitsiConference
97
+ * }}
98
+ */
99
+export function _mapStateToProps(state: Object) {
100
+    return {
101
+        _conference: state['features/base/conference'].conference
102
+    };
103
+}

+ 120
- 0
react/features/recording/components/Recording/AbstractStopRecordingDialog.js Целия файл

@@ -0,0 +1,120 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+import {
6
+    createRecordingDialogEvent,
7
+    sendAnalytics
8
+} from '../../../analytics';
9
+import { Dialog } from '../../../base/dialog';
10
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
11
+
12
+import { getActiveSession } from '../../functions';
13
+
14
+/**
15
+ * The type of the React {@code Component} props of
16
+ * {@link AbstractStopRecordingDialog}.
17
+ */
18
+export type Props = {
19
+
20
+    /**
21
+     * The {@code JitsiConference} for the current conference.
22
+     */
23
+    _conference: Object,
24
+
25
+    /**
26
+     * The redux representation of the recording session to be stopped.
27
+     */
28
+    _fileRecordingSession: Object,
29
+
30
+    /**
31
+     * Invoked to obtain translated strings.
32
+     */
33
+    t: Function
34
+};
35
+
36
+/**
37
+ * Abstract React Component for getting confirmation to stop a file recording
38
+ * session in progress.
39
+ *
40
+ * @extends Component
41
+ */
42
+export default class AbstractStopRecordingDialog<P: Props>
43
+    extends Component<P> {
44
+    /**
45
+     * Initializes a new {@code AbstrStopRecordingDialog} instance.
46
+     *
47
+     * @inheritdoc
48
+     */
49
+    constructor(props: P) {
50
+        super(props);
51
+
52
+        // Bind event handler so it is only bound once for every instance.
53
+        this._onSubmit = this._onSubmit.bind(this);
54
+    }
55
+
56
+    /**
57
+     * Implements React's {@link Component#render()}.
58
+     *
59
+     * @inheritdoc
60
+     * @returns {ReactElement}
61
+     */
62
+    render() {
63
+        return (
64
+            <Dialog
65
+                okTitleKey = 'dialog.confirm'
66
+                onSubmit = { this._onSubmit }
67
+                titleKey = 'dialog.recording'
68
+                width = 'small'>
69
+                { this._renderDialogContent() }
70
+            </Dialog>
71
+        );
72
+    }
73
+
74
+    _onSubmit: () => boolean;
75
+
76
+    /**
77
+     * Stops the recording session.
78
+     *
79
+     * @private
80
+     * @returns {boolean} - True (to note that the modal should be closed).
81
+     */
82
+    _onSubmit() {
83
+        sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
84
+
85
+        const { _fileRecordingSession } = this.props;
86
+
87
+        if (_fileRecordingSession) {
88
+            this.props._conference.stopRecording(_fileRecordingSession.id);
89
+        }
90
+
91
+        return true;
92
+    }
93
+
94
+    /**
95
+     * Renders the platform specific dialog content.
96
+     *
97
+     * @protected
98
+     * @returns {React$Component}
99
+     */
100
+    _renderDialogContent: () => React$Component<*>
101
+}
102
+
103
+/**
104
+ * Maps (parts of) the Redux state to the associated props for the
105
+ * {@code StopRecordingDialog} component.
106
+ *
107
+ * @param {Object} state - The Redux state.
108
+ * @private
109
+ * @returns {{
110
+ *     _conference: JitsiConference,
111
+ *     _fileRecordingSession: Object
112
+ * }}
113
+ */
114
+export function _mapStateToProps(state: Object) {
115
+    return {
116
+        _conference: state['features/base/conference'].conference,
117
+        _fileRecordingSession:
118
+            getActiveSession(state, JitsiRecordingConstants.mode.FILE)
119
+    };
120
+}

+ 108
- 0
react/features/recording/components/Recording/RecordButton.native.js Целия файл

@@ -0,0 +1,108 @@
1
+// @flow
2
+
3
+import { connect } from 'react-redux';
4
+
5
+import { openDialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
8
+import {
9
+    isLocalParticipantModerator
10
+} from '../../../base/participants';
11
+import {
12
+    AbstractButton,
13
+    type AbstractButtonProps
14
+} from '../../../base/toolbox';
15
+
16
+import { getActiveSession } from '../../functions';
17
+
18
+import StartRecordingDialog from './StartRecordingDialog';
19
+import StopRecordingDialog from './StopRecordingDialog';
20
+
21
+/**
22
+ * The type of the React {@code Component} props of {@link RecordButton}.
23
+ */
24
+type Props = AbstractButtonProps & {
25
+
26
+    /**
27
+     * The current conference object.
28
+     */
29
+    _conference: Object,
30
+
31
+    /**
32
+     * True if there is a running active recording, false otherwise.
33
+     */
34
+    _isRecordingRunning: boolean,
35
+
36
+    /**
37
+     * The redux {@code dispatch} function.
38
+     */
39
+    dispatch: Function,
40
+
41
+    /**
42
+     * The i18n translate function.
43
+     */
44
+    t: Function
45
+};
46
+
47
+/**
48
+ * An implementation of a button for starting and stopping recording.
49
+ */
50
+class RecordButton extends AbstractButton<Props, *> {
51
+    accessibilityLabel = 'Recording';
52
+    iconName = 'recEnable';
53
+    label = 'dialog.startRecording';
54
+    toggledIconName = 'recDisable';
55
+    toggledLabel = 'dialog.stopRecording';
56
+
57
+    /**
58
+     * Handles clicking / pressing the button.
59
+     *
60
+     * @override
61
+     * @protected
62
+     * @returns {void}
63
+     */
64
+    _handleClick() {
65
+        const { _isRecordingRunning, dispatch } = this.props;
66
+
67
+        dispatch(openDialog(
68
+            _isRecordingRunning ? StopRecordingDialog : StartRecordingDialog
69
+        ));
70
+    }
71
+
72
+    /**
73
+     * Indicates whether this button is in toggled state or not.
74
+     *
75
+     * @override
76
+     * @protected
77
+     * @returns {boolean}
78
+     */
79
+    _isToggled() {
80
+        return this.props._isRecordingRunning;
81
+    }
82
+}
83
+
84
+/**
85
+ * Maps (parts of) the redux state to the associated props for the
86
+ * {@code RecordButton} component.
87
+ *
88
+ * @param {Object} state - The Redux state.
89
+ * @private
90
+ * @returns {{
91
+ *     _conference: Object,
92
+ *     _isRecordingRunning: boolean,
93
+ *     visible: boolean
94
+ * }}
95
+ */
96
+function _mapStateToProps(state): Object {
97
+    const isModerator = isLocalParticipantModerator(state);
98
+    const { fileRecordingsEnabled } = state['features/base/config'];
99
+
100
+    return {
101
+        _conference: state['features/base/conference'].conference,
102
+        _isRecordingRunning:
103
+            Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE)),
104
+        visible: isModerator && fileRecordingsEnabled
105
+    };
106
+}
107
+
108
+export default translate(connect(_mapStateToProps)(RecordButton));

+ 0
- 0
react/features/recording/components/Recording/RecordButton.web.js Целия файл


+ 40
- 0
react/features/recording/components/Recording/StartRecordingDialog.native.js Целия файл

@@ -0,0 +1,40 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text, View } from 'react-native';
5
+import { connect } from 'react-redux';
6
+
7
+import { translate } from '../../../base/i18n';
8
+
9
+import styles from '../styles';
10
+
11
+import AbstractStartRecordingDialog, {
12
+    type Props,
13
+    _mapStateToProps
14
+} from './AbstractStartRecordingDialog';
15
+
16
+/**
17
+ * React Component for getting confirmation to start a file recording session.
18
+ *
19
+ * @extends Component
20
+ */
21
+class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
22
+    /**
23
+     * Renders the platform specific dialog content.
24
+     *
25
+     * @inheritdoc
26
+     */
27
+    _renderDialogContent() {
28
+        const { t } = this.props;
29
+
30
+        return (
31
+            <View style = { styles.messageContainer }>
32
+                <Text>
33
+                    { t('recording.startRecordingBody') }
34
+                </Text>
35
+            </View>
36
+        );
37
+    }
38
+}
39
+
40
+export default translate(connect(_mapStateToProps)(StartRecordingDialog));

+ 11
- 81
react/features/recording/components/Recording/StartRecordingDialog.web.js Целия файл

@@ -1,103 +1,33 @@
1 1
 // @flow
2 2
 
3
-import React, { Component } from 'react';
4 3
 import { connect } from 'react-redux';
5 4
 
6
-import {
7
-    createRecordingDialogEvent,
8
-    sendAnalytics
9
-} from '../../../analytics';
10
-import { Dialog } from '../../../base/dialog';
11 5
 import { translate } from '../../../base/i18n';
12
-import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
13 6
 
14
-/**
15
- * The type of the React {@code Component} props of
16
- * {@link StartRecordingDialog}.
17
- */
18
-type Props = {
19
-
20
-    /**
21
-     * The {@code JitsiConference} for the current conference.
22
-     */
23
-    _conference: Object,
24
-
25
-    /**
26
-     * Invoked to obtain translated strings.
27
-     */
28
-    t: Function
29
-};
7
+import AbstractStartRecordingDialog, {
8
+    type Props,
9
+    _mapStateToProps
10
+} from './AbstractStartRecordingDialog';
30 11
 
31 12
 /**
32 13
  * React Component for getting confirmation to start a file recording session.
33 14
  *
34 15
  * @extends Component
35 16
  */
36
-class StartRecordingDialog extends Component<Props> {
17
+class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
37 18
     /**
38
-     * Initializes a new {@code StartRecordingDialog} instance.
19
+     * Renders the platform specific dialog content.
39 20
      *
40
-     * @param {Props} props - The read-only properties with which the new
41
-     * instance is to be initialized.
21
+     * @protected
22
+     * @returns {React$Component}
42 23
      */
43
-    constructor(props: Props) {
44
-        super(props);
45
-
46
-        // Bind event handler so it is only bound once for every instance.
47
-        this._onSubmit = this._onSubmit.bind(this);
48
-    }
24
+    _renderDialogContent() {
25
+        const { t } = this.props;
49 26
 
50
-    /**
51
-     * Implements React's {@link Component#render()}.
52
-     *
53
-     * @inheritdoc
54
-     * @returns {ReactElement}
55
-     */
56
-    render() {
57 27
         return (
58
-            <Dialog
59
-                okTitleKey = 'dialog.confirm'
60
-                onSubmit = { this._onSubmit }
61
-                titleKey = 'dialog.recording'
62
-                width = 'small'>
63
-                { this.props.t('recording.startRecordingBody') }
64
-            </Dialog>
28
+            t('recording.startRecordingBody')
65 29
         );
66 30
     }
67
-
68
-    _onSubmit: () => boolean;
69
-
70
-    /**
71
-     * Starts a file recording session.
72
-     *
73
-     * @private
74
-     * @returns {boolean} - True (to note that the modal should be closed).
75
-     */
76
-    _onSubmit() {
77
-        sendAnalytics(createRecordingDialogEvent('start', 'confirm.button'));
78
-
79
-        this.props._conference.startRecording({
80
-            mode: JitsiRecordingConstants.mode.FILE
81
-        });
82
-
83
-        return true;
84
-    }
85
-}
86
-
87
-/**
88
- * Maps (parts of) the Redux state to the associated props for the
89
- * {@code StartRecordingDialog} component.
90
- *
91
- * @param {Object} state - The Redux state.
92
- * @private
93
- * @returns {{
94
- *     _conference: JitsiConference
95
- * }}
96
- */
97
-function _mapStateToProps(state) {
98
-    return {
99
-        _conference: state['features/base/conference'].conference
100
-    };
101 31
 }
102 32
 
103 33
 export default translate(connect(_mapStateToProps)(StartRecordingDialog));

+ 42
- 0
react/features/recording/components/Recording/StopRecordingDialog.native.js Целия файл

@@ -0,0 +1,42 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text, View } from 'react-native';
5
+import { connect } from 'react-redux';
6
+
7
+import { translate } from '../../../base/i18n';
8
+
9
+import styles from '../styles';
10
+
11
+import AbstractStopRecordingDialog, {
12
+    type Props,
13
+    _mapStateToProps
14
+} from './AbstractStopRecordingDialog';
15
+
16
+/**
17
+ * React Component for getting confirmation to stop a file recording session in
18
+ * progress.
19
+ *
20
+ * @extends Component
21
+ */
22
+class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
23
+
24
+    /**
25
+     * Renders the platform specific dialog content.
26
+     *
27
+     * @inheritdoc
28
+     */
29
+    _renderDialogContent() {
30
+        const { t } = this.props;
31
+
32
+        return (
33
+            <View style = { styles.messageContainer }>
34
+                <Text>
35
+                    { t('dialog.stopRecordingWarning') }
36
+                </Text>
37
+            </View>
38
+        );
39
+    }
40
+}
41
+
42
+export default translate(connect(_mapStateToProps)(StopRecordingDialog));

+ 12
- 86
react/features/recording/components/Recording/StopRecordingDialog.web.js Целия файл

@@ -1,35 +1,13 @@
1 1
 // @flow
2 2
 
3
-import React, { Component } from 'react';
4 3
 import { connect } from 'react-redux';
5 4
 
6
-import {
7
-    createRecordingDialogEvent,
8
-    sendAnalytics
9
-} from '../../../analytics';
10
-import { Dialog } from '../../../base/dialog';
11 5
 import { translate } from '../../../base/i18n';
12 6
 
13
-/**
14
- * The type of the React {@code Component} props of {@link StopRecordingDialog}.
15
- */
16
-type Props = {
17
-
18
-    /**
19
-     * The {@code JitsiConference} for the current conference.
20
-     */
21
-    _conference: Object,
22
-
23
-    /**
24
-     * The redux representation of the recording session to be stopped.
25
-     */
26
-    session: Object,
27
-
28
-    /**
29
-     * Invoked to obtain translated strings.
30
-     */
31
-    t: Function
32
-};
7
+import AbstractStopRecordingDialog, {
8
+    type Props,
9
+    _mapStateToProps
10
+} from './AbstractStopRecordingDialog';
33 11
 
34 12
 /**
35 13
  * React Component for getting confirmation to stop a file recording session in
@@ -37,73 +15,21 @@ type Props = {
37 15
  *
38 16
  * @extends Component
39 17
  */
40
-class StopRecordingDialog extends Component<Props> {
41
-    /**
42
-     * Initializes a new {@code StopRecordingDialog} instance.
43
-     *
44
-     * @param {Props} props - The read-only properties with which the new
45
-     * instance is to be initialized.
46
-     */
47
-    constructor(props) {
48
-        super(props);
49
-
50
-        // Bind event handler so it is only bound once for every instance.
51
-        this._onSubmit = this._onSubmit.bind(this);
52
-    }
18
+class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
53 19
 
54 20
     /**
55
-     * Implements React's {@link Component#render()}.
21
+     * Renders the platform specific dialog content.
56 22
      *
57
-     * @inheritdoc
58
-     * @returns {ReactElement}
23
+     * @protected
24
+     * @returns {React$Component}
59 25
      */
60
-    render() {
26
+    _renderDialogContent() {
27
+        const { t } = this.props;
28
+
61 29
         return (
62
-            <Dialog
63
-                okTitleKey = 'dialog.stopRecording'
64
-                onSubmit = { this._onSubmit }
65
-                titleKey = 'dialog.recording'
66
-                width = 'small'>
67
-                { this.props.t('dialog.stopRecordingWarning') }
68
-            </Dialog>
30
+            t('dialog.stopRecordingWarning')
69 31
         );
70 32
     }
71
-
72
-    _onSubmit: () => boolean;
73
-
74
-    /**
75
-     * Stops the recording session.
76
-     *
77
-     * @private
78
-     * @returns {boolean} - True (to note that the modal should be closed).
79
-     */
80
-    _onSubmit() {
81
-        sendAnalytics(createRecordingDialogEvent('stop', 'confirm.button'));
82
-
83
-        const { session } = this.props;
84
-
85
-        if (session) {
86
-            this.props._conference.stopRecording(session.id);
87
-        }
88
-
89
-        return true;
90
-    }
91
-}
92
-
93
-/**
94
- * Maps (parts of) the Redux state to the associated props for the
95
- * {@code StopRecordingDialog} component.
96
- *
97
- * @param {Object} state - The Redux state.
98
- * @private
99
- * @returns {{
100
- *     _conference: JitsiConference
101
- * }}
102
- */
103
-function _mapStateToProps(state) {
104
-    return {
105
-        _conference: state['features/base/conference'].conference
106
-    };
107 33
 }
108 34
 
109 35
 export default translate(connect(_mapStateToProps)(StopRecordingDialog));

+ 1
- 0
react/features/recording/components/Recording/index.js Целия файл

@@ -1,2 +1,3 @@
1
+export { default as RecordButton } from './RecordButton';
1 2
 export { default as StartRecordingDialog } from './StartRecordingDialog';
2 3
 export { default as StopRecordingDialog } from './StopRecordingDialog';

+ 9
- 46
react/features/recording/components/RecordingLabel.native.js Целия файл

@@ -6,52 +6,34 @@ import { connect } from 'react-redux';
6 6
 import { translate } from '../../base/i18n';
7 7
 import { CircularLabel } from '../../base/label';
8 8
 import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
9
-import { combineStyles } from '../../base/styles';
10 9
 
11 10
 import AbstractRecordingLabel, {
12
-    type Props as AbstractProps,
13
-    _abstractMapStateToProps
11
+    type Props,
12
+    _mapStateToProps
14 13
 } from './AbstractRecordingLabel';
15 14
 import styles from './styles';
16 15
 
17
-type Props = AbstractProps & {
18
-
19
-    /**
20
-     * Style of the component passed as props.
21
-     */
22
-    style: ?Object
23
-};
24
-
25 16
 /**
26 17
  * Implements a React {@link Component} which displays the current state of
27 18
  * conference recording.
28 19
  *
29 20
  * @extends {Component}
30 21
  */
31
-class RecordingLabel extends AbstractRecordingLabel<Props, *> {
22
+class RecordingLabel extends AbstractRecordingLabel<Props> {
32 23
 
33 24
     /**
34
-     * Implements React {@code Component}'s render.
25
+     * Renders the platform specific label component.
35 26
      *
36 27
      * @inheritdoc
37 28
      */
38
-    render() {
39
-        const { _visible, mode, style, t } = this.props;
40
-
41
-        if (!_visible) {
42
-            return null;
43
-        }
44
-
45
-        let labelKey;
29
+    _renderLabel() {
46 30
         let indicatorStyle;
47 31
 
48
-        switch (mode) {
32
+        switch (this.props.mode) {
49 33
         case JitsiRecordingConstants.mode.STREAM:
50
-            labelKey = 'recording.live';
51 34
             indicatorStyle = styles.indicatorLive;
52 35
             break;
53 36
         case JitsiRecordingConstants.mode.FILE:
54
-            labelKey = 'recording.rec';
55 37
             indicatorStyle = styles.indicatorRecording;
56 38
             break;
57 39
         default:
@@ -61,31 +43,12 @@ class RecordingLabel extends AbstractRecordingLabel<Props, *> {
61 43
 
62 44
         return (
63 45
             <CircularLabel
64
-                label = { t(labelKey) }
65
-                style = {
66
-                    combineStyles(indicatorStyle, style)
67
-                } />
46
+                label = { this.props.t(this._getLabelKey()) }
47
+                style = { indicatorStyle } />
68 48
         );
69 49
     }
70
-}
71 50
 
72
-/**
73
- * Maps (parts of) the Redux state to the associated
74
- * {@code RecordingLabel}'s props.
75
- *
76
- * NOTE: This component has no props other than the abstract ones but keeping
77
- * the coding style the same for consistency reasons.
78
- *
79
- * @param {Object} state - The Redux state.
80
- * @param {Object} ownProps - The component's own props.
81
- * @private
82
- * @returns {{
83
- * }}
84
- */
85
-function _mapStateToProps(state: Object, ownProps: Object) {
86
-    return {
87
-        ..._abstractMapStateToProps(state, ownProps)
88
-    };
51
+    _getLabelKey: () => ?string
89 52
 }
90 53
 
91 54
 export default translate(connect(_mapStateToProps)(RecordingLabel));

+ 10
- 218
react/features/recording/components/RecordingLabel.web.js Целия файл

@@ -5,243 +5,35 @@ import { connect } from 'react-redux';
5 5
 
6 6
 import { CircularLabel } from '../../base/label';
7 7
 import { translate } from '../../base/i18n';
8
-import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
9 8
 
10 9
 import AbstractRecordingLabel, {
11
-    type Props as AbstractProps,
12
-    _abstractMapStateToProps
10
+    type Props,
11
+    _mapStateToProps
13 12
 } from './AbstractRecordingLabel';
14 13
 
15
-/**
16
- * The translation keys to use when displaying messages. The values are set
17
- * lazily to work around circular dependency issues with lib-jitsi-meet causing
18
- * undefined imports.
19
- *
20
- * @private
21
- * @type {Object}
22
- */
23
-let TRANSLATION_KEYS_BY_MODE = null;
24
-
25
-/**
26
- * Lazily initializes TRANSLATION_KEYS_BY_MODE with translation keys to be used
27
- * by the {@code RecordingLabel} for messaging recording session state.
28
- *
29
- * @private
30
- * @returns {Object}
31
- */
32
-function _getTranslationKeysByMode() {
33
-    if (!TRANSLATION_KEYS_BY_MODE) {
34
-        const {
35
-            error: errorConstants,
36
-            mode: modeConstants,
37
-            status: statusConstants
38
-        } = JitsiRecordingConstants;
39
-
40
-        TRANSLATION_KEYS_BY_MODE = {
41
-            [modeConstants.FILE]: {
42
-                status: {
43
-                    [statusConstants.PENDING]: 'recording.pending',
44
-                    [statusConstants.OFF]: 'recording.off'
45
-                },
46
-                errors: {
47
-                    [errorConstants.BUSY]: 'recording.failedToStart',
48
-                    [errorConstants.ERROR]: 'recording.error'
49
-                }
50
-            },
51
-            [modeConstants.STREAM]: {
52
-                status: {
53
-                    [statusConstants.PENDING]: 'liveStreaming.pending',
54
-                    [statusConstants.OFF]: 'liveStreaming.off'
55
-                },
56
-                errors: {
57
-                    [errorConstants.BUSY]: 'liveStreaming.busy',
58
-                    [errorConstants.ERROR]: 'liveStreaming.error'
59
-                }
60
-            }
61
-        };
62
-    }
63
-
64
-    return TRANSLATION_KEYS_BY_MODE;
65
-}
66
-
67
-/**
68
- * The type of the React {@code Component} props of {@link RecordingLabel}.
69
- */
70
-type Props = AbstractProps & {
71
-
72
-    /**
73
-     * The redux representation of a recording session.
74
-     */
75
-    session: Object,
76
-
77
-    /**
78
-     * Invoked to obtain translated strings.
79
-     */
80
-    t: Function
81
-};
82
-
83
-/**
84
- * The type of the React {@code Component} state of {@link RecordingLabel}.
85
- */
86
-type State = {
87
-
88
-    /**
89
-     * Whether or not the {@link RecordingLabel} should be invisible.
90
-     */
91
-    hidden: boolean
92
-};
93
-
94 14
 /**
95 15
  * Implements a React {@link Component} which displays the current state of
96 16
  * conference recording.
97 17
  *
98 18
  * @extends {Component}
99 19
  */
100
-class RecordingLabel extends AbstractRecordingLabel<Props, State> {
101
-    _autohideTimeout: TimeoutID;
102
-
103
-    state = {
104
-        hidden: false
105
-    };
106
-
107
-    static defaultProps = {
108
-        session: {}
109
-    };
110
-
111
-    /**
112
-     * Sets a timeout to automatically hide the {@link RecordingLabel} if the
113
-     * recording session started as failed.
114
-     *
115
-     * @inheritdoc
116
-     */
117
-    componentDidMount() {
118
-        if (this.props.session.status === JitsiRecordingConstants.status.OFF) {
119
-            this._setHideTimeout();
120
-        }
121
-    }
122
-
123
-    /**
124
-     * Sets a timeout to automatically hide {the @link RecordingLabel} if it has
125
-     * transitioned to off.
126
-     *
127
-     * @inheritdoc
128
-     */
129
-    componentWillReceiveProps(nextProps) {
130
-        const { status } = this.props.session;
131
-        const nextStatus = nextProps.session.status;
132
-
133
-        if (status !== JitsiRecordingConstants.status.OFF
134
-            && nextStatus === JitsiRecordingConstants.status.OFF) {
135
-            this._setHideTimeout();
136
-        }
137
-    }
138
-
20
+class RecordingLabel extends AbstractRecordingLabel<Props> {
139 21
     /**
140
-     * Clears the timeout for automatically hiding the {@link RecordingLabel}.
22
+     * Renders the platform specific label component.
141 23
      *
142 24
      * @inheritdoc
143 25
      */
144
-    componentWillUnmount() {
145
-        this._clearAutoHideTimeout();
146
-    }
147
-
148
-    /**
149
-     * Implements React's {@link Component#render()}.
150
-     *
151
-     * @inheritdoc
152
-     * @returns {ReactElement}
153
-     */
154
-    render() {
155
-        if (this.state.hidden) {
156
-            return null;
157
-        }
158
-
159
-        const {
160
-            error: errorConstants,
161
-            mode: modeConstants,
162
-            status: statusConstants
163
-        } = JitsiRecordingConstants;
164
-        const { session } = this.props;
165
-        const allTranslationKeys = _getTranslationKeysByMode();
166
-        const translationKeys = allTranslationKeys[session.mode];
167
-        let circularLabelClass, circularLabelKey, messageKey;
168
-
169
-        switch (session.status) {
170
-        case statusConstants.OFF: {
171
-            if (session.error) {
172
-                messageKey = translationKeys.errors[session.error]
173
-                    || translationKeys.errors[errorConstants.ERROR];
174
-            } else {
175
-                messageKey = translationKeys.status[statusConstants.OFF];
176
-            }
177
-            break;
178
-        }
179
-        case statusConstants.ON:
180
-            circularLabelClass = session.mode;
181
-            circularLabelKey = session.mode === modeConstants.STREAM
182
-                ? 'recording.live' : 'recording.rec';
183
-            break;
184
-        case statusConstants.PENDING:
185
-            messageKey = translationKeys.status[statusConstants.PENDING];
186
-            break;
187
-        }
188
-
189
-        const className = `recording-label ${
190
-            messageKey ? 'center-message' : ''}`;
191
-
26
+    _renderLabel() {
192 27
         return (
193
-            <div className = { className }>
194
-                { messageKey
195
-                    ? <div>
196
-                        { this.props.t(messageKey) }
197
-                    </div>
198
-                    : <CircularLabel
199
-                        className = { circularLabelClass }
200
-                        label = { this.props.t(circularLabelKey) } /> }
28
+            <div>
29
+                <CircularLabel
30
+                    className = { this.props.mode }
31
+                    label = { this.props.t(this._getLabelKey()) } />
201 32
             </div>
202 33
         );
203 34
     }
204 35
 
205
-    /**
206
-     * Clears the timeout for automatically hiding {@link RecordingLabel}.
207
-     *
208
-     * @private
209
-     * @returns {void}
210
-     */
211
-    _clearAutoHideTimeout() {
212
-        clearTimeout(this._autohideTimeout);
213
-    }
214
-
215
-    /**
216
-     * Sets a timeout to automatically hide {@link RecordingLabel}.
217
-     *
218
-     * @private
219
-     * @returns {void}
220
-     */
221
-    _setHideTimeout() {
222
-        this._autohideTimeout = setTimeout(() => {
223
-            this.setState({ hidden: true });
224
-        }, 5000);
225
-    }
226
-}
227
-
228
-/**
229
- * Maps (parts of) the Redux state to the associated
230
- * {@code RecordingLabel}'s props.
231
- *
232
- * NOTE: This component has no props other than the abstract ones but keeping
233
- * the coding style the same for consistency reasons.
234
- *
235
- * @param {Object} state - The Redux state.
236
- * @param {Object} ownProps - The component's own props.
237
- * @private
238
- * @returns {{
239
- * }}
240
- */
241
-function _mapStateToProps(state: Object, ownProps: Object) {
242
-    return {
243
-        ..._abstractMapStateToProps(state, ownProps)
244
-    };
36
+    _getLabelKey: () => ?string
245 37
 }
246 38
 
247 39
 export default translate(connect(_mapStateToProps)(RecordingLabel));

+ 5
- 1
react/features/recording/components/index.js Целия файл

@@ -1,3 +1,7 @@
1 1
 export { StartLiveStreamDialog, StopLiveStreamDialog } from './LiveStream';
2
-export { StartRecordingDialog, StopRecordingDialog } from './Recording';
2
+export {
3
+    RecordButton,
4
+    StartRecordingDialog,
5
+    StopRecordingDialog
6
+} from './Recording';
3 7
 export { default as RecordingLabel } from './RecordingLabel';

+ 6
- 1
react/features/recording/components/styles.js Целия файл

@@ -1,6 +1,6 @@
1 1
 // @flow
2 2
 
3
-import { ColorPalette, createStyleSheet } from '../../base/styles';
3
+import { BoxModel, ColorPalette, createStyleSheet } from '../../base/styles';
4 4
 
5 5
 /**
6 6
  * The styles of the React {@code Components} of the feature recording.
@@ -19,5 +19,10 @@ export default createStyleSheet({
19 19
      */
20 20
     indicatorRecording: {
21 21
         backgroundColor: ColorPalette.red
22
+    },
23
+
24
+    messageContainer: {
25
+        paddingHorizontal: BoxModel.padding,
26
+        paddingVertical: 1.5 * BoxModel.padding
22 27
     }
23 28
 });

+ 110
- 24
react/features/recording/middleware.js Целия файл

@@ -1,11 +1,11 @@
1 1
 /* @flow */
2 2
 
3
-import { CONFERENCE_WILL_JOIN } from '../base/conference';
4
-import {
3
+import { CONFERENCE_WILL_JOIN, getCurrentConference } from '../base/conference';
4
+import JitsiMeetJS, {
5 5
     JitsiConferenceEvents,
6 6
     JitsiRecordingConstants
7 7
 } from '../base/lib-jitsi-meet';
8
-import { MiddlewareRegistry } from '../base/redux';
8
+import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
9 9
 import {
10 10
     playSound,
11 11
     registerSound,
@@ -15,7 +15,14 @@ import {
15 15
 
16 16
 import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
17 17
 
18
-import { updateRecordingSessionData } from './actions';
18
+import {
19
+    clearRecordingSessions,
20
+    hidePendingRecordingNotification,
21
+    showPendingRecordingNotification,
22
+    showRecordingError,
23
+    showStoppedRecordingNotification,
24
+    updateRecordingSessionData
25
+} from './actions';
19 26
 import { RECORDING_SESSION_UPDATED } from './actionTypes';
20 27
 import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from './constants';
21 28
 import { getSessionById } from './functions';
@@ -24,37 +31,50 @@ import {
24 31
     RECORDING_ON_SOUND_FILE
25 32
 } from './sounds';
26 33
 
34
+/**
35
+ * StateListenerRegistry provides a reliable way to detect the leaving of a
36
+ * conference, where we need to clean up the recording sessions.
37
+ */
38
+StateListenerRegistry.register(
39
+    /* selector */ state => getCurrentConference(state),
40
+    /* listener */ (conference, { dispatch }) => {
41
+        if (!conference) {
42
+            dispatch(clearRecordingSessions());
43
+        }
44
+    }
45
+);
46
+
27 47
 /**
28 48
  * The redux middleware to handle the recorder updates in a React way.
29 49
  *
30 50
  * @param {Store} store - The redux store.
31 51
  * @returns {Function}
32 52
  */
33
-MiddlewareRegistry.register(store => next => action => {
53
+MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
34 54
     let oldSessionData;
35 55
 
36 56
     if (action.type === RECORDING_SESSION_UPDATED) {
37 57
         oldSessionData
38
-            = getSessionById(store.getState(), action.sessionData.id);
58
+            = getSessionById(getState(), action.sessionData.id);
39 59
     }
40 60
 
41 61
     const result = next(action);
42 62
 
43 63
     switch (action.type) {
44 64
     case APP_WILL_MOUNT:
45
-        store.dispatch(registerSound(
65
+        dispatch(registerSound(
46 66
             RECORDING_OFF_SOUND_ID,
47 67
             RECORDING_OFF_SOUND_FILE));
48 68
 
49
-        store.dispatch(registerSound(
69
+        dispatch(registerSound(
50 70
             RECORDING_ON_SOUND_ID,
51 71
             RECORDING_ON_SOUND_FILE));
52 72
 
53 73
         break;
54 74
 
55 75
     case APP_WILL_UNMOUNT:
56
-        store.dispatch(unregisterSound(RECORDING_OFF_SOUND_ID));
57
-        store.dispatch(unregisterSound(RECORDING_ON_SOUND_ID));
76
+        dispatch(unregisterSound(RECORDING_OFF_SOUND_ID));
77
+        dispatch(unregisterSound(RECORDING_ON_SOUND_ID));
58 78
 
59 79
         break;
60 80
 
@@ -65,12 +85,17 @@ MiddlewareRegistry.register(store => next => action => {
65 85
             JitsiConferenceEvents.RECORDER_STATE_CHANGED,
66 86
             recorderSession => {
67 87
 
68
-                if (recorderSession && recorderSession.getID()) {
69
-                    store.dispatch(
70
-                        updateRecordingSessionData(recorderSession));
88
+                if (recorderSession) {
89
+                    recorderSession.getID()
90
+                        && dispatch(
91
+                            updateRecordingSessionData(recorderSession));
71 92
 
72
-                    return;
93
+                    recorderSession.getError()
94
+                        && _showRecordingErrorNotification(
95
+                            recorderSession, dispatch);
73 96
                 }
97
+
98
+                return;
74 99
             });
75 100
 
76 101
         break;
@@ -78,18 +103,26 @@ MiddlewareRegistry.register(store => next => action => {
78 103
 
79 104
     case RECORDING_SESSION_UPDATED: {
80 105
         const updatedSessionData
81
-            = getSessionById(store.getState(), action.sessionData.id);
106
+            = getSessionById(getState(), action.sessionData.id);
82 107
 
83 108
         if (updatedSessionData.mode === JitsiRecordingConstants.mode.FILE) {
84
-            const { OFF, ON } = JitsiRecordingConstants.status;
85
-
86
-            if (updatedSessionData.status === ON
87
-                && (!oldSessionData || oldSessionData.status !== ON)) {
88
-                store.dispatch(playSound(RECORDING_ON_SOUND_ID));
89
-            } else if (updatedSessionData.status === OFF
90
-                && (!oldSessionData || oldSessionData.status !== OFF)) {
91
-                store.dispatch(stopSound(RECORDING_ON_SOUND_ID));
92
-                store.dispatch(playSound(RECORDING_OFF_SOUND_ID));
109
+            const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
110
+
111
+            if (updatedSessionData.status === PENDING
112
+                && (!oldSessionData || oldSessionData.status !== PENDING)) {
113
+                dispatch(showPendingRecordingNotification());
114
+            } else if (updatedSessionData.status !== PENDING) {
115
+                dispatch(hidePendingRecordingNotification());
116
+
117
+                if (updatedSessionData.status === ON
118
+                    && (!oldSessionData || oldSessionData.status !== ON)) {
119
+                    dispatch(playSound(RECORDING_ON_SOUND_ID));
120
+                } else if (updatedSessionData.status === OFF
121
+                    && (!oldSessionData || oldSessionData.status !== OFF)) {
122
+                    dispatch(stopSound(RECORDING_ON_SOUND_ID));
123
+                    dispatch(playSound(RECORDING_OFF_SOUND_ID));
124
+                    dispatch(showStoppedRecordingNotification());
125
+                }
93 126
             }
94 127
         }
95 128
 
@@ -99,3 +132,56 @@ MiddlewareRegistry.register(store => next => action => {
99 132
 
100 133
     return result;
101 134
 });
135
+
136
+/**
137
+ * Shows a notification about an error in the recording session. A
138
+ * default notification will display if no error is specified in the passed
139
+ * in recording session.
140
+ *
141
+ * @private
142
+ * @param {Object} recorderSession - The recorder session model from the
143
+ * lib.
144
+ * @param {Dispatch} dispatch - The Redux Dispatch function.
145
+ * @returns {void}
146
+ */
147
+function _showRecordingErrorNotification(recorderSession, dispatch) {
148
+    const isStreamMode
149
+        = recorderSession.getMode()
150
+            === JitsiMeetJS.constants.recording.mode.STREAM;
151
+
152
+    switch (recorderSession.getError()) {
153
+    case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
154
+        dispatch(showRecordingError({
155
+            descriptionKey: 'recording.unavailable',
156
+            descriptionArguments: {
157
+                serviceName: isStreamMode
158
+                    ? 'Live Streaming service'
159
+                    : 'Recording service'
160
+            },
161
+            titleKey: isStreamMode
162
+                ? 'liveStreaming.unavailableTitle'
163
+                : 'recording.unavailableTitle'
164
+        }));
165
+        break;
166
+    case JitsiMeetJS.constants.recording.error.RESOURCE_CONSTRAINT:
167
+        dispatch(showRecordingError({
168
+            descriptionKey: isStreamMode
169
+                ? 'liveStreaming.busy'
170
+                : 'recording.busy',
171
+            titleKey: isStreamMode
172
+                ? 'liveStreaming.busyTitle'
173
+                : 'recording.busyTitle'
174
+        }));
175
+        break;
176
+    default:
177
+        dispatch(showRecordingError({
178
+            descriptionKey: isStreamMode
179
+                ? 'liveStreaming.error'
180
+                : 'recording.error',
181
+            titleKey: isStreamMode
182
+                ? 'liveStreaming.failedToStart'
183
+                : 'recording.failedToStart'
184
+        }));
185
+        break;
186
+    }
187
+}

+ 18
- 1
react/features/recording/reducer.js Целия файл

@@ -1,5 +1,9 @@
1 1
 import { ReducerRegistry } from '../base/redux';
2
-import { RECORDING_SESSION_UPDATED } from './actionTypes';
2
+import {
3
+    CLEAR_RECORDING_SESSIONS,
4
+    RECORDING_SESSION_UPDATED,
5
+    SET_PENDING_RECORDING_NOTIFICATION_UID
6
+} from './actionTypes';
3 7
 
4 8
 const DEFAULT_STATE = {
5 9
     sessionDatas: []
@@ -11,6 +15,13 @@ const DEFAULT_STATE = {
11 15
 ReducerRegistry.register('features/recording',
12 16
     (state = DEFAULT_STATE, action) => {
13 17
         switch (action.type) {
18
+
19
+        case CLEAR_RECORDING_SESSIONS:
20
+            return {
21
+                ...state,
22
+                sessionDatas: []
23
+            };
24
+
14 25
         case RECORDING_SESSION_UPDATED:
15 26
             return {
16 27
                 ...state,
@@ -18,6 +29,12 @@ ReducerRegistry.register('features/recording',
18 29
                     _updateSessionDatas(state.sessionDatas, action.sessionData)
19 30
             };
20 31
 
32
+        case SET_PENDING_RECORDING_NOTIFICATION_UID:
33
+            return {
34
+                ...state,
35
+                pendingNotificationUid: action.uid
36
+            };
37
+
21 38
         default:
22 39
             return state;
23 40
         }

+ 2
- 0
react/features/toolbox/components/native/OverflowMenu.js Целия файл

@@ -6,6 +6,7 @@ import { connect } from 'react-redux';
6 6
 import { BottomSheet, hideDialog } from '../../../base/dialog';
7 7
 import { AudioRouteButton } from '../../../mobile/audio-mode';
8 8
 import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
9
+import { RecordButton } from '../../../recording';
9 10
 import { RoomLockButton } from '../../../room-lock';
10 11
 
11 12
 import AudioOnlyButton from './AudioOnlyButton';
@@ -68,6 +69,7 @@ class OverflowMenu extends Component<Props> {
68 69
                 <ToggleCameraButton { ...buttonProps } />
69 70
                 <AudioOnlyButton { ...buttonProps } />
70 71
                 <RoomLockButton { ...buttonProps } />
72
+                <RecordButton { ...buttonProps } />
71 73
                 <PictureInPictureButton { ...buttonProps } />
72 74
             </BottomSheet>
73 75
         );

Loading…
Отказ
Запис