Przeglądaj źródła

Merge pull request #2636 from zbettenbuk/calendar-permission-fix

Reorganize calendar access request flow
j8
virtuacoplenny 7 lat temu
rodzic
commit
01db70fd3d
No account linked to committer's email address

+ 6
- 1
lang/main.json Wyświetl plik

@@ -539,11 +539,16 @@
539 539
         "later": "Later",
540 540
         "next": "Upcoming",
541 541
         "nextMeeting": "next meeting",
542
-        "now": "Now"
542
+        "now": "Now",
543
+        "permissionButton": "Open settings",
544
+        "permissionMessage": "Calendar permission is required to list your meetings in the app."
543 545
     },
544 546
     "recentList": {
545 547
         "today": "Today",
546 548
         "yesterday": "Yesterday",
547 549
         "earlier": "Earlier"
550
+    },
551
+    "sectionList": {
552
+        "pullToRefresh": "Pull to refresh"
548 553
     }
549 554
 }

+ 52
- 2
react/features/base/react/components/native/NavigateSectionList.js Wyświetl plik

@@ -10,6 +10,10 @@ import {
10 10
 
11 11
 import styles, { UNDERLAY_COLOR } from './styles';
12 12
 
13
+import { translate } from '../../../i18n';
14
+
15
+import { Icon } from '../../../font-icons';
16
+
13 17
 type Props = {
14 18
 
15 19
     /**
@@ -17,6 +21,11 @@ type Props = {
17 21
      */
18 22
     disabled: boolean,
19 23
 
24
+    /**
25
+     * The translate function.
26
+     */
27
+    t: Function,
28
+
20 29
     /**
21 30
      * Function to be invoked when an item is pressed. The item's URL is passed.
22 31
      */
@@ -27,6 +36,11 @@ type Props = {
27 36
      */
28 37
     onRefresh: Function,
29 38
 
39
+    /**
40
+     * Function to override the rendered default empty list component.
41
+     */
42
+    renderListEmptyComponent: Function,
43
+
30 44
     /**
31 45
      * Sections to be rendered in the following format:
32 46
      *
@@ -53,7 +67,7 @@ type Props = {
53 67
  * property and navigates to (probably) meetings, such as the recent list
54 68
  * or the meeting list components.
55 69
  */
56
-export default class NavigateSectionList extends Component<Props> {
70
+class NavigateSectionList extends Component<Props> {
57 71
     /**
58 72
      * Constructor of the NavigateSectionList component.
59 73
      *
@@ -69,6 +83,8 @@ export default class NavigateSectionList extends Component<Props> {
69 83
         this._renderItem = this._renderItem.bind(this);
70 84
         this._renderItemLine = this._renderItemLine.bind(this);
71 85
         this._renderItemLines = this._renderItemLines.bind(this);
86
+        this._renderListEmptyComponent
87
+            = this._renderListEmptyComponent.bind(this);
72 88
         this._renderSection = this._renderSection.bind(this);
73 89
     }
74 90
 
@@ -80,12 +96,16 @@ export default class NavigateSectionList extends Component<Props> {
80 96
      * @inheritdoc
81 97
      */
82 98
     render() {
83
-        const { sections } = this.props;
99
+        const { renderListEmptyComponent, sections } = this.props;
84 100
 
85 101
         return (
86 102
             <SafeAreaView
87 103
                 style = { styles.container } >
88 104
                 <SectionList
105
+                    ListEmptyComponent = {
106
+                        renderListEmptyComponent
107
+                        || this._renderListEmptyComponent
108
+                    }
89 109
                     keyExtractor = { this._getItemKey }
90 110
                     onRefresh = { this._onRefresh }
91 111
                     refreshing = { false }
@@ -274,6 +294,34 @@ export default class NavigateSectionList extends Component<Props> {
274 294
         return null;
275 295
     }
276 296
 
297
+    _renderListEmptyComponent: () => Object
298
+
299
+    /**
300
+     * Renders a component to display when the list is empty.
301
+     *
302
+     * @private
303
+     * @param {Object} section - The section being rendered.
304
+     * @returns {React$Node}
305
+     */
306
+    _renderListEmptyComponent() {
307
+        const { t, onRefresh } = this.props;
308
+
309
+        if (typeof onRefresh === 'function') {
310
+            return (
311
+                <View style = { styles.pullToRefresh }>
312
+                    <Text style = { styles.pullToRefreshText }>
313
+                        { t('sectionList.pullToRefresh') }
314
+                    </Text>
315
+                    <Icon
316
+                        name = 'menu-down'
317
+                        style = { styles.pullToRefreshIcon } />
318
+                </View>
319
+            );
320
+        }
321
+
322
+        return null;
323
+    }
324
+
277 325
     _renderSection: Object => Object
278 326
 
279 327
     /**
@@ -293,3 +341,5 @@ export default class NavigateSectionList extends Component<Props> {
293 341
         );
294 342
     }
295 343
 }
344
+
345
+export default translate(NavigateSectionList);

+ 19
- 0
react/features/base/react/components/native/styles.js Wyświetl plik

@@ -180,6 +180,25 @@ const SECTION_LIST_STYLES = {
180 180
         fontWeight: 'normal'
181 181
     },
182 182
 
183
+    pullToRefresh: {
184
+        alignItems: 'center',
185
+        flex: 1,
186
+        flexDirection: 'column',
187
+        justifyContent: 'center',
188
+        padding: 20
189
+    },
190
+
191
+    pullToRefreshIcon: {
192
+        backgroundColor: 'transparent',
193
+        color: OVERLAY_FONT_COLOR,
194
+        fontSize: 20
195
+    },
196
+
197
+    pullToRefreshText: {
198
+        backgroundColor: 'transparent',
199
+        color: OVERLAY_FONT_COLOR
200
+    },
201
+
183 202
     touchableView: {
184 203
         flexDirection: 'row'
185 204
     }

+ 7
- 0
react/features/calendar-sync/actionTypes.js Wyświetl plik

@@ -1,5 +1,12 @@
1 1
 // @flow
2 2
 
3
+/**
4
+ * Action to signal that calendar access has already been requested
5
+ * since the app started, so no new request should be done unless the
6
+ * user explicitly tries to refresh the calendar view.
7
+ */
8
+export const CALENDAR_ACCESS_REQUESTED = Symbol('CALENDAR_ACCESS_REQUESTED');
9
+
3 10
 /**
4 11
  * Action to update the current calendar entry list in the store.
5 12
  */

+ 24
- 2
react/features/calendar-sync/actions.js Wyświetl plik

@@ -1,10 +1,28 @@
1 1
 // @flow
2 2
 import {
3
+    CALENDAR_ACCESS_REQUESTED,
3 4
     NEW_CALENDAR_ENTRY_LIST,
4 5
     NEW_KNOWN_DOMAIN,
5 6
     REFRESH_CALENDAR_ENTRY_LIST
6 7
 } from './actionTypes';
7 8
 
9
+/**
10
+ * Sends an action to signal that a calendar access has been requested. For
11
+ * more info see the {@link CALENDAR_ACCESS_REQUESTED}.
12
+ *
13
+ * @param {string | undefined} status - The result of the last calendar
14
+ * access request.
15
+ * @returns {{
16
+ *   type: CALENDAR_ACCESS_REQUESTED
17
+ * }}
18
+ */
19
+export function updateCalendarAccessStatus(status: ?string) {
20
+    return {
21
+        status,
22
+        type: CALENDAR_ACCESS_REQUESTED
23
+    };
24
+}
25
+
8 26
 /**
9 27
  * Sends an action to add a new known domain if not present yet.
10 28
  *
@@ -24,12 +42,16 @@ export function maybeAddNewKnownDomain(domainName: string) {
24 42
 /**
25 43
  * Sends an action to refresh the entry list (fetches new data).
26 44
  *
45
+ * @param {boolean|undefined} forcePermission - Whether to force to re-ask
46
+ * for the permission or not.
27 47
  * @returns {{
28
- *   type: REFRESH_CALENDAR_ENTRY_LIST
48
+ *   type: REFRESH_CALENDAR_ENTRY_LIST,
49
+ *   forcePermission: boolean
29 50
  * }}
30 51
  */
31
-export function refreshCalendarEntryList() {
52
+export function refreshCalendarEntryList(forcePermission: boolean = false) {
32 53
     return {
54
+        forcePermission,
33 55
         type: REFRESH_CALENDAR_ENTRY_LIST
34 56
     };
35 57
 }

+ 64
- 16
react/features/calendar-sync/components/MeetingList.native.js Wyświetl plik

@@ -1,12 +1,16 @@
1 1
 // @flow
2 2
 import React, { Component } from 'react';
3
+import { Text, TouchableOpacity, View } from 'react-native';
3 4
 import { connect } from 'react-redux';
4 5
 
6
+import styles from './styles';
7
+
5 8
 import { refreshCalendarEntryList } from '../actions';
6 9
 
7 10
 import { appNavigate } from '../../app';
8 11
 import { getLocalizedDateFormatter, translate } from '../../base/i18n';
9 12
 import { NavigateSectionList } from '../../base/react';
13
+import { openSettings } from '../../mobile/permissions';
10 14
 
11 15
 type Props = {
12 16
 
@@ -28,6 +32,11 @@ type Props = {
28 32
      */
29 33
     displayed: boolean,
30 34
 
35
+    /**
36
+     * The current state of the calendar access permission.
37
+     */
38
+    _calendarAccessStatus: string,
39
+
31 40
     /**
32 41
      * The calendar event list.
33 42
      */
@@ -43,8 +52,6 @@ type Props = {
43 52
  * Component to display a list of events from the (mobile) user's calendar.
44 53
  */
45 54
 class MeetingList extends Component<Props> {
46
-    _initialLoaded: boolean
47
-
48 55
     /**
49 56
      * Default values for the component's props.
50 57
      */
@@ -60,6 +67,14 @@ class MeetingList extends Component<Props> {
60 67
     constructor(props) {
61 68
         super(props);
62 69
 
70
+        const { dispatch, displayed } = props;
71
+
72
+        if (displayed) {
73
+            dispatch(refreshCalendarEntryList());
74
+        }
75
+
76
+        this._getRenderListEmptyComponent
77
+            = this._getRenderListEmptyComponent.bind(this);
63 78
         this._onPress = this._onPress.bind(this);
64 79
         this._onRefresh = this._onRefresh.bind(this);
65 80
         this._toDisplayableItem = this._toDisplayableItem.bind(this);
@@ -73,16 +88,11 @@ class MeetingList extends Component<Props> {
73 88
      * @inheritdoc
74 89
      */
75 90
     componentWillReceiveProps(newProps) {
76
-        // This is a conditional logic to refresh the calendar entries (thus
77
-        // to request access to calendar) on component first receives a
78
-        // displayed=true prop - to avoid requesting calendar access on
79
-        // app start.
80
-        if (!this._initialLoaded
81
-                && newProps.displayed
82
-                && !this.props.displayed) {
91
+        const { displayed } = this.props;
92
+
93
+        if (newProps.displayed && !displayed) {
83 94
             const { dispatch } = this.props;
84 95
 
85
-            this._initialLoaded = true;
86 96
             dispatch(refreshCalendarEntryList());
87 97
         }
88 98
     }
@@ -100,10 +110,45 @@ class MeetingList extends Component<Props> {
100 110
                 disabled = { disabled }
101 111
                 onPress = { this._onPress }
102 112
                 onRefresh = { this._onRefresh }
113
+                renderListEmptyComponent = {
114
+                    this._getRenderListEmptyComponent
115
+                }
103 116
                 sections = { this._toDisplayableList() } />
104 117
         );
105 118
     }
106 119
 
120
+    _getRenderListEmptyComponent: () => Object
121
+
122
+    /**
123
+     * Returns a list empty component if a custom one has to be rendered instead
124
+     * of the default one in the {@link NavigateSectionList}.
125
+     *
126
+     * @private
127
+     * @returns {Component}
128
+     */
129
+    _getRenderListEmptyComponent() {
130
+        const { _calendarAccessStatus, t } = this.props;
131
+
132
+        if (_calendarAccessStatus === 'denied') {
133
+            return (
134
+                <View style = { styles.noPermissionMessageView }>
135
+                    <Text style = { styles.noPermissionMessageText }>
136
+                        { t('calendarSync.permissionMessage') }
137
+                    </Text>
138
+                    <TouchableOpacity
139
+                        onPress = { openSettings }
140
+                        style = { styles.noPermissionMessageButton } >
141
+                        <Text style = { styles.noPermissionMessageButtonText }>
142
+                            { t('calendarSync.permissionButton') }
143
+                        </Text>
144
+                    </TouchableOpacity>
145
+                </View>
146
+            );
147
+        }
148
+
149
+        return null;
150
+    }
151
+
107 152
     _onPress: string => Function
108 153
 
109 154
     /**
@@ -130,7 +175,7 @@ class MeetingList extends Component<Props> {
130 175
     _onRefresh() {
131 176
         const { dispatch } = this.props;
132 177
 
133
-        dispatch(refreshCalendarEntryList());
178
+        dispatch(refreshCalendarEntryList(true));
134 179
     }
135 180
 
136 181
     _toDisplayableItem: Object => Object
@@ -219,12 +264,12 @@ class MeetingList extends Component<Props> {
219 264
      * @returns {string}
220 265
      */
221 266
     _toDateString(event) {
222
-        /* eslint-disable max-len */
223
-        const startDateTime = getLocalizedDateFormatter(event.startDate).format('lll');
224
-        const endTime = getLocalizedDateFormatter(event.endDate).format('LT');
267
+        const startDateTime
268
+            = getLocalizedDateFormatter(event.startDate).format('lll');
269
+        const endTime
270
+            = getLocalizedDateFormatter(event.endDate).format('LT');
225 271
 
226 272
         return `${startDateTime} - ${endTime}`;
227
-        /* eslint-enable max-len */
228 273
     }
229 274
 }
230 275
 
@@ -237,8 +282,11 @@ class MeetingList extends Component<Props> {
237 282
  * }}
238 283
  */
239 284
 export function _mapStateToProps(state: Object) {
285
+    const calendarSyncState = state['features/calendar-sync'];
286
+
240 287
     return {
241
-        _eventList: state['features/calendar-sync'].events
288
+        _calendarAccessStatus: calendarSyncState.calendarAccessStatus,
289
+        _eventList: calendarSyncState.events
242 290
     };
243 291
 }
244 292
 

+ 41
- 1
react/features/calendar-sync/components/styles.js Wyświetl plik

@@ -1,4 +1,4 @@
1
-import { createStyleSheet } from '../../base/styles';
1
+import { ColorPalette, createStyleSheet } from '../../base/styles';
2 2
 
3 3
 const NOTIFICATION_SIZE = 55;
4 4
 
@@ -8,6 +8,46 @@ const NOTIFICATION_SIZE = 55;
8 8
  */
9 9
 export default createStyleSheet({
10 10
 
11
+    /**
12
+     * Button style of the open settings button.
13
+     */
14
+    noPermissionMessageButton: {
15
+        backgroundColor: ColorPalette.blue,
16
+        borderColor: ColorPalette.blue,
17
+        borderRadius: 4,
18
+        borderWidth: 1,
19
+        height: 30,
20
+        justifyContent: 'center',
21
+        margin: 15,
22
+        paddingHorizontal: 20
23
+    },
24
+
25
+    /**
26
+     * Text style of the open settings button.
27
+     */
28
+    noPermissionMessageButtonText: {
29
+        color: ColorPalette.white
30
+    },
31
+
32
+    /**
33
+     * Text style of the no permission message.
34
+     */
35
+    noPermissionMessageText: {
36
+        backgroundColor: 'transparent',
37
+        color: 'rgba(255, 255, 255, 0.6)'
38
+    },
39
+
40
+    /**
41
+     * Top level view of the no permission message.
42
+     */
43
+    noPermissionMessageView: {
44
+        alignItems: 'center',
45
+        flex: 1,
46
+        flexDirection: 'column',
47
+        justifyContent: 'center',
48
+        padding: 20
49
+    },
50
+
11 51
     /**
12 52
      * The top level container of the notification.
13 53
      */

+ 1
- 0
react/features/calendar-sync/index.js Wyświetl plik

@@ -1,3 +1,4 @@
1
+export * from './actions';
1 2
 export * from './components';
2 3
 
3 4
 import './middleware';

+ 145
- 66
react/features/calendar-sync/middleware.js Wyświetl plik

@@ -2,13 +2,18 @@
2 2
 import Logger from 'jitsi-meet-logger';
3 3
 import RNCalendarEvents from 'react-native-calendar-events';
4 4
 
5
+import { APP_WILL_MOUNT } from '../app';
5 6
 import { SET_ROOM } from '../base/conference';
6 7
 import { MiddlewareRegistry } from '../base/redux';
7 8
 import { APP_LINK_SCHEME, parseURIString } from '../base/util';
9
+import { APP_STATE_CHANGED } from '../mobile/background';
8 10
 
9
-import { APP_WILL_MOUNT } from '../app';
10 11
 
11
-import { maybeAddNewKnownDomain, updateCalendarEntryList } from './actions';
12
+import {
13
+    maybeAddNewKnownDomain,
14
+    updateCalendarAccessStatus,
15
+    updateCalendarEntryList
16
+} from './actions';
12 17
 import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
13 18
 
14 19
 const FETCH_END_DAYS = 10;
@@ -20,12 +25,15 @@ MiddlewareRegistry.register(store => next => action => {
20 25
     const result = next(action);
21 26
 
22 27
     switch (action.type) {
28
+    case APP_STATE_CHANGED:
29
+        _maybeClearAccessStatus(store, action);
30
+        break;
23 31
     case APP_WILL_MOUNT:
24 32
         _ensureDefaultServer(store);
25
-        _fetchCalendarEntries(store, false);
33
+        _fetchCalendarEntries(store, false, false);
26 34
         break;
27 35
     case REFRESH_CALENDAR_ENTRY_LIST:
28
-        _fetchCalendarEntries(store, true);
36
+        _fetchCalendarEntries(store, true, action.forcePermission);
29 37
         break;
30 38
     case SET_ROOM:
31 39
         _parseAndAddDomain(store);
@@ -34,34 +42,53 @@ MiddlewareRegistry.register(store => next => action => {
34 42
     return result;
35 43
 });
36 44
 
45
+/**
46
+ * Clears the calendar access status when the app comes back from
47
+ * the background. This is needed as some users may never quit the
48
+ * app, but puts it into the background and we need to try to request
49
+ * for a permission as often as possible, but not annoyingly often.
50
+ *
51
+ * @private
52
+ * @param {Object} store - The redux store.
53
+ * @param {Object} action - The Redux action.
54
+ * @returns {void}
55
+ */
56
+function _maybeClearAccessStatus(store, action) {
57
+    const { appState } = action;
58
+
59
+    if (appState === 'background') {
60
+        const { dispatch } = store;
61
+
62
+        dispatch(updateCalendarAccessStatus(undefined));
63
+    }
64
+}
65
+
37 66
 /**
38 67
  * Ensures calendar access if possible and resolves the promise if it's granted.
39 68
  *
40 69
  * @private
41 70
  * @param {boolean} promptForPermission - Flag to tell the app if it should
42 71
  * prompt for a calendar permission if it wasn't granted yet.
72
+ * @param {Function} dispatch - The Redux dispatch function.
43 73
  * @returns {Promise}
44 74
  */
45
-function _ensureCalendarAccess(promptForPermission) {
75
+function _ensureCalendarAccess(promptForPermission, dispatch) {
46 76
     return new Promise((resolve, reject) => {
47 77
         RNCalendarEvents.authorizationStatus()
48 78
             .then(status => {
49 79
                 if (status === 'authorized') {
50
-                    resolve();
80
+                    resolve(true);
51 81
                 } else if (promptForPermission) {
52 82
                     RNCalendarEvents.authorizeEventStore()
53 83
                         .then(result => {
54
-                            if (result === 'authorized') {
55
-                                resolve();
56
-                            } else {
57
-                                reject(result);
58
-                            }
84
+                            dispatch(updateCalendarAccessStatus(result));
85
+                            resolve(result === 'authorized');
59 86
                         })
60 87
                         .catch(error => {
61 88
                             reject(error);
62 89
                         });
63 90
                 } else {
64
-                    reject(status);
91
+                    resolve(false);
65 92
                 }
66 93
             })
67 94
             .catch(error => {
@@ -91,64 +118,49 @@ function _ensureDefaultServer(store) {
91 118
  *
92 119
  * @private
93 120
  * @param {Object} store - The redux store.
94
- * @param {boolean} promptForPermission - Flag to tell the app if it should
121
+ * @param {boolean} maybePromptForPermission - Flag to tell the app if it should
95 122
  * prompt for a calendar permission if it wasn't granted yet.
123
+ * @param {boolean|undefined} forcePermission - Whether to force to re-ask
124
+ * for the permission or not.
96 125
  * @returns {void}
97 126
  */
98
-function _fetchCalendarEntries(store, promptForPermission) {
99
-    _ensureCalendarAccess(promptForPermission)
100
-    .then(() => {
101
-        const startDate = new Date();
102
-        const endDate = new Date();
103
-
104
-        startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
105
-        endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
106
-
107
-        RNCalendarEvents.fetchAllEvents(
108
-            startDate.getTime(),
109
-            endDate.getTime(),
110
-            []
111
-        )
112
-        .then(events => {
113
-            const { knownDomains } = store.getState()['features/calendar-sync'];
114
-            const eventList = [];
115
-
116
-            if (events && events.length) {
117
-                for (const event of events) {
118
-                    const jitsiURL = _getURLFromEvent(event, knownDomains);
119
-                    const now = Date.now();
120
-
121
-                    if (jitsiURL) {
122
-                        const eventStartDate = Date.parse(event.startDate);
123
-                        const eventEndDate = Date.parse(event.endDate);
124
-
125
-                        if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
126
-                            logger.warn(
127
-                                'Skipping calendar event due to invalid dates',
128
-                                event.title,
129
-                                event.startDate,
130
-                                event.endDate
131
-                            );
132
-                        } else if (eventEndDate > now) {
133
-                            eventList.push({
134
-                                endDate: eventEndDate,
135
-                                id: event.id,
136
-                                startDate: eventStartDate,
137
-                                title: event.title,
138
-                                url: jitsiURL
139
-                            });
140
-                        }
141
-                    }
142
-                }
143
-            }
127
+function _fetchCalendarEntries(
128
+        store,
129
+        maybePromptForPermission,
130
+        forcePermission
131
+) {
132
+    const { dispatch } = store;
133
+    const state = store.getState()['features/calendar-sync'];
134
+    const { calendarAccessStatus } = state;
135
+    const promptForPermission
136
+        = (maybePromptForPermission && !calendarAccessStatus)
137
+        || forcePermission;
138
+
139
+    _ensureCalendarAccess(promptForPermission, dispatch)
140
+    .then(accessGranted => {
141
+        if (accessGranted) {
142
+            const startDate = new Date();
143
+            const endDate = new Date();
144
+
145
+            startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
146
+            endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
144 147
 
145
-            store.dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
146
-                a.startDate - b.startDate
147
-            ).slice(0, MAX_LIST_LENGTH)));
148
-        })
149
-        .catch(error => {
150
-            logger.error('Error fetching calendar.', error);
151
-        });
148
+            RNCalendarEvents.fetchAllEvents(
149
+                startDate.getTime(),
150
+                endDate.getTime(),
151
+                []
152
+            )
153
+            .then(events => {
154
+                const { knownDomains } = state;
155
+
156
+                _updateCalendarEntries(events, knownDomains, dispatch);
157
+            })
158
+            .catch(error => {
159
+                logger.error('Error fetching calendar.', error);
160
+            });
161
+        } else {
162
+            logger.warn('Calendar access not granted.');
163
+        }
152 164
     })
153 165
     .catch(reason => {
154 166
         logger.error('Error accessing calendar.', reason);
@@ -209,3 +221,70 @@ function _parseAndAddDomain(store) {
209 221
 
210 222
     store.dispatch(maybeAddNewKnownDomain(locationURL.host));
211 223
 }
224
+
225
+/**
226
+ * Updates the calendar entries in Redux when new list is received.
227
+ *
228
+ * @private
229
+ * @param {Object} event - An event returned from the native calendar.
230
+ * @param {Array<string>} knownDomains - The known domain list.
231
+ * @returns {CalendarEntry}
232
+ */
233
+function _parseCalendarEntry(event, knownDomains) {
234
+    if (event) {
235
+        const jitsiURL = _getURLFromEvent(event, knownDomains);
236
+
237
+        if (jitsiURL) {
238
+            const eventStartDate = Date.parse(event.startDate);
239
+            const eventEndDate = Date.parse(event.endDate);
240
+
241
+            if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
242
+                logger.warn(
243
+                    'Skipping invalid calendar event',
244
+                    event.title,
245
+                    event.startDate,
246
+                    event.endDate
247
+                );
248
+            } else {
249
+                return {
250
+                    endDate: eventEndDate,
251
+                    id: event.id,
252
+                    startDate: eventStartDate,
253
+                    title: event.title,
254
+                    url: jitsiURL
255
+                };
256
+            }
257
+        }
258
+    }
259
+
260
+    return null;
261
+}
262
+
263
+/**
264
+ * Updates the calendar entries in Redux when new list is received.
265
+ *
266
+ * @private
267
+ * @param {Array<CalendarEntry>} events - The new event list.
268
+ * @param {Array<string>} knownDomains - The known domain list.
269
+ * @param {Function} dispatch - The Redux dispatch function.
270
+ * @returns {void}
271
+ */
272
+function _updateCalendarEntries(events, knownDomains, dispatch) {
273
+    if (events && events.length) {
274
+        const eventList = [];
275
+
276
+        for (const event of events) {
277
+            const calendarEntry
278
+                = _parseCalendarEntry(event, knownDomains);
279
+            const now = Date.now();
280
+
281
+            if (calendarEntry && calendarEntry.endDate > now) {
282
+                eventList.push(calendarEntry);
283
+            }
284
+        }
285
+
286
+        dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
287
+            a.startDate - b.startDate
288
+        ).slice(0, MAX_LIST_LENGTH)));
289
+    }
290
+}

+ 17
- 5
react/features/calendar-sync/reducer.js Wyświetl plik

@@ -3,13 +3,19 @@
3 3
 import { ReducerRegistry } from '../base/redux';
4 4
 import { PersistenceRegistry } from '../base/storage';
5 5
 
6
-import { NEW_CALENDAR_ENTRY_LIST, NEW_KNOWN_DOMAIN } from './actionTypes';
6
+import {
7
+    CALENDAR_ACCESS_REQUESTED,
8
+    NEW_CALENDAR_ENTRY_LIST,
9
+    NEW_KNOWN_DOMAIN
10
+} from './actionTypes';
7 11
 
8
-/**
9
- * ZB: this is an object, as further data is to come here, like:
10
- * - known domain list
11
- */
12 12
 const DEFAULT_STATE = {
13
+    /**
14
+     * Note: If features/calendar-sync ever gets persisted, do not persist the
15
+     * calendarAccessStatus value as it's needed to remain a runtime value to
16
+     * see if we need to re-request the calendar permission from the user.
17
+     */
18
+    calendarAccessStatus: undefined,
13 19
     events: [],
14 20
     knownDomains: []
15 21
 };
@@ -26,6 +32,12 @@ ReducerRegistry.register(
26 32
     STORE_NAME,
27 33
     (state = DEFAULT_STATE, action) => {
28 34
         switch (action.type) {
35
+        case CALENDAR_ACCESS_REQUESTED:
36
+            return {
37
+                ...state,
38
+                calendarAccessStatus: action.status
39
+            };
40
+
29 41
         case NEW_CALENDAR_ENTRY_LIST:
30 42
             return {
31 43
                 ...state,

+ 31
- 0
react/features/mobile/permissions/functions.js Wyświetl plik

@@ -0,0 +1,31 @@
1
+// @flow
2
+
3
+import { Alert, Linking, NativeModules } from 'react-native';
4
+
5
+import { Platform } from '../../base/react';
6
+
7
+/**
8
+ * Opens the settings panel for the current platform.
9
+ *
10
+ * @private
11
+ * @returns {void}
12
+ */
13
+export function openSettings() {
14
+    switch (Platform.OS) {
15
+    case 'android':
16
+        NativeModules.AndroidSettings.open().catch(() => {
17
+            Alert.alert(
18
+                'Error opening settings',
19
+                'Please open settings and grant the required permissions',
20
+                [
21
+                    { text: 'OK' }
22
+                ]
23
+            );
24
+        });
25
+        break;
26
+
27
+    case 'ios':
28
+        Linking.openURL('app-settings:');
29
+        break;
30
+    }
31
+}

+ 2
- 0
react/features/mobile/permissions/index.js Wyświetl plik

@@ -1 +1,3 @@
1
+export * from './functions';
2
+
1 3
 import './middleware';

+ 4
- 29
react/features/mobile/permissions/middleware.js Wyświetl plik

@@ -1,9 +1,10 @@
1 1
 /* @flow */
2 2
 
3
-import { Alert, Linking, NativeModules } from 'react-native';
3
+import { Alert } from 'react-native';
4
+
5
+import { openSettings } from './functions';
4 6
 
5 7
 import { isRoomValid } from '../../base/conference';
6
-import { Platform } from '../../base/react';
7 8
 import { MiddlewareRegistry } from '../../base/redux';
8 9
 import { TRACK_CREATE_ERROR } from '../../base/tracks';
9 10
 
@@ -64,35 +65,9 @@ function _alertPermissionErrorWithSettings(trackType) {
64 65
         [
65 66
             { text: 'Cancel' },
66 67
             {
67
-                onPress: _openSettings,
68
+                onPress: openSettings,
68 69
                 text: 'Settings'
69 70
             }
70 71
         ],
71 72
         { cancelable: false });
72 73
 }
73
-
74
-/**
75
- * Opens the settings panel for the current platform.
76
- *
77
- * @private
78
- * @returns {void}
79
- */
80
-function _openSettings() {
81
-    switch (Platform.OS) {
82
-    case 'android':
83
-        NativeModules.AndroidSettings.open().catch(() => {
84
-            Alert.alert(
85
-                'Error opening settings',
86
-                'Please open settings and grant the required permissions',
87
-                [
88
-                    { text: 'OK' }
89
-                ]
90
-            );
91
-        });
92
-        break;
93
-
94
-    case 'ios':
95
-        Linking.openURL('app-settings:');
96
-        break;
97
-    }
98
-}

+ 5
- 0
react/features/welcome/components/AbstractPagedList.js Wyświetl plik

@@ -14,6 +14,11 @@ type Props = {
14 14
      */
15 15
     disabled: boolean,
16 16
 
17
+    /**
18
+     * The Redux dispatch function.
19
+     */
20
+    dispatch: Function,
21
+
17 22
     /**
18 23
      * The i18n translate function
19 24
      */

+ 13
- 4
react/features/welcome/components/PagedList.ios.js Wyświetl plik

@@ -2,9 +2,10 @@
2 2
 
3 3
 import React from 'react';
4 4
 import { View, TabBarIOS } from 'react-native';
5
+import { connect } from 'react-redux';
5 6
 
6 7
 import { translate } from '../../base/i18n';
7
-import { MeetingList } from '../../calendar-sync';
8
+import { MeetingList, refreshCalendarEntryList } from '../../calendar-sync';
8 9
 import { RecentList } from '../../recent-list';
9 10
 
10 11
 import AbstractPagedList from './AbstractPagedList';
@@ -59,8 +60,7 @@ class PagedList extends AbstractPagedList {
59 60
                         selected = { pageIndex === 1 }
60 61
                         title = { t('welcomepage.calendar') } >
61 62
                         <MeetingList
62
-                            disabled = { disabled }
63
-                            displayed = { pageIndex === 1 } />
63
+                            disabled = { disabled } />
64 64
                     </TabBarIOS.Item>
65 65
                 </TabBarIOS>
66 66
             </View>
@@ -81,8 +81,17 @@ class PagedList extends AbstractPagedList {
81 81
             this.setState({
82 82
                 pageIndex: tabIndex
83 83
             });
84
+
85
+            if (tabIndex === 1) {
86
+                /**
87
+                 * This is a workaround as TabBarIOS doesn't invoke
88
+                 * componentWillReciveProps on prop change of the
89
+                 * MeetingList component.
90
+                 */
91
+                this.props.dispatch(refreshCalendarEntryList());
92
+            }
84 93
         };
85 94
     }
86 95
 }
87 96
 
88
-export default translate(PagedList);
97
+export default translate(connect()(PagedList));

Ładowanie…
Anuluj
Zapisz