[RN] Add swipe to delete feature

This commit is contained in:
Bettenbuk Zoltan 2018-09-25 14:48:03 +02:00 committed by Saúl Ibarra Corretgé
parent 9d27c36d80
commit d8c1f107da
27 changed files with 538 additions and 567 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 B

View File

@ -63,6 +63,7 @@
"join": "JOIN",
"privacy": "Privacy",
"recentList": "Recent",
"recentListDelete": "Delete",
"recentListEmpty": "Your recent list is currently empty. Chat with your team and you will find all your recent meetings here.",
"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.",

35
package-lock.json generated
View File

@ -11426,8 +11426,7 @@
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"pify": {
"version": "2.3.0",
@ -12281,6 +12280,14 @@
"integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=",
"dev": true
},
"raf": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz",
"integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==",
"requires": {
"performance-now": "^2.1.0"
}
},
"raf-schd": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-2.1.2.tgz",
@ -12657,6 +12664,16 @@
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.9.tgz",
"integrity": "sha1-awCw9K/QF83gn7udFx3xtdW4Uag="
},
"react-native-swipeout": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/react-native-swipeout/-/react-native-swipeout-2.3.6.tgz",
"integrity": "sha512-t9suUCspzck4vp2pWggWe0frS/QOtX6yYCawHnEes75A7dZCEE74bxX2A1bQzGH9cUMjq6xsdfC94RbiDKIkJg==",
"requires": {
"create-react-class": "^15.6.0",
"prop-types": "^15.5.10",
"react-tween-state": "^0.1.5"
}
},
"react-native-vector-icons": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-4.4.2.tgz",
@ -12805,6 +12822,15 @@
}
}
},
"react-tween-state": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/react-tween-state/-/react-tween-state-0.1.5.tgz",
"integrity": "sha1-6YsGZVHvuTy5LdG+FJlcLj3q4zk=",
"requires": {
"raf": "^3.1.0",
"tween-functions": "^1.0.1"
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@ -15500,6 +15526,11 @@
"safe-buffer": "^5.0.1"
}
},
"tween-functions": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
"integrity": "sha1-GuOlDnxguz3vd06scHrLynO7w/8="
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",

View File

@ -73,6 +73,7 @@
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
"react-native-prompt": "1.0.0",
"react-native-sound": "0.10.9",
"react-native-swipeout": "2.3.6",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#be3de15bb988cfabbb62cd4b3b06f4c920ee5ba0",
"react-redux": "5.0.7",

View File

@ -17,6 +17,11 @@ export type Item = {
*/
elementAfter?: ?ComponentType<any>,
/**
* Unique ID of the item.
*/
id: Object | string,
/**
* Item title
*/

View File

@ -0,0 +1,22 @@
// @flow
import { Component } from 'react';
/**
* Abstract component that defines a refreshable page to be rendered by
* {@code PagedList}.
*/
export default class AbstractPage<P> extends Component<P> {
/**
* Method to be overriden by the implementing classes to refresh the data
* content of the component.
*
* Note: It is a static method as the {@code Component} may not be
* initialized yet when the UI invokes refresh (e.g. tab change).
*
* @returns {void}
*/
static refresh() {
// No implementation in abstract class.
}
}

View File

@ -43,7 +43,13 @@ type Props = {
/**
* An array of sections
*/
sections: Array<Section>
sections: Array<Section>,
/**
* Optional array of on-slide actions this list should support. For details
* see https://github.com/dancormier/react-native-swipeout.
*/
slideActions?: Array<Object>
};
/**
@ -205,7 +211,8 @@ class NavigateSectionList extends Component<Props> {
key = { key }
onPress = { url ? this._onPress(url) : undefined }
secondaryAction = {
url ? undefined : this._onSecondaryAction(id) } />
url ? undefined : this._onSecondaryAction(id) }
slideActions = { this.props.slideActions } />
);
}

View File

@ -1,2 +1,3 @@
export * from './_';
export { default as AbstractPage } from './AbstractPage';
export { default as NavigateSectionList } from './NavigateSectionList';

View File

@ -1,184 +0,0 @@
// @flow
import React, { Component } from 'react';
import { View } from 'react-native';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link AbstractPagedList}.
*/
type Props = {
/**
* The zero-based index of the page that should be rendered (selected) by
* default.
*/
defaultPage: number,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* Callback to execute on page change.
*/
onSelectPage: ?Function,
/**
* The pages of the PagedList component 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,
icon: string | number,
title: string
}>
};
/**
* The type of the React {@code Component} state of {@link AbstractPagedList}.
*/
type State = {
/**
* The currently selected page.
*/
pageIndex: number
};
/**
* Abstract class containing the platform independent logic of the paged lists.
*/
export default class AbstractPagedList extends Component<Props, State> {
/**
* Initializes a new {@code AbstractPagedList} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
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 {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._maybeRefreshSelectedPage(false);
}
/**
* Renders the component.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
const pages = this.props.pages.filter(({ component }) => component);
return (
<View
style = { [
styles.pagedListContainer,
disabled ? styles.pagedListContainerDisabled : null
] }>
{
pages.length > 1
? this._renderPagedList(disabled)
: pages.length === 1
? React.createElement(
// $FlowExpectedError
/* type */ pages[0].component,
/* props */ {
disabled,
style: styles.pagedList
})
: null
}
</View>
);
}
_maybeRefreshSelectedPage: ?boolean => void;
/**
* Components that this PagedList displays may have a refresh function to
* refresh its content when displayed (or based on custom logic). This
* function invokes this logic if it's present.
*
* @private
* @param {boolean} isInteractive - If true this refresh was caused by
* direct user interaction, false otherwise.
* @returns {void}
*/
_maybeRefreshSelectedPage(isInteractive: boolean = true) {
const selectedPage = this.props.pages[this.state.pageIndex];
let component;
if (selectedPage && (component = selectedPage.component)) {
const { refresh } = component;
typeof refresh === 'function'
&& refresh.call(component, this.props.dispatch, isInteractive);
}
}
_renderPagedList: boolean => React$Node;
_selectPage: number => void;
/**
* Sets the selected page.
*
* @param {number} pageIndex - The index of the selected page.
* @protected
* @returns {void}
*/
_selectPage(pageIndex: number) {
// eslint-disable-next-line no-param-reassign
pageIndex = this._validatePageIndex(pageIndex);
const { onSelectPage } = this.props;
typeof onSelectPage === 'function' && onSelectPage(pageIndex);
this.setState({ pageIndex }, this._maybeRefreshSelectedPage);
}
_validatePageIndex: number => number;
/**
* Validates the requested page index and returns a safe value.
*
* @private
* @param {number} pageIndex - The requested page index.
* @returns {number}
*/
_validatePageIndex(pageIndex) {
// 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(({ component }) => component).length - 1;
return Math.max(0, Math.min(maxPageIndex, pageIndex));
}
}

View File

@ -1,6 +1,9 @@
// @flow
import React, { Component } from 'react';
import Swipeout from 'react-native-swipeout';
import { ColorPalette } from '../../../styles';
import Container from './Container';
import Text from './Text';
@ -22,7 +25,13 @@ type Props = {
/**
* Function to be invoked when secondary action was performed on an Item.
*/
secondaryAction: ?Function
secondaryAction: ?Function,
/**
* Optional array of on-slide actions this list should support. For details
* see https://github.com/dancormier/react-native-swipeout.
*/
slideActions?: Array<Object>
}
/**
@ -129,37 +138,59 @@ export default class NavigateSectionListItem extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { colorBase, lines, title } = this.props.item;
const { slideActions } = this.props;
const { id, colorBase, lines, title } = this.props.item;
const avatarStyles = {
...styles.avatar,
...this._getAvatarColor(colorBase)
};
let right;
// NOTE: The {@code Swipeout} component has an onPress prop encapsulated
// in the {@code right} array, but we need to bind it to the ID of the
// item too.
if (slideActions) {
right = [];
for (const slideAction of slideActions) {
right.push({
backgroundColor: slideAction.backgroundColor,
onPress: slideAction.onPress.bind(undefined, id),
text: slideAction.text
});
}
}
return (
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
underlayColor = { UNDERLAY_COLOR }>
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
<Text style = { styles.avatarContent }>
{title.substr(0, 1).toUpperCase()}
</Text>
<Swipeout
backgroundColor = { ColorPalette.transparent }
right = { right }>
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
underlayColor = { UNDERLAY_COLOR }>
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
<Text style = { styles.avatarContent }>
{title.substr(0, 1).toUpperCase()}
</Text>
</Container>
</Container>
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = { [
styles.listItemText,
styles.listItemTitle
] }>
{title}
</Text>
{this._renderItemLines(lines)}
</Container>
{ this.props.secondaryAction
&& this._renderSecondaryAction() }
</Container>
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = {{
...styles.listItemText,
...styles.listItemTitle
}}>
{title}
</Text>
{this._renderItemLines(lines)}
</Container>
{ this.props.secondaryAction && this._renderSecondaryAction() }
</Container>
</Swipeout>
);
}
}

View File

@ -1,199 +0,0 @@
// @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 }
key = { index }
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

@ -1,100 +0,0 @@
// @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

@ -0,0 +1,286 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../../font-icons';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link PagedList}.
*/
type Props = {
/**
* The zero-based index of the page that should be rendered (selected) by
* default.
*/
defaultPage: number,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* Callback to execute on page change.
*/
onSelectPage: ?Function,
/**
* The pages of the PagedList component to be rendered.
*
* NOTE 1: An element's {@code component} may be {@code undefined} and then
* it won't need to be rendered.
*
* NOTE 2: There must be at least one page available and enabled.
*/
pages: Array<{
component: ?Object,
icon: string | number,
title: string
}>
};
/**
* The type of the React {@code Component} state of {@link PagedList}.
*/
type State = {
/**
* The currently selected page.
*/
pageIndex: number
};
/**
* A component that renders a paged list.
*
* @extends PagedList
*/
class PagedList extends Component<Props, State> {
/**
* Initializes a new {@code PagedList} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
pageIndex: this._validatePageIndex(props.defaultPage)
};
// Bind event handlers so they are only bound once per instance.
this._maybeRefreshSelectedPage
= this._maybeRefreshSelectedPage.bind(this);
}
/**
* Renders the component.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
const pages = this.props.pages.filter(({ component }) => component);
return (
<View
style = { [
styles.pagedListContainer,
disabled ? styles.pagedListContainerDisabled : null
] }>
{
pages.length > 1
? this._renderPagedList(disabled)
: React.createElement(
// $FlowExpectedError
/* type */ pages[0].component,
/* props */ {
disabled,
style: styles.pagedList
})
}
</View>
);
}
/**
* 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;
}
_maybeRefreshSelectedPage: ?boolean => void;
/**
* Components that this PagedList displays may have a refresh function to
* refresh its content when displayed (or based on custom logic). This
* function invokes this logic if it's present.
*
* @private
* @param {boolean} isInteractive - If true this refresh was caused by
* direct user interaction, false otherwise.
* @returns {void}
*/
_maybeRefreshSelectedPage(isInteractive: boolean = true) {
const selectedPage = this.props.pages[this.state.pageIndex];
let component;
if (selectedPage && (component = selectedPage.component)) {
const { refresh } = component;
refresh.call(component, this.props.dispatch, isInteractive);
}
}
/**
* Sets the selected page.
*
* @param {number} pageIndex - The index of the selected page.
* @protected
* @returns {void}
*/
_onSelectPage(pageIndex: number) {
return () => {
// eslint-disable-next-line no-param-reassign
pageIndex = this._validatePageIndex(pageIndex);
const { onSelectPage } = this.props;
onSelectPage && onSelectPage(pageIndex);
this.setState({ pageIndex }, this._maybeRefreshSelectedPage);
};
}
/**
* Renders a single page of the page list.
*
* @private
* @param {Object} page - The page to render.
* @param {boolean} disabled - Renders the page disabled.
* @returns {React$Node}
*/
_renderPage(page, disabled) {
if (!page.component) {
return null;
}
return (
<View style = { styles.pageContainer }>
{
React.createElement(
page.component,
{
disabled
})
}
</View>
);
}
/**
* Renders the paged list if multiple pages are to be rendered.
*
* @param {boolean} disabled - True if the rendered lists should be
* disabled.
* @returns {ReactElement}
*/
_renderPagedList(disabled) {
const { pages } = this.props;
const { pageIndex } = this.state;
return (
<View style = { styles.pagedListContainer }>
{
this._renderPage(pages[pageIndex], disabled)
}
<View style = { styles.pageIndicatorContainer }>
{
pages.map((page, index) => this._renderPageIndicator(
page, index, disabled
))
}
</View>
</View>
);
}
/**
* 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) {
if (!page.component) {
return null;
}
return (
<TouchableOpacity
disabled = { disabled }
key = { index }
onPress = { this._onSelectPage(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>
);
}
/**
* Validates the requested page index and returns a safe value.
*
* @private
* @param {number} pageIndex - The requested page index.
* @returns {number}
*/
_validatePageIndex(pageIndex) {
// 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(({ component }) => component).length - 1;
return Math.max(0, Math.min(maxPageIndex, pageIndex));
}
}
export default connect()(PagedList);

View File

@ -73,6 +73,13 @@ const HEADER_STYLES = {
*/
const PAGED_LIST_STYLES = {
/**
* Outermost container of a page in {@code PagedList}.
*/
pageContainer: {
flex: 1
},
/**
* Style of the page indicator (Android).
*/
@ -214,7 +221,7 @@ const SECTION_LIST_STYLES = {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
paddingVertical: 5
padding: 5
},
listItemDetails: {
@ -239,7 +246,8 @@ const SECTION_LIST_STYLES = {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
flex: 1,
flexDirection: 'row',
padding: 5
paddingVertical: 5,
paddingHorizontal: 10
},
listSectionText: {

View File

@ -26,6 +26,7 @@ export const ColorPalette = {
lightGrey: '#AAAAAA',
lighterGrey: '#EEEEEE',
red: '#D00000',
transparent: 'rgba(0, 0, 0, 0)',
white: 'white',
/**

View File

@ -1,16 +1,18 @@
// @flow
import React, { Component } from 'react';
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { openSettings } from '../../mobile/permissions';
import { translate } from '../../base/i18n';
import { AbstractPage } from '../../base/react';
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import styles from './styles';
import BaseCalendarList from './BaseCalendarList';
import CalendarListContent from './CalendarListContent';
/**
* The tyoe of the React {@code Component} props of {@link CalendarList}.
@ -36,7 +38,7 @@ type Props = {
/**
* Component to display a list of events from the (mobile) user's calendar.
*/
class CalendarList extends Component<Props> {
class CalendarList extends AbstractPage<Props> {
/**
* Initializes a new {@code CalendarList} instance.
*
@ -50,6 +52,21 @@ class CalendarList extends Component<Props> {
= this._getRenderListEmptyComponent.bind(this);
}
/**
* Public API method for {@code Component}s rendered in
* {@link AbstractPagedList}. When invoked, refreshes the calendar entries
* in the app.
*
* @param {Function} dispatch - The Redux dispatch function.
* @param {boolean} isInteractive - If true this refresh was caused by
* direct user interaction, false otherwise.
* @public
* @returns {void}
*/
static refresh(dispatch, isInteractive) {
dispatch(refreshCalendar(false, isInteractive));
}
/**
* Implements React's {@link Component#render}.
*
@ -59,8 +76,8 @@ class CalendarList extends Component<Props> {
const { disabled } = this.props;
return (
BaseCalendarList
? <BaseCalendarList
CalendarListContent
? <CalendarListContent
disabled = { disabled }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() } />

View File

@ -2,10 +2,11 @@
import Button from '@atlaskit/button';
import Spinner from '@atlaskit/spinner';
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { AbstractPage } from '../../base/react';
import { openSettingsDialog, SETTINGS_TABS } from '../../settings';
import {
createCalendarClickedEvent,
@ -15,7 +16,7 @@ import {
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import BaseCalendarList from './BaseCalendarList';
import CalendarListContent from './CalendarListContent';
declare var interfaceConfig: Object;
@ -53,7 +54,7 @@ type Props = {
/**
* Component to display a list of events from the user's calendar.
*/
class CalendarList extends Component<Props> {
class CalendarList extends AbstractPage<Props> {
/**
* Initializes a new {@code CalendarList} instance.
*
@ -78,8 +79,8 @@ class CalendarList extends Component<Props> {
const { disabled } = this.props;
return (
BaseCalendarList
? <BaseCalendarList
CalendarListContent
? <CalendarListContent
disabled = { disabled }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() } />

View File

@ -13,7 +13,6 @@ import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { refreshCalendar, openUpdateCalendarEventDialog } from '../actions';
import { isCalendarEnabled } from '../functions';
import AddMeetingUrlButton from './AddMeetingUrlButton';
@ -21,7 +20,7 @@ import JoinButton from './JoinButton';
/**
* The type of the React {@code Component} props of
* {@link BaseCalendarList}.
* {@link CalendarListContent}.
*/
type Props = {
@ -54,7 +53,7 @@ type Props = {
/**
* Component to display a list of events from a connected calendar.
*/
class BaseCalendarList extends Component<Props> {
class CalendarListContent extends Component<Props> {
/**
* Default values for the component's props.
*/
@ -63,26 +62,7 @@ class BaseCalendarList extends Component<Props> {
};
/**
* Public API method for {@code Component}s rendered in
* {@link AbstractPagedList}. When invoked, refreshes the calendar entries
* in the app.
*
* Note: It is a static method as the {@code Component} may not be
* initialized yet when the UI invokes refresh (e.g. {@link TabBarIOS} tab
* change).
*
* @param {Function} dispatch - The Redux dispatch function.
* @param {boolean} isInteractive - If true this refresh was caused by
* direct user interaction, false otherwise.
* @public
* @returns {void}
*/
static refresh(dispatch, isInteractive) {
dispatch(refreshCalendar(false, isInteractive));
}
/**
* Initializes a new {@code BaseCalendarList} instance.
* Initializes a new {@code CalendarListContent} instance.
*
* @inheritdoc
*/
@ -318,5 +298,5 @@ function _mapStateToProps(state: Object) {
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(BaseCalendarList))
? translate(connect(_mapStateToProps)(CalendarListContent))
: undefined;

View File

@ -1,5 +1,15 @@
// @flow
/**
* Action type to signal the deletion of a list entry.
*
* {
* type: DELETE_RECENT_LIST_ENTRY,
* entryId: Object
* }
*/
export const DELETE_RECENT_LIST_ENTRY = Symbol('DELETE_RECENT_LIST_ENTRY');
/**
* Action type to signal a new addition to the list.
*

View File

@ -2,9 +2,27 @@
import {
_STORE_CURRENT_CONFERENCE,
_UPDATE_CONFERENCE_DURATION
_UPDATE_CONFERENCE_DURATION,
DELETE_RECENT_LIST_ENTRY
} from './actionTypes';
/**
* Deletes a recent list entry based on url and date.
*
* @param {Object} entryId - An object constructed of the url and the date of
* the entry for easy identification.
* @returns {{
* type: DELETE_RECENT_LIST_ENTRY,
* entryId: Object
* }}
*/
export function deleteRecentListEntry(entryId: Object) {
return {
type: DELETE_RECENT_LIST_ENTRY,
entryId
};
}
/**
* Action to initiate a new addition to the list.
*

View File

@ -1,5 +1,5 @@
// @flow
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import {
@ -9,9 +9,15 @@ import {
} from '../../analytics';
import { appNavigate, getDefaultURL } from '../../app';
import { translate } from '../../base/i18n';
import { Container, NavigateSectionList, Text } from '../../base/react';
import {
AbstractPage,
Container,
NavigateSectionList,
Text
} from '../../base/react';
import type { Section } from '../../base/react';
import { deleteRecentListEntry } from '../actions';
import { isRecentListEnabled, toDisplayableList } from '../functions';
import styles from './styles';
@ -51,7 +57,7 @@ type Props = {
* The cross platform container rendering the list of the recently joined rooms.
*
*/
class RecentList extends Component<Props> {
class RecentList extends AbstractPage<Props> {
/**
* Initializes a new {@code RecentList} instance.
*
@ -60,6 +66,7 @@ class RecentList extends Component<Props> {
constructor(props: Props) {
super(props);
this._onDelete = this._onDelete.bind(this);
this._onPress = this._onPress.bind(this);
}
@ -85,6 +92,11 @@ class RecentList extends Component<Props> {
}
const { disabled, t, _defaultServerURL, _recentList } = this.props;
const recentList = toDisplayableList(_recentList, t, _defaultServerURL);
const slideActions = [ {
backgroundColor: 'red',
onPress: this._onDelete,
text: t('welcomepage.recentListDelete')
} ];
return (
<NavigateSectionList
@ -92,7 +104,8 @@ class RecentList extends Component<Props> {
onPress = { this._onPress }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() }
sections = { recentList } />
sections = { recentList }
slideActions = { slideActions } />
);
}
@ -121,6 +134,19 @@ class RecentList extends Component<Props> {
);
}
_onDelete: Object => void
/**
* Callback for the delete action of the list.
*
* @param {Object} itemId - The ID of the entry thats deletion is
* requested.
* @returns {void}
*/
_onDelete(itemId) {
this.props.dispatch(deleteRecentListEntry(itemId));
}
_onPress: string => Function;
/**

View File

@ -20,6 +20,10 @@ export function toDisplayableItem(item, defaultServerURL, t) {
return {
colorBase: serverName,
id: {
date: item.date,
url: item.conference
},
key: `key-${item.conference}-${item.date}`,
lines: [
_toDateString(item.date, t),

View File

@ -6,7 +6,8 @@ import { PersistenceRegistry } from '../base/storage';
import {
_STORE_CURRENT_CONFERENCE,
_UPDATE_CONFERENCE_DURATION
_UPDATE_CONFERENCE_DURATION,
DELETE_RECENT_LIST_ENTRY
} from './actionTypes';
import { isRecentListEnabled } from './functions';
@ -54,6 +55,8 @@ ReducerRegistry.register(
switch (action.type) {
case APP_WILL_MOUNT:
return _appWillMount(state);
case DELETE_RECENT_LIST_ENTRY:
return _deleteRecentListEntry(state, action.entryId);
case _STORE_CURRENT_CONFERENCE:
return _storeCurrentConference(state, action);
@ -67,6 +70,19 @@ ReducerRegistry.register(
return state;
});
/**
* Deletes a recent list entry based on the url and date of the item.
*
* @param {Array<Object>} state - The Redux state.
* @param {Object} entryId - The ID object of the entry.
* @returns {Array<Object>}
*/
function _deleteRecentListEntry(
state: Array<Object>, entryId: Object): Array<Object> {
return state.filter(entry =>
entry.conference !== entryId.url || entry.date !== entryId.date);
}
/**
* Reduces the redux action {@link APP_WILL_MOUNT}.
*

View File

@ -1,7 +1,6 @@
// @flow
import React, { Component } from 'react';
import { Platform } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
@ -37,16 +36,6 @@ type Props = {
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.
*/
@ -73,17 +62,16 @@ class WelcomePageLists extends Component<Props> {
super(props);
const { t } = props;
const android = Platform.OS === 'android';
this.pages = [
{
component: RecentList,
icon: android ? 'restore' : IOS_RECENT_LIST_ICON,
icon: 'restore',
title: t('welcomepage.recentList')
},
{
component: CalendarList,
icon: android ? 'event_note' : IOS_CALENDAR_ICON,
icon: 'event_note',
title: t('welcomepage.calendar')
}
];