Selaa lähdekoodia

feat(mobile): add 1 liner notifications

Adds 1 liner notifications to mobile. Only the title is displayed. In
case the title is missing there's a fallback to the description.
j8
paweldomas 6 vuotta sitten
vanhempi
commit
c7979a3944

+ 3
- 3
lang/main.json Näytä tiedosto

458
     },
458
     },
459
     "me": "me",
459
     "me": "me",
460
     "notify": {
460
     "notify": {
461
-        "connectedOneMember": "__name__ connected",
462
-        "connectedThreePlusMembers": "__name__ and __count__ others connected",
463
-        "connectedTwoMembers": "__first__ and __second__ connected",
461
+        "connectedOneMember": "__name__ joined the meeting",
462
+        "connectedThreePlusMembers": "__name__ and __count__ others joined the meeting",
463
+        "connectedTwoMembers": "__first__ and __second__ joined the meeting",
464
         "disconnected": "disconnected",
464
         "disconnected": "disconnected",
465
         "focus": "Conference focus",
465
         "focus": "Conference focus",
466
         "focusFail": "__component__ not available - retry in __ms__ sec",
466
         "focusFail": "__component__ not available - retry in __ms__ sec",

+ 42
- 0
react/features/conference/components/AbstractConference.js Näytä tiedosto

1
 // @flow
1
 // @flow
2
 
2
 
3
+import React, { Component } from 'react';
4
+
5
+import { NotificationsContainer } from '../../notifications/components';
6
+
7
+import { shouldDisplayNotifications } from '../functions';
3
 import { shouldDisplayTileView } from '../../video-layout';
8
 import { shouldDisplayTileView } from '../../video-layout';
4
 
9
 
5
 /**
10
 /**
7
  */
12
  */
8
 export type AbstractProps = {
13
 export type AbstractProps = {
9
 
14
 
15
+    /**
16
+     * Set to {@code true} when the notifications are to be displayed.
17
+     *
18
+     * @protected
19
+     * @type {boolean}
20
+     */
21
+    _notificationsVisible: boolean,
22
+
10
     /**
23
     /**
11
      * Conference room name.
24
      * Conference room name.
12
      *
25
      *
24
     _shouldDisplayTileView: boolean
37
     _shouldDisplayTileView: boolean
25
 };
38
 };
26
 
39
 
40
+/**
41
+ * A container to hold video status labels, including recording status and
42
+ * current large video quality.
43
+ *
44
+ * @extends Component
45
+ */
46
+export class AbstractConference<P: AbstractProps, S>
47
+    extends Component<P, S> {
48
+
49
+    /**
50
+     * Renders the {@code LocalRecordingLabel}.
51
+     *
52
+     * @param {Object} props - The properties to be passed to
53
+     * the {@code NotificationsContainer}.
54
+     * @protected
55
+     * @returns {React$Element}
56
+     */
57
+    renderNotificationsContainer(props: ?Object) {
58
+        if (this.props._notificationsVisible) {
59
+            return (
60
+                React.createElement(NotificationsContainer, props)
61
+            );
62
+        }
63
+
64
+        return null;
65
+    }
66
+}
67
+
27
 /**
68
 /**
28
  * Maps (parts of) the redux state to the associated props of the {@link Labels}
69
  * Maps (parts of) the redux state to the associated props of the {@link Labels}
29
  * {@code Component}.
70
  * {@code Component}.
34
  */
75
  */
35
 export function abstractMapStateToProps(state: Object) {
76
 export function abstractMapStateToProps(state: Object) {
36
     return {
77
     return {
78
+        _notificationsVisible: shouldDisplayNotifications(state),
37
         _room: state['features/base/conference'].room,
79
         _room: state['features/base/conference'].room,
38
         _shouldDisplayTileView: shouldDisplayTileView(state)
80
         _shouldDisplayTileView: shouldDisplayTileView(state)
39
     };
81
     };

+ 46
- 5
react/features/conference/components/native/Conference.js Näytä tiedosto

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { Component } from 'react';
3
+import React from 'react';
4
 
4
 
5
-import { BackHandler, StatusBar, View } from 'react-native';
5
+import { BackHandler, SafeAreaView, StatusBar, View } from 'react-native';
6
 import { connect as reactReduxConnect } from 'react-redux';
6
 import { connect as reactReduxConnect } from 'react-redux';
7
 
7
 
8
 import { appNavigate } from '../../../app';
8
 import { appNavigate } from '../../../app';
10
 import { getParticipantCount } from '../../../base/participants';
10
 import { getParticipantCount } from '../../../base/participants';
11
 import { Container, LoadingIndicator, TintedView } from '../../../base/react';
11
 import { Container, LoadingIndicator, TintedView } from '../../../base/react';
12
 import {
12
 import {
13
+    isNarrowAspectRatio,
13
     makeAspectRatioAware
14
     makeAspectRatioAware
14
 } from '../../../base/responsive-ui';
15
 } from '../../../base/responsive-ui';
15
 import { TestConnectionInfo } from '../../../base/testing';
16
 import { TestConnectionInfo } from '../../../base/testing';
17
 import { ConferenceNotification } from '../../../calendar-sync';
18
 import { ConferenceNotification } from '../../../calendar-sync';
18
 import { Chat } from '../../../chat';
19
 import { Chat } from '../../../chat';
19
 import {
20
 import {
21
+    FILMSTRIP_SIZE,
20
     Filmstrip,
22
     Filmstrip,
21
     isFilmstripVisible,
23
     isFilmstripVisible,
22
     TileView
24
     TileView
26
 import { Captions } from '../../../subtitles';
28
 import { Captions } from '../../../subtitles';
27
 import { setToolboxVisible, Toolbox } from '../../../toolbox';
29
 import { setToolboxVisible, Toolbox } from '../../../toolbox';
28
 
30
 
29
-import { abstractMapStateToProps } from '../AbstractConference';
31
+import {
32
+    AbstractConference,
33
+    abstractMapStateToProps
34
+} from '../AbstractConference';
30
 import DisplayNameLabel from './DisplayNameLabel';
35
 import DisplayNameLabel from './DisplayNameLabel';
31
 import Labels from './Labels';
36
 import Labels from './Labels';
32
 import NavigationBar from './NavigationBar';
37
 import NavigationBar from './NavigationBar';
134
 /**
139
 /**
135
  * The conference page of the mobile (i.e. React Native) application.
140
  * The conference page of the mobile (i.e. React Native) application.
136
  */
141
  */
137
-class Conference extends Component<Props> {
142
+class Conference extends AbstractConference<Props, *> {
138
     /**
143
     /**
139
      * Initializes a new Conference instance.
144
      * Initializes a new Conference instance.
140
      *
145
      *
296
                     }
301
                     }
297
                 </View>
302
                 </View>
298
 
303
 
299
-                <NavigationBar />
304
+                <SafeAreaView
305
+                    pointerEvents = 'box-none'
306
+                    style = { styles.navBarSafeView }>
307
+                    <NavigationBar />
308
+                    { this.renderNotificationsContainer() }
309
+                </SafeAreaView>
300
 
310
 
301
                 <TestConnectionInfo />
311
                 <TestConnectionInfo />
302
 
312
 
341
                 ? <ConferenceNotification />
351
                 ? <ConferenceNotification />
342
                 : undefined);
352
                 : undefined);
343
     }
353
     }
354
+
355
+    /**
356
+     * Renders a container for notifications to be displayed by the
357
+     * base/notifications feature.
358
+     *
359
+     * @private
360
+     * @returns {React$Element}
361
+     */
362
+    renderNotificationsContainer() {
363
+        const notificationsStyle = {};
364
+
365
+        // In the landscape mode (wide) there's problem with notifications being
366
+        // shadowed by the filmstrip rendered on the right. This makes the "x"
367
+        // button not clickable. In order to avoid that a margin of the
368
+        // filmstrip's size is added to the right.
369
+        //
370
+        // Pawel: after many attempts I failed to make notifications adjust to
371
+        // their contents width because of column and rows being used in the
372
+        // flex layout. The only option that seemed to limit the notification's
373
+        // size was explicit 'width' value which is not better than the margin
374
+        // added here.
375
+        if (this.props._filmstripVisible && !isNarrowAspectRatio(this)) {
376
+            notificationsStyle.marginRight = FILMSTRIP_SIZE;
377
+        }
378
+
379
+        return super.renderNotificationsContainer(
380
+            {
381
+                style: notificationsStyle
382
+            }
383
+        );
384
+    }
344
 }
385
 }
345
 
386
 
346
 /**
387
 /**

+ 4
- 1
react/features/conference/components/native/Labels.js Näytä tiedosto

21
     _abstractMapStateToProps,
21
     _abstractMapStateToProps,
22
     type Props as AbstractLabelsProps
22
     type Props as AbstractLabelsProps
23
 } from '../AbstractLabels';
23
 } from '../AbstractLabels';
24
+import { shouldDisplayNotifications } from '../../functions';
24
 import styles from './styles';
25
 import styles from './styles';
25
 
26
 
26
 /**
27
 /**
363
     return {
364
     return {
364
         ..._abstractMapStateToProps(state),
365
         ..._abstractMapStateToProps(state),
365
         _reducedUI: state['features/base/responsive-ui'].reducedUI,
366
         _reducedUI: state['features/base/responsive-ui'].reducedUI,
366
-        _visible: !isToolboxVisible(state) && !shouldDisplayTileView(state)
367
+        _visible: !isToolboxVisible(state)
368
+            && !shouldDisplayTileView(state)
369
+            && !shouldDisplayNotifications(state)
367
     };
370
     };
368
 }
371
 }
369
 
372
 

+ 23
- 29
react/features/conference/components/native/NavigationBar.js Näytä tiedosto

40
             return null;
40
             return null;
41
         }
41
         }
42
 
42
 
43
-        return (
43
+        return [
44
+            <LinearGradient
45
+                colors = { NAVBAR_GRADIENT_COLORS }
46
+                key = { 1 }
47
+                pointerEvents = 'none'
48
+                style = { styles.gradient }>
49
+                <SafeAreaView>
50
+                    <View style = { styles.gradientStretch } />
51
+                </SafeAreaView>
52
+            </LinearGradient>,
44
             <View
53
             <View
54
+                key = { 2 }
45
                 pointerEvents = 'box-none'
55
                 pointerEvents = 'box-none'
46
-                style = { styles.navBarContainer }>
47
-                <LinearGradient
48
-                    colors = { NAVBAR_GRADIENT_COLORS }
49
-                    pointerEvents = 'none'
50
-                    style = { styles.gradient }>
51
-                    <SafeAreaView>
52
-                        <View style = { styles.gradientStretch } />
53
-                    </SafeAreaView>
54
-                </LinearGradient>
55
-                <SafeAreaView
56
+                style = { styles.navBarWrapper }>
57
+                <PictureInPictureButton
58
+                    styles = { styles.navBarButton } />
59
+                <View
56
                     pointerEvents = 'box-none'
60
                     pointerEvents = 'box-none'
57
-                    style = { styles.navBarSafeView }>
58
-                    <View
59
-                        pointerEvents = 'box-none'
60
-                        style = { styles.navBarWrapper }>
61
-                        <PictureInPictureButton
62
-                            styles = { styles.navBarButton } />
63
-                        <View
64
-                            pointerEvents = 'box-none'
65
-                            style = { styles.roomNameWrapper }>
66
-                            <Text
67
-                                numberOfLines = { 1 }
68
-                                style = { styles.roomName }>
69
-                                { this.props._meetingName }
70
-                            </Text>
71
-                        </View>
72
-                    </View>
73
-                </SafeAreaView>
61
+                    style = { styles.roomNameWrapper }>
62
+                    <Text
63
+                        numberOfLines = { 1 }
64
+                        style = { styles.roomName }>
65
+                        { this.props._meetingName }
66
+                    </Text>
67
+                </View>
74
             </View>
68
             </View>
75
-        );
69
+        ];
76
     }
70
     }
77
 
71
 
78
 }
72
 }

+ 4
- 0
react/features/conference/components/native/styles.js Näytä tiedosto

37
     },
37
     },
38
 
38
 
39
     gradient: {
39
     gradient: {
40
+        position: 'absolute',
41
+        top: 0,
42
+        left: 0,
43
+        right: 0,
40
         flex: 1
44
         flex: 1
41
     },
45
     },
42
 
46
 

+ 7
- 5
react/features/conference/components/web/Conference.js Näytä tiedosto

1
 // @flow
1
 // @flow
2
 
2
 
3
 import _ from 'lodash';
3
 import _ from 'lodash';
4
-import React, { Component } from 'react';
4
+import React from 'react';
5
 import { connect as reactReduxConnect } from 'react-redux';
5
 import { connect as reactReduxConnect } from 'react-redux';
6
 
6
 
7
 import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
7
 import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
13
 import { Filmstrip } from '../../../filmstrip';
13
 import { Filmstrip } from '../../../filmstrip';
14
 import { CalleeInfoContainer } from '../../../invite';
14
 import { CalleeInfoContainer } from '../../../invite';
15
 import { LargeVideo } from '../../../large-video';
15
 import { LargeVideo } from '../../../large-video';
16
-import { NotificationsContainer } from '../../../notifications';
17
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
16
 import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
18
 
17
 
19
 import {
18
 import {
28
 import Labels from './Labels';
27
 import Labels from './Labels';
29
 import { default as Notice } from './Notice';
28
 import { default as Notice } from './Notice';
30
 import { default as Subject } from './Subject';
29
 import { default as Subject } from './Subject';
31
-import { abstractMapStateToProps } from '../AbstractConference';
30
+import {
31
+    AbstractConference,
32
+    abstractMapStateToProps
33
+} from '../AbstractConference';
32
 
34
 
33
 import type { AbstractProps } from '../AbstractConference';
35
 import type { AbstractProps } from '../AbstractConference';
34
 
36
 
87
 /**
89
 /**
88
  * The conference page of the Web application.
90
  * The conference page of the Web application.
89
  */
91
  */
90
-class Conference extends Component<Props> {
92
+class Conference extends AbstractConference<Props, *> {
91
     _onFullScreenChange: Function;
93
     _onFullScreenChange: Function;
92
     _onShowToolbar: Function;
94
     _onShowToolbar: Function;
93
     _originalOnShowToolbar: Function;
95
     _originalOnShowToolbar: Function;
218
                 { filmstripOnly || <Toolbox /> }
220
                 { filmstripOnly || <Toolbox /> }
219
                 { filmstripOnly || <Chat /> }
221
                 { filmstripOnly || <Chat /> }
220
 
222
 
221
-                <NotificationsContainer />
223
+                { this.renderNotificationsContainer() }
222
 
224
 
223
                 <CalleeInfoContainer />
225
                 <CalleeInfoContainer />
224
             </div>
226
             </div>

+ 25
- 2
react/features/conference/functions.js Näytä tiedosto

1
-import { getName } from '../app';
2
 import { translateToHTML } from '../base/i18n';
1
 import { translateToHTML } from '../base/i18n';
3
 import { browser } from '../base/lib-jitsi-meet';
2
 import { browser } from '../base/lib-jitsi-meet';
4
-import { showWarningNotification } from '../notifications';
3
+import { toState } from '../base/redux';
4
+
5
+import { getName } from '../app';
6
+import {
7
+    areThereNotifications,
8
+    showWarningNotification
9
+} from '../notifications';
10
+import { getOverlayToRender } from '../overlay';
5
 
11
 
6
 /**
12
 /**
7
  * Shows the suboptimal experience notification if needed.
13
  * Shows the suboptimal experience notification if needed.
36
         );
42
         );
37
     }
43
     }
38
 }
44
 }
45
+
46
+/**
47
+ * Tells whether or not the notifications should be displayed within
48
+ * the conference feature based on the current Redux state.
49
+ *
50
+ * @param {Object|Function} stateful - The redux store state.
51
+ * @returns {boolean}
52
+ */
53
+export function shouldDisplayNotifications(stateful) {
54
+    const state = toState(stateful);
55
+    const isAnyOverlayVisible = Boolean(getOverlayToRender(state));
56
+    const { calleeInfoVisible } = state['features/invite'];
57
+
58
+    return areThereNotifications(state)
59
+            && !isAnyOverlayVisible
60
+            && !calleeInfoVisible;
61
+}

+ 4
- 7
react/features/notifications/components/AbstractNotificationsContainer.js Näytä tiedosto

2
 
2
 
3
 import { Component } from 'react';
3
 import { Component } from 'react';
4
 
4
 
5
-import { getOverlayToRender } from '../../overlay';
6
-
7
 import { hideNotification } from '../actions';
5
 import { hideNotification } from '../actions';
6
+import { areThereNotifications } from '../functions';
8
 
7
 
9
 export type Props = {
8
 export type Props = {
10
 
9
 
165
  * }}
164
  * }}
166
  */
165
  */
167
 export function _abstractMapStateToProps(state: Object) {
166
 export function _abstractMapStateToProps(state: Object) {
168
-    const isAnyOverlayVisible = Boolean(getOverlayToRender(state));
169
-    const { enabled, notifications } = state['features/notifications'];
170
-    const { calleeInfoVisible } = state['features/invite'];
167
+    const { notifications } = state['features/notifications'];
168
+    const _visible = areThereNotifications(state);
171
 
169
 
172
     return {
170
     return {
173
-        _notifications: enabled && !isAnyOverlayVisible && !calleeInfoVisible
174
-            ? notifications : []
171
+        _notifications: _visible ? notifications : []
175
     };
172
     };
176
 }
173
 }

+ 92
- 0
react/features/notifications/components/Notification.native.js Näytä tiedosto

1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text, TouchableOpacity, View } from 'react-native';
5
+
6
+import { Icon } from '../../base/font-icons';
7
+import { translate } from '../../base/i18n';
8
+
9
+import AbstractNotification, {
10
+    type Props
11
+} from './AbstractNotification';
12
+import styles from './styles';
13
+
14
+/**
15
+ * Implements a React {@link Component} to display a notification.
16
+ *
17
+ * @extends Component
18
+ */
19
+class Notification extends AbstractNotification<Props> {
20
+    /**
21
+     * Implements React's {@link Component#render()}.
22
+     *
23
+     * @inheritdoc
24
+     * @returns {ReactElement}
25
+     */
26
+    render() {
27
+        const {
28
+            isDismissAllowed
29
+        } = this.props;
30
+
31
+        return (
32
+            <View
33
+                pointerEvents = 'box-none'
34
+                style = { styles.notification }>
35
+                <View style = { styles.contentColumn }>
36
+                    <View
37
+                        pointerEvents = 'box-none'
38
+                        style = { styles.notificationContent }>
39
+                        {
40
+                            this._renderContent()
41
+                        }
42
+                    </View>
43
+                </View>
44
+                {
45
+                    isDismissAllowed
46
+                    && <TouchableOpacity onPress = { this._onDismissed }>
47
+                        <Icon
48
+                            name = { 'close' }
49
+                            style = { styles.dismissIcon } />
50
+                    </TouchableOpacity>
51
+                }
52
+            </View>
53
+        );
54
+    }
55
+
56
+    /**
57
+     * Renders the notification's content. If the title or title key is present
58
+     * it will be just the title. Otherwise it will fallback to description.
59
+     *
60
+     * @returns {Array<ReactElement>}
61
+     * @private
62
+     */
63
+    _renderContent() {
64
+        const { t, title, titleArguments, titleKey } = this.props;
65
+        const titleText = title || (titleKey && t(titleKey, titleArguments));
66
+
67
+        if (titleText) {
68
+            return (
69
+                <Text
70
+                    numberOfLines = { 1 }
71
+                    style = { styles.contentText } >
72
+                    { titleText }
73
+                </Text>
74
+            );
75
+        }
76
+
77
+        return this._getDescription().map((line, index) => (
78
+            <Text
79
+                key = { index }
80
+                numberOfLines = { 1 }
81
+                style = { styles.contentText }>
82
+                { line }
83
+            </Text>
84
+        ));
85
+    }
86
+
87
+    _getDescription: () => Array<string>;
88
+
89
+    _onDismissed: () => void;
90
+}
91
+
92
+export default translate(Notification);

+ 67
- 0
react/features/notifications/components/NotificationsContainer.native.js Näytä tiedosto

1
+// @flow
2
+
3
+import React from 'react';
4
+import { View } from 'react-native';
5
+import { connect } from 'react-redux';
6
+
7
+import AbstractNotificationsContainer, {
8
+    _abstractMapStateToProps,
9
+    type Props as AbstractProps
10
+} from './AbstractNotificationsContainer';
11
+import Notification from './Notification';
12
+import styles from './styles';
13
+
14
+type Props = AbstractProps & {
15
+
16
+    /**
17
+     * Any custom styling applied to the notifications container.
18
+     */
19
+    style: Object
20
+};
21
+
22
+/**
23
+ * Implements a React {@link Component} which displays notifications and handles
24
+ * automatic dismissmal after a notification is shown for a defined timeout
25
+ * period.
26
+ *
27
+ * @extends {Component}
28
+ */
29
+class NotificationsContainer
30
+    extends AbstractNotificationsContainer<Props> {
31
+
32
+    /**
33
+     * Implements React's {@link Component#render()}.
34
+     *
35
+     * @inheritdoc
36
+     */
37
+    render() {
38
+        const { _notifications } = this.props;
39
+
40
+        if (!_notifications || !_notifications.length) {
41
+            return null;
42
+        }
43
+
44
+        return (
45
+            <View
46
+                pointerEvents = 'box-none'
47
+                style = { [
48
+                    styles.notificationContainer,
49
+                    this.props.style
50
+                ] } >
51
+                {
52
+                    _notifications.map(
53
+                        ({ props, uid }) => (
54
+                            <Notification
55
+                                { ...props }
56
+                                key = { uid }
57
+                                onDismissed = { this._onDismissed }
58
+                                uid = { uid } />))
59
+                }
60
+            </View>
61
+        );
62
+    }
63
+
64
+    _onDismissed: number => void;
65
+}
66
+
67
+export default connect(_abstractMapStateToProps)(NotificationsContainer);

+ 61
- 0
react/features/notifications/components/styles.js Näytä tiedosto

1
+// @flow
2
+
3
+import { BoxModel, createStyleSheet, ColorPalette } from '../../base/styles';
4
+
5
+/**
6
+ * The styles of the React {@code Components} of the feature notifications.
7
+ */
8
+export default createStyleSheet({
9
+
10
+    /**
11
+     * The content (left) column of the notification.
12
+     */
13
+    contentColumn: {
14
+        justifyContent: 'center',
15
+        flex: 1,
16
+        flexDirection: 'column',
17
+        paddingLeft: 1.5 * BoxModel.padding
18
+    },
19
+
20
+    /**
21
+     * Test style of the notification.
22
+     */
23
+    contentText: {
24
+        alignSelf: 'flex-start',
25
+        color: ColorPalette.white
26
+    },
27
+
28
+    /**
29
+     * Dismiss icon style.
30
+     */
31
+    dismissIcon: {
32
+        color: ColorPalette.white,
33
+        fontSize: 20,
34
+        padding: 1.5 * BoxModel.padding
35
+    },
36
+
37
+    /**
38
+     * Outermost view of a single notification.
39
+     */
40
+    notification: {
41
+        backgroundColor: '#768898',
42
+        flexDirection: 'row',
43
+        height: 48,
44
+        marginTop: 0.5 * BoxModel.margin
45
+    },
46
+
47
+    /**
48
+     * Outermost container of a list of notifications.
49
+     */
50
+    notificationContainer: {
51
+        flexGrow: 0,
52
+        justifyContent: 'flex-end'
53
+    },
54
+
55
+    /**
56
+     * Wrapper for the message.
57
+     */
58
+    notificationContent: {
59
+        flexDirection: 'column'
60
+    }
61
+});

+ 15
- 0
react/features/notifications/functions.js Näytä tiedosto

1
+import { toState } from '../base/redux';
2
+
3
+/**
4
+ * Tells whether or not the notifications are enabled and if there are any
5
+ * notifications to be displayed based on the current Redux state.
6
+ *
7
+ * @param {Object|Function} stateful - The redux store state.
8
+ * @returns {boolean}
9
+ */
10
+export function areThereNotifications(stateful) {
11
+    const state = toState(stateful);
12
+    const { enabled, notifications } = state['features/notifications'];
13
+
14
+    return enabled && notifications.length > 0;
15
+}

+ 1
- 0
react/features/notifications/index.js Näytä tiedosto

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

Loading…
Peruuta
Tallenna