diff --git a/lang/main.json b/lang/main.json index ecd96612a..6b9319624 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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" } } diff --git a/react/features/base/react/components/native/NavigateSectionList.js b/react/features/base/react/components/native/NavigateSectionList.js index c3040a7fd..d0449e8d7 100644 --- a/react/features/base/react/components/native/NavigateSectionList.js +++ b/react/features/base/react/components/native/NavigateSectionList.js @@ -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 { +class NavigateSectionList extends Component { /** * Constructor of the NavigateSectionList component. * @@ -69,6 +83,8 @@ export default class NavigateSectionList extends Component { 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 { * @inheritdoc */ render() { - const { sections } = this.props; + const { renderListEmptyComponent, sections } = this.props; return ( { 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 ( + + + { t('sectionList.pullToRefresh') } + + + + ); + } + + return null; + } + _renderSection: Object => Object /** @@ -293,3 +341,5 @@ export default class NavigateSectionList extends Component { ); } } + +export default translate(NavigateSectionList); diff --git a/react/features/base/react/components/native/styles.js b/react/features/base/react/components/native/styles.js index 6fa8ca9e4..935553544 100644 --- a/react/features/base/react/components/native/styles.js +++ b/react/features/base/react/components/native/styles.js @@ -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' } diff --git a/react/features/calendar-sync/actionTypes.js b/react/features/calendar-sync/actionTypes.js index 3366ac424..67d623e1e 100644 --- a/react/features/calendar-sync/actionTypes.js +++ b/react/features/calendar-sync/actionTypes.js @@ -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. */ diff --git a/react/features/calendar-sync/actions.js b/react/features/calendar-sync/actions.js index 6d26df3ab..f1f2e4d4a 100644 --- a/react/features/calendar-sync/actions.js +++ b/react/features/calendar-sync/actions.js @@ -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 }; } diff --git a/react/features/calendar-sync/components/MeetingList.native.js b/react/features/calendar-sync/components/MeetingList.native.js index 6de63937a..ffdd88813 100644 --- a/react/features/calendar-sync/components/MeetingList.native.js +++ b/react/features/calendar-sync/components/MeetingList.native.js @@ -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 { - _initialLoaded: boolean - /** * Default values for the component's props. */ @@ -60,6 +67,14 @@ class MeetingList extends Component { 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 { * @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 { 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 ( + + + { t('calendarSync.permissionMessage') } + + + + { t('calendarSync.permissionButton') } + + + + ); + } + + return null; + } + _onPress: string => Function /** @@ -130,7 +175,7 @@ class MeetingList extends Component { _onRefresh() { const { dispatch } = this.props; - dispatch(refreshCalendarEntryList()); + dispatch(refreshCalendarEntryList(true)); } _toDisplayableItem: Object => Object @@ -219,12 +264,12 @@ class MeetingList extends Component { * @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 { * }} */ export function _mapStateToProps(state: Object) { + const calendarSyncState = state['features/calendar-sync']; + return { - _eventList: state['features/calendar-sync'].events + _calendarAccessStatus: calendarSyncState.calendarAccessStatus, + _eventList: calendarSyncState.events }; } diff --git a/react/features/calendar-sync/components/styles.js b/react/features/calendar-sync/components/styles.js index 4665b0070..9acd47cd3 100644 --- a/react/features/calendar-sync/components/styles.js +++ b/react/features/calendar-sync/components/styles.js @@ -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. */ diff --git a/react/features/calendar-sync/index.js b/react/features/calendar-sync/index.js index daaa5d02a..7f0ef0251 100644 --- a/react/features/calendar-sync/index.js +++ b/react/features/calendar-sync/index.js @@ -1,3 +1,4 @@ +export * from './actions'; export * from './components'; import './middleware'; diff --git a/react/features/calendar-sync/middleware.js b/react/features/calendar-sync/middleware.js index 150cd129a..2720589ba 100644 --- a/react/features/calendar-sync/middleware.js +++ b/react/features/calendar-sync/middleware.js @@ -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,64 +118,49 @@ 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(() => { - const startDate = new Date(); - const endDate = new Date(); +function _fetchCalendarEntries( + store, + maybePromptForPermission, + forcePermission +) { + 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); - endDate.setDate(endDate.getDate() + FETCH_END_DAYS); + _ensureCalendarAccess(promptForPermission, dispatch) + .then(accessGranted => { + if (accessGranted) { + const startDate = new Date(); + const endDate = new Date(); - RNCalendarEvents.fetchAllEvents( - startDate.getTime(), - endDate.getTime(), - [] - ) - .then(events => { - const { knownDomains } = store.getState()['features/calendar-sync']; - const eventList = []; + startDate.setDate(startDate.getDate() + FETCH_START_DAYS); + endDate.setDate(endDate.getDate() + FETCH_END_DAYS); - if (events && events.length) { - for (const event of events) { - const jitsiURL = _getURLFromEvent(event, knownDomains); - const now = Date.now(); + RNCalendarEvents.fetchAllEvents( + startDate.getTime(), + endDate.getTime(), + [] + ) + .then(events => { + const { knownDomains } = state; - 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))); - }) - .catch(error => { - logger.error('Error fetching calendar.', error); - }); + _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} 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} events - The new event list. + * @param {Array} 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))); + } +} diff --git a/react/features/calendar-sync/reducer.js b/react/features/calendar-sync/reducer.js index 17a946bce..1aeaf3902 100644 --- a/react/features/calendar-sync/reducer.js +++ b/react/features/calendar-sync/reducer.js @@ -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, diff --git a/react/features/mobile/permissions/functions.js b/react/features/mobile/permissions/functions.js new file mode 100644 index 000000000..3b2ea1ae0 --- /dev/null +++ b/react/features/mobile/permissions/functions.js @@ -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; + } +} diff --git a/react/features/mobile/permissions/index.js b/react/features/mobile/permissions/index.js index d43689289..e975ed0e8 100644 --- a/react/features/mobile/permissions/index.js +++ b/react/features/mobile/permissions/index.js @@ -1 +1,3 @@ +export * from './functions'; + import './middleware'; diff --git a/react/features/mobile/permissions/middleware.js b/react/features/mobile/permissions/middleware.js index 04e32ee45..694461fb5 100644 --- a/react/features/mobile/permissions/middleware.js +++ b/react/features/mobile/permissions/middleware.js @@ -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; - } -} diff --git a/react/features/welcome/components/AbstractPagedList.js b/react/features/welcome/components/AbstractPagedList.js index 6bb626656..0ea76ce49 100644 --- a/react/features/welcome/components/AbstractPagedList.js +++ b/react/features/welcome/components/AbstractPagedList.js @@ -14,6 +14,11 @@ type Props = { */ disabled: boolean, + /** + * The Redux dispatch function. + */ + dispatch: Function, + /** * The i18n translate function */ diff --git a/react/features/welcome/components/PagedList.ios.js b/react/features/welcome/components/PagedList.ios.js index 6779985c8..fd72c2ad1 100644 --- a/react/features/welcome/components/PagedList.ios.js +++ b/react/features/welcome/components/PagedList.ios.js @@ -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') } > + disabled = { disabled } /> @@ -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));