From b0966229950915243b578eb45ac3fe3fcdc95347 Mon Sep 17 00:00:00 2001 From: zbettenbuk Date: Tue, 13 Feb 2018 17:33:03 -0600 Subject: [PATCH] Unify recent and meeting lists --- lang/main.json | 5 + .../components/native/NavigateSectionList.js | 269 +++++++++++++ .../base/react/components/native/index.js | 1 + .../base/react/components/native/styles.js | 136 ++++++- react/features/base/util/dateUtil.js | 13 + .../components/MeetingList.native.js | 217 +++-------- .../calendar-sync/components/styles.js | 113 +----- .../components/AbstractRecentList.js | 75 ---- .../components/RecentList.native.js | 358 ++++++++++-------- react/features/recent-list/functions.js | 166 -------- .../welcome/components/PagedList.android.js | 11 +- .../welcome/components/PagedList.ios.js | 6 +- react/features/welcome/components/styles.js | 7 + 13 files changed, 711 insertions(+), 666 deletions(-) create mode 100644 react/features/base/react/components/native/NavigateSectionList.js delete mode 100644 react/features/recent-list/components/AbstractRecentList.js delete mode 100644 react/features/recent-list/functions.js diff --git a/lang/main.json b/lang/main.json index 134d45760..4558553db 100644 --- a/lang/main.json +++ b/lang/main.json @@ -533,5 +533,10 @@ "next": "Upcoming", "nextMeeting": "next meeting", "now": "Now" + }, + "recentList": { + "today": "Today", + "yesterday": "Yesterday", + "earlier": "Earlier" } } diff --git a/react/features/base/react/components/native/NavigateSectionList.js b/react/features/base/react/components/native/NavigateSectionList.js new file mode 100644 index 000000000..01f1badd9 --- /dev/null +++ b/react/features/base/react/components/native/NavigateSectionList.js @@ -0,0 +1,269 @@ +// @flow +import React, { Component } from 'react'; +import { + SafeAreaView, + SectionList, + Text, + TouchableHighlight, + View +} from 'react-native'; + +import styles, { UNDERLAY_COLOR } from './styles'; + +type Props = { + + /** + * Indicates if the list is disabled or not. + */ + disabled: boolean, + + /** + * Function to be invoked when an item is pressed. The item's URL is passed. + */ + onPress: Function, + + /** + * Sections to be rendered in the following format: + * + * [ + * { + * title: string, <- section title + * key: string, <- unique key for the section + * data: [ <- Array of items in the section + * { + * colorBase: string, <- the color base of the avatar + * title: string, <- item title + * url: string, <- item url + * lines: Array <- additional lines to be rendered + * } + * ] + * } + * ] + */ + sections: Array +} + +/** + * Implements a general section list to display items that have a URL + * property and navigates to (probably) meetings, such as the recent list + * or the meeting list components. + */ +export default class NavigateSectionList extends Component { + /** + * Constructor of the NavigateSectionList component. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._getAvatarColor = this._getAvatarColor.bind(this); + this._getItemKey = this._getItemKey.bind(this); + this._onPress = this._onPress.bind(this); + this._renderItem = this._renderItem.bind(this); + this._renderItemLine = this._renderItemLine.bind(this); + this._renderItemLines = this._renderItemLines.bind(this); + this._renderSection = this._renderSection.bind(this); + } + + /** + * Implements React's Component.render function. + * + * @inheritdoc + */ + render() { + const { sections } = this.props; + + return ( + + + + ); + } + + /** + * Creates an empty section object. + * + * @private + * @param {string} title - The title of the section. + * @param {string} key - The key of the section. It must be unique. + * @returns {Object} + */ + static createSection(title, key) { + return { + data: [], + key, + title + }; + } + + _getAvatarColor: string => Object + + /** + * Returns a style (color) based on the string that determines the + * color of the avatar. + * + * @param {string} colorBase - The string that is the base of the color. + * @private + * @returns {Object} + */ + _getAvatarColor(colorBase) { + if (!colorBase) { + return null; + } + + let nameHash = 0; + + for (let i = 0; i < colorBase.length; i++) { + nameHash += colorBase.codePointAt(i); + } + + return styles[`avatarColor${(nameHash % 5) + 1}`]; + } + + _getItemKey: (Object, number) => string; + + /** + * Generates a unique id to every item. + * + * @private + * @param {Object} item - The item. + * @param {number} index - The item index. + * @returns {string} + */ + _getItemKey(item, index) { + return `${index}-${item.key}`; + } + + _onPress: string => Function + + /** + * Returns a function that is used in the onPress callback of the items. + * + * @private + * @param {string} url - The URL of the item to navigate to. + * @returns {Function} + */ + _onPress(url) { + return () => { + const { disabled, onPress } = this.props; + + !disabled && url && typeof onPress === 'function' && onPress(url); + }; + } + + _renderItem: Object => Object; + + /** + * Renders a single item in the list. + * + * @private + * @param {Object} listItem - The item to render. + * @returns {Component} + */ + _renderItem(listItem) { + const { item } = listItem; + + return ( + + + + + + { item.title.substr(0, 1).toUpperCase() } + + + + + + { item.title } + + { + this._renderItemLines(item.lines) + } + + + + ); + } + + _renderItemLine: (string, number) => React$Node; + + /** + * Renders a single line from the additional lines. + * + * @private + * @param {string} line - The line text. + * @param {number} index - The index of the line. + * @returns {React$Node} + */ + _renderItemLine(line, index) { + if (!line) { + return null; + } + + return ( + + { line } + + ); + } + + _renderItemLines: (Array) => Array; + + /** + * Renders the additional item lines, if any. + * + * @private + * @param {Array} lines - The lines to render. + * @returns {Array} + */ + _renderItemLines(lines) { + if (lines && lines.length) { + return lines.map((line, index) => + this._renderItemLine(line, index) + ); + } + + return null; + } + + _renderSection: Object => Object + + /** + * Renders a section title. + * + * @private + * @param {Object} section - The section being rendered. + * @returns {React$Node} + */ + _renderSection(section) { + return ( + + + { section.section.title } + + + ); + } +} diff --git a/react/features/base/react/components/native/index.js b/react/features/base/react/components/native/index.js index aed450682..786a1c4e9 100644 --- a/react/features/base/react/components/native/index.js +++ b/react/features/base/react/components/native/index.js @@ -1,5 +1,6 @@ export { default as Container } from './Container'; 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 SideBar } from './SideBar'; diff --git a/react/features/base/react/components/native/styles.js b/react/features/base/react/components/native/styles.js index aa4aa550e..43f6402ab 100644 --- a/react/features/base/react/components/native/styles.js +++ b/react/features/base/react/components/native/styles.js @@ -4,20 +4,20 @@ import { createStyleSheet } from '../../../styles'; +const AVATAR_OPACITY = 0.4; +const AVATAR_SIZE = 65; const HEADER_COLOR = ColorPalette.blue; // Header height is from iOS guidelines. Also, this looks good. const HEADER_HEIGHT = 44; +const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)'; export const HEADER_PADDING = BoxModel.padding; export const STATUSBAR_COLOR = ColorPalette.blueHighlight; export const SIDEBAR_WIDTH = 250; +export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)'; -/** - * The styles of the generic React {@code Components} of the app. - */ -export default createStyleSheet({ - +const HEADER_STYLES = { /** * Platform specific header button (e.g. back, menu...etc). */ @@ -68,8 +68,124 @@ export default createStyleSheet({ height: HEADER_HEIGHT, justifyContent: 'flex-start', padding: HEADER_PADDING + } +}; + +const SECTIONLIST_STYLES = { + /** + * The style of the actual avatar. + */ + avatar: { + alignItems: 'center', + backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`, + borderRadius: AVATAR_SIZE, + height: AVATAR_SIZE, + justifyContent: 'center', + width: AVATAR_SIZE }, + /** + * List of styles of the avatar of a remote meeting (not the default + * server). The number of colors are limited because they should match + * nicely. + */ + avatarColor1: { + backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})` + }, + + avatarColor2: { + backgroundColor: `rgba(255, 198, 115, ${AVATAR_OPACITY})` + }, + + avatarColor3: { + backgroundColor: `rgba(128, 128, 255, ${AVATAR_OPACITY})` + }, + + avatarColor4: { + backgroundColor: `rgba(105, 232, 194, ${AVATAR_OPACITY})` + }, + + avatarColor5: { + backgroundColor: `rgba(234, 255, 128, ${AVATAR_OPACITY})` + }, + + /** + * The style of the avatar container that makes the avatar rounded. + */ + avatarContainer: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-around', + padding: 5 + }, + + /** + * Simple {@code Text} content of the avatar (the actual initials). + */ + avatarContent: { + backgroundColor: 'rgba(0, 0, 0, 0)', + color: OVERLAY_FONT_COLOR, + fontSize: 32, + fontWeight: '100', + textAlign: 'center' + }, + + /** + * The top level container style of the list. + */ + container: { + flex: 1 + }, + + list: { + flex: 1, + flexDirection: 'column' + }, + + listItem: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + paddingVertical: 5 + }, + + listItemDetails: { + flex: 1, + flexDirection: 'column', + overflow: 'hidden', + paddingHorizontal: 5 + }, + + listItemText: { + color: OVERLAY_FONT_COLOR, + fontSize: 14 + }, + + listItemTitle: { + fontWeight: 'bold', + fontSize: 16 + }, + + listSection: { + alignItems: 'center', + backgroundColor: 'rgba(255, 255, 255, 0.2)', + flex: 1, + flexDirection: 'row', + padding: 5 + }, + + listSectionText: { + color: OVERLAY_FONT_COLOR, + fontSize: 14, + fontWeight: 'normal' + }, + + touchableView: { + flexDirection: 'row' + } +}; + +const SIDEBAR_STYLES = { /** * The topmost container of the side bar. */ @@ -105,4 +221,14 @@ export default createStyleSheet({ sideMenuShadowTouchable: { flex: 1 } +}; + +/** + * The styles of the React {@code Components} of the generic components + * in the app. + */ +export default createStyleSheet({ + ...HEADER_STYLES, + ...SECTIONLIST_STYLES, + ...SIDEBAR_STYLES }); diff --git a/react/features/base/util/dateUtil.js b/react/features/base/util/dateUtil.js index 9fe2eaa2b..5b8a81703 100644 --- a/react/features/base/util/dateUtil.js +++ b/react/features/base/util/dateUtil.js @@ -43,6 +43,19 @@ export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) { return moment(dateOrTimeStamp).locale(_getSupportedLocale()); } +/** + * Returns a localized duration formatter initialized with a + * specific duration ({@code number}). + * + * @private + * @param {number} duration - The duration (ms) + * to format. + * @returns {Object} + */ +export function getLocalizedDurationFormatter(duration: number) { + return moment.duration(duration).locale(_getSupportedLocale()); +} + /** * A lenient locale matcher to match language and dialect if possible. * diff --git a/react/features/calendar-sync/components/MeetingList.native.js b/react/features/calendar-sync/components/MeetingList.native.js index 89ae61915..c00b401ab 100644 --- a/react/features/calendar-sync/components/MeetingList.native.js +++ b/react/features/calendar-sync/components/MeetingList.native.js @@ -1,20 +1,12 @@ // @flow import React, { Component } from 'react'; -import { - SafeAreaView, - SectionList, - Text, - TouchableHighlight, - View -} from 'react-native'; import { connect } from 'react-redux'; import { appNavigate } from '../../app'; import { translate } from '../../base/i18n'; +import { NavigateSectionList } from '../../base/react'; import { getLocalizedDateFormatter } from '../../base/util'; -import styles, { UNDERLAY_COLOR } from './styles'; - type Props = { /** @@ -43,6 +35,13 @@ type Props = { */ class MeetingList extends Component { + /** + * Default values for the component's props. + */ + static defaultProps = { + _eventList: [] + }; + /** * Constructor of the MeetingList component. * @@ -51,12 +50,8 @@ class MeetingList extends Component { constructor(props) { super(props); - this._createSection = this._createSection.bind(this); - this._getItemKey = this._getItemKey.bind(this); - this._onJoin = this._onJoin.bind(this); - this._onSelect = this._onSelect.bind(this); - this._renderItem = this._renderItem.bind(this); - this._renderSection = this._renderSection.bind(this); + this._onPress = this._onPress.bind(this); + this._toDisplayableItem = this._toDisplayableItem.bind(this); this._toDisplayableList = this._toDisplayableList.bind(this); this._toDateString = this._toDateString.bind(this); } @@ -70,145 +65,47 @@ class MeetingList extends Component { const { disabled } = this.props; return ( - - - + ); } - _createSection: string => Object; + _onPress: string => Function /** - * Creates a section object of a list of events. + * Handles the list's navigate action. * * @private - * @param {string} i18Title - The i18 title of the section. - * @returns {Object} - */ - _createSection(i18Title) { - return { - data: [], - key: `key-${i18Title}`, - title: this.props.t(i18Title) - }; - } - - _getItemKey: (Object, number) => string; - - /** - * Generates a unique id to every item. - * - * @private - * @param {Object} item - The item. - * @param {number} index - The item index. - * @returns {string} - */ - _getItemKey(item, index) { - return `${index}-${item.id}-${item.startDate}`; - } - - _onJoin: string => void; - - /** - * Joins the selected URL. - * - * @param {string} url - The URL to join to. + * @param {string} url - The url string to navigate to. * @returns {void} */ - _onJoin(url) { - const { disabled, dispatch } = this.props; + _onPress(url) { + const { dispatch } = this.props; - !disabled && url && dispatch(appNavigate(url)); + dispatch(appNavigate(url)); } - _onSelect: string => Function; + _toDisplayableItem: Object => Object /** - * Creates a function that when invoked, joins the given URL. + * Creates a displayable object from an event. * * @private - * @param {string} url - The URL to join to. - * @returns {Function} + * @param {Object} event - The calendar event. + * @returns {Object} */ - _onSelect(url) { - return this._onJoin.bind(this, url); - } - - _renderItem: Object => Object; - - /** - * Renders a single item in the list. - * - * @private - * @param {Object} listItem - The item to render. - * @returns {Component} - */ - _renderItem(listItem) { - const { item } = listItem; - - return ( - - - - - - { item.title.substr(0, 1).toUpperCase() } - - - - - - { item.title } - - - { item.url } - - - { this._toDateString(item) } - - - - - ); - } - - _renderSection: Object => Object; - - /** - * Renders a section title. - * - * @private - * @param {Object} section - The section being rendered. - * @returns {Component} - */ - _renderSection(section) { - return ( - - - { section.section.title } - - - ); + _toDisplayableItem(event) { + return { + key: `${event.id}-${event.startDate}`, + lines: [ + event.url, + this._toDateString(event) + ], + title: event.title, + url: event.url + }; } _toDisplayableList: () => Array @@ -221,23 +118,32 @@ class MeetingList extends Component { * @returns {Array} */ _toDisplayableList() { - const { _eventList } = this.props; + const { _eventList, t } = this.props; const now = Date.now(); - const nowSection = this._createSection('calendarSync.now'); - const nextSection = this._createSection('calendarSync.next'); - const laterSection = this._createSection('calendarSync.later'); + const nowSection = NavigateSectionList.createSection( + t('calendarSync.now'), + 'now' + ); + const nextSection = NavigateSectionList.createSection( + t('calendarSync.next'), + 'next' + ); + const laterSection = NavigateSectionList.createSection( + t('calendarSync.later'), + 'later' + ); - if (_eventList && _eventList.length) { - for (const event of _eventList) { - if (event.startDate < now && event.endDate > now) { - nowSection.data.push(event); - } else if (event.startDate > now) { - if (nextSection.data.length - && nextSection.data[0].startDate !== event.startDate) { - laterSection.data.push(event); - } else { - nextSection.data.push(event); - } + for (const event of _eventList) { + const displayableEvent = this._toDisplayableItem(event); + + if (event.startDate < now && event.endDate > now) { + nowSection.data.push(displayableEvent); + } else if (event.startDate > now) { + if (nextSection.data.length + && nextSection.data[0].startDate !== event.startDate) { + laterSection.data.push(displayableEvent); + } else { + nextSection.data.push(displayableEvent); } } } @@ -257,7 +163,7 @@ class MeetingList extends Component { return sectionList; } - _toDateString: Object => string; + _toDateString: Object => string /** * Generates a date (interval) string for a given event. @@ -268,7 +174,10 @@ class MeetingList extends Component { */ _toDateString(event) { /* eslint-disable max-len */ - return `${getLocalizedDateFormatter(event.startDate).format('lll')} - ${getLocalizedDateFormatter(event.endDate).format('LT')}`; + const startDateTime = getLocalizedDateFormatter(event.startDate).format('lll'); + const endTime = getLocalizedDateFormatter(event.endDate).format('LT'); + + return `${startDateTime} - ${endTime}`; /* eslint-enable max-len */ } } diff --git a/react/features/calendar-sync/components/styles.js b/react/features/calendar-sync/components/styles.js index 09dc07dfc..4665b0070 100644 --- a/react/features/calendar-sync/components/styles.js +++ b/react/features/calendar-sync/components/styles.js @@ -1,58 +1,18 @@ import { createStyleSheet } from '../../base/styles'; -const AVATAR_OPACITY = 0.4; -const AVATAR_SIZE = 65; const NOTIFICATION_SIZE = 55; -const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)'; - -export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)'; /** - * The styles of the React {@code Component}s of the feature recent-list i.e. - * {@code RecentList}. + * The styles of the React {@code Component}s of the feature meeting-list i.e. + * {@code MeetingList}. */ export default createStyleSheet({ /** - * The style of the actual avatar. - * Recent-list copy! - */ - avatar: { - alignItems: 'center', - backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`, - borderRadius: AVATAR_SIZE, - height: AVATAR_SIZE, - justifyContent: 'center', - width: AVATAR_SIZE - }, - - /** - * The style of the avatar container that makes the avatar rounded. - * Recent-list copy! - */ - avatarContainer: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'space-around', - padding: 5 - }, - - /** - * Simple {@code Text} content of the avatar (the actual initials). - * Recent-list copy! - */ - avatarContent: { - backgroundColor: 'rgba(0, 0, 0, 0)', - color: OVERLAY_FONT_COLOR, - fontSize: 32, - fontWeight: '100', - textAlign: 'center' - }, - - /** - * Style for the actual notification content. + * The top level container of the notification. */ notificationContainer: { + alignSelf: 'flex-start', flexDirection: 'row', justifyContent: 'center', overflow: 'hidden', @@ -130,6 +90,9 @@ export default createStyleSheet({ fontSize: 25 }, + /** + * The container that contains the icon. + */ notificationIconContainer: { alignItems: 'center', flexDirection: 'row', @@ -137,11 +100,17 @@ export default createStyleSheet({ justifyContent: 'center' }, + /** + * A single line of text of the notification. + */ notificationText: { color: 'white', fontSize: 13 }, + /** + * The container for all the lines if the norification. + */ notificationTextContainer: { flexDirection: 'column', height: NOTIFICATION_SIZE, @@ -149,62 +118,8 @@ export default createStyleSheet({ }, /** - * The top level container style of the list. + * The touchable component. */ - container: { - flex: 1 - }, - - /** - * Shows the container disabled. - */ - containerDisabled: { - opacity: 0.2 - }, - - list: { - flex: 1, - flexDirection: 'column' - }, - - listItem: { - alignItems: 'center', - flex: 1, - flexDirection: 'row', - paddingVertical: 5 - }, - - listItemDetails: { - flex: 1, - flexDirection: 'column', - overflow: 'hidden', - paddingHorizontal: 5 - }, - - listItemText: { - color: OVERLAY_FONT_COLOR, - fontSize: 16 - }, - - listItemTitle: { - fontWeight: 'bold', - fontSize: 18 - }, - - listSection: { - alignItems: 'center', - backgroundColor: 'rgba(255, 255, 255, 0.2)', - flex: 1, - flexDirection: 'row', - padding: 5 - }, - - listSectionText: { - color: OVERLAY_FONT_COLOR, - fontSize: 14, - fontWeight: 'normal' - }, - touchableView: { flexDirection: 'row' } diff --git a/react/features/recent-list/components/AbstractRecentList.js b/react/features/recent-list/components/AbstractRecentList.js deleted file mode 100644 index bd8972c78..000000000 --- a/react/features/recent-list/components/AbstractRecentList.js +++ /dev/null @@ -1,75 +0,0 @@ -// @flow - -import { Component } from 'react'; - -import { appNavigate } from '../../app'; - -/** - * The type of the React {@code Component} props of {@link AbstractRecentList} - */ -type Props = { - _defaultURL: string, - - _recentList: Array, - - /** - * Indicates if the list is disabled or not. - */ - disabled: boolean, - - /** - * The redux store's {@code dispatch} function. - */ - dispatch: Dispatch<*> -}; - -/** - * Implements a React {@link Component} which represents the list of conferences - * recently joined, similar to how a list of last dialed numbers list would do - * on a mobile device. - * - * @extends Component - */ -export default class AbstractRecentList extends Component { - - /** - * Joins the selected room. - * - * @param {string} room - The selected room. - * @protected - * @returns {void} - */ - _onJoin(room) { - const { dispatch, disabled } = this.props; - - !disabled && room && dispatch(appNavigate(room)); - } - - /** - * Creates a bound onPress action for the list item. - * - * @param {string} room - The selected room. - * @protected - * @returns {Function} - */ - _onSelect(room) { - return this._onJoin.bind(this, room); - } -} - -/** - * Maps (parts of) the redux state into {@code AbstractRecentList}'s React - * {@code Component} props. - * - * @param {Object} state - The redux state. - * @returns {{ - * _defaultURL: string, - * _recentList: Array - * }} - */ -export function _mapStateToProps(state: Object) { - return { - _defaultURL: state['features/app'].app._getDefaultURL(), - _recentList: state['features/recent-list'] - }; -} diff --git a/react/features/recent-list/components/RecentList.native.js b/react/features/recent-list/components/RecentList.native.js index a568adf9e..19347c1f7 100644 --- a/react/features/recent-list/components/RecentList.native.js +++ b/react/features/recent-list/components/RecentList.native.js @@ -1,208 +1,240 @@ -import React from 'react'; -import { - ListView, - SafeAreaView, - Text, - TouchableHighlight, - View -} from 'react-native'; +// @flow +import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { Icon } from '../../base/font-icons'; +import { appNavigate } from '../../app'; +import { translate } from '../../base/i18n'; +import { NavigateSectionList } from '../../base/react'; +import { + getLocalizedDateFormatter, + getLocalizedDurationFormatter, + parseURIString +} from '../../base/util'; -import AbstractRecentList, { _mapStateToProps } from './AbstractRecentList'; -import { getRecentRooms } from '../functions'; -import styles, { UNDERLAY_COLOR } from './styles'; +/** + * The type of the React {@code Component} props of {@link RecentList} + */ +type Props = { + + /** + * Renders the list disabled. + */ + disabled: boolean, + + /** + * The redux store's {@code dispatch} function. + */ + dispatch: Dispatch<*>, + + /** + * The translate function. + */ + t: Function, + + /** + * The default server URL. + */ + _defaultServerURL: string, + + /** + * The recent list from the Redux store. + */ + _recentList: Array +}; /** * The native container rendering the list of the recently joined rooms. * - * @extends AbstractRecentList */ -class RecentList extends AbstractRecentList { - /** - * The datasource wrapper to be used for the display. - */ - dataSource = new ListView.DataSource({ - rowHasChanged: (r1, r2) => - r1.conference !== r2.conference - && r1.dateTimeStamp !== r2.dateTimeStamp - }); - +class RecentList extends Component { /** * Initializes a new {@code RecentList} instance. * * @inheritdoc */ - constructor(props) { + constructor(props: Props) { super(props); - // Bind event handlers so they are only bound once per instance. - this._getAvatarStyle = this._getAvatarStyle.bind(this); - this._onSelect = this._onSelect.bind(this); - this._renderConfDuration = this._renderConfDuration.bind(this); - this._renderRow = this._renderRow.bind(this); - this._renderServerInfo = this._renderServerInfo.bind(this); + this._onPress = this._onPress.bind(this); + this._toDateString = this._toDateString.bind(this); + this._toDurationString = this._toDurationString.bind(this); + this._toDisplayableItem = this._toDisplayableItem.bind(this); + this._toDisplayableList = this._toDisplayableList.bind(this); } /** - * Implements React's {@link Component#render()}. Renders a list of recently - * joined rooms. + * Implements the React Components's render method. * * @inheritdoc - * @returns {ReactElement} */ render() { - const { disabled, _recentList } = this.props; - - if (!_recentList) { - return null; - } - - const listViewDataSource - = this.dataSource.cloneWithRows(getRecentRooms(_recentList)); + const { disabled } = this.props; return ( - - - + ); } + _onPress: string => Function + /** - * Assembles the style array of the avatar based on if the conference was - * hosted on the default Jitsi Meet deployment or on a non-default one - * (based on current app setting). + * Handles the list's navigate action. + * + * @private + * @param {string} url - The url string to navigate to. + * @returns {void} + */ + _onPress(url) { + const { dispatch } = this.props; + + dispatch(appNavigate(url)); + } + + _toDisplayableItem: Object => Object + + /** + * Creates a displayable list item of a recent list entry. + * + * @private + * @param {Object} item - The recent list entry. + * @returns {Object} + */ + _toDisplayableItem(item) { + const { _defaultServerURL } = this.props; + const location = parseURIString(item.conference); + const baseURL = `${location.protocol}//${location.host}`; + const serverName = baseURL === _defaultServerURL ? null : location.host; + + return { + colorBase: serverName, + key: `key-${item.conference}-${item.date}`, + lines: [ + this._toDateString(item.date), + this._toDurationString(item.duration), + serverName + ], + title: location.room, + url: item.conference + }; + } + + _toDisplayableList: () => Array + + /** + * Transforms the history list to a displayable list + * with sections. * - * @param {Object} recentListEntry - The recent list entry being rendered. * @private * @returns {Array} */ - _getAvatarStyle({ baseURL, serverName }) { - const avatarStyles = [ styles.avatar ]; - - if (baseURL !== this.props._defaultURL) { - avatarStyles.push(this._getColorForServerName(serverName)); - } - - return avatarStyles; - } - - /** - * Returns a style (color) based on the server name, so then the same server - * will always be rendered with the same avatar color. - * - * @param {string} serverName - The recent list entry being rendered. - * @private - * @returns {Object} - */ - _getColorForServerName(serverName) { - let nameHash = 0; - - for (let i = 0; i < serverName.length; i++) { - nameHash += serverName.codePointAt(i); - } - - return styles[`avatarRemoteServer${(nameHash % 5) + 1}`]; - } - - /** - * Renders the conference duration if available. - * - * @param {Object} recentListEntry - The recent list entry being rendered. - * @private - * @returns {ReactElement} - */ - _renderConfDuration({ durationString }) { - if (durationString) { - return ( - - - - { durationString } - - - ); - } - - return null; - } - - /** - * Renders the list of recently joined rooms. - * - * @param {Object} data - The row data to be rendered. - * @private - * @returns {ReactElement} - */ - _renderRow(data) { - return ( - - - - - - { data.initials } - - - - - - { data.room } - - - - - { data.dateString } - - - { this._renderConfDuration(data) } - { this._renderServerInfo(data) } - - - + _toDisplayableList() { + const { _recentList, t } = this.props; + const todaySection = NavigateSectionList.createSection( + t('recentList.today'), + 'today' ); + const yesterdaySection = NavigateSectionList.createSection( + t('recentList.yesterday'), + 'yesterday' + ); + const earlierSection = NavigateSectionList.createSection( + t('recentList.earlier'), + 'earlier' + ); + const today = new Date().toDateString(); + const yesterdayDate = new Date(); + + yesterdayDate.setDate(yesterdayDate.getDate() - 1); + + const yesterday = yesterdayDate.toDateString(); + + for (const item of _recentList) { + const itemDay = new Date(item.date).toDateString(); + const displayableItem = this._toDisplayableItem(item); + + if (itemDay === today) { + todaySection.data.push(displayableItem); + } else if (itemDay === yesterday) { + yesterdaySection.data.push(displayableItem); + } else { + earlierSection.data.push(displayableItem); + } + } + + const displayableList = []; + + if (todaySection.data.length) { + todaySection.data.reverse(); + displayableList.push(todaySection); + } + if (yesterdaySection.data.length) { + yesterdaySection.data.reverse(); + displayableList.push(yesterdaySection); + } + if (earlierSection.data.length) { + earlierSection.data.reverse(); + displayableList.push(earlierSection); + } + + return displayableList; } + _toDateString: number => string + /** - * Renders the server info component based on whether the entry was on a - * different server. + * Generates a date string for the item. * - * @param {Object} recentListEntry - The recent list entry being rendered. * @private - * @returns {ReactElement} + * @param {number} itemDate - The item's timestamp. + * @returns {string} */ - _renderServerInfo({ baseURL, serverName }) { - if (baseURL !== this.props._defaultURL) { - return ( - - - - { serverName } - - - ); + _toDateString(itemDate) { + const date = new Date(itemDate); + const m = getLocalizedDateFormatter(itemDate); + + if (date.toDateString() === new Date().toDateString()) { + // The date is today, we use fromNow format. + return m.fromNow(); + } + + return m.format('lll'); + } + + _toDurationString: number => string + + /** + * Generates a duration string for the item. + * + * @private + * @param {number} duration - The item's duration. + * @returns {string} + */ + _toDurationString(duration) { + if (duration) { + return getLocalizedDurationFormatter(duration).humanize(); } return null; } } -export default connect(_mapStateToProps)(RecentList); +/** + * Maps redux state to component props. + * + * @param {Object} state - The redux state. + * @returns {{ + * _defaultServerURL: string, + * _recentList: Array + * }} + */ +export function _mapStateToProps(state: Object) { + return { + _defaultServerURL: state['features/app'].app._getDefaultURL(), + _recentList: state['features/recent-list'] + }; +} + +export default translate(connect(_mapStateToProps)(RecentList)); diff --git a/react/features/recent-list/functions.js b/react/features/recent-list/functions.js deleted file mode 100644 index 876c751df..000000000 --- a/react/features/recent-list/functions.js +++ /dev/null @@ -1,166 +0,0 @@ -// @flow - -import moment from 'moment'; - -// MomentJS uses static language bundle loading, so in order to support dynamic -// language selection in the app we need to load all bundles that we support in -// the app. -// FIXME: If we decide to support MomentJS in other features as well we may need -// to move this import and the lenient matcher to the i18n feature. -require('moment/locale/bg'); -require('moment/locale/de'); -require('moment/locale/eo'); -require('moment/locale/es'); -require('moment/locale/fr'); -require('moment/locale/hy-am'); -require('moment/locale/it'); -require('moment/locale/nb'); - -// OC is not available. Please submit OC translation to the MomentJS project. -require('moment/locale/pl'); -require('moment/locale/pt'); -require('moment/locale/pt-br'); -require('moment/locale/ru'); -require('moment/locale/sk'); -require('moment/locale/sl'); -require('moment/locale/sv'); -require('moment/locale/tr'); -require('moment/locale/zh-cn'); - -import { i18next } from '../base/i18n'; -import { parseURIString } from '../base/util'; - -/** - * Retrieves the recent room list and generates all the data needed to be - * displayed. - * - * @param {Array} list - The stored recent list retrieved from redux. - * @returns {Array} - */ -export function getRecentRooms(list: Array): Array { - const recentRoomDS = []; - - if (list.length) { - // We init the locale on every list render, so then it changes - // immediately if a language change happens in the app. - const locale = _getSupportedLocale(); - - for (const e of list) { - const uri = parseURIString(e.conference); - - if (uri && uri.room && uri.hostname) { - const duration - = e.duration || /* legacy */ e.conferenceDuration || 0; - - recentRoomDS.push({ - baseURL: `${uri.protocol}//${uri.host}`, - conference: e.conference, - dateString: _getDateString(e.date, locale), - dateTimeStamp: e.date, - duration, - durationString: _getDurationString(duration, locale), - initials: _getInitials(uri.room), - room: uri.room, - serverName: uri.hostname - }); - } - } - } - - return recentRoomDS.reverse(); -} - -/** - * Returns a well formatted date string to be displayed in the list. - * - * @param {number} dateTimeStamp - The UTC timestamp to be converted to String. - * @param {string} locale - The locale to init the formatter with. Note: This - * locale must be supported by the formatter so ensure this prerequisite before - * invoking the function. - * @private - * @returns {string} - */ -function _getDateString(dateTimeStamp: number, locale: string) { - const date = new Date(dateTimeStamp); - const m = _getLocalizedFormatter(date, locale); - - if (date.toDateString() === new Date().toDateString()) { - // The date is today, we use fromNow format. - return m.fromNow(); - } - - return m.format('lll'); -} - -/** - * Returns a well formatted duration string to be displayed as the conference - * length. - * - * @param {number} duration - The duration in MS. - * @param {string} locale - The locale to init the formatter with. Note: This - * locale must be supported by the formatter so ensure this prerequisite before - * invoking the function. - * @private - * @returns {string} - */ -function _getDurationString(duration: number, locale: string) { - return _getLocalizedFormatter(duration, locale).humanize(); -} - -/** - * Returns the initials supposed to be used based on the room name. - * - * @param {string} room - The room name. - * @private - * @returns {string} - */ -function _getInitials(room: string) { - return room && room.charAt(0) ? room.charAt(0).toUpperCase() : '?'; -} - -/** - * Returns a localized date formatter initialized with a specific {@code Date} - * or duration ({@code number}). - * - * @private - * @param {Date|number} dateOrDuration - The date or duration to format. - * @param {string} locale - The locale to init the formatter with. Note: The - * specified locale must be supported by the formatter so ensure the - * prerequisite is met before invoking the function. - * @returns {Object} - */ -function _getLocalizedFormatter(dateOrDuration: Date | number, locale: string) { - const m - = typeof dateOrDuration === 'number' - ? moment.duration(dateOrDuration) - : moment(dateOrDuration); - - return m.locale(locale); -} - -/** - * A lenient locale matcher to match language and dialect if possible. - * - * @private - * @returns {string} - */ -function _getSupportedLocale() { - const i18nLocale = i18next.language; - let supportedLocale; - - if (i18nLocale) { - const localeRegexp = new RegExp('^([a-z]{2,2})(-)*([a-z]{2,2})*$'); - const localeResult = localeRegexp.exec(i18nLocale.toLowerCase()); - - if (localeResult) { - const currentLocaleRegexp - = new RegExp( - `^${localeResult[1]}(-)*${`(${localeResult[3]})*` || ''}`); - - supportedLocale - = moment.locales().find(lang => currentLocaleRegexp.exec(lang)); - } - } - - return supportedLocale || 'en'; -} diff --git a/react/features/welcome/components/PagedList.android.js b/react/features/welcome/components/PagedList.android.js index 9b78e1d96..b3bafc0a9 100644 --- a/react/features/welcome/components/PagedList.android.js +++ b/react/features/welcome/components/PagedList.android.js @@ -20,8 +20,9 @@ export default class PagedList extends AbstractPagedList { * * @inheritdoc */ - constructor() { - super(); + constructor(props) { + super(props); + this._getIndicatorStyle = this._getIndicatorStyle.bind(this); this._onPageSelected = this._onPageSelected.bind(this); } @@ -35,7 +36,11 @@ export default class PagedList extends AbstractPagedList { const { disabled } = this.props; return ( - + + diff --git a/react/features/welcome/components/styles.js b/react/features/welcome/components/styles.js index 9c6d6f0bb..e674d9988 100644 --- a/react/features/welcome/components/styles.js +++ b/react/features/welcome/components/styles.js @@ -179,6 +179,13 @@ export default createStyleSheet({ flexDirection: 'column' }, + /** + * Disabled style for the container. + */ + pagedListContainerDisabled: { + opacity: 0.2 + }, + /** * Container for room name input box and 'join' button. */