Browse Source

rn: add shared document support using Etherpad

j8
Saúl Ibarra Corretgé 5 years ago
parent
commit
19d1e3829d

+ 3
- 0
lang/main.json View File

@@ -275,6 +275,9 @@
275 275
     "dialOut": {
276 276
         "statusMessage": "is now {{status}}"
277 277
     },
278
+    "documentSharing" : {
279
+        "title": "Shared Document"
280
+    },
278 281
     "feedback": {
279 282
         "average": "Average",
280 283
         "bad": "Bad",

+ 3
- 1
react/features/conference/components/native/Conference.js View File

@@ -16,6 +16,7 @@ import { TestConnectionInfo } from '../../../base/testing';
16 16
 import { ConferenceNotification, isCalendarEnabled } from '../../../calendar-sync';
17 17
 import { Chat } from '../../../chat';
18 18
 import { DisplayNameLabel } from '../../../display-name';
19
+import { SharedDocument } from '../../../etherpad';
19 20
 import {
20 21
     FILMSTRIP_SIZE,
21 22
     Filmstrip,
@@ -179,8 +180,9 @@ class Conference extends AbstractConference<Props, *> {
179 180
                     hidden = { true }
180 181
                     translucent = { true } />
181 182
 
182
-                <Chat />
183 183
                 <AddPeopleDialog />
184
+                <Chat />
185
+                <SharedDocument />
184 186
 
185 187
                 {/*
186 188
                   * The LargeVideo is the lowermost stacking layer.

+ 10
- 2
react/features/etherpad/actionTypes.js View File

@@ -15,8 +15,16 @@ export const ETHERPAD_INITIALIZED = 'ETHERPAD_INITIALIZED';
15 15
  *     type: SET_DOCUMENT_EDITING_STATUS
16 16
  * }
17 17
  */
18
-export const SET_DOCUMENT_EDITING_STATUS
19
-    = 'SET_DOCUMENT_EDITING_STATUS';
18
+export const SET_DOCUMENT_EDITING_STATUS = 'SET_DOCUMENT_EDITING_STATUS';
19
+
20
+/**
21
+ * The type of the action which updates the shared document URL.
22
+ *
23
+ * {
24
+ *     type: SET_DOCUMENT_URL
25
+ * }
26
+ */
27
+export const SET_DOCUMENT_URL = 'SET_DOCUMENT_URL';
20 28
 
21 29
 /**
22 30
  * The type of the action which signals to start or stop editing a shared

+ 17
- 0
react/features/etherpad/actions.js View File

@@ -3,6 +3,7 @@
3 3
 import {
4 4
     ETHERPAD_INITIALIZED,
5 5
     SET_DOCUMENT_EDITING_STATUS,
6
+    SET_DOCUMENT_URL,
6 7
     TOGGLE_DOCUMENT_EDITING
7 8
 } from './actionTypes';
8 9
 
@@ -23,6 +24,22 @@ export function setDocumentEditingState(editing: boolean) {
23 24
     };
24 25
 }
25 26
 
27
+/**
28
+ * Dispatches an action to set the shared document URL.
29
+ *
30
+ * @param {string} documentUrl - The shared document URL.
31
+ * @returns {{
32
+ *    type: SET_DOCUMENT_URL,
33
+ *    documentUrl: string
34
+ * }}
35
+ */
36
+export function setDocumentUrl(documentUrl: ?string) {
37
+    return {
38
+        type: SET_DOCUMENT_URL,
39
+        documentUrl
40
+    };
41
+}
42
+
26 43
 /**
27 44
  * Dispatches an action to set Etherpad as having been initialized.
28 45
  *

+ 81
- 0
react/features/etherpad/components/SharedDocumentButton.js View File

@@ -0,0 +1,81 @@
1
+// @flow
2
+
3
+import type { Dispatch } from 'redux';
4
+
5
+import { createToolbarEvent, sendAnalytics } from '../../analytics';
6
+import { translate } from '../../base/i18n';
7
+import { IconShareDoc } from '../../base/icons';
8
+import { connect } from '../../base/redux';
9
+import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox';
10
+
11
+import { toggleDocument } from '../actions';
12
+
13
+
14
+type Props = AbstractButtonProps & {
15
+
16
+    /**
17
+     * Whether the shared document is being edited or not.
18
+     */
19
+    _editing: boolean,
20
+
21
+    /**
22
+     * Redux dispatch function.
23
+     */
24
+    dispatch: Dispatch<any>,
25
+};
26
+
27
+/**
28
+ * Implements an {@link AbstractButton} to open the chat screen on mobile.
29
+ */
30
+class SharedDocumentButton extends AbstractButton<Props, *> {
31
+    accessibilityLabel = 'toolbar.accessibilityLabel.document';
32
+    icon = IconShareDoc;
33
+    label = 'toolbar.documentOpen';
34
+    toggledLabel = 'toolbar.documentClose';
35
+
36
+    /**
37
+     * Handles clicking / pressing the button, and opens / closes the appropriate dialog.
38
+     *
39
+     * @private
40
+     * @returns {void}
41
+     */
42
+    _handleClick() {
43
+        sendAnalytics(createToolbarEvent(
44
+            'toggle.etherpad',
45
+            {
46
+                enable: !this.props._editing
47
+            }));
48
+        this.props.dispatch(toggleDocument());
49
+    }
50
+
51
+    /**
52
+     * Indicates whether this button is in toggled state or not.
53
+     *
54
+     * @override
55
+     * @protected
56
+     * @returns {boolean}
57
+     */
58
+    _isToggled() {
59
+        return this.props._editing;
60
+    }
61
+}
62
+
63
+/**
64
+ * Maps part of the redux state to the component's props.
65
+ *
66
+ * @param {Object} state - The redux store/state.
67
+ * @param {Object} ownProps - The properties explicitly passed to the component
68
+ * instance.
69
+ * @returns {Object}
70
+ */
71
+function _mapStateToProps(state: Object, ownProps: Object) {
72
+    const { documentUrl, editing } = state['features/etherpad'];
73
+    const { visible = Boolean(documentUrl) } = ownProps;
74
+
75
+    return {
76
+        _editing: editing,
77
+        visible
78
+    };
79
+}
80
+
81
+export default translate(connect(_mapStateToProps)(SharedDocumentButton));

+ 2
- 0
react/features/etherpad/components/index.native.js View File

@@ -0,0 +1,2 @@
1
+export { default as SharedDocument } from './native/SharedDocument';
2
+export { default as SharedDocumentButton } from './SharedDocumentButton';

+ 1
- 0
react/features/etherpad/components/index.web.js View File

@@ -0,0 +1 @@
1
+export { default as SharedDocumentButton } from './SharedDocumentButton';

+ 178
- 0
react/features/etherpad/components/native/SharedDocument.js View File

@@ -0,0 +1,178 @@
1
+// @flow
2
+
3
+import React, { PureComponent } from 'react';
4
+import { SafeAreaView, View } from 'react-native';
5
+import { WebView } from 'react-native-webview';
6
+import type { Dispatch } from 'redux';
7
+
8
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
9
+import { translate } from '../../../base/i18n';
10
+import { HeaderWithNavigation, LoadingIndicator, SlidingView } from '../../../base/react';
11
+import { connect } from '../../../base/redux';
12
+
13
+import { toggleDocument } from '../../actions';
14
+import { getSharedDocumentUrl } from '../../functions';
15
+
16
+import styles, { INDICATOR_COLOR } from './styles';
17
+
18
+/**
19
+ * The type of the React {@code Component} props of {@code ShareDocument}.
20
+ */
21
+type Props = {
22
+
23
+    /**
24
+     * URL for the shared document.
25
+     */
26
+    _documentUrl: string,
27
+
28
+    /**
29
+     * Color schemed style of the header component.
30
+     */
31
+    _headerStyles: Object,
32
+
33
+    /**
34
+     * True if the chat window should be rendered.
35
+     */
36
+    _isOpen: boolean,
37
+
38
+    /**
39
+     * The Redux dispatch function.
40
+     */
41
+    dispatch: Dispatch<any>,
42
+
43
+    /**
44
+     * Function to be used to translate i18n labels.
45
+     */
46
+    t: Function
47
+};
48
+
49
+/**
50
+ * Implements a React native component that renders the shared document window.
51
+ */
52
+class SharedDocument extends PureComponent<Props> {
53
+    /**
54
+     * Instantiates a new instance.
55
+     *
56
+     * @inheritdoc
57
+     */
58
+    constructor(props: Props) {
59
+        super(props);
60
+
61
+        this._onClose = this._onClose.bind(this);
62
+        this._onError = this._onError.bind(this);
63
+        this._renderLoading = this._renderLoading.bind(this);
64
+    }
65
+
66
+    /**
67
+     * Implements React's {@link Component#render()}.
68
+     *
69
+     * @inheritdoc
70
+     */
71
+    render() {
72
+        const { _documentUrl, _isOpen } = this.props;
73
+        const webViewStyles = this._getWebViewStyles();
74
+
75
+        return (
76
+            <SlidingView
77
+                onHide = { this._onClose }
78
+                position = 'bottom'
79
+                show = { _isOpen } >
80
+                <View style = { styles.webViewWrapper }>
81
+                    <HeaderWithNavigation
82
+                        headerLabelKey = 'documentSharing.title'
83
+                        onPressBack = { this._onClose } />
84
+                    <SafeAreaView style = { webViewStyles }>
85
+                        <WebView
86
+                            onError = { this._onError }
87
+                            renderLoading = { this._renderLoading }
88
+                            source = {{ uri: _documentUrl }}
89
+                            startInLoadingState = { true } />
90
+                    </SafeAreaView>
91
+                </View>
92
+            </SlidingView>
93
+        );
94
+    }
95
+
96
+    /**
97
+     * Computes the styles required for the WebView component.
98
+     *
99
+     * @returns {Object}
100
+     */
101
+    _getWebViewStyles() {
102
+        return {
103
+            ...styles.webView,
104
+            backgroundColor: this.props._headerStyles.screenHeader.backgroundColor
105
+        };
106
+    }
107
+
108
+    _onClose: () => boolean
109
+
110
+    /**
111
+     * Closes the window.
112
+     *
113
+     * @returns {boolean}
114
+     */
115
+    _onClose() {
116
+        const { _isOpen, dispatch } = this.props;
117
+
118
+        if (_isOpen) {
119
+            dispatch(toggleDocument());
120
+
121
+            return true;
122
+        }
123
+
124
+        return false;
125
+    }
126
+
127
+    _onError: () => void;
128
+
129
+    /**
130
+     * Callback to handle the error if the page fails to load.
131
+     *
132
+     * @returns {void}
133
+     */
134
+    _onError() {
135
+        const { _isOpen, dispatch } = this.props;
136
+
137
+        if (_isOpen) {
138
+            dispatch(toggleDocument());
139
+        }
140
+    }
141
+
142
+    _renderLoading: () => React$Component<any>;
143
+
144
+    /**
145
+     * Renders the loading indicator.
146
+     *
147
+     * @returns {React$Component<any>}
148
+     */
149
+    _renderLoading() {
150
+        return (
151
+            <View style = { styles.indicatorWrapper }>
152
+                <LoadingIndicator
153
+                    color = { INDICATOR_COLOR }
154
+                    size = 'large' />
155
+            </View>
156
+        );
157
+    }
158
+}
159
+
160
+/**
161
+ * Maps (parts of) the redux state to {@link SharedDocument} React {@code Component} props.
162
+ *
163
+ * @param {Object} state - The redux store/state.
164
+ * @private
165
+ * @returns {Object}
166
+ */
167
+export function _mapStateToProps(state: Object) {
168
+    const { editing } = state['features/etherpad'];
169
+    const documentUrl = getSharedDocumentUrl(state);
170
+
171
+    return {
172
+        _documentUrl: documentUrl,
173
+        _headerStyles: ColorSchemeRegistry.get(state, 'Header'),
174
+        _isOpen: editing
175
+    };
176
+}
177
+
178
+export default translate(connect(_mapStateToProps)(SharedDocument));

+ 24
- 0
react/features/etherpad/components/native/styles.js View File

@@ -0,0 +1,24 @@
1
+// @flow
2
+
3
+import { ColorPalette } from '../../../base/styles';
4
+
5
+export const INDICATOR_COLOR = ColorPalette.lightGrey;
6
+
7
+export default {
8
+
9
+    indicatorWrapper: {
10
+        alignItems: 'center',
11
+        backgroundColor: ColorPalette.white,
12
+        height: '100%',
13
+        justifyContent: 'center'
14
+    },
15
+
16
+    webView: {
17
+        flex: 1
18
+    },
19
+
20
+    webViewWrapper: {
21
+        flex: 1,
22
+        flexDirection: 'column'
23
+    }
24
+};

+ 34
- 0
react/features/etherpad/functions.js View File

@@ -0,0 +1,34 @@
1
+// @flow
2
+
3
+import { toState } from '../base/redux';
4
+
5
+const ETHERPAD_OPTIONS = {
6
+    showControls: 'true',
7
+    showChat: 'false',
8
+    showLineNumbers: 'true',
9
+    useMonospaceFont: 'false'
10
+};
11
+
12
+/**
13
+ * Retrieves the current sahred document URL.
14
+ *
15
+ * @param {Function|Object} stateful - The redux store or {@code getState} function.
16
+ * @returns {?string} - Current shared document URL or undefined.
17
+ */
18
+export function getSharedDocumentUrl(stateful: Function | Object) {
19
+    const state = toState(stateful);
20
+    const { documentUrl } = state['features/etherpad'];
21
+    const { displayName } = state['features/base/settings'];
22
+
23
+    if (!documentUrl) {
24
+        return undefined;
25
+    }
26
+
27
+    const params = new URLSearchParams(ETHERPAD_OPTIONS);
28
+
29
+    if (displayName) {
30
+        params.append('userName', displayName);
31
+    }
32
+
33
+    return `${documentUrl}?${params.toString()}`;
34
+}

+ 2
- 0
react/features/etherpad/index.js View File

@@ -1,5 +1,7 @@
1 1
 export * from './actions';
2 2
 export * from './actionTypes';
3
+export * from './components';
4
+export * from './functions';
3 5
 
4 6
 import './middleware';
5 7
 import './reducer';

+ 45
- 8
react/features/etherpad/middleware.js View File

@@ -1,12 +1,16 @@
1 1
 // @flow
2 2
 
3
-import { MiddlewareRegistry } from '../base/redux';
3
+import { getCurrentConference } from '../base/conference';
4
+import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
4 5
 import UIEvents from '../../../service/UI/UIEvents';
5 6
 
6 7
 import { TOGGLE_DOCUMENT_EDITING } from './actionTypes';
8
+import { setDocumentEditingState, setDocumentUrl } from './actions';
7 9
 
8 10
 declare var APP: Object;
9 11
 
12
+const ETHERPAD_COMMAND = 'etherpad';
13
+
10 14
 /**
11 15
  * Middleware that captures actions related to collaborative document editing
12 16
  * and notifies components not hooked into redux.
@@ -15,16 +19,49 @@ declare var APP: Object;
15 19
  * @returns {Function}
16 20
  */
17 21
 // eslint-disable-next-line no-unused-vars
18
-MiddlewareRegistry.register(store => next => action => {
19
-    if (typeof APP === 'undefined') {
20
-        return next(action);
21
-    }
22
-
22
+MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
23 23
     switch (action.type) {
24
-    case TOGGLE_DOCUMENT_EDITING:
25
-        APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
24
+    case TOGGLE_DOCUMENT_EDITING: {
25
+        if (typeof APP === 'undefined') {
26
+            const { editing } = getState()['features/etherpad'];
27
+
28
+            dispatch(setDocumentEditingState(!editing));
29
+        } else {
30
+            APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
31
+        }
26 32
         break;
27 33
     }
34
+    }
28 35
 
29 36
     return next(action);
30 37
 });
38
+
39
+/**
40
+ * Set up state change listener to perform maintenance tasks when the conference
41
+ * is left or failed, e.g. clear messages or close the chat modal if it's left
42
+ * open.
43
+ */
44
+StateListenerRegistry.register(
45
+    state => getCurrentConference(state),
46
+    (conference, { dispatch, getState }, previousConference) => {
47
+        if (conference) {
48
+            conference.addCommandListener(ETHERPAD_COMMAND,
49
+                ({ value }) => {
50
+                    let url;
51
+                    const { etherpad_base: etherpadBase } = getState()['features/base/config'];
52
+
53
+                    if (etherpadBase) {
54
+                        const u = new URL(value, etherpadBase);
55
+
56
+                        url = u.toString();
57
+                    }
58
+
59
+                    dispatch(setDocumentUrl(url));
60
+                }
61
+            );
62
+        }
63
+
64
+        if (previousConference) {
65
+            dispatch(setDocumentUrl(undefined));
66
+        }
67
+    });

+ 13
- 1
react/features/etherpad/reducer.js View File

@@ -4,11 +4,17 @@ import { ReducerRegistry } from '../base/redux';
4 4
 
5 5
 import {
6 6
     ETHERPAD_INITIALIZED,
7
-    SET_DOCUMENT_EDITING_STATUS
7
+    SET_DOCUMENT_EDITING_STATUS,
8
+    SET_DOCUMENT_URL
8 9
 } from './actionTypes';
9 10
 
10 11
 const DEFAULT_STATE = {
11 12
 
13
+    /**
14
+     * URL for the shared document.
15
+     */
16
+    documentUrl: undefined,
17
+
12 18
     /**
13 19
      * Whether or not Etherpad is currently open.
14 20
      *
@@ -45,6 +51,12 @@ ReducerRegistry.register(
45 51
                 editing: action.editing
46 52
             };
47 53
 
54
+        case SET_DOCUMENT_URL:
55
+            return {
56
+                ...state,
57
+                documentUrl: action.documentUrl
58
+            };
59
+
48 60
         default:
49 61
             return state;
50 62
         }

+ 2
- 0
react/features/toolbox/components/native/OverflowMenu.js View File

@@ -8,6 +8,7 @@ import { BottomSheet, hideDialog, isDialogOpen } from '../../../base/dialog';
8 8
 import { CHAT_ENABLED, IOS_RECORDING_ENABLED, getFeatureFlag } from '../../../base/flags';
9 9
 import { connect } from '../../../base/redux';
10 10
 import { StyleType } from '../../../base/styles';
11
+import { SharedDocumentButton } from '../../../etherpad';
11 12
 import { InfoDialogButton, InviteButton } from '../../../invite';
12 13
 import { AudioRouteButton } from '../../../mobile/audio-mode';
13 14
 import { LiveStreamButton, RecordButton } from '../../../recording';
@@ -108,6 +109,7 @@ class OverflowMenu extends Component<Props> {
108 109
                         && <InfoDialogButton { ...buttonProps } />
109 110
                 }
110 111
                 <RaiseHandButton { ...buttonProps } />
112
+                <SharedDocumentButton { ...buttonProps } />
111 113
             </BottomSheet>
112 114
         );
113 115
     }

Loading…
Cancel
Save