Unify recent and meeting lists
This commit is contained in:
parent
ae0bf876a8
commit
b096622995
|
@ -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;
|
||||
_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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
_renderSection: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a section title.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @returns {Component}
|
||||
*/
|
||||
_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 { 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<Object>
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<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.
|
||||
*
|
||||
* @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<Object>
|
||||
|
||||
/**
|
||||
* Transforms the history list to a displayable list
|
||||
* with sections.
|
||||
*
|
||||
* @param {Object} recentListEntry - The recent list entry being rendered.
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_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 (
|
||||
<View style = { styles.infoWithIcon } >
|
||||
<Icon
|
||||
name = 'timer'
|
||||
style = { styles.inlineIcon } />
|
||||
<Text style = { styles.confLength }>
|
||||
{ durationString }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the list of recently joined rooms.
|
||||
*
|
||||
* @param {Object} data - The row data to be rendered.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_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>
|
||||
_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 (
|
||||
<View style = { styles.infoWithIcon } >
|
||||
<Icon
|
||||
name = 'public'
|
||||
style = { styles.inlineIcon } />
|
||||
<Text style = { styles.serverName }>
|
||||
{ serverName }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
_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));
|
||||
|
|
|
@ -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…
Reference in New Issue