Reorganize calendar access request flow
This commit is contained in:
parent
ea431ca90c
commit
b258e0d397
|
@ -539,11 +539,16 @@
|
||||||
"later": "Later",
|
"later": "Later",
|
||||||
"next": "Upcoming",
|
"next": "Upcoming",
|
||||||
"nextMeeting": "next meeting",
|
"nextMeeting": "next meeting",
|
||||||
"now": "Now"
|
"now": "Now",
|
||||||
|
"permissionButton": "Open settings",
|
||||||
|
"permissionMessage": "Calendar permission is required to list your meetings in the app."
|
||||||
},
|
},
|
||||||
"recentList": {
|
"recentList": {
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"yesterday": "Yesterday",
|
"yesterday": "Yesterday",
|
||||||
"earlier": "Earlier"
|
"earlier": "Earlier"
|
||||||
|
},
|
||||||
|
"sectionList": {
|
||||||
|
"pullToRefresh": "Pull to refresh"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,10 @@ import {
|
||||||
|
|
||||||
import styles, { UNDERLAY_COLOR } from './styles';
|
import styles, { UNDERLAY_COLOR } from './styles';
|
||||||
|
|
||||||
|
import { translate } from '../../../i18n';
|
||||||
|
|
||||||
|
import { Icon } from '../../../font-icons';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,6 +21,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translate function.
|
||||||
|
*/
|
||||||
|
t: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||||
*/
|
*/
|
||||||
|
@ -27,6 +36,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
onRefresh: Function,
|
onRefresh: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to override the rendered default empty list component.
|
||||||
|
*/
|
||||||
|
renderListEmptyComponent: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sections to be rendered in the following format:
|
* 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
|
* property and navigates to (probably) meetings, such as the recent list
|
||||||
* or the meeting list components.
|
* or the meeting list components.
|
||||||
*/
|
*/
|
||||||
export default class NavigateSectionList extends Component<Props> {
|
class NavigateSectionList extends Component<Props> {
|
||||||
/**
|
/**
|
||||||
* Constructor of the NavigateSectionList component.
|
* Constructor of the NavigateSectionList component.
|
||||||
*
|
*
|
||||||
|
@ -69,6 +83,8 @@ export default class NavigateSectionList extends Component<Props> {
|
||||||
this._renderItem = this._renderItem.bind(this);
|
this._renderItem = this._renderItem.bind(this);
|
||||||
this._renderItemLine = this._renderItemLine.bind(this);
|
this._renderItemLine = this._renderItemLine.bind(this);
|
||||||
this._renderItemLines = this._renderItemLines.bind(this);
|
this._renderItemLines = this._renderItemLines.bind(this);
|
||||||
|
this._renderListEmptyComponent
|
||||||
|
= this._renderListEmptyComponent.bind(this);
|
||||||
this._renderSection = this._renderSection.bind(this);
|
this._renderSection = this._renderSection.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,12 +96,16 @@ export default class NavigateSectionList extends Component<Props> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { sections } = this.props;
|
const { renderListEmptyComponent, sections } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style = { styles.container } >
|
style = { styles.container } >
|
||||||
<SectionList
|
<SectionList
|
||||||
|
ListEmptyComponent = {
|
||||||
|
renderListEmptyComponent
|
||||||
|
|| this._renderListEmptyComponent
|
||||||
|
}
|
||||||
keyExtractor = { this._getItemKey }
|
keyExtractor = { this._getItemKey }
|
||||||
onRefresh = { this._onRefresh }
|
onRefresh = { this._onRefresh }
|
||||||
refreshing = { false }
|
refreshing = { false }
|
||||||
|
@ -274,6 +294,34 @@ export default class NavigateSectionList extends Component<Props> {
|
||||||
return null;
|
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
|
_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'
|
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: {
|
touchableView: {
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
// @flow
|
// @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.
|
* Action to update the current calendar entry list in the store.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,10 +1,28 @@
|
||||||
// @flow
|
// @flow
|
||||||
import {
|
import {
|
||||||
|
CALENDAR_ACCESS_REQUESTED,
|
||||||
NEW_CALENDAR_ENTRY_LIST,
|
NEW_CALENDAR_ENTRY_LIST,
|
||||||
NEW_KNOWN_DOMAIN,
|
NEW_KNOWN_DOMAIN,
|
||||||
REFRESH_CALENDAR_ENTRY_LIST
|
REFRESH_CALENDAR_ENTRY_LIST
|
||||||
} from './actionTypes';
|
} 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.
|
* 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).
|
* 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 {{
|
* @returns {{
|
||||||
* type: REFRESH_CALENDAR_ENTRY_LIST
|
* type: REFRESH_CALENDAR_ENTRY_LIST,
|
||||||
|
* forcePermission: boolean
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export function refreshCalendarEntryList() {
|
export function refreshCalendarEntryList(forcePermission: boolean = false) {
|
||||||
return {
|
return {
|
||||||
|
forcePermission,
|
||||||
type: REFRESH_CALENDAR_ENTRY_LIST
|
type: REFRESH_CALENDAR_ENTRY_LIST
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
import { refreshCalendarEntryList } from '../actions';
|
import { refreshCalendarEntryList } from '../actions';
|
||||||
|
|
||||||
import { appNavigate } from '../../app';
|
import { appNavigate } from '../../app';
|
||||||
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
||||||
import { NavigateSectionList } from '../../base/react';
|
import { NavigateSectionList } from '../../base/react';
|
||||||
|
import { openSettings } from '../../mobile/permissions';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
@ -28,6 +32,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
displayed: boolean,
|
displayed: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state of the calendar access permission.
|
||||||
|
*/
|
||||||
|
_calendarAccessStatus: string,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The calendar event list.
|
* The calendar event list.
|
||||||
*/
|
*/
|
||||||
|
@ -43,8 +52,6 @@ type Props = {
|
||||||
* Component to display a list of events from the (mobile) user's calendar.
|
* Component to display a list of events from the (mobile) user's calendar.
|
||||||
*/
|
*/
|
||||||
class MeetingList extends Component<Props> {
|
class MeetingList extends Component<Props> {
|
||||||
_initialLoaded: boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default values for the component's props.
|
* Default values for the component's props.
|
||||||
*/
|
*/
|
||||||
|
@ -60,6 +67,14 @@ class MeetingList extends Component<Props> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const { dispatch, displayed } = props;
|
||||||
|
|
||||||
|
if (displayed) {
|
||||||
|
dispatch(refreshCalendarEntryList());
|
||||||
|
}
|
||||||
|
|
||||||
|
this._getRenderListEmptyComponent
|
||||||
|
= this._getRenderListEmptyComponent.bind(this);
|
||||||
this._onPress = this._onPress.bind(this);
|
this._onPress = this._onPress.bind(this);
|
||||||
this._onRefresh = this._onRefresh.bind(this);
|
this._onRefresh = this._onRefresh.bind(this);
|
||||||
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
||||||
|
@ -73,16 +88,11 @@ class MeetingList extends Component<Props> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
componentWillReceiveProps(newProps) {
|
componentWillReceiveProps(newProps) {
|
||||||
// This is a conditional logic to refresh the calendar entries (thus
|
const { displayed } = this.props;
|
||||||
// to request access to calendar) on component first receives a
|
|
||||||
// displayed=true prop - to avoid requesting calendar access on
|
if (newProps.displayed && !displayed) {
|
||||||
// app start.
|
|
||||||
if (!this._initialLoaded
|
|
||||||
&& newProps.displayed
|
|
||||||
&& !this.props.displayed) {
|
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
this._initialLoaded = true;
|
|
||||||
dispatch(refreshCalendarEntryList());
|
dispatch(refreshCalendarEntryList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,10 +110,45 @@ class MeetingList extends Component<Props> {
|
||||||
disabled = { disabled }
|
disabled = { disabled }
|
||||||
onPress = { this._onPress }
|
onPress = { this._onPress }
|
||||||
onRefresh = { this._onRefresh }
|
onRefresh = { this._onRefresh }
|
||||||
|
renderListEmptyComponent = {
|
||||||
|
this._getRenderListEmptyComponent
|
||||||
|
}
|
||||||
sections = { this._toDisplayableList() } />
|
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
|
_onPress: string => Function
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,7 +175,7 @@ class MeetingList extends Component<Props> {
|
||||||
_onRefresh() {
|
_onRefresh() {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(refreshCalendarEntryList());
|
dispatch(refreshCalendarEntryList(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
_toDisplayableItem: Object => Object
|
_toDisplayableItem: Object => Object
|
||||||
|
@ -219,12 +264,12 @@ class MeetingList extends Component<Props> {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
_toDateString(event) {
|
_toDateString(event) {
|
||||||
/* eslint-disable max-len */
|
const startDateTime
|
||||||
const startDateTime = getLocalizedDateFormatter(event.startDate).format('lll');
|
= getLocalizedDateFormatter(event.startDate).format('lll');
|
||||||
const endTime = getLocalizedDateFormatter(event.endDate).format('LT');
|
const endTime
|
||||||
|
= getLocalizedDateFormatter(event.endDate).format('LT');
|
||||||
|
|
||||||
return `${startDateTime} - ${endTime}`;
|
return `${startDateTime} - ${endTime}`;
|
||||||
/* eslint-enable max-len */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,8 +282,11 @@ class MeetingList extends Component<Props> {
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export function _mapStateToProps(state: Object) {
|
export function _mapStateToProps(state: Object) {
|
||||||
|
const calendarSyncState = state['features/calendar-sync'];
|
||||||
|
|
||||||
return {
|
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;
|
const NOTIFICATION_SIZE = 55;
|
||||||
|
|
||||||
|
@ -8,6 +8,46 @@ const NOTIFICATION_SIZE = 55;
|
||||||
*/
|
*/
|
||||||
export default createStyleSheet({
|
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.
|
* The top level container of the notification.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './actions';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
|
||||||
import './middleware';
|
import './middleware';
|
||||||
|
|
|
@ -2,13 +2,18 @@
|
||||||
import Logger from 'jitsi-meet-logger';
|
import Logger from 'jitsi-meet-logger';
|
||||||
import RNCalendarEvents from 'react-native-calendar-events';
|
import RNCalendarEvents from 'react-native-calendar-events';
|
||||||
|
|
||||||
|
import { APP_WILL_MOUNT } from '../app';
|
||||||
import { SET_ROOM } from '../base/conference';
|
import { SET_ROOM } from '../base/conference';
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
|
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';
|
import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
|
||||||
|
|
||||||
const FETCH_END_DAYS = 10;
|
const FETCH_END_DAYS = 10;
|
||||||
|
@ -20,12 +25,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
const result = next(action);
|
const result = next(action);
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case APP_STATE_CHANGED:
|
||||||
|
_maybeClearAccessStatus(store, action);
|
||||||
|
break;
|
||||||
case APP_WILL_MOUNT:
|
case APP_WILL_MOUNT:
|
||||||
_ensureDefaultServer(store);
|
_ensureDefaultServer(store);
|
||||||
_fetchCalendarEntries(store, false);
|
_fetchCalendarEntries(store, false, false);
|
||||||
break;
|
break;
|
||||||
case REFRESH_CALENDAR_ENTRY_LIST:
|
case REFRESH_CALENDAR_ENTRY_LIST:
|
||||||
_fetchCalendarEntries(store, true);
|
_fetchCalendarEntries(store, true, action.forcePermission);
|
||||||
break;
|
break;
|
||||||
case SET_ROOM:
|
case SET_ROOM:
|
||||||
_parseAndAddDomain(store);
|
_parseAndAddDomain(store);
|
||||||
|
@ -34,34 +42,53 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
return result;
|
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.
|
* Ensures calendar access if possible and resolves the promise if it's granted.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {boolean} promptForPermission - Flag to tell the app if it should
|
* @param {boolean} promptForPermission - Flag to tell the app if it should
|
||||||
* prompt for a calendar permission if it wasn't granted yet.
|
* prompt for a calendar permission if it wasn't granted yet.
|
||||||
|
* @param {Function} dispatch - The Redux dispatch function.
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
function _ensureCalendarAccess(promptForPermission) {
|
function _ensureCalendarAccess(promptForPermission, dispatch) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
RNCalendarEvents.authorizationStatus()
|
RNCalendarEvents.authorizationStatus()
|
||||||
.then(status => {
|
.then(status => {
|
||||||
if (status === 'authorized') {
|
if (status === 'authorized') {
|
||||||
resolve();
|
resolve(true);
|
||||||
} else if (promptForPermission) {
|
} else if (promptForPermission) {
|
||||||
RNCalendarEvents.authorizeEventStore()
|
RNCalendarEvents.authorizeEventStore()
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result === 'authorized') {
|
dispatch(updateCalendarAccessStatus(result));
|
||||||
resolve();
|
resolve(result === 'authorized');
|
||||||
} else {
|
|
||||||
reject(result);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reject(status);
|
resolve(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -91,64 +118,49 @@ function _ensureDefaultServer(store) {
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {Object} store - The redux store.
|
* @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.
|
* 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}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function _fetchCalendarEntries(store, promptForPermission) {
|
function _fetchCalendarEntries(
|
||||||
_ensureCalendarAccess(promptForPermission)
|
store,
|
||||||
.then(() => {
|
maybePromptForPermission,
|
||||||
const startDate = new Date();
|
forcePermission
|
||||||
const endDate = new Date();
|
) {
|
||||||
|
const { dispatch } = store;
|
||||||
|
const state = store.getState()['features/calendar-sync'];
|
||||||
|
const { calendarAccessStatus } = state;
|
||||||
|
const promptForPermission
|
||||||
|
= (maybePromptForPermission && !calendarAccessStatus)
|
||||||
|
|| forcePermission;
|
||||||
|
|
||||||
startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
|
_ensureCalendarAccess(promptForPermission, dispatch)
|
||||||
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
|
.then(accessGranted => {
|
||||||
|
if (accessGranted) {
|
||||||
|
const startDate = new Date();
|
||||||
|
const endDate = new Date();
|
||||||
|
|
||||||
RNCalendarEvents.fetchAllEvents(
|
startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
|
||||||
startDate.getTime(),
|
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
|
||||||
endDate.getTime(),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
.then(events => {
|
|
||||||
const { knownDomains } = store.getState()['features/calendar-sync'];
|
|
||||||
const eventList = [];
|
|
||||||
|
|
||||||
if (events && events.length) {
|
RNCalendarEvents.fetchAllEvents(
|
||||||
for (const event of events) {
|
startDate.getTime(),
|
||||||
const jitsiURL = _getURLFromEvent(event, knownDomains);
|
endDate.getTime(),
|
||||||
const now = Date.now();
|
[]
|
||||||
|
)
|
||||||
|
.then(events => {
|
||||||
|
const { knownDomains } = state;
|
||||||
|
|
||||||
if (jitsiURL) {
|
_updateCalendarEntries(events, knownDomains, dispatch);
|
||||||
const eventStartDate = Date.parse(event.startDate);
|
})
|
||||||
const eventEndDate = Date.parse(event.endDate);
|
.catch(error => {
|
||||||
|
logger.error('Error fetching calendar.', error);
|
||||||
if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
|
});
|
||||||
logger.warn(
|
} else {
|
||||||
'Skipping calendar event due to invalid dates',
|
logger.warn('Calendar access not granted.');
|
||||||
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)));
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
logger.error('Error fetching calendar.', error);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(reason => {
|
.catch(reason => {
|
||||||
logger.error('Error accessing calendar.', reason);
|
logger.error('Error accessing calendar.', reason);
|
||||||
|
@ -209,3 +221,70 @@ function _parseAndAddDomain(store) {
|
||||||
|
|
||||||
store.dispatch(maybeAddNewKnownDomain(locationURL.host));
|
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 { ReducerRegistry } from '../base/redux';
|
||||||
import { PersistenceRegistry } from '../base/storage';
|
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 = {
|
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: [],
|
events: [],
|
||||||
knownDomains: []
|
knownDomains: []
|
||||||
};
|
};
|
||||||
|
@ -26,6 +32,12 @@ ReducerRegistry.register(
|
||||||
STORE_NAME,
|
STORE_NAME,
|
||||||
(state = DEFAULT_STATE, action) => {
|
(state = DEFAULT_STATE, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case CALENDAR_ACCESS_REQUESTED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
calendarAccessStatus: action.status
|
||||||
|
};
|
||||||
|
|
||||||
case NEW_CALENDAR_ENTRY_LIST:
|
case NEW_CALENDAR_ENTRY_LIST:
|
||||||
return {
|
return {
|
||||||
...state,
|
...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';
|
import './middleware';
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import { Alert, Linking, NativeModules } from 'react-native';
|
import { Alert } from 'react-native';
|
||||||
|
|
||||||
|
import { openSettings } from './functions';
|
||||||
|
|
||||||
import { isRoomValid } from '../../base/conference';
|
import { isRoomValid } from '../../base/conference';
|
||||||
import { Platform } from '../../base/react';
|
|
||||||
import { MiddlewareRegistry } from '../../base/redux';
|
import { MiddlewareRegistry } from '../../base/redux';
|
||||||
import { TRACK_CREATE_ERROR } from '../../base/tracks';
|
import { TRACK_CREATE_ERROR } from '../../base/tracks';
|
||||||
|
|
||||||
|
@ -64,35 +65,9 @@ function _alertPermissionErrorWithSettings(trackType) {
|
||||||
[
|
[
|
||||||
{ text: 'Cancel' },
|
{ text: 'Cancel' },
|
||||||
{
|
{
|
||||||
onPress: _openSettings,
|
onPress: openSettings,
|
||||||
text: 'Settings'
|
text: 'Settings'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
{ cancelable: false });
|
{ 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,
|
disabled: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Redux dispatch function.
|
||||||
|
*/
|
||||||
|
dispatch: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The i18n translate function
|
* The i18n translate function
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, TabBarIOS } from 'react-native';
|
import { View, TabBarIOS } from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { MeetingList } from '../../calendar-sync';
|
import { MeetingList, refreshCalendarEntryList } from '../../calendar-sync';
|
||||||
import { RecentList } from '../../recent-list';
|
import { RecentList } from '../../recent-list';
|
||||||
|
|
||||||
import AbstractPagedList from './AbstractPagedList';
|
import AbstractPagedList from './AbstractPagedList';
|
||||||
|
@ -59,8 +60,7 @@ class PagedList extends AbstractPagedList {
|
||||||
selected = { pageIndex === 1 }
|
selected = { pageIndex === 1 }
|
||||||
title = { t('welcomepage.calendar') } >
|
title = { t('welcomepage.calendar') } >
|
||||||
<MeetingList
|
<MeetingList
|
||||||
disabled = { disabled }
|
disabled = { disabled } />
|
||||||
displayed = { pageIndex === 1 } />
|
|
||||||
</TabBarIOS.Item>
|
</TabBarIOS.Item>
|
||||||
</TabBarIOS>
|
</TabBarIOS>
|
||||||
</View>
|
</View>
|
||||||
|
@ -81,8 +81,17 @@ class PagedList extends AbstractPagedList {
|
||||||
this.setState({
|
this.setState({
|
||||||
pageIndex: tabIndex
|
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