Reorganize calendar access request flow
This commit is contained in:
parent
ea431ca90c
commit
b258e0d397
|
@ -539,11 +539,16 @@
|
|||
"later": "Later",
|
||||
"next": "Upcoming",
|
||||
"nextMeeting": "next meeting",
|
||||
"now": "Now"
|
||||
"now": "Now",
|
||||
"permissionButton": "Open settings",
|
||||
"permissionMessage": "Calendar permission is required to list your meetings in the app."
|
||||
},
|
||||
"recentList": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"earlier": "Earlier"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Pull to refresh"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ import {
|
|||
|
||||
import styles, { UNDERLAY_COLOR } from './styles';
|
||||
|
||||
import { translate } from '../../../i18n';
|
||||
|
||||
import { Icon } from '../../../font-icons';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
|
@ -17,6 +21,11 @@ type Props = {
|
|||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||
*/
|
||||
|
@ -27,6 +36,11 @@ type Props = {
|
|||
*/
|
||||
onRefresh: Function,
|
||||
|
||||
/**
|
||||
* Function to override the rendered default empty list component.
|
||||
*/
|
||||
renderListEmptyComponent: Function,
|
||||
|
||||
/**
|
||||
* Sections to be rendered in the following format:
|
||||
*
|
||||
|
@ -53,7 +67,7 @@ type Props = {
|
|||
* property and navigates to (probably) meetings, such as the recent list
|
||||
* or the meeting list components.
|
||||
*/
|
||||
export default class NavigateSectionList extends Component<Props> {
|
||||
class NavigateSectionList extends Component<Props> {
|
||||
/**
|
||||
* Constructor of the NavigateSectionList component.
|
||||
*
|
||||
|
@ -69,6 +83,8 @@ export default class NavigateSectionList extends Component<Props> {
|
|||
this._renderItem = this._renderItem.bind(this);
|
||||
this._renderItemLine = this._renderItemLine.bind(this);
|
||||
this._renderItemLines = this._renderItemLines.bind(this);
|
||||
this._renderListEmptyComponent
|
||||
= this._renderListEmptyComponent.bind(this);
|
||||
this._renderSection = this._renderSection.bind(this);
|
||||
}
|
||||
|
||||
|
@ -80,12 +96,16 @@ export default class NavigateSectionList extends Component<Props> {
|
|||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { sections } = this.props;
|
||||
const { renderListEmptyComponent, sections } = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style = { styles.container } >
|
||||
<SectionList
|
||||
ListEmptyComponent = {
|
||||
renderListEmptyComponent
|
||||
|| this._renderListEmptyComponent
|
||||
}
|
||||
keyExtractor = { this._getItemKey }
|
||||
onRefresh = { this._onRefresh }
|
||||
refreshing = { false }
|
||||
|
@ -274,6 +294,34 @@ export default class NavigateSectionList extends Component<Props> {
|
|||
return null;
|
||||
}
|
||||
|
||||
_renderListEmptyComponent: () => Object
|
||||
|
||||
/**
|
||||
* Renders a component to display when the list is empty.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderListEmptyComponent() {
|
||||
const { t, onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
return (
|
||||
<View style = { styles.pullToRefresh }>
|
||||
<Text style = { styles.pullToRefreshText }>
|
||||
{ t('sectionList.pullToRefresh') }
|
||||
</Text>
|
||||
<Icon
|
||||
name = 'menu-down'
|
||||
style = { styles.pullToRefreshIcon } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_renderSection: Object => Object
|
||||
|
||||
/**
|
||||
|
@ -293,3 +341,5 @@ export default class NavigateSectionList extends Component<Props> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(NavigateSectionList);
|
||||
|
|
|
@ -180,6 +180,25 @@ const SECTION_LIST_STYLES = {
|
|||
fontWeight: 'normal'
|
||||
},
|
||||
|
||||
pullToRefresh: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: 20
|
||||
},
|
||||
|
||||
pullToRefreshIcon: {
|
||||
backgroundColor: 'transparent',
|
||||
color: OVERLAY_FONT_COLOR,
|
||||
fontSize: 20
|
||||
},
|
||||
|
||||
pullToRefreshText: {
|
||||
backgroundColor: 'transparent',
|
||||
color: OVERLAY_FONT_COLOR
|
||||
},
|
||||
|
||||
touchableView: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* Action to signal that calendar access has already been requested
|
||||
* since the app started, so no new request should be done unless the
|
||||
* user explicitly tries to refresh the calendar view.
|
||||
*/
|
||||
export const CALENDAR_ACCESS_REQUESTED = Symbol('CALENDAR_ACCESS_REQUESTED');
|
||||
|
||||
/**
|
||||
* Action to update the current calendar entry list in the store.
|
||||
*/
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
// @flow
|
||||
import {
|
||||
CALENDAR_ACCESS_REQUESTED,
|
||||
NEW_CALENDAR_ENTRY_LIST,
|
||||
NEW_KNOWN_DOMAIN,
|
||||
REFRESH_CALENDAR_ENTRY_LIST
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sends an action to signal that a calendar access has been requested. For
|
||||
* more info see the {@link CALENDAR_ACCESS_REQUESTED}.
|
||||
*
|
||||
* @param {string | undefined} status - The result of the last calendar
|
||||
* access request.
|
||||
* @returns {{
|
||||
* type: CALENDAR_ACCESS_REQUESTED
|
||||
* }}
|
||||
*/
|
||||
export function updateCalendarAccessStatus(status: ?string) {
|
||||
return {
|
||||
status,
|
||||
type: CALENDAR_ACCESS_REQUESTED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an action to add a new known domain if not present yet.
|
||||
*
|
||||
|
@ -24,12 +42,16 @@ export function maybeAddNewKnownDomain(domainName: string) {
|
|||
/**
|
||||
* Sends an action to refresh the entry list (fetches new data).
|
||||
*
|
||||
* @param {boolean|undefined} forcePermission - Whether to force to re-ask
|
||||
* for the permission or not.
|
||||
* @returns {{
|
||||
* type: REFRESH_CALENDAR_ENTRY_LIST
|
||||
* type: REFRESH_CALENDAR_ENTRY_LIST,
|
||||
* forcePermission: boolean
|
||||
* }}
|
||||
*/
|
||||
export function refreshCalendarEntryList() {
|
||||
export function refreshCalendarEntryList(forcePermission: boolean = false) {
|
||||
return {
|
||||
forcePermission,
|
||||
type: REFRESH_CALENDAR_ENTRY_LIST
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
import { refreshCalendarEntryList } from '../actions';
|
||||
|
||||
import { appNavigate } from '../../app';
|
||||
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
||||
import { NavigateSectionList } from '../../base/react';
|
||||
import { openSettings } from '../../mobile/permissions';
|
||||
|
||||
type Props = {
|
||||
|
||||
|
@ -28,6 +32,11 @@ type Props = {
|
|||
*/
|
||||
displayed: boolean,
|
||||
|
||||
/**
|
||||
* The current state of the calendar access permission.
|
||||
*/
|
||||
_calendarAccessStatus: string,
|
||||
|
||||
/**
|
||||
* The calendar event list.
|
||||
*/
|
||||
|
@ -43,8 +52,6 @@ type Props = {
|
|||
* Component to display a list of events from the (mobile) user's calendar.
|
||||
*/
|
||||
class MeetingList extends Component<Props> {
|
||||
_initialLoaded: boolean
|
||||
|
||||
/**
|
||||
* Default values for the component's props.
|
||||
*/
|
||||
|
@ -60,6 +67,14 @@ class MeetingList extends Component<Props> {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { dispatch, displayed } = props;
|
||||
|
||||
if (displayed) {
|
||||
dispatch(refreshCalendarEntryList());
|
||||
}
|
||||
|
||||
this._getRenderListEmptyComponent
|
||||
= this._getRenderListEmptyComponent.bind(this);
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._onRefresh = this._onRefresh.bind(this);
|
||||
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
||||
|
@ -73,16 +88,11 @@ class MeetingList extends Component<Props> {
|
|||
* @inheritdoc
|
||||
*/
|
||||
componentWillReceiveProps(newProps) {
|
||||
// This is a conditional logic to refresh the calendar entries (thus
|
||||
// to request access to calendar) on component first receives a
|
||||
// displayed=true prop - to avoid requesting calendar access on
|
||||
// app start.
|
||||
if (!this._initialLoaded
|
||||
&& newProps.displayed
|
||||
&& !this.props.displayed) {
|
||||
const { displayed } = this.props;
|
||||
|
||||
if (newProps.displayed && !displayed) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
this._initialLoaded = true;
|
||||
dispatch(refreshCalendarEntryList());
|
||||
}
|
||||
}
|
||||
|
@ -100,10 +110,45 @@ class MeetingList extends Component<Props> {
|
|||
disabled = { disabled }
|
||||
onPress = { this._onPress }
|
||||
onRefresh = { this._onRefresh }
|
||||
renderListEmptyComponent = {
|
||||
this._getRenderListEmptyComponent
|
||||
}
|
||||
sections = { this._toDisplayableList() } />
|
||||
);
|
||||
}
|
||||
|
||||
_getRenderListEmptyComponent: () => Object
|
||||
|
||||
/**
|
||||
* Returns a list empty component if a custom one has to be rendered instead
|
||||
* of the default one in the {@link NavigateSectionList}.
|
||||
*
|
||||
* @private
|
||||
* @returns {Component}
|
||||
*/
|
||||
_getRenderListEmptyComponent() {
|
||||
const { _calendarAccessStatus, t } = this.props;
|
||||
|
||||
if (_calendarAccessStatus === 'denied') {
|
||||
return (
|
||||
<View style = { styles.noPermissionMessageView }>
|
||||
<Text style = { styles.noPermissionMessageText }>
|
||||
{ t('calendarSync.permissionMessage') }
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress = { openSettings }
|
||||
style = { styles.noPermissionMessageButton } >
|
||||
<Text style = { styles.noPermissionMessageButtonText }>
|
||||
{ t('calendarSync.permissionButton') }
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_onPress: string => Function
|
||||
|
||||
/**
|
||||
|
@ -130,7 +175,7 @@ class MeetingList extends Component<Props> {
|
|||
_onRefresh() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(refreshCalendarEntryList());
|
||||
dispatch(refreshCalendarEntryList(true));
|
||||
}
|
||||
|
||||
_toDisplayableItem: Object => Object
|
||||
|
@ -219,12 +264,12 @@ class MeetingList extends Component<Props> {
|
|||
* @returns {string}
|
||||
*/
|
||||
_toDateString(event) {
|
||||
/* eslint-disable max-len */
|
||||
const startDateTime = getLocalizedDateFormatter(event.startDate).format('lll');
|
||||
const endTime = getLocalizedDateFormatter(event.endDate).format('LT');
|
||||
const startDateTime
|
||||
= getLocalizedDateFormatter(event.startDate).format('lll');
|
||||
const endTime
|
||||
= getLocalizedDateFormatter(event.endDate).format('LT');
|
||||
|
||||
return `${startDateTime} - ${endTime}`;
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,8 +282,11 @@ class MeetingList extends Component<Props> {
|
|||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
const calendarSyncState = state['features/calendar-sync'];
|
||||
|
||||
return {
|
||||
_eventList: state['features/calendar-sync'].events
|
||||
_calendarAccessStatus: calendarSyncState.calendarAccessStatus,
|
||||
_eventList: calendarSyncState.events
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createStyleSheet } from '../../base/styles';
|
||||
import { ColorPalette, createStyleSheet } from '../../base/styles';
|
||||
|
||||
const NOTIFICATION_SIZE = 55;
|
||||
|
||||
|
@ -8,6 +8,46 @@ const NOTIFICATION_SIZE = 55;
|
|||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* Button style of the open settings button.
|
||||
*/
|
||||
noPermissionMessageButton: {
|
||||
backgroundColor: ColorPalette.blue,
|
||||
borderColor: ColorPalette.blue,
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
height: 30,
|
||||
justifyContent: 'center',
|
||||
margin: 15,
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* Text style of the open settings button.
|
||||
*/
|
||||
noPermissionMessageButtonText: {
|
||||
color: ColorPalette.white
|
||||
},
|
||||
|
||||
/**
|
||||
* Text style of the no permission message.
|
||||
*/
|
||||
noPermissionMessageText: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'rgba(255, 255, 255, 0.6)'
|
||||
},
|
||||
|
||||
/**
|
||||
* Top level view of the no permission message.
|
||||
*/
|
||||
noPermissionMessageView: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* The top level container of the notification.
|
||||
*/
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
|
|
|
@ -2,13 +2,18 @@
|
|||
import Logger from 'jitsi-meet-logger';
|
||||
import RNCalendarEvents from 'react-native-calendar-events';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../app';
|
||||
import { SET_ROOM } from '../base/conference';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
|
||||
import { APP_STATE_CHANGED } from '../mobile/background';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../app';
|
||||
|
||||
import { maybeAddNewKnownDomain, updateCalendarEntryList } from './actions';
|
||||
import {
|
||||
maybeAddNewKnownDomain,
|
||||
updateCalendarAccessStatus,
|
||||
updateCalendarEntryList
|
||||
} from './actions';
|
||||
import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
|
||||
|
||||
const FETCH_END_DAYS = 10;
|
||||
|
@ -20,12 +25,15 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case APP_STATE_CHANGED:
|
||||
_maybeClearAccessStatus(store, action);
|
||||
break;
|
||||
case APP_WILL_MOUNT:
|
||||
_ensureDefaultServer(store);
|
||||
_fetchCalendarEntries(store, false);
|
||||
_fetchCalendarEntries(store, false, false);
|
||||
break;
|
||||
case REFRESH_CALENDAR_ENTRY_LIST:
|
||||
_fetchCalendarEntries(store, true);
|
||||
_fetchCalendarEntries(store, true, action.forcePermission);
|
||||
break;
|
||||
case SET_ROOM:
|
||||
_parseAndAddDomain(store);
|
||||
|
@ -34,34 +42,53 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Clears the calendar access status when the app comes back from
|
||||
* the background. This is needed as some users may never quit the
|
||||
* app, but puts it into the background and we need to try to request
|
||||
* for a permission as often as possible, but not annoyingly often.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {Object} action - The Redux action.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeClearAccessStatus(store, action) {
|
||||
const { appState } = action;
|
||||
|
||||
if (appState === 'background') {
|
||||
const { dispatch } = store;
|
||||
|
||||
dispatch(updateCalendarAccessStatus(undefined));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures calendar access if possible and resolves the promise if it's granted.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} promptForPermission - Flag to tell the app if it should
|
||||
* prompt for a calendar permission if it wasn't granted yet.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _ensureCalendarAccess(promptForPermission) {
|
||||
function _ensureCalendarAccess(promptForPermission, dispatch) {
|
||||
return new Promise((resolve, reject) => {
|
||||
RNCalendarEvents.authorizationStatus()
|
||||
.then(status => {
|
||||
if (status === 'authorized') {
|
||||
resolve();
|
||||
resolve(true);
|
||||
} else if (promptForPermission) {
|
||||
RNCalendarEvents.authorizeEventStore()
|
||||
.then(result => {
|
||||
if (result === 'authorized') {
|
||||
resolve();
|
||||
} else {
|
||||
reject(result);
|
||||
}
|
||||
dispatch(updateCalendarAccessStatus(result));
|
||||
resolve(result === 'authorized');
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
reject(status);
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -91,13 +118,27 @@ function _ensureDefaultServer(store) {
|
|||
*
|
||||
* @private
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {boolean} promptForPermission - Flag to tell the app if it should
|
||||
* @param {boolean} maybePromptForPermission - Flag to tell the app if it should
|
||||
* prompt for a calendar permission if it wasn't granted yet.
|
||||
* @param {boolean|undefined} forcePermission - Whether to force to re-ask
|
||||
* for the permission or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _fetchCalendarEntries(store, promptForPermission) {
|
||||
_ensureCalendarAccess(promptForPermission)
|
||||
.then(() => {
|
||||
function _fetchCalendarEntries(
|
||||
store,
|
||||
maybePromptForPermission,
|
||||
forcePermission
|
||||
) {
|
||||
const { dispatch } = store;
|
||||
const state = store.getState()['features/calendar-sync'];
|
||||
const { calendarAccessStatus } = state;
|
||||
const promptForPermission
|
||||
= (maybePromptForPermission && !calendarAccessStatus)
|
||||
|| forcePermission;
|
||||
|
||||
_ensureCalendarAccess(promptForPermission, dispatch)
|
||||
.then(accessGranted => {
|
||||
if (accessGranted) {
|
||||
const startDate = new Date();
|
||||
const endDate = new Date();
|
||||
|
||||
|
@ -110,45 +151,16 @@ function _fetchCalendarEntries(store, promptForPermission) {
|
|||
[]
|
||||
)
|
||||
.then(events => {
|
||||
const { knownDomains } = store.getState()['features/calendar-sync'];
|
||||
const eventList = [];
|
||||
const { knownDomains } = state;
|
||||
|
||||
if (events && events.length) {
|
||||
for (const event of events) {
|
||||
const jitsiURL = _getURLFromEvent(event, knownDomains);
|
||||
const now = Date.now();
|
||||
|
||||
if (jitsiURL) {
|
||||
const eventStartDate = Date.parse(event.startDate);
|
||||
const eventEndDate = Date.parse(event.endDate);
|
||||
|
||||
if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
|
||||
logger.warn(
|
||||
'Skipping calendar event due to invalid dates',
|
||||
event.title,
|
||||
event.startDate,
|
||||
event.endDate
|
||||
);
|
||||
} else if (eventEndDate > now) {
|
||||
eventList.push({
|
||||
endDate: eventEndDate,
|
||||
id: event.id,
|
||||
startDate: eventStartDate,
|
||||
title: event.title,
|
||||
url: jitsiURL
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
|
||||
a.startDate - b.startDate
|
||||
).slice(0, MAX_LIST_LENGTH)));
|
||||
_updateCalendarEntries(events, knownDomains, dispatch);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('Error fetching calendar.', error);
|
||||
});
|
||||
} else {
|
||||
logger.warn('Calendar access not granted.');
|
||||
}
|
||||
})
|
||||
.catch(reason => {
|
||||
logger.error('Error accessing calendar.', reason);
|
||||
|
@ -209,3 +221,70 @@ function _parseAndAddDomain(store) {
|
|||
|
||||
store.dispatch(maybeAddNewKnownDomain(locationURL.host));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the calendar entries in Redux when new list is received.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} event - An event returned from the native calendar.
|
||||
* @param {Array<string>} knownDomains - The known domain list.
|
||||
* @returns {CalendarEntry}
|
||||
*/
|
||||
function _parseCalendarEntry(event, knownDomains) {
|
||||
if (event) {
|
||||
const jitsiURL = _getURLFromEvent(event, knownDomains);
|
||||
|
||||
if (jitsiURL) {
|
||||
const eventStartDate = Date.parse(event.startDate);
|
||||
const eventEndDate = Date.parse(event.endDate);
|
||||
|
||||
if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
|
||||
logger.warn(
|
||||
'Skipping invalid calendar event',
|
||||
event.title,
|
||||
event.startDate,
|
||||
event.endDate
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
endDate: eventEndDate,
|
||||
id: event.id,
|
||||
startDate: eventStartDate,
|
||||
title: event.title,
|
||||
url: jitsiURL
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the calendar entries in Redux when new list is received.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<CalendarEntry>} events - The new event list.
|
||||
* @param {Array<string>} knownDomains - The known domain list.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateCalendarEntries(events, knownDomains, dispatch) {
|
||||
if (events && events.length) {
|
||||
const eventList = [];
|
||||
|
||||
for (const event of events) {
|
||||
const calendarEntry
|
||||
= _parseCalendarEntry(event, knownDomains);
|
||||
const now = Date.now();
|
||||
|
||||
if (calendarEntry && calendarEntry.endDate > now) {
|
||||
eventList.push(calendarEntry);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
|
||||
a.startDate - b.startDate
|
||||
).slice(0, MAX_LIST_LENGTH)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,13 +3,19 @@
|
|||
import { ReducerRegistry } from '../base/redux';
|
||||
import { PersistenceRegistry } from '../base/storage';
|
||||
|
||||
import { NEW_CALENDAR_ENTRY_LIST, NEW_KNOWN_DOMAIN } from './actionTypes';
|
||||
import {
|
||||
CALENDAR_ACCESS_REQUESTED,
|
||||
NEW_CALENDAR_ENTRY_LIST,
|
||||
NEW_KNOWN_DOMAIN
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* ZB: this is an object, as further data is to come here, like:
|
||||
* - known domain list
|
||||
*/
|
||||
const DEFAULT_STATE = {
|
||||
/**
|
||||
* Note: If features/calendar-sync ever gets persisted, do not persist the
|
||||
* calendarAccessStatus value as it's needed to remain a runtime value to
|
||||
* see if we need to re-request the calendar permission from the user.
|
||||
*/
|
||||
calendarAccessStatus: undefined,
|
||||
events: [],
|
||||
knownDomains: []
|
||||
};
|
||||
|
@ -26,6 +32,12 @@ ReducerRegistry.register(
|
|||
STORE_NAME,
|
||||
(state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case CALENDAR_ACCESS_REQUESTED:
|
||||
return {
|
||||
...state,
|
||||
calendarAccessStatus: action.status
|
||||
};
|
||||
|
||||
case NEW_CALENDAR_ENTRY_LIST:
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// @flow
|
||||
|
||||
import { Alert, Linking, NativeModules } from 'react-native';
|
||||
|
||||
import { Platform } from '../../base/react';
|
||||
|
||||
/**
|
||||
* Opens the settings panel for the current platform.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
export function openSettings() {
|
||||
switch (Platform.OS) {
|
||||
case 'android':
|
||||
NativeModules.AndroidSettings.open().catch(() => {
|
||||
Alert.alert(
|
||||
'Error opening settings',
|
||||
'Please open settings and grant the required permissions',
|
||||
[
|
||||
{ text: 'OK' }
|
||||
]
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'ios':
|
||||
Linking.openURL('app-settings:');
|
||||
break;
|
||||
}
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
/* @flow */
|
||||
|
||||
import { Alert, Linking, NativeModules } from 'react-native';
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
import { openSettings } from './functions';
|
||||
|
||||
import { isRoomValid } from '../../base/conference';
|
||||
import { Platform } from '../../base/react';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
import { TRACK_CREATE_ERROR } from '../../base/tracks';
|
||||
|
||||
|
@ -64,35 +65,9 @@ function _alertPermissionErrorWithSettings(trackType) {
|
|||
[
|
||||
{ text: 'Cancel' },
|
||||
{
|
||||
onPress: _openSettings,
|
||||
onPress: openSettings,
|
||||
text: 'Settings'
|
||||
}
|
||||
],
|
||||
{ cancelable: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the settings panel for the current platform.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _openSettings() {
|
||||
switch (Platform.OS) {
|
||||
case 'android':
|
||||
NativeModules.AndroidSettings.open().catch(() => {
|
||||
Alert.alert(
|
||||
'Error opening settings',
|
||||
'Please open settings and grant the required permissions',
|
||||
[
|
||||
{ text: 'OK' }
|
||||
]
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'ios':
|
||||
Linking.openURL('app-settings:');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,11 @@ type Props = {
|
|||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The i18n translate function
|
||||
*/
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
import React from 'react';
|
||||
import { View, TabBarIOS } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { MeetingList } from '../../calendar-sync';
|
||||
import { MeetingList, refreshCalendarEntryList } from '../../calendar-sync';
|
||||
import { RecentList } from '../../recent-list';
|
||||
|
||||
import AbstractPagedList from './AbstractPagedList';
|
||||
|
@ -59,8 +60,7 @@ class PagedList extends AbstractPagedList {
|
|||
selected = { pageIndex === 1 }
|
||||
title = { t('welcomepage.calendar') } >
|
||||
<MeetingList
|
||||
disabled = { disabled }
|
||||
displayed = { pageIndex === 1 } />
|
||||
disabled = { disabled } />
|
||||
</TabBarIOS.Item>
|
||||
</TabBarIOS>
|
||||
</View>
|
||||
|
@ -81,8 +81,17 @@ class PagedList extends AbstractPagedList {
|
|||
this.setState({
|
||||
pageIndex: tabIndex
|
||||
});
|
||||
|
||||
if (tabIndex === 1) {
|
||||
/**
|
||||
* This is a workaround as TabBarIOS doesn't invoke
|
||||
* componentWillReciveProps on prop change of the
|
||||
* MeetingList component.
|
||||
*/
|
||||
this.props.dispatch(refreshCalendarEntryList());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(PagedList);
|
||||
export default translate(connect()(PagedList));
|
||||
|
|
Loading…
Reference in New Issue