Unify recent and meeting lists
This commit is contained in:
parent
ae0bf876a8
commit
b096622995
|
@ -533,5 +533,10 @@
|
||||||
"next": "Upcoming",
|
"next": "Upcoming",
|
||||||
"nextMeeting": "next meeting",
|
"nextMeeting": "next meeting",
|
||||||
"now": "Now"
|
"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 Container } from './Container';
|
||||||
export { default as Header } from './Header';
|
export { default as Header } from './Header';
|
||||||
|
export { default as NavigateSectionList } from './NavigateSectionList';
|
||||||
export { default as Link } from './Link';
|
export { default as Link } from './Link';
|
||||||
export { default as LoadingIndicator } from './LoadingIndicator';
|
export { default as LoadingIndicator } from './LoadingIndicator';
|
||||||
export { default as SideBar } from './SideBar';
|
export { default as SideBar } from './SideBar';
|
||||||
|
|
|
@ -4,20 +4,20 @@ import {
|
||||||
createStyleSheet
|
createStyleSheet
|
||||||
} from '../../../styles';
|
} from '../../../styles';
|
||||||
|
|
||||||
|
const AVATAR_OPACITY = 0.4;
|
||||||
|
const AVATAR_SIZE = 65;
|
||||||
const HEADER_COLOR = ColorPalette.blue;
|
const HEADER_COLOR = ColorPalette.blue;
|
||||||
|
|
||||||
// Header height is from iOS guidelines. Also, this looks good.
|
// Header height is from iOS guidelines. Also, this looks good.
|
||||||
const HEADER_HEIGHT = 44;
|
const HEADER_HEIGHT = 44;
|
||||||
|
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
||||||
|
|
||||||
export const HEADER_PADDING = BoxModel.padding;
|
export const HEADER_PADDING = BoxModel.padding;
|
||||||
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
|
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
|
||||||
export const SIDEBAR_WIDTH = 250;
|
export const SIDEBAR_WIDTH = 250;
|
||||||
|
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
|
||||||
|
|
||||||
/**
|
const HEADER_STYLES = {
|
||||||
* The styles of the generic React {@code Components} of the app.
|
|
||||||
*/
|
|
||||||
export default createStyleSheet({
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform specific header button (e.g. back, menu...etc).
|
* Platform specific header button (e.g. back, menu...etc).
|
||||||
*/
|
*/
|
||||||
|
@ -68,8 +68,124 @@ export default createStyleSheet({
|
||||||
height: HEADER_HEIGHT,
|
height: HEADER_HEIGHT,
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
padding: HEADER_PADDING
|
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.
|
* The topmost container of the side bar.
|
||||||
*/
|
*/
|
||||||
|
@ -105,4 +221,14 @@ export default createStyleSheet({
|
||||||
sideMenuShadowTouchable: {
|
sideMenuShadowTouchable: {
|
||||||
flex: 1
|
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());
|
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.
|
* A lenient locale matcher to match language and dialect if possible.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,20 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import {
|
|
||||||
SafeAreaView,
|
|
||||||
SectionList,
|
|
||||||
Text,
|
|
||||||
TouchableHighlight,
|
|
||||||
View
|
|
||||||
} from 'react-native';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { appNavigate } from '../../app';
|
import { appNavigate } from '../../app';
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
|
import { NavigateSectionList } from '../../base/react';
|
||||||
import { getLocalizedDateFormatter } from '../../base/util';
|
import { getLocalizedDateFormatter } from '../../base/util';
|
||||||
|
|
||||||
import styles, { UNDERLAY_COLOR } from './styles';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,6 +35,13 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
class MeetingList extends Component<Props> {
|
class MeetingList extends Component<Props> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default values for the component's props.
|
||||||
|
*/
|
||||||
|
static defaultProps = {
|
||||||
|
_eventList: []
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor of the MeetingList component.
|
* Constructor of the MeetingList component.
|
||||||
*
|
*
|
||||||
|
@ -51,12 +50,8 @@ class MeetingList extends Component<Props> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._createSection = this._createSection.bind(this);
|
this._onPress = this._onPress.bind(this);
|
||||||
this._getItemKey = this._getItemKey.bind(this);
|
this._toDisplayableItem = this._toDisplayableItem.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._toDisplayableList = this._toDisplayableList.bind(this);
|
this._toDisplayableList = this._toDisplayableList.bind(this);
|
||||||
this._toDateString = this._toDateString.bind(this);
|
this._toDateString = this._toDateString.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -70,145 +65,47 @@ class MeetingList extends Component<Props> {
|
||||||
const { disabled } = this.props;
|
const { disabled } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<NavigateSectionList
|
||||||
style = { [
|
disabled = { disabled }
|
||||||
styles.container,
|
onPress = { this._onPress }
|
||||||
disabled ? styles.containerDisabled : null
|
sections = { this._toDisplayableList() } />
|
||||||
] } >
|
|
||||||
<SectionList
|
|
||||||
keyExtractor = { this._getItemKey }
|
|
||||||
renderItem = { this._renderItem }
|
|
||||||
renderSectionHeader = { this._renderSection }
|
|
||||||
sections = { this._toDisplayableList() }
|
|
||||||
style = { styles.list } />
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createSection: string => Object;
|
_onPress: string => Function
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a section object of a list of events.
|
* Handles the list's navigate action.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {string} i18Title - The i18 title of the section.
|
* @param {string} url - The url string to navigate to.
|
||||||
* @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.
|
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onJoin(url) {
|
_onPress(url) {
|
||||||
const { disabled, dispatch } = this.props;
|
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
|
* @private
|
||||||
* @param {string} url - The URL to join to.
|
* @param {Object} event - The calendar event.
|
||||||
* @returns {Function}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
_onSelect(url) {
|
_toDisplayableItem(event) {
|
||||||
return this._onJoin.bind(this, url);
|
return {
|
||||||
}
|
key: `${event.id}-${event.startDate}`,
|
||||||
|
lines: [
|
||||||
_renderItem: Object => Object;
|
event.url,
|
||||||
|
this._toDateString(event)
|
||||||
/**
|
],
|
||||||
* Renders a single item in the list.
|
title: event.title,
|
||||||
*
|
url: event.url
|
||||||
* @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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_toDisplayableList: () => Array<Object>
|
_toDisplayableList: () => Array<Object>
|
||||||
|
@ -221,23 +118,32 @@ class MeetingList extends Component<Props> {
|
||||||
* @returns {Array<Object>}
|
* @returns {Array<Object>}
|
||||||
*/
|
*/
|
||||||
_toDisplayableList() {
|
_toDisplayableList() {
|
||||||
const { _eventList } = this.props;
|
const { _eventList, t } = this.props;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const nowSection = this._createSection('calendarSync.now');
|
const nowSection = NavigateSectionList.createSection(
|
||||||
const nextSection = this._createSection('calendarSync.next');
|
t('calendarSync.now'),
|
||||||
const laterSection = this._createSection('calendarSync.later');
|
'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) {
|
||||||
for (const event of _eventList) {
|
const displayableEvent = this._toDisplayableItem(event);
|
||||||
if (event.startDate < now && event.endDate > now) {
|
|
||||||
nowSection.data.push(event);
|
if (event.startDate < now && event.endDate > now) {
|
||||||
} else if (event.startDate > now) {
|
nowSection.data.push(displayableEvent);
|
||||||
if (nextSection.data.length
|
} else if (event.startDate > now) {
|
||||||
&& nextSection.data[0].startDate !== event.startDate) {
|
if (nextSection.data.length
|
||||||
laterSection.data.push(event);
|
&& nextSection.data[0].startDate !== event.startDate) {
|
||||||
} else {
|
laterSection.data.push(displayableEvent);
|
||||||
nextSection.data.push(event);
|
} else {
|
||||||
}
|
nextSection.data.push(displayableEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,7 +163,7 @@ class MeetingList extends Component<Props> {
|
||||||
return sectionList;
|
return sectionList;
|
||||||
}
|
}
|
||||||
|
|
||||||
_toDateString: Object => string;
|
_toDateString: Object => string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a date (interval) string for a given event.
|
* Generates a date (interval) string for a given event.
|
||||||
|
@ -268,7 +174,10 @@ class MeetingList extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
_toDateString(event) {
|
_toDateString(event) {
|
||||||
/* eslint-disable max-len */
|
/* 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 */
|
/* eslint-enable max-len */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +1,18 @@
|
||||||
import { createStyleSheet } from '../../base/styles';
|
import { createStyleSheet } from '../../base/styles';
|
||||||
|
|
||||||
const AVATAR_OPACITY = 0.4;
|
|
||||||
const AVATAR_SIZE = 65;
|
|
||||||
const NOTIFICATION_SIZE = 55;
|
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.
|
* The styles of the React {@code Component}s of the feature meeting-list i.e.
|
||||||
* {@code RecentList}.
|
* {@code MeetingList}.
|
||||||
*/
|
*/
|
||||||
export default createStyleSheet({
|
export default createStyleSheet({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The style of the actual avatar.
|
* The top level container of the notification.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
notificationContainer: {
|
notificationContainer: {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
@ -130,6 +90,9 @@ export default createStyleSheet({
|
||||||
fontSize: 25
|
fontSize: 25
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The container that contains the icon.
|
||||||
|
*/
|
||||||
notificationIconContainer: {
|
notificationIconContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -137,11 +100,17 @@ export default createStyleSheet({
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single line of text of the notification.
|
||||||
|
*/
|
||||||
notificationText: {
|
notificationText: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: 13
|
fontSize: 13
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The container for all the lines if the norification.
|
||||||
|
*/
|
||||||
notificationTextContainer: {
|
notificationTextContainer: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: NOTIFICATION_SIZE,
|
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: {
|
touchableView: {
|
||||||
flexDirection: 'row'
|
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';
|
// @flow
|
||||||
import {
|
import React, { Component } from 'react';
|
||||||
ListView,
|
|
||||||
SafeAreaView,
|
|
||||||
Text,
|
|
||||||
TouchableHighlight,
|
|
||||||
View
|
|
||||||
} from 'react-native';
|
|
||||||
import { connect } from 'react-redux';
|
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';
|
* The type of the React {@code Component} props of {@link RecentList}
|
||||||
import styles, { UNDERLAY_COLOR } from './styles';
|
*/
|
||||||
|
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.
|
* The native container rendering the list of the recently joined rooms.
|
||||||
*
|
*
|
||||||
* @extends AbstractRecentList
|
|
||||||
*/
|
*/
|
||||||
class RecentList extends AbstractRecentList {
|
class RecentList extends Component<Props> {
|
||||||
/**
|
|
||||||
* The datasource wrapper to be used for the display.
|
|
||||||
*/
|
|
||||||
dataSource = new ListView.DataSource({
|
|
||||||
rowHasChanged: (r1, r2) =>
|
|
||||||
r1.conference !== r2.conference
|
|
||||||
&& r1.dateTimeStamp !== r2.dateTimeStamp
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new {@code RecentList} instance.
|
* Initializes a new {@code RecentList} instance.
|
||||||
*
|
*
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once per instance.
|
this._onPress = this._onPress.bind(this);
|
||||||
this._getAvatarStyle = this._getAvatarStyle.bind(this);
|
this._toDateString = this._toDateString.bind(this);
|
||||||
this._onSelect = this._onSelect.bind(this);
|
this._toDurationString = this._toDurationString.bind(this);
|
||||||
this._renderConfDuration = this._renderConfDuration.bind(this);
|
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
||||||
this._renderRow = this._renderRow.bind(this);
|
this._toDisplayableList = this._toDisplayableList.bind(this);
|
||||||
this._renderServerInfo = this._renderServerInfo.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}. Renders a list of recently
|
* Implements the React Components's render method.
|
||||||
* joined rooms.
|
|
||||||
*
|
*
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { disabled, _recentList } = this.props;
|
const { disabled } = this.props;
|
||||||
|
|
||||||
if (!_recentList) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listViewDataSource
|
|
||||||
= this.dataSource.cloneWithRows(getRecentRooms(_recentList));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<NavigateSectionList
|
||||||
style = { [
|
disabled = { disabled }
|
||||||
styles.container,
|
onPress = { this._onPress }
|
||||||
disabled ? styles.containerDisabled : null
|
sections = { this._toDisplayableList() } />
|
||||||
] }>
|
|
||||||
<ListView
|
|
||||||
dataSource = { listViewDataSource }
|
|
||||||
enableEmptySections = { true }
|
|
||||||
renderRow = { this._renderRow } />
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onPress: string => Function
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assembles the style array of the avatar based on if the conference was
|
* Handles the list's navigate action.
|
||||||
* hosted on the default Jitsi Meet deployment or on a non-default one
|
*
|
||||||
* (based on current app setting).
|
* @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
|
* @private
|
||||||
* @returns {Array<Object>}
|
* @returns {Array<Object>}
|
||||||
*/
|
*/
|
||||||
_getAvatarStyle({ baseURL, serverName }) {
|
_toDisplayableList() {
|
||||||
const avatarStyles = [ styles.avatar ];
|
const { _recentList, t } = this.props;
|
||||||
|
const todaySection = NavigateSectionList.createSection(
|
||||||
if (baseURL !== this.props._defaultURL) {
|
t('recentList.today'),
|
||||||
avatarStyles.push(this._getColorForServerName(serverName));
|
'today'
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
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
|
* Generates a date string for the item.
|
||||||
* different server.
|
|
||||||
*
|
*
|
||||||
* @param {Object} recentListEntry - The recent list entry being rendered.
|
|
||||||
* @private
|
* @private
|
||||||
* @returns {ReactElement}
|
* @param {number} itemDate - The item's timestamp.
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
_renderServerInfo({ baseURL, serverName }) {
|
_toDateString(itemDate) {
|
||||||
if (baseURL !== this.props._defaultURL) {
|
const date = new Date(itemDate);
|
||||||
return (
|
const m = getLocalizedDateFormatter(itemDate);
|
||||||
<View style = { styles.infoWithIcon } >
|
|
||||||
<Icon
|
if (date.toDateString() === new Date().toDateString()) {
|
||||||
name = 'public'
|
// The date is today, we use fromNow format.
|
||||||
style = { styles.inlineIcon } />
|
return m.fromNow();
|
||||||
<Text style = { styles.serverName }>
|
}
|
||||||
{ serverName }
|
|
||||||
</Text>
|
return m.format('lll');
|
||||||
</View>
|
}
|
||||||
);
|
|
||||||
|
_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;
|
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
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
|
|
||||||
this._getIndicatorStyle = this._getIndicatorStyle.bind(this);
|
this._getIndicatorStyle = this._getIndicatorStyle.bind(this);
|
||||||
this._onPageSelected = this._onPageSelected.bind(this);
|
this._onPageSelected = this._onPageSelected.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -35,7 +36,11 @@ export default class PagedList extends AbstractPagedList {
|
||||||
const { disabled } = this.props;
|
const { disabled } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style = { styles.pagedListContainer }>
|
<View
|
||||||
|
style = { [
|
||||||
|
styles.pagedListContainer,
|
||||||
|
disabled ? styles.pagedListContainerDisabled : null
|
||||||
|
] }>
|
||||||
<ViewPagerAndroid
|
<ViewPagerAndroid
|
||||||
initialPage = { DEFAULT_PAGE }
|
initialPage = { DEFAULT_PAGE }
|
||||||
keyboardDismissMode = 'on-drag'
|
keyboardDismissMode = 'on-drag'
|
||||||
|
|
|
@ -39,7 +39,11 @@ class PagedList extends AbstractPagedList {
|
||||||
const { disabled, t } = this.props;
|
const { disabled, t } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style = { styles.pagedListContainer }>
|
<View
|
||||||
|
style = { [
|
||||||
|
styles.pagedListContainer,
|
||||||
|
disabled ? styles.pagedListContainerDisabled : null
|
||||||
|
] }>
|
||||||
<TabBarIOS
|
<TabBarIOS
|
||||||
itemPositioning = 'fill'
|
itemPositioning = 'fill'
|
||||||
style = { styles.pagedList }>
|
style = { styles.pagedList }>
|
||||||
|
|
|
@ -179,6 +179,13 @@ export default createStyleSheet({
|
||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disabled style for the container.
|
||||||
|
*/
|
||||||
|
pagedListContainerDisabled: {
|
||||||
|
opacity: 0.2
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container for room name input box and 'join' button.
|
* Container for room name input box and 'join' button.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue