Bladeren bron

Reorganize calendar access request flow

j8
zbettenbuk 7 jaren geleden
bovenliggende
commit
b258e0d397

+ 6
- 1
lang/main.json Bestand weergeven

539
         "later": "Later",
539
         "later": "Later",
540
         "next": "Upcoming",
540
         "next": "Upcoming",
541
         "nextMeeting": "next meeting",
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
     "recentList": {
546
     "recentList": {
545
         "today": "Today",
547
         "today": "Today",
546
         "yesterday": "Yesterday",
548
         "yesterday": "Yesterday",
547
         "earlier": "Earlier"
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 Bestand weergeven

10
 
10
 
11
 import styles, { UNDERLAY_COLOR } from './styles';
11
 import styles, { UNDERLAY_COLOR } from './styles';
12
 
12
 
13
+import { translate } from '../../../i18n';
14
+
15
+import { Icon } from '../../../font-icons';
16
+
13
 type Props = {
17
 type Props = {
14
 
18
 
15
     /**
19
     /**
17
      */
21
      */
18
     disabled: boolean,
22
     disabled: boolean,
19
 
23
 
24
+    /**
25
+     * The translate function.
26
+     */
27
+    t: Function,
28
+
20
     /**
29
     /**
21
      * Function to be invoked when an item is pressed. The item's URL is passed.
30
      * Function to be invoked when an item is pressed. The item's URL is passed.
22
      */
31
      */
27
      */
36
      */
28
     onRefresh: Function,
37
     onRefresh: Function,
29
 
38
 
39
+    /**
40
+     * Function to override the rendered default empty list component.
41
+     */
42
+    renderListEmptyComponent: Function,
43
+
30
     /**
44
     /**
31
      * Sections to be rendered in the following format:
45
      * Sections to be rendered in the following format:
32
      *
46
      *
53
  * property and navigates to (probably) meetings, such as the recent list
67
  * property and navigates to (probably) meetings, such as the recent list
54
  * or the meeting list components.
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
      * Constructor of the NavigateSectionList component.
72
      * Constructor of the NavigateSectionList component.
59
      *
73
      *
69
         this._renderItem = this._renderItem.bind(this);
83
         this._renderItem = this._renderItem.bind(this);
70
         this._renderItemLine = this._renderItemLine.bind(this);
84
         this._renderItemLine = this._renderItemLine.bind(this);
71
         this._renderItemLines = this._renderItemLines.bind(this);
85
         this._renderItemLines = this._renderItemLines.bind(this);
86
+        this._renderListEmptyComponent
87
+            = this._renderListEmptyComponent.bind(this);
72
         this._renderSection = this._renderSection.bind(this);
88
         this._renderSection = this._renderSection.bind(this);
73
     }
89
     }
74
 
90
 
80
      * @inheritdoc
96
      * @inheritdoc
81
      */
97
      */
82
     render() {
98
     render() {
83
-        const { sections } = this.props;
99
+        const { renderListEmptyComponent, sections } = this.props;
84
 
100
 
85
         return (
101
         return (
86
             <SafeAreaView
102
             <SafeAreaView
87
                 style = { styles.container } >
103
                 style = { styles.container } >
88
                 <SectionList
104
                 <SectionList
105
+                    ListEmptyComponent = {
106
+                        renderListEmptyComponent
107
+                        || this._renderListEmptyComponent
108
+                    }
89
                     keyExtractor = { this._getItemKey }
109
                     keyExtractor = { this._getItemKey }
90
                     onRefresh = { this._onRefresh }
110
                     onRefresh = { this._onRefresh }
91
                     refreshing = { false }
111
                     refreshing = { false }
274
         return null;
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
     _renderSection: Object => Object
325
     _renderSection: Object => Object
278
 
326
 
279
     /**
327
     /**
293
         );
341
         );
294
     }
342
     }
295
 }
343
 }
344
+
345
+export default translate(NavigateSectionList);

+ 19
- 0
react/features/base/react/components/native/styles.js Bestand weergeven

180
         fontWeight: 'normal'
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
     touchableView: {
202
     touchableView: {
184
         flexDirection: 'row'
203
         flexDirection: 'row'
185
     }
204
     }

+ 7
- 0
react/features/calendar-sync/actionTypes.js Bestand weergeven

1
 // @flow
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
  * Action to update the current calendar entry list in the store.
11
  * Action to update the current calendar entry list in the store.
5
  */
12
  */

+ 24
- 2
react/features/calendar-sync/actions.js Bestand weergeven

1
 // @flow
1
 // @flow
2
 import {
2
 import {
3
+    CALENDAR_ACCESS_REQUESTED,
3
     NEW_CALENDAR_ENTRY_LIST,
4
     NEW_CALENDAR_ENTRY_LIST,
4
     NEW_KNOWN_DOMAIN,
5
     NEW_KNOWN_DOMAIN,
5
     REFRESH_CALENDAR_ENTRY_LIST
6
     REFRESH_CALENDAR_ENTRY_LIST
6
 } from './actionTypes';
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
  * Sends an action to add a new known domain if not present yet.
27
  * Sends an action to add a new known domain if not present yet.
10
  *
28
  *
24
 /**
42
 /**
25
  * Sends an action to refresh the entry list (fetches new data).
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
  * @returns {{
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
     return {
53
     return {
54
+        forcePermission,
33
         type: REFRESH_CALENDAR_ENTRY_LIST
55
         type: REFRESH_CALENDAR_ENTRY_LIST
34
     };
56
     };
35
 }
57
 }

+ 64
- 16
react/features/calendar-sync/components/MeetingList.native.js Bestand weergeven

1
 // @flow
1
 // @flow
2
 import React, { Component } from 'react';
2
 import React, { Component } from 'react';
3
+import { Text, TouchableOpacity, View } from 'react-native';
3
 import { connect } from 'react-redux';
4
 import { connect } from 'react-redux';
4
 
5
 
6
+import styles from './styles';
7
+
5
 import { refreshCalendarEntryList } from '../actions';
8
 import { refreshCalendarEntryList } from '../actions';
6
 
9
 
7
 import { appNavigate } from '../../app';
10
 import { appNavigate } from '../../app';
8
 import { getLocalizedDateFormatter, translate } from '../../base/i18n';
11
 import { getLocalizedDateFormatter, translate } from '../../base/i18n';
9
 import { NavigateSectionList } from '../../base/react';
12
 import { NavigateSectionList } from '../../base/react';
13
+import { openSettings } from '../../mobile/permissions';
10
 
14
 
11
 type Props = {
15
 type Props = {
12
 
16
 
28
      */
32
      */
29
     displayed: boolean,
33
     displayed: boolean,
30
 
34
 
35
+    /**
36
+     * The current state of the calendar access permission.
37
+     */
38
+    _calendarAccessStatus: string,
39
+
31
     /**
40
     /**
32
      * The calendar event list.
41
      * The calendar event list.
33
      */
42
      */
43
  * Component to display a list of events from the (mobile) user's calendar.
52
  * Component to display a list of events from the (mobile) user's calendar.
44
  */
53
  */
45
 class MeetingList extends Component<Props> {
54
 class MeetingList extends Component<Props> {
46
-    _initialLoaded: boolean
47
-
48
     /**
55
     /**
49
      * Default values for the component's props.
56
      * Default values for the component's props.
50
      */
57
      */
60
     constructor(props) {
67
     constructor(props) {
61
         super(props);
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
         this._onPress = this._onPress.bind(this);
78
         this._onPress = this._onPress.bind(this);
64
         this._onRefresh = this._onRefresh.bind(this);
79
         this._onRefresh = this._onRefresh.bind(this);
65
         this._toDisplayableItem = this._toDisplayableItem.bind(this);
80
         this._toDisplayableItem = this._toDisplayableItem.bind(this);
73
      * @inheritdoc
88
      * @inheritdoc
74
      */
89
      */
75
     componentWillReceiveProps(newProps) {
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
             const { dispatch } = this.props;
94
             const { dispatch } = this.props;
84
 
95
 
85
-            this._initialLoaded = true;
86
             dispatch(refreshCalendarEntryList());
96
             dispatch(refreshCalendarEntryList());
87
         }
97
         }
88
     }
98
     }
100
                 disabled = { disabled }
110
                 disabled = { disabled }
101
                 onPress = { this._onPress }
111
                 onPress = { this._onPress }
102
                 onRefresh = { this._onRefresh }
112
                 onRefresh = { this._onRefresh }
113
+                renderListEmptyComponent = {
114
+                    this._getRenderListEmptyComponent
115
+                }
103
                 sections = { this._toDisplayableList() } />
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
     _onPress: string => Function
152
     _onPress: string => Function
108
 
153
 
109
     /**
154
     /**
130
     _onRefresh() {
175
     _onRefresh() {
131
         const { dispatch } = this.props;
176
         const { dispatch } = this.props;
132
 
177
 
133
-        dispatch(refreshCalendarEntryList());
178
+        dispatch(refreshCalendarEntryList(true));
134
     }
179
     }
135
 
180
 
136
     _toDisplayableItem: Object => Object
181
     _toDisplayableItem: Object => Object
219
      * @returns {string}
264
      * @returns {string}
220
      */
265
      */
221
     _toDateString(event) {
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
         return `${startDateTime} - ${endTime}`;
272
         return `${startDateTime} - ${endTime}`;
227
-        /* eslint-enable max-len */
228
     }
273
     }
229
 }
274
 }
230
 
275
 
237
  * }}
282
  * }}
238
  */
283
  */
239
 export function _mapStateToProps(state: Object) {
284
 export function _mapStateToProps(state: Object) {
285
+    const calendarSyncState = state['features/calendar-sync'];
286
+
240
     return {
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 Bestand weergeven

1
-import { createStyleSheet } from '../../base/styles';
1
+import { ColorPalette, createStyleSheet } from '../../base/styles';
2
 
2
 
3
 const NOTIFICATION_SIZE = 55;
3
 const NOTIFICATION_SIZE = 55;
4
 
4
 
8
  */
8
  */
9
 export default createStyleSheet({
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
      * The top level container of the notification.
52
      * The top level container of the notification.
13
      */
53
      */

+ 1
- 0
react/features/calendar-sync/index.js Bestand weergeven

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

+ 145
- 66
react/features/calendar-sync/middleware.js Bestand weergeven

2
 import Logger from 'jitsi-meet-logger';
2
 import Logger from 'jitsi-meet-logger';
3
 import RNCalendarEvents from 'react-native-calendar-events';
3
 import RNCalendarEvents from 'react-native-calendar-events';
4
 
4
 
5
+import { APP_WILL_MOUNT } from '../app';
5
 import { SET_ROOM } from '../base/conference';
6
 import { SET_ROOM } from '../base/conference';
6
 import { MiddlewareRegistry } from '../base/redux';
7
 import { MiddlewareRegistry } from '../base/redux';
7
 import { APP_LINK_SCHEME, parseURIString } from '../base/util';
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
 import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
17
 import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
13
 
18
 
14
 const FETCH_END_DAYS = 10;
19
 const FETCH_END_DAYS = 10;
20
     const result = next(action);
25
     const result = next(action);
21
 
26
 
22
     switch (action.type) {
27
     switch (action.type) {
28
+    case APP_STATE_CHANGED:
29
+        _maybeClearAccessStatus(store, action);
30
+        break;
23
     case APP_WILL_MOUNT:
31
     case APP_WILL_MOUNT:
24
         _ensureDefaultServer(store);
32
         _ensureDefaultServer(store);
25
-        _fetchCalendarEntries(store, false);
33
+        _fetchCalendarEntries(store, false, false);
26
         break;
34
         break;
27
     case REFRESH_CALENDAR_ENTRY_LIST:
35
     case REFRESH_CALENDAR_ENTRY_LIST:
28
-        _fetchCalendarEntries(store, true);
36
+        _fetchCalendarEntries(store, true, action.forcePermission);
29
         break;
37
         break;
30
     case SET_ROOM:
38
     case SET_ROOM:
31
         _parseAndAddDomain(store);
39
         _parseAndAddDomain(store);
34
     return result;
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
  * Ensures calendar access if possible and resolves the promise if it's granted.
67
  * Ensures calendar access if possible and resolves the promise if it's granted.
39
  *
68
  *
40
  * @private
69
  * @private
41
  * @param {boolean} promptForPermission - Flag to tell the app if it should
70
  * @param {boolean} promptForPermission - Flag to tell the app if it should
42
  * prompt for a calendar permission if it wasn't granted yet.
71
  * prompt for a calendar permission if it wasn't granted yet.
72
+ * @param {Function} dispatch - The Redux dispatch function.
43
  * @returns {Promise}
73
  * @returns {Promise}
44
  */
74
  */
45
-function _ensureCalendarAccess(promptForPermission) {
75
+function _ensureCalendarAccess(promptForPermission, dispatch) {
46
     return new Promise((resolve, reject) => {
76
     return new Promise((resolve, reject) => {
47
         RNCalendarEvents.authorizationStatus()
77
         RNCalendarEvents.authorizationStatus()
48
             .then(status => {
78
             .then(status => {
49
                 if (status === 'authorized') {
79
                 if (status === 'authorized') {
50
-                    resolve();
80
+                    resolve(true);
51
                 } else if (promptForPermission) {
81
                 } else if (promptForPermission) {
52
                     RNCalendarEvents.authorizeEventStore()
82
                     RNCalendarEvents.authorizeEventStore()
53
                         .then(result => {
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
                         .catch(error => {
87
                         .catch(error => {
61
                             reject(error);
88
                             reject(error);
62
                         });
89
                         });
63
                 } else {
90
                 } else {
64
-                    reject(status);
91
+                    resolve(false);
65
                 }
92
                 }
66
             })
93
             })
67
             .catch(error => {
94
             .catch(error => {
91
  *
118
  *
92
  * @private
119
  * @private
93
  * @param {Object} store - The redux store.
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
  * prompt for a calendar permission if it wasn't granted yet.
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
  * @returns {void}
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
     .catch(reason => {
165
     .catch(reason => {
154
         logger.error('Error accessing calendar.', reason);
166
         logger.error('Error accessing calendar.', reason);
209
 
221
 
210
     store.dispatch(maybeAddNewKnownDomain(locationURL.host));
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 Bestand weergeven

3
 import { ReducerRegistry } from '../base/redux';
3
 import { ReducerRegistry } from '../base/redux';
4
 import { PersistenceRegistry } from '../base/storage';
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
 const DEFAULT_STATE = {
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
     events: [],
19
     events: [],
14
     knownDomains: []
20
     knownDomains: []
15
 };
21
 };
26
     STORE_NAME,
32
     STORE_NAME,
27
     (state = DEFAULT_STATE, action) => {
33
     (state = DEFAULT_STATE, action) => {
28
         switch (action.type) {
34
         switch (action.type) {
35
+        case CALENDAR_ACCESS_REQUESTED:
36
+            return {
37
+                ...state,
38
+                calendarAccessStatus: action.status
39
+            };
40
+
29
         case NEW_CALENDAR_ENTRY_LIST:
41
         case NEW_CALENDAR_ENTRY_LIST:
30
             return {
42
             return {
31
                 ...state,
43
                 ...state,

+ 31
- 0
react/features/mobile/permissions/functions.js Bestand weergeven

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 Bestand weergeven

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

+ 4
- 29
react/features/mobile/permissions/middleware.js Bestand weergeven

1
 /* @flow */
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
 import { isRoomValid } from '../../base/conference';
7
 import { isRoomValid } from '../../base/conference';
6
-import { Platform } from '../../base/react';
7
 import { MiddlewareRegistry } from '../../base/redux';
8
 import { MiddlewareRegistry } from '../../base/redux';
8
 import { TRACK_CREATE_ERROR } from '../../base/tracks';
9
 import { TRACK_CREATE_ERROR } from '../../base/tracks';
9
 
10
 
64
         [
65
         [
65
             { text: 'Cancel' },
66
             { text: 'Cancel' },
66
             {
67
             {
67
-                onPress: _openSettings,
68
+                onPress: openSettings,
68
                 text: 'Settings'
69
                 text: 'Settings'
69
             }
70
             }
70
         ],
71
         ],
71
         { cancelable: false });
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 Bestand weergeven

14
      */
14
      */
15
     disabled: boolean,
15
     disabled: boolean,
16
 
16
 
17
+    /**
18
+     * The Redux dispatch function.
19
+     */
20
+    dispatch: Function,
21
+
17
     /**
22
     /**
18
      * The i18n translate function
23
      * The i18n translate function
19
      */
24
      */

+ 13
- 4
react/features/welcome/components/PagedList.ios.js Bestand weergeven

2
 
2
 
3
 import React from 'react';
3
 import React from 'react';
4
 import { View, TabBarIOS } from 'react-native';
4
 import { View, TabBarIOS } from 'react-native';
5
+import { connect } from 'react-redux';
5
 
6
 
6
 import { translate } from '../../base/i18n';
7
 import { translate } from '../../base/i18n';
7
-import { MeetingList } from '../../calendar-sync';
8
+import { MeetingList, refreshCalendarEntryList } from '../../calendar-sync';
8
 import { RecentList } from '../../recent-list';
9
 import { RecentList } from '../../recent-list';
9
 
10
 
10
 import AbstractPagedList from './AbstractPagedList';
11
 import AbstractPagedList from './AbstractPagedList';
59
                         selected = { pageIndex === 1 }
60
                         selected = { pageIndex === 1 }
60
                         title = { t('welcomepage.calendar') } >
61
                         title = { t('welcomepage.calendar') } >
61
                         <MeetingList
62
                         <MeetingList
62
-                            disabled = { disabled }
63
-                            displayed = { pageIndex === 1 } />
63
+                            disabled = { disabled } />
64
                     </TabBarIOS.Item>
64
                     </TabBarIOS.Item>
65
                 </TabBarIOS>
65
                 </TabBarIOS>
66
             </View>
66
             </View>
81
             this.setState({
81
             this.setState({
82
                 pageIndex: tabIndex
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));

Laden…
Annuleren
Opslaan