[RN] Change default WelcomeScreen tab and persist user choice (coding style)

This commit is contained in:
Lyubo Marinov 2018-06-11 12:28:45 -05:00
parent dcfebf746f
commit 0d3fac7c0f
12 changed files with 166 additions and 179 deletions

View File

@ -56,13 +56,13 @@
</dict>
</dict>
<key>NSCalendarsUsageDescription</key>
<string>See your scheduled conferences in the app.</string>
<string>See your scheduled meetings in the app.</string>
<key>NSCameraUsageDescription</key>
<string>Participate in conferences with video.</string>
<string>Participate in meetings with video.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSMicrophoneUsageDescription</key>
<string>Participate in conferences with voice.</string>
<string>Participate in meetings with voice.</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>

View File

@ -608,7 +608,7 @@
"now": "Now",
"ongoingMeeting": "ongoing meeting",
"permissionButton": "Open settings",
"permissionMessage": "Calendar permission is required to list your meetings in the app."
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
},
"recentList": {
"today": "Today",

View File

@ -5,11 +5,14 @@ import { View } from 'react-native';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link AbstractPagedList}.
*/
type Props = {
/**
* The index (starting from 0) of the page that should be rendered
* active as default.
* The zero-based index of the page that should be rendered (selected) by
* default.
*/
defaultPage: number,
@ -30,16 +33,20 @@ type Props = {
/**
* The pages of the PagedList component to be rendered.
* Note: page.component may be undefined and then they don't need to be
* rendered.
*
* Note: An element's {@code component} may be {@code undefined} and then it
* won't need to be rendered.
*/
pages: Array<{
component: Object,
component: ?Object,
icon: string | number,
title: string
}>
};
/**
* The type of the React {@code Component} state of {@link AbstractPagedList}.
*/
type State = {
/**
@ -53,7 +60,7 @@ type State = {
*/
export default class AbstractPagedList extends Component<Props, State> {
/**
* Constructor of the component.
* Initializes a new {@code AbstractPagedList} instance.
*
* @inheritdoc
*/
@ -63,15 +70,19 @@ export default class AbstractPagedList extends Component<Props, State> {
this.state = {
pageIndex: this._validatePageIndex(props.defaultPage)
};
// Bind event handlers so they are only bound once per instance.
this._maybeRefreshSelectedPage
= this._maybeRefreshSelectedPage.bind(this);
}
/**
* Implements React's {@code Component} componentDidMount.
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._maybeRefreshActivePage();
this._maybeRefreshSelectedPage();
}
/**
@ -80,8 +91,8 @@ export default class AbstractPagedList extends Component<Props, State> {
* @inheritdoc
*/
render() {
const { disabled, pages } = this.props;
const enabledPages = pages.filter(page => page.component);
const { disabled } = this.props;
const pages = this.props.pages.filter(({ component }) => component);
return (
<View
@ -90,35 +101,24 @@ export default class AbstractPagedList extends Component<Props, State> {
disabled ? styles.pagedListContainerDisabled : null
] }>
{
enabledPages.length > 1
pages.length > 1
? this._renderPagedList(disabled)
: enabledPages.length === 1
: pages.length === 1
? React.createElement(
/* type */ enabledPages[0].component,
// $FlowExpectedError
/* type */ pages[0].component,
/* props */ {
disabled,
style: styles.pagedList
}) : null
})
: null
}
</View>
);
}
_platformSpecificPageSelect: number => void
/**
* Method to be overriden by the components implementing this abstract class
* to handle platform specific actions on page select.
*
* @protected
* @param {number} pageIndex - The selected page index.
* @returns {void}
*/
_platformSpecificPageSelect(pageIndex) {
this._selectPage(pageIndex);
}
_maybeRefreshActivePage: () => void
_maybeRefreshSelectedPage: () => void;
/**
* Components that this PagedList displays may have a refresh function to
@ -128,13 +128,15 @@ export default class AbstractPagedList extends Component<Props, State> {
* @private
* @returns {void}
*/
_maybeRefreshActivePage() {
_maybeRefreshSelectedPage() {
const selectedPage = this.props.pages[this.state.pageIndex];
let component;
if (selectedPage && selectedPage.component) {
const { refresh } = selectedPage.component;
if (selectedPage && (component = selectedPage.component)) {
const { refresh } = component;
typeof refresh === 'function' && refresh(this.props.dispatch);
typeof refresh === 'function'
&& refresh.call(component, this.props.dispatch);
}
}
@ -145,25 +147,22 @@ export default class AbstractPagedList extends Component<Props, State> {
/**
* Sets the selected page.
*
* @param {number} pageIndex - The index of the active page.
* @param {number} pageIndex - The index of the selected page.
* @protected
* @returns {void}
*/
_selectPage(pageIndex: number) {
const validatedPageIndex = this._validatePageIndex(pageIndex);
// eslint-disable-next-line no-param-reassign
pageIndex = this._validatePageIndex(pageIndex);
const { onSelectPage } = this.props;
if (typeof onSelectPage === 'function') {
onSelectPage(validatedPageIndex);
}
typeof onSelectPage === 'function' && onSelectPage(pageIndex);
this.setState({
pageIndex: validatedPageIndex
}, () => this._maybeRefreshActivePage());
this.setState({ pageIndex }, this._maybeRefreshSelectedPage);
}
_validatePageIndex: number => number
_validatePageIndex: number => number;
/**
* Validates the requested page index and returns a safe value.
@ -173,10 +172,10 @@ export default class AbstractPagedList extends Component<Props, State> {
* @returns {number}
*/
_validatePageIndex(pageIndex) {
// pageIndex may point to a non existing page if some of the pages are
// pageIndex may point to a non-existing page if some of the pages are
// disabled (their component property is undefined).
const maxPageIndex
= this.props.pages.filter(page => page.component).length - 1;
= this.props.pages.filter(({ component }) => component).length - 1;
return Math.max(0, Math.min(maxPageIndex, pageIndex));
}

View File

@ -86,18 +86,6 @@ class PagedList extends AbstractPagedList {
}
}
/**
* Platform specific actions to run on page select.
*
* @private
* @param {number} pageIndex - The selected page index.
* @returns {void}
*/
_platformSpecificPageSelect(pageIndex) {
this._viewPager.setPage(pageIndex);
this._selectPage(pageIndex);
}
/**
* Renders a single page of the page list.
*

View File

@ -1,9 +1,9 @@
// @flow
import {
REFRESH_CALENDAR,
SET_CALENDAR_AUTHORIZATION,
SET_CALENDAR_EVENTS,
REFRESH_CALENDAR
SET_CALENDAR_EVENTS
} from './actionTypes';
/**

View File

@ -21,7 +21,7 @@ type Props = {
/**
* The current state of the calendar access permission.
*/
_authorization: string,
_authorization: ?string,
/**
* The calendar event list.
@ -73,7 +73,7 @@ class MeetingList extends Component<Props> {
}
/**
* Constructor of the MeetingList component.
* Initializes a new {@code MeetingList} instance.
*
* @inheritdoc
*/
@ -85,32 +85,26 @@ class MeetingList extends Component<Props> {
= this._getRenderListEmptyComponent.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._toDateString = this._toDateString.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
this._toDisplayableList = this._toDisplayableList.bind(this);
this._toDateString = this._toDateString.bind(this);
}
/**
* Implements the React Components's render.
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { _authorization, disabled } = this.props;
const { disabled } = this.props;
return (
<NavigateSectionList
disabled = { disabled }
onPress = { this._onPress }
onRefresh = { this._onRefresh }
// If we don't provide a list specific renderListEmptyComponent,
// then the default empty component of the NavigateSectionList
// will be rendered, which (atm) is a simple "Pull to refresh"
// message.
renderListEmptyComponent
= { _authorization === 'denied'
? this._getRenderListEmptyComponent() : undefined }
= { this._getRenderListEmptyComponent() }
sections = { this._toDisplayableList() } />
);
}
@ -122,10 +116,17 @@ class MeetingList extends Component<Props> {
* of the default one in the {@link NavigateSectionList}.
*
* @private
* @returns {Function}
* @returns {?React$Component}
*/
_getRenderListEmptyComponent() {
const { t } = this.props;
const { _authorization, t } = this.props;
// If we don't provide a list specific renderListEmptyComponent, then
// the default empty component of the NavigateSectionList will be
// rendered, which (atm) is a simple "Pull to refresh" message.
if (_authorization !== 'denied') {
return undefined;
}
return (
<View style = { styles.noPermissionMessageView }>
@ -168,6 +169,24 @@ class MeetingList extends Component<Props> {
this.props.dispatch(refreshCalendar(true));
}
_toDateString: Object => string;
/**
* Generates a date (interval) string for a given event.
*
* @param {Object} event - The event.
* @private
* @returns {string}
*/
_toDateString(event) {
const startDateTime
= getLocalizedDateFormatter(event.startDate).format('lll');
const endTime
= getLocalizedDateFormatter(event.endDate).format('LT');
return `${startDateTime} - ${endTime}`;
}
_toDisplayableItem: Object => Object;
/**
@ -199,7 +218,9 @@ class MeetingList extends Component<Props> {
*/
_toDisplayableList() {
const { _eventList, t } = this.props;
const now = Date.now();
const { createSection } = NavigateSectionList;
const nowSection = createSection(t('calendarSync.now'), 'now');
const nextSection = createSection(t('calendarSync.next'), 'next');
@ -227,31 +248,11 @@ class MeetingList extends Component<Props> {
nextSection,
laterSection
]) {
if (section.data.length) {
sectionList.push(section);
}
section.data.length && sectionList.push(section);
}
return sectionList;
}
_toDateString: Object => string;
/**
* Generates a date (interval) string for a given event.
*
* @param {Object} event - The event.
* @private
* @returns {string}
*/
_toDateString(event) {
const startDateTime
= getLocalizedDateFormatter(event.startDate).format('lll');
const endTime
= getLocalizedDateFormatter(event.endDate).format('LT');
return `${startDateTime} - ${endTime}`;
}
}
/**
@ -259,15 +260,16 @@ class MeetingList extends Component<Props> {
*
* @param {Object} state - The redux state.
* @returns {{
* _eventList: Array
* _authorization: ?string,
* _eventList: Array<Object>
* }}
*/
function _mapStateToProps(state: Object) {
const calendarSyncState = state['features/calendar-sync'];
const { authorization, events } = state['features/calendar-sync'];
return {
_authorization: calendarSyncState.authorization,
_eventList: calendarSyncState.events
_authorization: authorization,
_eventList: events
};
}

View File

@ -34,7 +34,8 @@ export default createStyleSheet({
*/
noPermissionMessageText: {
backgroundColor: 'transparent',
color: 'rgba(255, 255, 255, 0.6)'
color: 'rgba(255, 255, 255, 0.6)',
textAlign: 'center'
},
/**

View File

@ -43,16 +43,10 @@ CALENDAR_ENABLED
break;
case SET_CALENDAR_AUTHORIZATION:
return {
...state,
authorization: action.authorization
};
return set(state, 'authorization', action.authorization);
case SET_CALENDAR_EVENTS:
return {
...state,
events: action.events
};
return set(state, 'events', action.events);
}
return state;

View File

@ -12,13 +12,13 @@
export const SET_SIDEBAR_VISIBLE = Symbol('SET_SIDEBAR_VISIBLE');
/**
* Action to update the default page index of the {@code WelcomePageLists}
* component.
* The type of (redux) action to set the default page index of
* {@link WelcomePageLists}.
*
* {
* type: SET_WELCOME_PAGE_LIST_DEFAULT_PAGE,
* type: SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE,
* pageIndex: number
* }
*/
export const SET_WELCOME_PAGE_LIST_DEFAULT_PAGE
export const SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE
= Symbol('SET_WELCOME_PAGE_LIST_DEFAULT_PAGE');

View File

@ -2,26 +2,9 @@
import {
SET_SIDEBAR_VISIBLE,
SET_WELCOME_PAGE_LIST_DEFAULT_PAGE
SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE
} from './actionTypes';
/**
* Action to update the default page index of the {@code WelcomePageLists}
* component.
*
* @param {number} pageIndex - The index of the selected page.
* @returns {{
* type: SET_WELCOME_PAGE_LIST_DEFAULT_PAGE,
* pageIndex: number
* }}
*/
export function setWelcomePageListDefaultPage(pageIndex: number) {
return {
type: SET_WELCOME_PAGE_LIST_DEFAULT_PAGE,
pageIndex
};
}
/**
* Sets the visibility of {@link WelcomePageSideBar}.
*
@ -38,3 +21,19 @@ export function setSideBarVisible(visible: boolean) {
visible
};
}
/**
* Sets the default page index of {@link WelcomePageLists}.
*
* @param {number} pageIndex - The index of the default page.
* @returns {{
* type: SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE,
* pageIndex: number
* }}
*/
export function setWelcomePageListsDefaultPage(pageIndex: number) {
return {
type: SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE,
pageIndex
};
}

View File

@ -9,8 +9,11 @@ import { PagedList } from '../../base/react';
import { MeetingList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
import { setWelcomePageListDefaultPage } from '../actions';
import { setWelcomePageListsDefaultPage } from '../actions';
/**
* The type of the React {@code Component} props of {@link WelcomePageLists}.
*/
type Props = {
/**
@ -50,17 +53,19 @@ const IOS_RECENT_LIST_ICON = require('../../../../images/history.png');
class WelcomePageLists extends Component<Props> {
/**
* The pages to be rendered.
* Note: The component field may be undefined if a feature (such as
* Calendar) is disabled, and that means that the page must not be rendered.
*
* Note: An element's {@code component} may be {@code undefined} if a
* feature (such as Calendar) is disabled, and that means that the page must
* not be rendered.
*/
pages: Array<{
component: Object,
component: ?Object,
icon: string | number,
title: string
}>
}>;
/**
* Component contructor.
* Initializes a new {@code WelcomePageLists} instance.
*
* @inheritdoc
*/
@ -68,28 +73,32 @@ class WelcomePageLists extends Component<Props> {
super(props);
const { t } = props;
const isAndroid = Platform.OS === 'android';
const android = Platform.OS === 'android';
this.pages = [ {
component: RecentList,
icon: isAndroid ? 'restore' : IOS_RECENT_LIST_ICON,
title: t('welcomepage.recentList')
}, {
component: MeetingList,
icon: isAndroid ? 'event_note' : IOS_CALENDAR_ICON,
title: t('welcomepage.calendar')
} ];
this.pages = [
{
component: RecentList,
icon: android ? 'restore' : IOS_RECENT_LIST_ICON,
title: t('welcomepage.recentList')
},
{
component: MeetingList,
icon: android ? 'event_note' : IOS_CALENDAR_ICON,
title: t('welcomepage.calendar')
}
];
// Bind event handlers so they are only bound once per instance.
this._onSelectPage = this._onSelectPage.bind(this);
}
/**
* Implements React Component's render.
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { disabled, _defaultPage } = this.props;
const { _defaultPage } = this.props;
if (typeof _defaultPage === 'undefined') {
return null;
@ -98,13 +107,13 @@ class WelcomePageLists extends Component<Props> {
return (
<PagedList
defaultPage = { _defaultPage }
disabled = { disabled }
disabled = { this.props.disabled }
onSelectPage = { this._onSelectPage }
pages = { this.pages } />
);
}
_onSelectPage: number => void
_onSelectPage: number => void;
/**
* Callback for the {@code PagedList} page select action.
@ -114,9 +123,7 @@ class WelcomePageLists extends Component<Props> {
* @returns {void}
*/
_onSelectPage(pageIndex) {
const { dispatch } = this.props;
dispatch(setWelcomePageListDefaultPage(pageIndex));
this.props.dispatch(setWelcomePageListsDefaultPage(pageIndex));
}
}
@ -127,18 +134,20 @@ class WelcomePageLists extends Component<Props> {
* @param {Object} state - The redux state.
* @protected
* @returns {{
* _hasRecentListEntries: boolean
* _defaultPage: number
* }}
*/
function _mapStateToProps(state: Object) {
const { defaultPage } = state['features/welcome'];
const recentList = state['features/recent-list'];
const _hasRecentListEntries = Boolean(recentList && recentList.length);
let { defaultPage } = state['features/welcome'];
if (typeof defaultPage === 'undefined') {
const recentList = state['features/recent-list'];
defaultPage = recentList && recentList.length ? 0 : 1;
}
return {
_defaultPage: defaultPage === 'undefined'
? _hasRecentListEntries ? 0 : 1
: defaultPage
_defaultPage: defaultPage
};
}

View File

@ -1,42 +1,37 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { ReducerRegistry, set } from '../base/redux';
import { PersistenceRegistry } from '../base/storage';
import {
SET_SIDEBAR_VISIBLE,
SET_WELCOME_PAGE_LIST_DEFAULT_PAGE
SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE
} from './actionTypes';
/**
* The Redux store name this feature uses.
* The name of the redux store/state property which is the root of the redux
* state of the feature {@code welcome}.
*/
const STORE_NAME = 'features/welcome';
/**
* Sets up the persistence of the feature {@code features/welcome}.
* Sets up the persistence of the feature {@code welcome}.
*/
PersistenceRegistry.register(STORE_NAME, {
defaultPage: true
});
/**
* Reduces redux actions for the purposes of {@code features/welcome}.
* Reduces redux actions for the purposes of the feature {@code welcome}.
*/
ReducerRegistry.register(STORE_NAME, (state = {}, action) => {
switch (action.type) {
case SET_SIDEBAR_VISIBLE:
return {
...state,
sideBarVisible: action.visible
};
return set(state, 'sideBarVisible', action.visible);
case SET_WELCOME_PAGE_LIST_DEFAULT_PAGE:
return {
...state,
defaultPage: action.pageIndex
};
default:
return state;
case SET_WELCOME_PAGE_LISTS_DEFAULT_PAGE:
return set(state, 'defaultPage', action.pageIndex);
}
return state;
});