Unify recent and meeting lists

pull/2488/head
zbettenbuk 7 years ago committed by Lyubo Marinov
parent ae0bf876a8
commit b096622995
  1. 5
      lang/main.json
  2. 269
      react/features/base/react/components/native/NavigateSectionList.js
  3. 1
      react/features/base/react/components/native/index.js
  4. 136
      react/features/base/react/components/native/styles.js
  5. 13
      react/features/base/util/dateUtil.js
  6. 217
      react/features/calendar-sync/components/MeetingList.native.js
  7. 113
      react/features/calendar-sync/components/styles.js
  8. 75
      react/features/recent-list/components/AbstractRecentList.js
  9. 318
      react/features/recent-list/components/RecentList.native.js
  10. 166
      react/features/recent-list/functions.js
  11. 11
      react/features/welcome/components/PagedList.android.js
  12. 6
      react/features/welcome/components/PagedList.ios.js
  13. 7
      react/features/welcome/components/styles.js

@ -533,5 +533,10 @@
"next": "Upcoming",
"nextMeeting": "next meeting",
"now": "Now"
},
"recentList": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
}
}

@ -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<string> <- additional lines to be rendered
* }
* ]
* }
* ]
*/
sections: Array<Object>
}
/**
* 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<Props> {
/**
* 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 (
<SafeAreaView
style = { styles.container } >
<SectionList
keyExtractor = { this._getItemKey }
renderItem = { this._renderItem }
renderSectionHeader = { this._renderSection }
sections = { sections }
style = { styles.list } />
</SafeAreaView>
);
}
/**
* 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 (
<TouchableHighlight
onPress = { this._onPress(item.url) }
underlayColor = { UNDERLAY_COLOR }>
<View style = { styles.listItem }>
<View style = { styles.avatarContainer } >
<View
style = { [
styles.avatar,
this._getAvatarColor(item.colorBase)
] } >
<Text style = { styles.avatarContent }>
{ item.title.substr(0, 1).toUpperCase() }
</Text>
</View>
</View>
<View style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = { [
styles.listItemText,
styles.listItemTitle
] }>
{ item.title }
</Text>
{
this._renderItemLines(item.lines)
}
</View>
</View>
</TouchableHighlight>
);
}
_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 (
<Text
key = { index }
numberOfLines = { 1 }
style = { styles.listItemText }>
{ line }
</Text>
);
}
_renderItemLines: (Array<string>) => Array<React$Node>;
/**
* Renders the additional item lines, if any.
*
* @private
* @param {Array<string>} lines - The lines to render.
* @returns {Array<React$Node>}
*/
_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 (
<View style = { styles.listSection }>
<Text style = { styles.listSectionText }>
{ section.section.title }
</Text>
</View>
);
}
}

@ -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';

@ -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
});

@ -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.
*

@ -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<Props> {
/**
* Default values for the component's props.
*/
static defaultProps = {
_eventList: []
};
/**
* Constructor of the MeetingList component.
*
@ -51,12 +50,8 @@ class MeetingList extends Component<Props> {
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<Props> {
const { disabled } = this.props;
return (
<SafeAreaView
style = { [
styles.container,
disabled ? styles.containerDisabled : null
] } >
<SectionList
keyExtractor = { this._getItemKey }
renderItem = { this._renderItem }
renderSectionHeader = { this._renderSection }
sections = { this._toDisplayableList() }
style = { styles.list } />
</SafeAreaView>
<NavigateSectionList
disabled = { disabled }
onPress = { this._onPress }
sections = { this._toDisplayableList() } />
);
}
_createSection: string => Object;
/**
* Creates a section object of a list of events.
*
* @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;
_onPress: string => Function
/**
* Generates a unique id to every item.
* Handles the list's navigate action.
*
* @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;
!disabled && url && dispatch(appNavigate(url));
}
_onSelect: string => Function;
/**
* Creates a function that when invoked, joins the given URL.
*
* @private
* @param {string} url - The URL to join to.
* @returns {Function}
*/
_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;
_onPress(url) {
const { dispatch } = this.props;
return (
<TouchableHighlight
onPress = { this._onSelect(item.url) }
underlayColor = { UNDERLAY_COLOR }>
<View style = { styles.listItem }>
<View style = { styles.avatarContainer } >
<View style = { styles.avatar } >
<Text style = { styles.avatarContent }>
{ item.title.substr(0, 1).toUpperCase() }
</Text>
</View>
</View>
<View style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = { [
styles.listItemText,
styles.listItemTitle
] }>
{ item.title }
</Text>
<Text
numberOfLines = { 1 }
style = { styles.listItemText }>
{ item.url }
</Text>
<Text
numberOfLines = { 1 }
style = { styles.listItemText }>
{ this._toDateString(item) }
</Text>
</View>
</View>
</TouchableHighlight>
);
dispatch(appNavigate(url));
}
_renderSection: Object => Object;
_toDisplayableItem: Object => Object
/**
* Renders a section title.
* Creates a displayable object from an event.
*
* @private
* @param {Object} section - The section being rendered.
* @returns {Component}
* @param {Object} event - The calendar event.
* @returns {Object}
*/
_renderSection(section) {
return (
<View style = { styles.listSection }>
<Text style = { styles.listSectionText }>
{ section.section.title }
</Text>
</View>
);
_toDisplayableItem(event) {
return {
key: `${event.id}-${event.startDate}`,
lines: [
event.url,
this._toDateString(event)
],
title: event.title,
url: event.url
};
}
_toDisplayableList: () => Array<Object>
@ -221,23 +118,32 @@ class MeetingList extends Component<Props> {
* @returns {Array<Object>}
*/
_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<Props> {
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<Props> {
*/
_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 */
}
}

@ -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'
}

@ -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<Object>,
/**
* 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<Props> {
/**
* 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']
};
}

@ -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 AbstractRecentList, { _mapStateToProps } from './AbstractRecentList';
import { getRecentRooms } from '../functions';
import styles, { UNDERLAY_COLOR } from './styles';
import { appNavigate } from '../../app';
import { translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter,
parseURIString
} from '../../base/util';
/**
* The native container rendering the list of the recently joined rooms.
*
* @extends AbstractRecentList
* The type of the React {@code Component} props of {@link RecentList}
*/
class RecentList extends AbstractRecentList {
type Props = {
/**
* Renders the list disabled.
*/
disabled: boolean,
/**
* The redux store's {@code dispatch} function.
*/
dispatch: Dispatch<*>,
/**
* The translate function.
*/
t: Function,
/**
* The datasource wrapper to be used for the display.
* The default server URL.
*/
dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) =>
r1.conference !== r2.conference
&& r1.dateTimeStamp !== r2.dateTimeStamp
});
_defaultServerURL: string,
/**
* The recent list from the Redux store.
*/
_recentList: Array<Object>
};
/**
* The native container rendering the list of the recently joined rooms.
*
*/
class RecentList extends Component<Props> {
/**
* 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 (
<SafeAreaView
style = { [
styles.container,
disabled ? styles.containerDisabled : null
] }>
<ListView
dataSource = { listViewDataSource }
enableEmptySections = { true }
renderRow = { this._renderRow } />
</SafeAreaView>
<NavigateSectionList
disabled = { disabled }
onPress = { this._onPress }
sections = { this._toDisplayableList() } />
);
}
_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.
*
* @param {Object} recentListEntry - The recent list entry being rendered.
* @private
* @returns {Array<Object>}
* @param {string} url - The url string to navigate to.
* @returns {void}
*/
_getAvatarStyle({ baseURL, serverName }) {
const avatarStyles = [ styles.avatar ];
_onPress(url) {
const { dispatch } = this.props;
if (baseURL !== this.props._defaultURL) {
avatarStyles.push(this._getColorForServerName(serverName));
}
return avatarStyles;
dispatch(appNavigate(url));
}
_toDisplayableItem: Object => Object
/**
* Returns a style (color) based on the server name, so then the same server
* will always be rendered with the same avatar color.
* Creates a displayable list item of a recent list entry.
*
* @param {string} serverName - The recent list entry being rendered.
* @private
* @param {Object} item - The recent list entry.
* @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}`];
_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<Object>
/**
* Renders the conference duration if available.
* Transforms the history list to a displayable list
* with sections.
*
* @param {Object} recentListEntry - The recent list entry being rendered.
* @private
* @returns {ReactElement}
* @returns {Array<Object>}
*/
_renderConfDuration({ durationString }) {
if (durationString) {
return (
<View style = { styles.infoWithIcon } >
<Icon
name = 'timer'
style = { styles.inlineIcon } />
<Text style = { styles.confLength }>
{ durationString }
</Text>
</View>
);
_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);
}
}
return null;
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 list of recently joined rooms.
* Generates a date string for the item.
*
* @param {Object} data - The row data to be rendered.
* @private
* @returns {ReactElement}
* @param {number} itemDate - The item's timestamp.
* @returns {string}
*/
_renderRow(data) {
return (
<TouchableHighlight
onPress = { this._onSelect(data.conference) }
underlayColor = { UNDERLAY_COLOR } >
<View style = { styles.row } >
<View style = { styles.avatarContainer } >
<View style = { this._getAvatarStyle(data) } >
<Text style = { styles.avatarContent }>
{ data.initials }
</Text>
</View>
</View>
<View style = { styles.detailsContainer } >
<Text
numberOfLines = { 1 }
style = { styles.roomName }>
{ data.room }
</Text>
<View style = { styles.infoWithIcon } >
<Icon
name = 'event_note'
style = { styles.inlineIcon } />
<Text style = { styles.date }>
{ data.dateString }
</Text>
</View>
{ this._renderConfDuration(data) }
{ this._renderServerInfo(data) }
</View>
</View>
</TouchableHighlight>
);
_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
/**
* Renders the server info component based on whether the entry was on a
* different server.
* Generates a duration string for the item.
*
* @param {Object} recentListEntry - The recent list entry being rendered.
* @private
* @returns {ReactElement}
* @param {number} duration - The item's duration.
* @returns {string}
*/
_renderServerInfo({ baseURL, serverName }) {
if (baseURL !== this.props._defaultURL) {
return (
<View style = { styles.infoWithIcon } >
<Icon
name = 'public'
style = { styles.inlineIcon } />
<Text style = { styles.serverName }>
{ serverName }
</Text>
</View>
);
_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));

@ -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<Object>} list - The stored recent list retrieved from redux.
* @returns {Array}
*/
export function getRecentRooms(list: Array<Object>): Array<Object> {
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';
}

@ -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 (
<View style = { styles.pagedListContainer }>
<View
style = { [
styles.pagedListContainer,
disabled ? styles.pagedListContainerDisabled : null
] }>
<ViewPagerAndroid
initialPage = { DEFAULT_PAGE }
keyboardDismissMode = 'on-drag'

@ -39,7 +39,11 @@ class PagedList extends AbstractPagedList {
const { disabled, t } = this.props;
return (
<View style = { styles.pagedListContainer }>
<View
style = { [
styles.pagedListContainer,
disabled ? styles.pagedListContainerDisabled : null
] }>
<TabBarIOS
itemPositioning = 'fill'
style = { styles.pagedList }>

@ -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.
*/

Loading…
Cancel
Save