diff --git a/images/calendar@2x.png b/images/calendar@2x.png deleted file mode 100644 index 2554057c1..000000000 Binary files a/images/calendar@2x.png and /dev/null differ diff --git a/images/calendar@3x.png b/images/calendar@3x.png deleted file mode 100644 index 4bf8170e2..000000000 Binary files a/images/calendar@3x.png and /dev/null differ diff --git a/images/history@2x.png b/images/history@2x.png deleted file mode 100644 index f6096cab3..000000000 Binary files a/images/history@2x.png and /dev/null differ diff --git a/images/history@3x.png b/images/history@3x.png deleted file mode 100644 index f837fda0e..000000000 Binary files a/images/history@3x.png and /dev/null differ diff --git a/lang/main.json b/lang/main.json index 020b25a5a..b7b09e93e 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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.", diff --git a/package-lock.json b/package-lock.json index 85c441dac..25bf9aa4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6211b1541..2c5bab386 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/react/features/base/react/Types.js b/react/features/base/react/Types.js index a10c6ba2f..25e9700c7 100644 --- a/react/features/base/react/Types.js +++ b/react/features/base/react/Types.js @@ -17,6 +17,11 @@ export type Item = { */ elementAfter?: ?ComponentType, + /** + * Unique ID of the item. + */ + id: Object | string, + /** * Item title */ diff --git a/react/features/base/react/components/AbstractPage.js b/react/features/base/react/components/AbstractPage.js new file mode 100644 index 000000000..be7e8265b --- /dev/null +++ b/react/features/base/react/components/AbstractPage.js @@ -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

extends Component

{ + /** + * 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. + } +} diff --git a/react/features/base/react/components/NavigateSectionList.js b/react/features/base/react/components/NavigateSectionList.js index 2a6858d3f..22ed69777 100644 --- a/react/features/base/react/components/NavigateSectionList.js +++ b/react/features/base/react/components/NavigateSectionList.js @@ -43,7 +43,13 @@ type Props = { /** * An array of sections */ - sections: Array

+ sections: Array
, + + /** + * Optional array of on-slide actions this list should support. For details + * see https://github.com/dancormier/react-native-swipeout. + */ + slideActions?: Array }; /** @@ -205,7 +211,8 @@ class NavigateSectionList extends Component { key = { key } onPress = { url ? this._onPress(url) : undefined } secondaryAction = { - url ? undefined : this._onSecondaryAction(id) } /> + url ? undefined : this._onSecondaryAction(id) } + slideActions = { this.props.slideActions } /> ); } diff --git a/react/features/base/react/components/index.js b/react/features/base/react/components/index.js index bf512d974..0020d19af 100644 --- a/react/features/base/react/components/index.js +++ b/react/features/base/react/components/index.js @@ -1,2 +1,3 @@ export * from './_'; +export { default as AbstractPage } from './AbstractPage'; export { default as NavigateSectionList } from './NavigateSectionList'; diff --git a/react/features/base/react/components/native/AbstractPagedList.js b/react/features/base/react/components/native/AbstractPagedList.js deleted file mode 100644 index c991106e2..000000000 --- a/react/features/base/react/components/native/AbstractPagedList.js +++ /dev/null @@ -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 { - /** - * 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 ( - - { - pages.length > 1 - ? this._renderPagedList(disabled) - : pages.length === 1 - ? React.createElement( - - // $FlowExpectedError - /* type */ pages[0].component, - /* props */ { - disabled, - style: styles.pagedList - }) - : 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; - - 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)); - } -} diff --git a/react/features/base/react/components/native/NavigateSectionListItem.js b/react/features/base/react/components/native/NavigateSectionListItem.js index 9018b01df..b7c6a43b5 100644 --- a/react/features/base/react/components/native/NavigateSectionListItem.js +++ b/react/features/base/react/components/native/NavigateSectionListItem.js @@ -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 } /** @@ -129,37 +138,59 @@ export default class NavigateSectionListItem extends Component { * @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 ( - - - - - {title.substr(0, 1).toUpperCase()} - + + + + + + {title.substr(0, 1).toUpperCase()} + + + + + {title} + + {this._renderItemLines(lines)} + + { this.props.secondaryAction + && this._renderSecondaryAction() } - - - {title} - - {this._renderItemLines(lines)} - - { this.props.secondaryAction && this._renderSecondaryAction() } - + ); } } diff --git a/react/features/base/react/components/native/PagedList.android.js b/react/features/base/react/components/native/PagedList.android.js deleted file mode 100644 index 6563eb708..000000000 --- a/react/features/base/react/components/native/PagedList.android.js +++ /dev/null @@ -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 - ? - { - React.createElement( - page.component, - { - disabled - }) - } - - : 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 - ? - - - - { page.title } - - - - : 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 ( - - - { - pages.map((page, index) => this._renderPage( - page, index, disabled - )) - } - - - { - pages.map((page, index) => this._renderPageIndicator( - page, index, disabled - )) - } - - - ); - } - - _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); diff --git a/react/features/base/react/components/native/PagedList.ios.js b/react/features/base/react/components/native/PagedList.ios.js deleted file mode 100644 index dbc939fff..000000000 --- a/react/features/base/react/components/native/PagedList.ios.js +++ /dev/null @@ -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 - ? - { - React.createElement( - page.component, - { - disabled - }) - } - - : 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 ( - - { - pages.map((page, index) => this._renderPage( - page, index, disabled - )) - } - - ); - } -} - -export default connect()(PagedList); diff --git a/react/features/base/react/components/native/PagedList.js b/react/features/base/react/components/native/PagedList.js new file mode 100644 index 000000000..d02b70460 --- /dev/null +++ b/react/features/base/react/components/native/PagedList.js @@ -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 { + + /** + * 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 ( + + { + pages.length > 1 + ? this._renderPagedList(disabled) + : React.createElement( + + // $FlowExpectedError + /* type */ pages[0].component, + /* props */ { + disabled, + style: styles.pagedList + }) + } + + ); + } + + /** + * 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 ( + + { + React.createElement( + page.component, + { + disabled + }) + } + + ); + } + + /** + * 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 ( + + { + this._renderPage(pages[pageIndex], disabled) + } + + { + pages.map((page, index) => this._renderPageIndicator( + page, index, disabled + )) + } + + + ); + } + + /** + * 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 ( + + + + + { page.title } + + + + ); + } + + /** + * 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); diff --git a/react/features/base/react/components/native/styles.js b/react/features/base/react/components/native/styles.js index 53f193843..ef9b7cce5 100644 --- a/react/features/base/react/components/native/styles.js +++ b/react/features/base/react/components/native/styles.js @@ -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: { diff --git a/react/features/base/styles/components/styles/ColorPalette.js b/react/features/base/styles/components/styles/ColorPalette.js index ac3e8bfd6..209795820 100644 --- a/react/features/base/styles/components/styles/ColorPalette.js +++ b/react/features/base/styles/components/styles/ColorPalette.js @@ -26,6 +26,7 @@ export const ColorPalette = { lightGrey: '#AAAAAA', lighterGrey: '#EEEEEE', red: '#D00000', + transparent: 'rgba(0, 0, 0, 0)', white: 'white', /** diff --git a/react/features/calendar-sync/components/CalendarList.native.js b/react/features/calendar-sync/components/CalendarList.native.js index a2f2b987d..45ca25b1c 100644 --- a/react/features/calendar-sync/components/CalendarList.native.js +++ b/react/features/calendar-sync/components/CalendarList.native.js @@ -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 { +class CalendarList extends AbstractPage { /** * Initializes a new {@code CalendarList} instance. * @@ -50,6 +52,21 @@ class CalendarList extends Component { = 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 { const { disabled } = this.props; return ( - BaseCalendarList - ? diff --git a/react/features/calendar-sync/components/CalendarList.web.js b/react/features/calendar-sync/components/CalendarList.web.js index 40b56c529..acc48dc67 100644 --- a/react/features/calendar-sync/components/CalendarList.web.js +++ b/react/features/calendar-sync/components/CalendarList.web.js @@ -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 { +class CalendarList extends AbstractPage { /** * Initializes a new {@code CalendarList} instance. * @@ -78,8 +79,8 @@ class CalendarList extends Component { const { disabled } = this.props; return ( - BaseCalendarList - ? diff --git a/react/features/calendar-sync/components/BaseCalendarList.js b/react/features/calendar-sync/components/CalendarListContent.js similarity index 89% rename from react/features/calendar-sync/components/BaseCalendarList.js rename to react/features/calendar-sync/components/CalendarListContent.js index 3dc7b490e..bd496d3c5 100644 --- a/react/features/calendar-sync/components/BaseCalendarList.js +++ b/react/features/calendar-sync/components/CalendarListContent.js @@ -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 { +class CalendarListContent extends Component { /** * Default values for the component's props. */ @@ -63,26 +62,7 @@ class BaseCalendarList extends Component { }; /** - * 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; diff --git a/react/features/recent-list/actionTypes.js b/react/features/recent-list/actionTypes.js index 4377b64ef..a72524b21 100644 --- a/react/features/recent-list/actionTypes.js +++ b/react/features/recent-list/actionTypes.js @@ -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. * diff --git a/react/features/recent-list/actions.js b/react/features/recent-list/actions.js index aab9b8ce4..d288fbce3 100644 --- a/react/features/recent-list/actions.js +++ b/react/features/recent-list/actions.js @@ -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. * diff --git a/react/features/recent-list/components/RecentList.js b/react/features/recent-list/components/RecentList.js index 5d290cc5a..eed501db2 100644 --- a/react/features/recent-list/components/RecentList.js +++ b/react/features/recent-list/components/RecentList.js @@ -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 { +class RecentList extends AbstractPage { /** * Initializes a new {@code RecentList} instance. * @@ -60,6 +66,7 @@ class RecentList extends Component { 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 { } 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 ( { onPress = { this._onPress } renderListEmptyComponent = { this._getRenderListEmptyComponent() } - sections = { recentList } /> + sections = { recentList } + slideActions = { slideActions } /> ); } @@ -121,6 +134,19 @@ class RecentList extends Component { ); } + _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; /** diff --git a/react/features/recent-list/functions.any.js b/react/features/recent-list/functions.any.js index 301dea330..ee85ec410 100644 --- a/react/features/recent-list/functions.any.js +++ b/react/features/recent-list/functions.any.js @@ -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), diff --git a/react/features/recent-list/reducer.js b/react/features/recent-list/reducer.js index e49ec46a3..ae877782b 100644 --- a/react/features/recent-list/reducer.js +++ b/react/features/recent-list/reducer.js @@ -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} state - The Redux state. + * @param {Object} entryId - The ID object of the entry. + * @returns {Array} + */ +function _deleteRecentListEntry( + state: Array, entryId: Object): Array { + return state.filter(entry => + entry.conference !== entryId.url || entry.date !== entryId.date); +} + /** * Reduces the redux action {@link APP_WILL_MOUNT}. * diff --git a/react/features/welcome/components/WelcomePageLists.js b/react/features/welcome/components/WelcomePageLists.js index 3227b39bb..2eb7c7065 100644 --- a/react/features/welcome/components/WelcomePageLists.js +++ b/react/features/welcome/components/WelcomePageLists.js @@ -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 { 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') } ];