mirror of https://github.com/jitsi/jitsi-meet
parent
ae0bf876a8
commit
b096622995
@ -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,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 AbstractRecentList, { _mapStateToProps } from './AbstractRecentList'; |
import { NavigateSectionList } from '../../base/react'; |
||||||
import { getRecentRooms } from '../functions'; |
import { |
||||||
import styles, { UNDERLAY_COLOR } from './styles'; |
getLocalizedDateFormatter, |
||||||
|
getLocalizedDurationFormatter, |
||||||
|
parseURIString |
||||||
|
} from '../../base/util'; |
||||||
|
|
||||||
/** |
/** |
||||||
* The native container rendering the list of the recently joined rooms. |
* The type of the React {@code Component} props of {@link RecentList} |
||||||
* |
*/ |
||||||
* @extends AbstractRecentList |
type Props = { |
||||||
|
|
||||||
|
/** |
||||||
|
* Renders the list disabled. |
||||||
|
*/ |
||||||
|
disabled: boolean, |
||||||
|
|
||||||
|
/** |
||||||
|
* The redux store's {@code dispatch} function. |
||||||
|
*/ |
||||||
|
dispatch: Dispatch<*>, |
||||||
|
|
||||||
|
/** |
||||||
|
* The translate function. |
||||||
|
*/ |
||||||
|
t: Function, |
||||||
|
|
||||||
|
/** |
||||||
|
* The default server URL. |
||||||
*/ |
*/ |
||||||
class RecentList extends AbstractRecentList { |
_defaultServerURL: string, |
||||||
|
|
||||||
/** |
/** |
||||||
* The datasource wrapper to be used for the display. |
* The recent list from the Redux store. |
||||||
*/ |
*/ |
||||||
dataSource = new ListView.DataSource({ |
_recentList: Array<Object> |
||||||
rowHasChanged: (r1, r2) => |
}; |
||||||
r1.conference !== r2.conference |
|
||||||
&& r1.dateTimeStamp !== r2.dateTimeStamp |
|
||||||
}); |
|
||||||
|
|
||||||
|
/** |
||||||
|
* The native container rendering the list of the recently joined rooms. |
||||||
|
* |
||||||
|
*/ |
||||||
|
class RecentList extends Component<Props> { |
||||||
/** |
/** |
||||||
* 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). |
|
||||||
* |
* |
||||||
* @param {Object} recentListEntry - The recent list entry being rendered. |
|
||||||
* @private |
* @private |
||||||
* @returns {Array<Object>} |
* @param {string} url - The url string to navigate to. |
||||||
|
* @returns {void} |
||||||
*/ |
*/ |
||||||
_getAvatarStyle({ baseURL, serverName }) { |
_onPress(url) { |
||||||
const avatarStyles = [ styles.avatar ]; |
const { dispatch } = this.props; |
||||||
|
|
||||||
if (baseURL !== this.props._defaultURL) { |
dispatch(appNavigate(url)); |
||||||
avatarStyles.push(this._getColorForServerName(serverName)); |
|
||||||
} |
} |
||||||
|
|
||||||
return avatarStyles; |
_toDisplayableItem: Object => Object |
||||||
} |
|
||||||
|
|
||||||
/** |
/** |
||||||
* Returns a style (color) based on the server name, so then the same server |
* Creates a displayable list item of a recent list entry. |
||||||
* will always be rendered with the same avatar color. |
|
||||||
* |
* |
||||||
* @param {string} serverName - The recent list entry being rendered. |
|
||||||
* @private |
* @private |
||||||
|
* @param {Object} item - The recent list entry. |
||||||
* @returns {Object} |
* @returns {Object} |
||||||
*/ |
*/ |
||||||
_getColorForServerName(serverName) { |
_toDisplayableItem(item) { |
||||||
let nameHash = 0; |
const { _defaultServerURL } = this.props; |
||||||
|
const location = parseURIString(item.conference); |
||||||
for (let i = 0; i < serverName.length; i++) { |
const baseURL = `${location.protocol}//${location.host}`; |
||||||
nameHash += serverName.codePointAt(i); |
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 |
||||||
|
}; |
||||||
} |
} |
||||||
|
|
||||||
return styles[`avatarRemoteServer${(nameHash % 5) + 1}`]; |
_toDisplayableList: () => Array<Object> |
||||||
} |
|
||||||
|
|
||||||
/** |
/** |
||||||
* Renders the conference duration if available. |
* Transforms the history list to a displayable list |
||||||
|
* with sections. |
||||||
* |
* |
||||||
* @param {Object} recentListEntry - The recent list entry being rendered. |
|
||||||
* @private |
* @private |
||||||
* @returns {ReactElement} |
* @returns {Array<Object>} |
||||||
*/ |
*/ |
||||||
_renderConfDuration({ durationString }) { |
_toDisplayableList() { |
||||||
if (durationString) { |
const { _recentList, t } = this.props; |
||||||
return ( |
const todaySection = NavigateSectionList.createSection( |
||||||
<View style = { styles.infoWithIcon } > |
t('recentList.today'), |
||||||
<Icon |
'today' |
||||||
name = 'timer' |
); |
||||||
style = { styles.inlineIcon } /> |
const yesterdaySection = NavigateSectionList.createSection( |
||||||
<Text style = { styles.confLength }> |
t('recentList.yesterday'), |
||||||
{ durationString } |
'yesterday' |
||||||
</Text> |
|
||||||
</View> |
|
||||||
); |
); |
||||||
|
const earlierSection = NavigateSectionList.createSection( |
||||||
|
t('recentList.earlier'), |
||||||
|
'earlier' |
||||||
|
); |
||||||
|
const today = new Date().toDateString(); |
||||||
|
const yesterdayDate = new Date(); |
||||||
|
|
||||||
|
yesterdayDate.setDate(yesterdayDate.getDate() - 1); |
||||||
|
|
||||||
|
const yesterday = yesterdayDate.toDateString(); |
||||||
|
|
||||||
|
for (const item of _recentList) { |
||||||
|
const itemDay = new Date(item.date).toDateString(); |
||||||
|
const displayableItem = this._toDisplayableItem(item); |
||||||
|
|
||||||
|
if (itemDay === today) { |
||||||
|
todaySection.data.push(displayableItem); |
||||||
|
} else if (itemDay === yesterday) { |
||||||
|
yesterdaySection.data.push(displayableItem); |
||||||
|
} else { |
||||||
|
earlierSection.data.push(displayableItem); |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
return null; |
const displayableList = []; |
||||||
|
|
||||||
|
if (todaySection.data.length) { |
||||||
|
todaySection.data.reverse(); |
||||||
|
displayableList.push(todaySection); |
||||||
|
} |
||||||
|
if (yesterdaySection.data.length) { |
||||||
|
yesterdaySection.data.reverse(); |
||||||
|
displayableList.push(yesterdaySection); |
||||||
|
} |
||||||
|
if (earlierSection.data.length) { |
||||||
|
earlierSection.data.reverse(); |
||||||
|
displayableList.push(earlierSection); |
||||||
|
} |
||||||
|
|
||||||
|
return displayableList; |
||||||
} |
} |
||||||
|
|
||||||
|
_toDateString: number => string |
||||||
|
|
||||||
/** |
/** |
||||||
* Renders the list of recently joined rooms. |
* Generates a date string for the item. |
||||||
* |
* |
||||||
* @param {Object} data - The row data to be rendered. |
|
||||||
* @private |
* @private |
||||||
* @returns {ReactElement} |
* @param {number} itemDate - The item's timestamp. |
||||||
|
* @returns {string} |
||||||
*/ |
*/ |
||||||
_renderRow(data) { |
_toDateString(itemDate) { |
||||||
return ( |
const date = new Date(itemDate); |
||||||
<TouchableHighlight |
const m = getLocalizedDateFormatter(itemDate); |
||||||
onPress = { this._onSelect(data.conference) } |
|
||||||
underlayColor = { UNDERLAY_COLOR } > |
if (date.toDateString() === new Date().toDateString()) { |
||||||
<View style = { styles.row } > |
// The date is today, we use fromNow format.
|
||||||
<View style = { styles.avatarContainer } > |
return m.fromNow(); |
||||||
<View style = { this._getAvatarStyle(data) } > |
} |
||||||
<Text style = { styles.avatarContent }> |
|
||||||
{ data.initials } |
return m.format('lll'); |
||||||
</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> |
|
||||||
); |
|
||||||
} |
} |
||||||
|
|
||||||
|
_toDurationString: number => string |
||||||
|
|
||||||
/** |
/** |
||||||
* Renders the server info component based on whether the entry was on a |
* Generates a duration string for the item. |
||||||
* different server. |
|
||||||
* |
* |
||||||
* @param {Object} recentListEntry - The recent list entry being rendered. |
|
||||||
* @private |
* @private |
||||||
* @returns {ReactElement} |
* @param {number} duration - The item's duration. |
||||||
|
* @returns {string} |
||||||
*/ |
*/ |
||||||
_renderServerInfo({ baseURL, serverName }) { |
_toDurationString(duration) { |
||||||
if (baseURL !== this.props._defaultURL) { |
if (duration) { |
||||||
return ( |
return getLocalizedDurationFormatter(duration).humanize(); |
||||||
<View style = { styles.infoWithIcon } > |
|
||||||
<Icon |
|
||||||
name = 'public' |
|
||||||
style = { styles.inlineIcon } /> |
|
||||||
<Text style = { styles.serverName }> |
|
||||||
{ serverName } |
|
||||||
</Text> |
|
||||||
</View> |
|
||||||
); |
|
||||||
} |
} |
||||||
|
|
||||||
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'; |
|
||||||
} |
|
Loading…
Reference in new issue