Refactor PagedList components to be independent from the lists it renders

This commit is contained in:
zbettenbuk 2018-04-24 15:11:25 +02:00 committed by Paweł Domas
parent 2ee8f1ef58
commit 68608478f6
14 changed files with 495 additions and 338 deletions

BIN
images/history@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

BIN
images/history@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

View File

@ -55,6 +55,7 @@
"go": "GO",
"join": "JOIN",
"privacy": "Privacy",
"recentList": "History",
"roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",

View File

@ -3,18 +3,16 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import { MeetingList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
import styles from './styles';
/**
* The page to be displayed on render.
*/
export const DEFAULT_PAGE = 0;
type Props = {
/**
* The index (starting from 0) of the page that should be rendered
* active as default.
*/
defaultPage: number,
/**
* Indicates if the list is disabled or not.
*/
@ -26,9 +24,15 @@ type Props = {
dispatch: Function,
/**
* The i18n translate function
* The pages of the PagedList component to be rendered.
* Note: page.component may be undefined and then they don't need to be
* rendered.
*/
t: Function
pages: Array<{
component: Object,
icon: string | number,
title: string
}>
};
type State = {
@ -40,14 +44,9 @@ type State = {
};
/**
* Abstract class for the platform specific paged lists.
* Abstract class containing the platform independent logic of the paged lists.
*/
export default class AbstractPagedList extends Component<Props, State> {
/**
* The list of pages displayed in the component, referenced by page index.
*/
_pages: Array<Object>;
/**
* Constructor of the component.
*
@ -56,16 +55,13 @@ export default class AbstractPagedList extends Component<Props, State> {
constructor(props: Props) {
super(props);
this._pages = [];
for (const component of [ RecentList, MeetingList ]) {
// XXX Certain pages may be contributed by optional features. For
// example, MeetingList is contributed by the calendar feature and
// apps i.e. SDK consumers may not enable the calendar feature.
component && this._pages.push(component);
}
// props.defaultPage may point to a non existing page if some of the
// pages are disabled.
const maxPageIndex
= props.pages.filter(page => page.component).length - 1;
this.state = {
pageIndex: DEFAULT_PAGE
pageIndex: Math.max(0, Math.min(maxPageIndex, props.defaultPage))
};
}
@ -75,7 +71,8 @@ export default class AbstractPagedList extends Component<Props, State> {
* @inheritdoc
*/
render() {
const { disabled } = this.props;
const { disabled, pages } = this.props;
const enabledPages = pages.filter(page => page.component);
return (
<View
@ -84,14 +81,15 @@ export default class AbstractPagedList extends Component<Props, State> {
disabled ? styles.pagedListContainerDisabled : null
] }>
{
this._pages.length > 1
enabledPages.length > 1
? this._renderPagedList(disabled)
: React.createElement(
/* type */ this._pages[0],
/* props */ {
disabled,
style: styles.pagedList
})
: enabledPages.length === 1
? React.createElement(
/* type */ enabledPages[0].component,
/* props */ {
disabled,
style: styles.pagedList
}) : null
}
</View>
);
@ -115,10 +113,10 @@ export default class AbstractPagedList extends Component<Props, State> {
// The page's Component may have a refresh(dispatch) function which we
// invoke when the page is selected.
const selectedPageComponent = this._pages[pageIndex];
const selectedPage = this.props.pages[pageIndex];
if (selectedPageComponent) {
const { refresh } = selectedPageComponent;
if (selectedPage && selectedPage.component) {
const { refresh } = selectedPage.component;
typeof refresh === 'function' && refresh(this.props.dispatch);
}

View File

@ -0,0 +1,198 @@
// @flow
import React from 'react';
import { Text, TouchableOpacity, View, ViewPagerAndroid } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../../font-icons';
import AbstractPagedList from './AbstractPagedList';
import styles from './styles';
/**
* An Android specific component to render a paged list.
*
* @extends PagedList
*/
class PagedList extends AbstractPagedList {
/**
* A reference to the viewpager.
*/
_viewPager: ViewPagerAndroid;
/**
* Initializes a new {@code PagedList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onIconPress = this._onIconPress.bind(this);
this._getIndicatorStyle = this._getIndicatorStyle.bind(this);
this._onPageSelected = this._onPageSelected.bind(this);
this._setViewPager = this._setViewPager.bind(this);
}
_onIconPress: number => Function;
/**
* Constructs a function to be used as a callback for the icons in the tab
* bar.
*
* @param {number} pageIndex - The index of the page to activate via the
* callback.
* @private
* @returns {Function}
*/
_onIconPress(pageIndex) {
return () => {
this._viewPager.setPage(pageIndex);
this._selectPage(pageIndex);
};
}
_getIndicatorStyle: number => Object;
/**
* Constructs the style of an indicator.
*
* @param {number} indicatorIndex - The index of the indicator.
* @private
* @returns {Object}
*/
_getIndicatorStyle(indicatorIndex) {
if (this.state.pageIndex === indicatorIndex) {
return styles.pageIndicatorActive;
}
return null;
}
_onPageSelected: Object => void;
/**
* Updates the index of the currently selected page, based on the native
* event received from the {@link ViewPagerAndroid} component.
*
* @param {Object} event - The native event of the callback.
* @private
* @returns {void}
*/
_onPageSelected({ nativeEvent: { position } }) {
if (this.state.pageIndex !== position) {
this._selectPage(position);
}
}
/**
* Renders a single page of the page list.
*
* @private
* @param {Object} page - The page to render.
* @param {number} index - The index of the rendered page.
* @param {boolean} disabled - Renders the page disabled.
* @returns {React$Node}
*/
_renderPage(page, index, disabled) {
return page.component
? <View key = { index }>
{
React.createElement(
page.component,
{
disabled
})
}
</View>
: null;
}
/**
* Renders a page indicator (icon) for the page.
*
* @private
* @param {Object} page - The page the indicator is rendered for.
* @param {number} index - The index of the page the indicator is rendered
* for.
* @param {boolean} disabled - Renders the indicator disabled.
* @returns {React$Node}
*/
_renderPageIndicator(page, index, disabled) {
return page.component
? <TouchableOpacity
disabled = { disabled }
onPress = { this._onIconPress(index) }
style = { styles.pageIndicator } >
<View style = { styles.pageIndicator }>
<Icon
name = { page.icon }
style = { [
styles.pageIndicatorIcon,
this._getIndicatorStyle(index)
] } />
<Text
style = { [
styles.pageIndicatorText,
this._getIndicatorStyle(index)
] }>
{ page.title }
</Text>
</View>
</TouchableOpacity>
: null;
}
/**
* Renders the paged list if multiple pages are to be rendered. This is the
* platform dependent part of the component.
*
* @param {boolean} disabled - True if the rendered lists should be
* disabled.
* @returns {ReactElement}
*/
_renderPagedList(disabled) {
const { defaultPage, pages } = this.props;
return (
<View style = { styles.pagedListContainer }>
<ViewPagerAndroid
initialPage = { defaultPage }
onPageSelected = { this._onPageSelected }
peekEnabled = { true }
ref = { this._setViewPager }
style = { styles.pagedList }>
{
pages.map((page, index) => this._renderPage(
page, index, disabled
))
}
</ViewPagerAndroid>
<View style = { styles.pageIndicatorContainer }>
{
pages.map((page, index) => this._renderPageIndicator(
page, index, disabled
))
}
</View>
</View>
);
}
_setViewPager: Object => void;
/**
* Sets the {@link ViewPagerAndroid} instance.
*
* @param {ViewPagerAndroid} viewPager - The {@code ViewPagerAndroid}
* instance.
* @private
* @returns {void}
*/
_setViewPager(viewPager) {
this._viewPager = viewPager;
}
}
export default connect()(PagedList);

View File

@ -0,0 +1,100 @@
// @flow
import React from 'react';
import { TabBarIOS } from 'react-native';
import { connect } from 'react-redux';
import AbstractPagedList from './AbstractPagedList';
import styles from './styles';
/**
* An iOS specific component to render a paged list.
*
* @extends PagedList
*/
class PagedList extends AbstractPagedList {
/**
* Initializes a new {@code PagedList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onTabSelected = this._onTabSelected.bind(this);
}
_onTabSelected: number => Function;
/**
* Constructs a callback to update the selected tab when the bottom bar icon
* is pressed.
*
* @param {number} tabIndex - The selected tab.
* @private
* @returns {Function}
*/
_onTabSelected(tabIndex) {
return () => super._selectPage(tabIndex);
}
_renderPage: (Object, number, boolean) => React$Node
/**
* Renders a single page of the page list.
*
* @private
* @param {Object} page - The page to render.
* @param {number} index - The index of the rendered page.
* @param {boolean} disabled - Renders the page disabled.
* @returns {React$Node}
*/
_renderPage(page, index, disabled) {
const { pageIndex } = this.state;
return page.component
? <TabBarIOS.Item
icon = { page.icon }
key = { index }
onPress = { this._onTabSelected(index) }
selected = { pageIndex === index }
title = { page.title }>
{
React.createElement(
page.component,
{
disabled
})
}
</TabBarIOS.Item>
: null;
}
/**
* Renders the paged list if multiple pages are to be rendered. This is the
* platform dependent part of the component.
*
* @param {boolean} disabled - True if the rendered lists should be
* disabled.
* @returns {ReactElement}
*/
_renderPagedList(disabled) {
const { pages } = this.props;
return (
<TabBarIOS
itemPositioning = 'fill'
style = { styles.pagedList }>
{
pages.map((page, index) => this._renderPage(
page, index, disabled
))
}
</TabBarIOS>
);
}
}
export default connect()(PagedList);

View File

@ -3,6 +3,7 @@ export { default as Header } from './Header';
export { default as NavigateSectionList } from './NavigateSectionList';
export { default as Link } from './Link';
export { default as LoadingIndicator } from './LoadingIndicator';
export { default as PagedList } from './PagedList';
export { default as Pressable } from './Pressable';
export { default as SideBar } from './SideBar';
export { default as Text } from './Text';

View File

@ -71,6 +71,77 @@ const HEADER_STYLES = {
}
};
/**
* Style classes of the PagedList-based components.
*/
const PAGED_LIST_STYLES = {
/**
* Style of the page indicator (Android).
*/
pageIndicator: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
/**
* Additional style for the active indicator icon (Android).
*/
pageIndicatorActive: {
color: ColorPalette.white
},
/**
* Container for the page indicators (Android).
*/
pageIndicatorContainer: {
alignItems: 'stretch',
backgroundColor: ColorPalette.blue,
flexDirection: 'row',
height: 56,
justifyContent: 'center'
},
/**
* Icon of the page indicator (Android).
*/
pageIndicatorIcon: {
color: ColorPalette.blueHighlight,
fontSize: 24
},
/**
* Label of the page indicator (Android).
*/
pageIndicatorText: {
color: ColorPalette.blueHighlight
},
/**
* Top level style of the paged list.
*/
pagedList: {
flex: 1
},
/**
* The paged list container View.
*/
pagedListContainer: {
flex: 1,
flexDirection: 'column'
},
/**
* Disabled style for the container.
*/
pagedListContainerDisabled: {
opacity: 0.2
}
};
const SECTION_LIST_STYLES = {
/**
* The style of the actual avatar.
@ -248,6 +319,7 @@ const SIDEBAR_STYLES = {
*/
export default createStyleSheet({
...HEADER_STYLES,
...PAGED_LIST_STYLES,
...SECTION_LIST_STYLES,
...SIDEBAR_STYLES
});

View File

@ -1,173 +0,0 @@
// @flow
import React from 'react';
import { Text, TouchableOpacity, View, ViewPagerAndroid } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../base/font-icons';
import { MeetingList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
import AbstractPagedList, { DEFAULT_PAGE } from './AbstractPagedList';
import styles from './styles';
/**
* A platform specific component to render a paged or tabbed list/view.
*
* @extends PagedList
*/
class PagedList extends AbstractPagedList {
/**
* A reference to the viewpager.
*/
_viewPager: Object;
/**
* Initializes a new {@code PagedList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._getIndicatorStyle = this._getIndicatorStyle.bind(this);
this._onPageSelected = this._onPageSelected.bind(this);
this._onSelectPage = this._onSelectPage.bind(this);
this._setViewPager = this._setViewPager.bind(this);
}
_getIndicatorStyle: number => Object;
/**
* Constructs the style of an indicator.
*
* @param {number} indicatorIndex - The index of the indicator.
* @private
* @returns {Object}
*/
_getIndicatorStyle(indicatorIndex) {
if (this.state.pageIndex === indicatorIndex) {
return styles.pageIndicatorTextActive;
}
return null;
}
_onPageSelected: Object => void;
/**
* Updates the index of the currently selected page.
*
* @param {Object} event - The native event of the callback.
* @private
* @returns {void}
*/
_onPageSelected({ nativeEvent: { position } }) {
if (this.state.pageIndex !== position) {
this._selectPage(position);
}
}
_onSelectPage: number => Function;
/**
* Constructs a function to be used as a callback for the tab bar.
*
* @param {number} pageIndex - The index of the page to activate via the
* callback.
* @private
* @returns {Function}
*/
_onSelectPage(pageIndex) {
return () => {
this._viewPager.setPage(pageIndex);
this._selectPage(pageIndex);
};
}
/**
* Renders the entire paged list if calendar is enabled.
*
* @param {boolean} disabled - True if the rendered lists should be
* disabled.
* @returns {ReactElement}
*/
_renderPagedList(disabled) {
return (
<View style = { styles.pagedListContainer }>
<ViewPagerAndroid
initialPage = { DEFAULT_PAGE }
onPageSelected = { this._onPageSelected }
peekEnabled = { true }
ref = { this._setViewPager }
style = { styles.pagedList }>
<View key = { 0 }>
<RecentList disabled = { disabled } />
</View>
<View key = { 1 }>
<MeetingList disabled = { disabled } />
</View>
</ViewPagerAndroid>
<View style = { styles.pageIndicatorContainer }>
<TouchableOpacity
disabled = { disabled }
onPress = { this._onSelectPage(0) }
style = { styles.pageIndicator } >
<View style = { styles.pageIndicator }>
<Icon
name = 'restore'
style = { [
styles.pageIndicatorIcon,
this._getIndicatorStyle(0)
] } />
<Text
style = { [
styles.pageIndicatorText,
this._getIndicatorStyle(0)
] }>
History
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
disabled = { disabled }
onPress = { this._onSelectPage(1) }
style = { styles.pageIndicator } >
<View style = { styles.pageIndicator }>
<Icon
name = 'event_note'
style = { [
styles.pageIndicatorIcon,
this._getIndicatorStyle(1)
] } />
<Text
style = { [
styles.pageIndicatorText,
this._getIndicatorStyle(1)
] }>
Calendar
</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
}
_setViewPager: Object => void;
/**
* Sets the {@link ViewPagerAndroid} instance.
*
* @param {ViewPagerAndroid} viewPager - The {@code ViewPagerAndroid}
* instance.
* @private
* @returns {void}
*/
_setViewPager(viewPager) {
this._viewPager = viewPager;
}
}
export default connect()(PagedList);

View File

@ -1,81 +0,0 @@
// @flow
import React from 'react';
import { TabBarIOS } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { MeetingList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
import AbstractPagedList from './AbstractPagedList';
import styles from './styles';
const CALENDAR_ICON = require('../../../../images/calendar.png');
/**
* A platform specific component to render a paged or tabbed list/view.
*
* @extends PagedList
*/
class PagedList extends AbstractPagedList {
/**
* Initializes a new {@code PagedList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onTabSelected = this._onTabSelected.bind(this);
}
_onTabSelected: number => Function;
/**
* Constructs a callback to update the selected tab.
*
* @param {number} tabIndex - The selected tab.
* @private
* @returns {Function}
*/
_onTabSelected(tabIndex) {
return () => super._selectPage(tabIndex);
}
/**
* Renders the entire paged list if calendar is enabled.
*
* @param {boolean} disabled - True if the rendered lists should be
* disabled.
* @returns {ReactElement}
*/
_renderPagedList(disabled) {
const { pageIndex } = this.state;
const { t } = this.props;
return (
<TabBarIOS
itemPositioning = 'fill'
style = { styles.pagedList }>
<TabBarIOS.Item
onPress = { this._onTabSelected(0) }
selected = { pageIndex === 0 }
systemIcon = 'history'>
<RecentList disabled = { disabled } />
</TabBarIOS.Item>
<TabBarIOS.Item
icon = { CALENDAR_ICON }
onPress = { this._onTabSelected(1) }
selected = { pageIndex === 1 }
title = { t('welcomepage.calendar') }>
<MeetingList disabled = { disabled } />
</TabBarIOS.Item>
</TabBarIOS>
);
}
}
export default translate(connect()(PagedList));

View File

@ -26,12 +26,12 @@ import { SettingsView } from '../../settings';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
import { setSideBarVisible } from '../actions';
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
import PagedList from './PagedList';
import styles, {
PLACEHOLDER_TEXT_COLOR,
SWITCH_THUMB_COLOR,
SWITCH_UNDER_COLOR
} from './styles';
import WelcomePageLists from './WelcomePageLists';
import WelcomePageSideBar from './WelcomePageSideBar';
/**
@ -139,7 +139,7 @@ class WelcomePage extends AbstractWelcomePage {
}
</View>
</SafeAreaView>
<PagedList disabled = { this.state._fieldFocused } />
<WelcomePageLists disabled = { this.state._fieldFocused } />
<SettingsView />
</View>
<WelcomePageSideBar />

View File

@ -0,0 +1,88 @@
// @flow
import React, { Component } from 'react';
import { Platform } from 'react-native';
import { translate } from '../../base/i18n';
import { PagedList } from '../../base/react';
import { MeetingList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
type Props = {
/**
* Renders the lists disabled.
*/
disabled: boolean,
/**
* The i18n translate function.
*/
t: Function
};
/**
* Icon to be used for the calendar page on iOS.
*/
const IOS_CALENDAR_ICON = require('../../../../images/calendar.png');
/**
* Icon to be used for the recent list page on iOS.
*/
const IOS_RECENT_LIST_ICON = require('../../../../images/history.png');
/**
* Implements the lists displayed on the mobile welcome screen.
*/
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.
*/
pages: Array<{
component: Object,
icon: string | number,
title: string
}>
/**
* Component contructor.
*
* @inheritdoc
*/
constructor(props) {
super(props);
const { t } = props;
const isAndroid = 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')
} ];
}
/**
* Implements React Component's render.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
return (
<PagedList
defaultPage = { 0 }
disabled = { disabled }
pages = { this.pages } />
);
}
}
export default translate(WelcomePageLists);

View File

@ -161,53 +161,6 @@ export default createStyleSheet({
flexDirection: 'column'
},
pageIndicator: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
pageIndicatorContainer: {
alignItems: 'stretch',
backgroundColor: ColorPalette.blue,
flexDirection: 'row',
height: 56,
justifyContent: 'center'
},
pageIndicatorIcon: {
color: ColorPalette.blueHighlight,
fontSize: 24
},
pageIndicatorText: {
color: ColorPalette.blueHighlight
},
pageIndicatorTextActive: {
color: ColorPalette.white
},
/**
* Top level style of the paged list.
*/
pagedList: {
flex: 1
},
pagedListContainer: {
flex: 1,
flexDirection: 'column'
},
/**
* Disabled style for the container.
*/
pagedListContainerDisabled: {
opacity: 0.2
},
/**
* Container for room name input box and 'join' button.
*/