浏览代码

rn: add shared document support using Etherpad

master
Saúl Ibarra Corretgé 5 年前
父节点
当前提交
19d1e3829d

+ 3
- 0
lang/main.json 查看文件

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

+ 3
- 1
react/features/conference/components/native/Conference.js 查看文件

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

+ 10
- 2
react/features/etherpad/actionTypes.js 查看文件

15
  *     type: SET_DOCUMENT_EDITING_STATUS
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
  * The type of the action which signals to start or stop editing a shared
30
  * The type of the action which signals to start or stop editing a shared

+ 17
- 0
react/features/etherpad/actions.js 查看文件

3
 import {
3
 import {
4
     ETHERPAD_INITIALIZED,
4
     ETHERPAD_INITIALIZED,
5
     SET_DOCUMENT_EDITING_STATUS,
5
     SET_DOCUMENT_EDITING_STATUS,
6
+    SET_DOCUMENT_URL,
6
     TOGGLE_DOCUMENT_EDITING
7
     TOGGLE_DOCUMENT_EDITING
7
 } from './actionTypes';
8
 } from './actionTypes';
8
 
9
 
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
  * Dispatches an action to set Etherpad as having been initialized.
44
  * Dispatches an action to set Etherpad as having been initialized.
28
  *
45
  *

+ 81
- 0
react/features/etherpad/components/SharedDocumentButton.js 查看文件

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 查看文件

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

+ 1
- 0
react/features/etherpad/components/index.web.js 查看文件

1
+export { default as SharedDocumentButton } from './SharedDocumentButton';

+ 178
- 0
react/features/etherpad/components/native/SharedDocument.js 查看文件

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 查看文件

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 查看文件

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 查看文件

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

+ 45
- 8
react/features/etherpad/middleware.js 查看文件

1
 // @flow
1
 // @flow
2
 
2
 
3
-import { MiddlewareRegistry } from '../base/redux';
3
+import { getCurrentConference } from '../base/conference';
4
+import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
4
 import UIEvents from '../../../service/UI/UIEvents';
5
 import UIEvents from '../../../service/UI/UIEvents';
5
 
6
 
6
 import { TOGGLE_DOCUMENT_EDITING } from './actionTypes';
7
 import { TOGGLE_DOCUMENT_EDITING } from './actionTypes';
8
+import { setDocumentEditingState, setDocumentUrl } from './actions';
7
 
9
 
8
 declare var APP: Object;
10
 declare var APP: Object;
9
 
11
 
12
+const ETHERPAD_COMMAND = 'etherpad';
13
+
10
 /**
14
 /**
11
  * Middleware that captures actions related to collaborative document editing
15
  * Middleware that captures actions related to collaborative document editing
12
  * and notifies components not hooked into redux.
16
  * and notifies components not hooked into redux.
15
  * @returns {Function}
19
  * @returns {Function}
16
  */
20
  */
17
 // eslint-disable-next-line no-unused-vars
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
     switch (action.type) {
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
         break;
32
         break;
27
     }
33
     }
34
+    }
28
 
35
 
29
     return next(action);
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 查看文件

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

+ 2
- 0
react/features/toolbox/components/native/OverflowMenu.js 查看文件

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

正在加载...
取消
保存