[RN] Add recent-list feature
This commit is contained in:
parent
45c405de0e
commit
1e0550c746
|
@ -25,12 +25,12 @@
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-public:before {
|
|
||||||
content: "\e80b";
|
|
||||||
}
|
|
||||||
.icon-event_note:before {
|
.icon-event_note:before {
|
||||||
content: "\e616";
|
content: "\e616";
|
||||||
}
|
}
|
||||||
|
.icon-public:before {
|
||||||
|
content: "\e80b";
|
||||||
|
}
|
||||||
.icon-timer:before {
|
.icon-timer:before {
|
||||||
content: "\e425";
|
content: "\e425";
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,22 +90,20 @@ export default class Storage {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the value associated with a specific key in this storage in an
|
* Returns the value associated with a specific key in this storage in an
|
||||||
* async manner. This method is required for those cases where we need
|
* async manner. This method is required for those cases where we need the
|
||||||
* the stored data but we're not sure yet whether the {@code Storage}
|
* stored data but we're not sure yet whether the {@code Storage} is already
|
||||||
* is already initialised or not - e.g. on app start.
|
* initialised or not - e.g. on app start.
|
||||||
*
|
*
|
||||||
* @private
|
|
||||||
* @param {string} key - The name of the key to retrieve the value of.
|
* @param {string} key - The name of the key to retrieve the value of.
|
||||||
|
* @private
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
_getItemAsync(key) {
|
_getItemAsync(key) {
|
||||||
return new Promise(resolve => {
|
return new Promise(
|
||||||
AsyncStorage.getItem(
|
resolve =>
|
||||||
`${String(this._keyPrefix)}${key}`,
|
AsyncStorage.getItem(
|
||||||
(error, result) => {
|
`${String(this._keyPrefix)}${key}`,
|
||||||
resolve(result ? result : null);
|
(error, result) => resolve(result ? result : null)));
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,22 +3,9 @@
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { ListView } from 'react-native';
|
import { ListView } from 'react-native';
|
||||||
|
|
||||||
import { getRecentRooms } from '../functions';
|
|
||||||
|
|
||||||
import { appNavigate } from '../../app';
|
import { appNavigate } from '../../app';
|
||||||
|
|
||||||
/**
|
import { getRecentRooms } from '../functions';
|
||||||
* The type of the React {@code Component} state of {@link AbstractRecentList}.
|
|
||||||
*/
|
|
||||||
type State = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@code ListView.DataSource} to be used for the {@code ListView}.
|
|
||||||
* Its content comes from the native implementation of
|
|
||||||
* {@code window.localStorage}.
|
|
||||||
*/
|
|
||||||
dataSource: Object
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the React {@code Component} props of {@link AbstractRecentList}
|
* The type of the React {@code Component} props of {@link AbstractRecentList}
|
||||||
|
@ -26,28 +13,41 @@ type State = {
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redux store dispatch function.
|
* The redux store's {@code dispatch} function.
|
||||||
*/
|
*/
|
||||||
dispatch: Dispatch<*>,
|
dispatch: Dispatch<*>
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React {@link Component} which represents the list of
|
* The type of the React {@code Component} state of {@link AbstractRecentList}.
|
||||||
* conferences recently joined, similar to how a list of last dialed
|
*/
|
||||||
* numbers list would do on a mobile
|
type State = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@code ListView.DataSource} to be used for the {@code ListView}. Its
|
||||||
|
* content comes from the native implementation of
|
||||||
|
* {@code window.localStorage}.
|
||||||
|
*/
|
||||||
|
dataSource: Object
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
export default class AbstractRecentList extends Component<Props, State> {
|
export default class AbstractRecentList extends Component<Props, State> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The datasource that backs the {@code ListView}
|
* The datasource that backs the {@code ListView}.
|
||||||
*/
|
*/
|
||||||
listDataSource = new ListView.DataSource({
|
listDataSource = new ListView.DataSource({
|
||||||
rowHasChanged: (r1, r2) =>
|
rowHasChanged: (r1, r2) =>
|
||||||
r1.conference !== r2.conference
|
r1.conference !== r2.conference
|
||||||
&& r1.dateTimeStamp !== r2.dateTimeStamp
|
&& r1.dateTimeStamp !== r2.dateTimeStamp
|
||||||
});;
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new {@code AbstractRecentList} instance.
|
* Initializes a new {@code AbstractRecentList} instance.
|
||||||
|
@ -67,35 +67,32 @@ export default class AbstractRecentList extends Component<Props, State> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
// this must be done asynchronously because we don't have the storage
|
// The following must be done asynchronously because we don't have the
|
||||||
// initiated on app startup immediately.
|
// storage initiated on app startup immediately.
|
||||||
getRecentRooms().then(rooms => {
|
getRecentRooms()
|
||||||
this.setState({
|
.then(rooms =>
|
||||||
dataSource: this.listDataSource.cloneWithRows(rooms)
|
this.setState({
|
||||||
});
|
dataSource: this.listDataSource.cloneWithRows(rooms)
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a bound onPress action for the list item.
|
* Joins the selected room.
|
||||||
*
|
*
|
||||||
* @param {string} room - The selected room.
|
* @param {string} room - The selected room.
|
||||||
* @returns {Function}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
|
_onJoin(room) {
|
||||||
|
room && this.props.dispatch(appNavigate(room));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bound onPress action for the list item.
|
||||||
|
*
|
||||||
|
* @param {string} room - The selected room.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
_onSelect(room) {
|
_onSelect(room) {
|
||||||
return this._onJoin.bind(this, room);
|
return this._onJoin.bind(this, room);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Joins the selected room.
|
|
||||||
*
|
|
||||||
* @param {string} room - The selected room.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
_onJoin(room) {
|
|
||||||
if (room) {
|
|
||||||
this.props.dispatch(appNavigate(room));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@ import React from 'react';
|
||||||
import { ListView, Text, TouchableHighlight, View } from 'react-native';
|
import { ListView, Text, TouchableHighlight, View } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { Icon } from '../../base/font-icons';
|
||||||
|
|
||||||
import AbstractRecentList from './AbstractRecentList';
|
import AbstractRecentList from './AbstractRecentList';
|
||||||
import styles, { UNDERLAY_COLOR } from './styles';
|
import styles, { UNDERLAY_COLOR } from './styles';
|
||||||
|
|
||||||
import { Icon } from '../../base/font-icons';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The native container rendering the list of the recently joined rooms.
|
* The native container rendering the list of the recently joined rooms.
|
||||||
*
|
*
|
||||||
|
@ -15,11 +15,11 @@ import { Icon } from '../../base/font-icons';
|
||||||
class RecentList extends AbstractRecentList {
|
class RecentList extends AbstractRecentList {
|
||||||
/**
|
/**
|
||||||
* Initializes a new {@code RecentList} instance.
|
* Initializes a new {@code RecentList} instance.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._getAvatarStyle = this._getAvatarStyle.bind(this);
|
this._getAvatarStyle = this._getAvatarStyle.bind(this);
|
||||||
this._onSelect = this._onSelect.bind(this);
|
this._onSelect = this._onSelect.bind(this);
|
||||||
this._renderConfDuration = this._renderConfDuration.bind(this);
|
this._renderConfDuration = this._renderConfDuration.bind(this);
|
||||||
|
@ -28,8 +28,8 @@ class RecentList extends AbstractRecentList {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}. Renders a list of
|
* Implements React's {@link Component#render()}. Renders a list of recently
|
||||||
* recently joined rooms.
|
* joined rooms.
|
||||||
*
|
*
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
|
@ -50,12 +50,98 @@ class RecentList extends AbstractRecentList {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the list of recently joined rooms.
|
* Assembles the style array of the avatar based on if the conference was a
|
||||||
*
|
* home or remote server conference (based on current app setting).
|
||||||
* @private
|
*
|
||||||
* @param {Object} data - The row data to be rendered.
|
* @param {Object} recentListEntry - The recent list entry being rendered.
|
||||||
* @returns {ReactElement}
|
* @private
|
||||||
*/
|
* @returns {Array<Object>}
|
||||||
|
*/
|
||||||
|
_getAvatarStyle(recentListEntry) {
|
||||||
|
const avatarStyles = [ styles.avatar ];
|
||||||
|
|
||||||
|
if (recentListEntry.baseURL !== this.props._homeServer) {
|
||||||
|
avatarStyles.push(
|
||||||
|
this._getColorForServerName(recentListEntry.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({ conferenceDurationString }) {
|
||||||
|
if (conferenceDurationString) {
|
||||||
|
return (
|
||||||
|
<View style = { styles.infoWithIcon } >
|
||||||
|
<Icon
|
||||||
|
name = 'timer'
|
||||||
|
style = { styles.inlineIcon } />
|
||||||
|
<Text style = { styles.confLength }>
|
||||||
|
{ conferenceDurationString }
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the server info component based on if the entry was on a
|
||||||
|
* different server or not.
|
||||||
|
*
|
||||||
|
* @param {Object} recentListEntry - The recent list entry being rendered.
|
||||||
|
* @private
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
_renderServerInfo(recentListEntry) {
|
||||||
|
if (recentListEntry.baseURL !== this.props._homeServer) {
|
||||||
|
return (
|
||||||
|
<View style = { styles.infoWithIcon } >
|
||||||
|
<Icon
|
||||||
|
name = 'public'
|
||||||
|
style = { styles.inlineIcon } />
|
||||||
|
<Text style = { styles.serverName }>
|
||||||
|
{ recentListEntry.serverName }
|
||||||
|
</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) {
|
_renderRow(data) {
|
||||||
return (
|
return (
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
|
@ -94,93 +180,6 @@ class RecentList extends AbstractRecentList {
|
||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Assembles the style array of the avatar based on if the conference
|
|
||||||
* was a home or remote server conference (based on current app setting).
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {Object} recentListEntry - The recent list entry being rendered.
|
|
||||||
* @returns {Array<Object>}
|
|
||||||
*/
|
|
||||||
_getAvatarStyle(recentListEntry) {
|
|
||||||
const avatarStyles = [ styles.avatar ];
|
|
||||||
|
|
||||||
if (recentListEntry.baseURL !== this.props._homeServer) {
|
|
||||||
avatarStyles.push(
|
|
||||||
this._getColorForServerName(recentListEntry.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.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {string} serverName - The recent list entry being rendered.
|
|
||||||
* @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 server info component based on if the entry was
|
|
||||||
* on a different server or not.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {Object} recentListEntry - The recent list entry being rendered.
|
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
|
||||||
_renderServerInfo(recentListEntry) {
|
|
||||||
if (recentListEntry.baseURL !== this.props._homeServer) {
|
|
||||||
return (
|
|
||||||
<View style = { styles.infoWithIcon } >
|
|
||||||
<Icon
|
|
||||||
name = 'public'
|
|
||||||
style = { styles.inlineIcon } />
|
|
||||||
<Text style = { styles.serverName }>
|
|
||||||
{ recentListEntry.serverName }
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the conference duration if available.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {Object} recentListEntry - The recent list entry being rendered.
|
|
||||||
* @returns {ReactElement}
|
|
||||||
*/
|
|
||||||
_renderConfDuration(recentListEntry) {
|
|
||||||
if (recentListEntry.conferenceDurationString) {
|
|
||||||
return (
|
|
||||||
<View style = { styles.infoWithIcon } >
|
|
||||||
<Icon
|
|
||||||
name = 'timer'
|
|
||||||
style = { styles.inlineIcon } />
|
|
||||||
<Text style = { styles.confLength }>
|
|
||||||
{ recentListEntry.conferenceDurationString }
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -195,8 +194,8 @@ class RecentList extends AbstractRecentList {
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
* The default server name based on which we determine
|
* The default server name based on which we determine the render
|
||||||
* the render method.
|
* method.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
|
|
@ -1,58 +1,57 @@
|
||||||
import {
|
import { createStyleSheet, BoxModel } from '../../base/styles';
|
||||||
createStyleSheet,
|
|
||||||
BoxModel
|
|
||||||
} from '../../base/styles';
|
|
||||||
|
|
||||||
const AVATAR_OPACITY = 0.4;
|
const AVATAR_OPACITY = 0.4;
|
||||||
|
|
||||||
const AVATAR_SIZE = 65;
|
const AVATAR_SIZE = 65;
|
||||||
|
|
||||||
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
||||||
|
|
||||||
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
|
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The styles of the React {@code Components} of the feature: recent list
|
* The styles of the React {@code Component}s of the feature recent-list i.e.
|
||||||
* {@code RecentList}.
|
* {@code RecentList}.
|
||||||
*/
|
*/
|
||||||
export default createStyleSheet({
|
export default createStyleSheet({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The style of the actual avatar
|
* The style of the actual avatar.
|
||||||
*/
|
*/
|
||||||
avatar: {
|
avatar: {
|
||||||
width: AVATAR_SIZE,
|
|
||||||
height: AVATAR_SIZE,
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
|
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
|
||||||
|
borderRadius: AVATAR_SIZE,
|
||||||
|
height: AVATAR_SIZE,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderRadius: AVATAR_SIZE
|
width: AVATAR_SIZE
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The style of the avatar container that makes the avatar rounded.
|
* The style of the avatar container that makes the avatar rounded.
|
||||||
*/
|
*/
|
||||||
avatarContainer: {
|
avatarContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
alignItems: 'center',
|
|
||||||
paddingTop: 5
|
paddingTop: 5
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple {@code Text} content of the avatar (the actual initials)
|
* Simple {@code Text} content of the avatar (the actual initials).
|
||||||
*/
|
*/
|
||||||
avatarContent: {
|
avatarContent: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||||
color: OVERLAY_FONT_COLOR,
|
color: OVERLAY_FONT_COLOR,
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: '100',
|
fontWeight: '100',
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of styles of the avatar of a remote meeting
|
* List of styles of the avatar of a remote meeting (not the default
|
||||||
* (not the default server). The number of colors are limited
|
* server). The number of colors are limited because they should match
|
||||||
* because they should match nicely.
|
* nicely.
|
||||||
*/
|
*/
|
||||||
avatarRemoteServer1: {
|
avatarRemoteServer1: {
|
||||||
backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})`
|
backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})`
|
||||||
},
|
},
|
||||||
|
@ -74,79 +73,78 @@ export default createStyleSheet({
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Style of the conference length (if rendered)
|
* The style of the conference length (if rendered).
|
||||||
*/
|
*/
|
||||||
confLength: {
|
confLength: {
|
||||||
color: OVERLAY_FONT_COLOR,
|
color: OVERLAY_FONT_COLOR,
|
||||||
fontWeight: 'normal'
|
fontWeight: 'normal'
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the top level container style of the list
|
* The top level container style of the list.
|
||||||
*/
|
*/
|
||||||
container: {
|
container: {
|
||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Second line of the list (date).
|
* Second line of the list (date). May be extended with server name later.
|
||||||
* May be extended with server name later.
|
*/
|
||||||
*/
|
|
||||||
date: {
|
date: {
|
||||||
color: OVERLAY_FONT_COLOR
|
color: OVERLAY_FONT_COLOR
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The style of the details container (right side) of the list
|
* The style of the details container (right side) of the list.
|
||||||
*/
|
*/
|
||||||
detailsContainer: {
|
detailsContainer: {
|
||||||
|
alignItems: 'flex-start',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginLeft: 2 * BoxModel.margin,
|
marginLeft: 2 * BoxModel.margin
|
||||||
alignItems: 'flex-start'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The container for an info line with an inline icon.
|
* The container for an info line with an inline icon.
|
||||||
*/
|
*/
|
||||||
infoWithIcon: {
|
infoWithIcon: {
|
||||||
|
alignItems: 'center',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start'
|
||||||
alignItems: 'center'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Style of an inline icon in an info line.
|
* The style of an inline icon in an info line.
|
||||||
*/
|
*/
|
||||||
inlineIcon: {
|
inlineIcon: {
|
||||||
color: OVERLAY_FONT_COLOR,
|
color: OVERLAY_FONT_COLOR,
|
||||||
marginRight: 5
|
marginRight: 5
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* First line of the list (room name)
|
* First line of the list (room name).
|
||||||
*/
|
*/
|
||||||
roomName: {
|
roomName: {
|
||||||
|
color: OVERLAY_FONT_COLOR,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold'
|
||||||
color: OVERLAY_FONT_COLOR
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The style of one single row in the list
|
* The style of one single row in the list.
|
||||||
*/
|
*/
|
||||||
row: {
|
row: {
|
||||||
padding: 8,
|
alignItems: 'center',
|
||||||
paddingBottom: 0,
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center'
|
padding: 8,
|
||||||
|
paddingBottom: 0
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Style of the server name component (if rendered)
|
* The style of the server name component (if rendered).
|
||||||
*/
|
*/
|
||||||
serverName: {
|
serverName: {
|
||||||
color: OVERLAY_FONT_COLOR,
|
color: OVERLAY_FONT_COLOR,
|
||||||
fontWeight: 'normal'
|
fontWeight: 'normal'
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* The max size of the list.
|
* The max size of the list.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
export const LIST_SIZE = 30;
|
export const LIST_SIZE = 30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The name of the {@code localStorage} item where recent rooms are stored.
|
* The name of the {@code window.localStorage} item where recent rooms are
|
||||||
|
* stored.
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,131 +2,116 @@
|
||||||
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import { RECENT_URL_STORAGE } from './constants';
|
|
||||||
|
|
||||||
import { i18next } from '../base/i18n';
|
import { i18next } from '../base/i18n';
|
||||||
import { parseURIString } from '../base/util';
|
import { parseURIString } from '../base/util';
|
||||||
|
|
||||||
/**
|
import { RECENT_URL_STORAGE } from './constants';
|
||||||
* Retreives the recent room list and generates all the data needed
|
|
||||||
* to be displayed.
|
|
||||||
*
|
|
||||||
* @returns {Promise} The {@code Promise} to be resolved when the list
|
|
||||||
* is available.
|
|
||||||
*/
|
|
||||||
export function getRecentRooms(): Promise<Array<Object>> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
window.localStorage._getItemAsync(RECENT_URL_STORAGE)
|
|
||||||
.then(recentUrls => {
|
|
||||||
if (recentUrls) {
|
|
||||||
const recentUrlsObj = JSON.parse(recentUrls);
|
|
||||||
const recentRoomDS = [];
|
|
||||||
|
|
||||||
for (const entry of recentUrlsObj) {
|
/**
|
||||||
const location = parseURIString(entry.conference);
|
* Retreives the recent room list and generates all the data needed to be
|
||||||
|
* displayed.
|
||||||
|
*
|
||||||
|
* @returns {Promise} The {@code Promise} to be resolved when the list is
|
||||||
|
* available.
|
||||||
|
*/
|
||||||
|
export function getRecentRooms(): Promise<Array<Object>> {
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
window.localStorage._getItemAsync(RECENT_URL_STORAGE).then(
|
||||||
|
/* onFulfilled */ recentURLs => {
|
||||||
|
const recentRoomDS = [];
|
||||||
|
|
||||||
|
if (recentURLs) {
|
||||||
|
for (const e of JSON.parse(recentURLs)) {
|
||||||
|
const location = parseURIString(e.conference);
|
||||||
|
|
||||||
if (location && location.room && location.hostname) {
|
if (location && location.room && location.hostname) {
|
||||||
recentRoomDS.push({
|
recentRoomDS.push({
|
||||||
baseURL:
|
baseURL:
|
||||||
`${location.protocol}//${location.host}`,
|
`${location.protocol}//${location.host}`,
|
||||||
conference: entry.conference,
|
conference: e.conference,
|
||||||
dateTimeStamp: entry.date,
|
conferenceDuration: e.conferenceDuration,
|
||||||
conferenceDuration: entry.conferenceDuration,
|
conferenceDurationString:
|
||||||
dateString: _getDateString(
|
_getDurationString(e.conferenceDuration),
|
||||||
entry.date
|
dateString: _getDateString(e.date),
|
||||||
),
|
dateTimeStamp: e.date,
|
||||||
conferenceDurationString: _getLengthString(
|
|
||||||
entry.conferenceDuration
|
|
||||||
),
|
|
||||||
initials: _getInitials(location.room),
|
initials: _getInitials(location.room),
|
||||||
room: location.room,
|
room: location.room,
|
||||||
serverName: location.hostname
|
serverName: location.hostname
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(recentRoomDS.reverse());
|
|
||||||
} else {
|
|
||||||
resolve([]);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
resolve(recentRoomDS.reverse());
|
||||||
* Retreives the recent URL list as a list of objects.
|
},
|
||||||
*
|
/* onRejected */ reject)
|
||||||
* @returns {Array} The list of already stored recent URLs.
|
|
||||||
*/
|
|
||||||
export function getRecentUrls() {
|
|
||||||
let recentUrls = window.localStorage.getItem(RECENT_URL_STORAGE);
|
|
||||||
|
|
||||||
if (recentUrls) {
|
|
||||||
recentUrls = JSON.parse(recentUrls);
|
|
||||||
} else {
|
|
||||||
recentUrls = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return recentUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the recent URL list.
|
|
||||||
*
|
|
||||||
* @param {Array} recentUrls - The new URL list.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function updaterecentUrls(recentUrls: Array<Object>) {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
RECENT_URL_STORAGE,
|
|
||||||
JSON.stringify(recentUrls)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a well formatted date string to be displayed in the list.
|
* Retreives the recent URL list as a list of objects.
|
||||||
*
|
*
|
||||||
* @private
|
* @returns {Array} The list of already stored recent URLs.
|
||||||
* @param {number} dateTimeStamp - The UTC timestamp to be converted to String.
|
*/
|
||||||
* @returns {string}
|
export function getRecentURLs() {
|
||||||
*/
|
const recentURLs = window.localStorage.getItem(RECENT_URL_STORAGE);
|
||||||
function _getDateString(dateTimeStamp: number) {
|
|
||||||
const date = new Date(dateTimeStamp);
|
|
||||||
|
|
||||||
if (date.toDateString() === new Date().toDateString()) {
|
return recentURLs ? JSON.parse(recentURLs) : [];
|
||||||
// the date is today, we use fromNow format
|
|
||||||
|
|
||||||
return moment(date)
|
|
||||||
.locale(i18next.language)
|
|
||||||
.fromNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
return moment(date)
|
|
||||||
.locale(i18next.language)
|
|
||||||
.format('lll');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a well formatted duration string to be displayed
|
* Updates the recent URL list.
|
||||||
* as the conference length.
|
*
|
||||||
*
|
* @param {Array} recentURLs - The new URL list.
|
||||||
* @private
|
* @returns {void}
|
||||||
* @param {number} duration - The duration in MS.
|
*/
|
||||||
* @returns {string}
|
export function updateRecentURLs(recentURLs: Array<Object>) {
|
||||||
*/
|
window.localStorage.setItem(
|
||||||
function _getLengthString(duration: number) {
|
RECENT_URL_STORAGE,
|
||||||
|
JSON.stringify(recentURLs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a well formatted date string to be displayed in the list.
|
||||||
|
*
|
||||||
|
* @param {number} dateTimeStamp - The UTC timestamp to be converted to String.
|
||||||
|
* @private
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function _getDateString(dateTimeStamp: number) {
|
||||||
|
const date = new Date(dateTimeStamp);
|
||||||
|
const m = moment(date).locale(i18next.language);
|
||||||
|
|
||||||
|
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.
|
||||||
|
* @private
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function _getDurationString(duration: number) {
|
||||||
return moment.duration(duration)
|
return moment.duration(duration)
|
||||||
.locale(i18next.language)
|
.locale(i18next.language)
|
||||||
.humanize();
|
.humanize();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the initials supposed to be used based on the room name.
|
* Returns the initials supposed to be used based on the room name.
|
||||||
*
|
*
|
||||||
* @private
|
* @param {string} room - The room name.
|
||||||
* @param {string} room - The room name.
|
* @private
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function _getInitials(room: string) {
|
function _getInitials(room: string) {
|
||||||
return room && room.charAt(0) ? room.charAt(0).toUpperCase() : '?';
|
return room && room.charAt(0) ? room.charAt(0).toUpperCase() : '?';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
/* @flow */
|
// @flow
|
||||||
|
|
||||||
import { LIST_SIZE } from './constants';
|
|
||||||
import { getRecentUrls, updaterecentUrls } from './functions';
|
|
||||||
|
|
||||||
import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference';
|
import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference';
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import { LIST_SIZE } from './constants';
|
||||||
|
import { getRecentURLs, updateRecentURLs } from './functions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware that captures joined rooms so then it can be saved to
|
* Middleware that captures joined rooms so they can be saved into
|
||||||
* {@code localStorage}
|
* {@code window.localStorage}.
|
||||||
*
|
*
|
||||||
* @param {Store} store - Redux store.
|
* @param {Store} store - The redux store.
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
MiddlewareRegistry.register(store => next => action => {
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
@ -26,44 +26,44 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the recently joined room in {@code localStorage}.
|
* Stores the recently joined room into {@code window.localStorage}.
|
||||||
*
|
*
|
||||||
* @param {Store} store - The redux store in which the specified action is being
|
* @param {Store} store - The redux store in which the specified action is being
|
||||||
* dispatched.
|
* dispatched.
|
||||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||||
* specified action to the specified store.
|
* specified action to the specified store.
|
||||||
* @param {Action} action - The redux action CONFERENCE_JOINED which is being
|
* @param {Action} action - The redux action {@code SET_ROOM} which is being
|
||||||
* dispatched in the specified store.
|
* dispatched in the specified store.
|
||||||
|
* @private
|
||||||
* @returns {Object} The new state that is the result of the reduction of the
|
* @returns {Object} The new state that is the result of the reduction of the
|
||||||
* specified action.
|
* specified action.
|
||||||
*/
|
*/
|
||||||
function _storeJoinedRoom(store, next, action) {
|
function _storeJoinedRoom(store, next, action) {
|
||||||
const result = next(action);
|
const result = next(action);
|
||||||
|
|
||||||
const { room } = action;
|
const { room } = action;
|
||||||
|
|
||||||
if (room) {
|
if (room) {
|
||||||
const { locationURL } = store.getState()['features/base/connection'];
|
const { locationURL } = store.getState()['features/base/connection'];
|
||||||
const conferenceLink = locationURL.href;
|
const conference = locationURL.href;
|
||||||
|
|
||||||
// if the current conference is already in the list,
|
// If the current conference is already in the list, we remove it to add
|
||||||
// we remove it to add it
|
// it to the top at the end.
|
||||||
// to the top at the end
|
const recentURLs
|
||||||
const recentUrls = getRecentUrls().filter(
|
= getRecentURLs()
|
||||||
entry => entry.conference !== conferenceLink
|
.filter(e => e.conference !== conference);
|
||||||
);
|
|
||||||
|
|
||||||
// please note, this is a reverse sorted array
|
// XXX This is a reverse sorted array (i.e. newer elements at the end).
|
||||||
// (newer elements at the end)
|
recentURLs.push({
|
||||||
recentUrls.push({
|
conference,
|
||||||
conference: conferenceLink,
|
conferenceDuration: 0,
|
||||||
date: Date.now(),
|
date: Date.now()
|
||||||
conferenceDuration: 0
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// maximising the size
|
// maximising the size
|
||||||
recentUrls.splice(0, recentUrls.length - LIST_SIZE);
|
recentURLs.splice(0, recentURLs.length - LIST_SIZE);
|
||||||
|
|
||||||
updaterecentUrls(recentUrls);
|
updateRecentURLs(recentURLs);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -72,33 +72,35 @@ function _storeJoinedRoom(store, next, action) {
|
||||||
/**
|
/**
|
||||||
* Updates the conference length when left.
|
* Updates the conference length when left.
|
||||||
*
|
*
|
||||||
* @private
|
|
||||||
* @param {Store} store - The redux store in which the specified action is being
|
* @param {Store} store - The redux store in which the specified action is being
|
||||||
* dispatched.
|
* dispatched.
|
||||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||||
* specified action to the specified store.
|
* specified action to the specified store.
|
||||||
* @param {Action} action - The redux action CONFERENCE_JOINED which is being
|
* @param {Action} action - The redux action {@code CONFERENCE_WILL_LEAVE} which
|
||||||
* dispatched in the specified store.
|
* is being dispatched in the specified store.
|
||||||
|
* @private
|
||||||
* @returns {Object} The new state that is the result of the reduction of the
|
* @returns {Object} The new state that is the result of the reduction of the
|
||||||
* specified action.
|
* specified action.
|
||||||
*/
|
*/
|
||||||
function _updateConferenceDuration(store, next, action) {
|
function _updateConferenceDuration({ getState }, next, action) {
|
||||||
const result = next(action);
|
const result = next(action);
|
||||||
const { locationURL } = store.getState()['features/base/connection'];
|
|
||||||
|
const { locationURL } = getState()['features/base/connection'];
|
||||||
|
|
||||||
if (locationURL && locationURL.href) {
|
if (locationURL && locationURL.href) {
|
||||||
const recentUrls = getRecentUrls();
|
const recentURLs = getRecentURLs();
|
||||||
|
|
||||||
if (recentUrls.length > 0
|
if (recentURLs.length > 0) {
|
||||||
&& recentUrls[recentUrls.length - 1].conference
|
const mostRecentURL = recentURLs[recentURLs.length - 1];
|
||||||
=== locationURL.href) {
|
|
||||||
// the last conference start was stored
|
|
||||||
// so we need to update the length
|
|
||||||
|
|
||||||
recentUrls[recentUrls.length - 1].conferenceDuration
|
if (mostRecentURL.conference === locationURL.href) {
|
||||||
= Date.now() - recentUrls[recentUrls.length - 1].date;
|
// The last conference start was stored so we need to update the
|
||||||
|
// length.
|
||||||
|
mostRecentURL.conferenceDuration
|
||||||
|
= Date.now() - mostRecentURL.date;
|
||||||
|
|
||||||
updaterecentUrls(recentUrls);
|
updateRecentURLs(recentURLs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,6 @@ import React from 'react';
|
||||||
import { TextInput, TouchableHighlight, View } from 'react-native';
|
import { TextInput, TouchableHighlight, View } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
|
||||||
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
|
|
||||||
import styles, { PLACEHOLDER_TEXT_COLOR } from './styles';
|
|
||||||
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { MEDIA_TYPE } from '../../base/media';
|
import { MEDIA_TYPE } from '../../base/media';
|
||||||
import { Link, LoadingIndicator, Text } from '../../base/react';
|
import { Link, LoadingIndicator, Text } from '../../base/react';
|
||||||
|
@ -13,6 +9,10 @@ import { ColorPalette } from '../../base/styles';
|
||||||
import { createDesiredLocalTracks } from '../../base/tracks';
|
import { createDesiredLocalTracks } from '../../base/tracks';
|
||||||
import { RecentList } from '../../recent-list';
|
import { RecentList } from '../../recent-list';
|
||||||
|
|
||||||
|
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
||||||
|
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
|
||||||
|
import styles, { PLACEHOLDER_TEXT_COLOR } from './styles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL at which the privacy policy is available to the user.
|
* The URL at which the privacy policy is available to the user.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -5,13 +5,13 @@ import {
|
||||||
fixAndroidViewClipping
|
fixAndroidViewClipping
|
||||||
} from '../../base/styles';
|
} from '../../base/styles';
|
||||||
|
|
||||||
|
export const PLACEHOLDER_TEXT_COLOR = 'rgba(255, 255, 255, 0.3)';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default color of text on the WelcomePage.
|
* The default color of text on the WelcomePage.
|
||||||
*/
|
*/
|
||||||
const TEXT_COLOR = ColorPalette.white;
|
const TEXT_COLOR = ColorPalette.white;
|
||||||
|
|
||||||
export const PLACEHOLDER_TEXT_COLOR = 'rgba(255, 255, 255, 0.3)';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The styles of the React {@code Components} of the feature welcome including
|
* The styles of the React {@code Components} of the feature welcome including
|
||||||
* {@code WelcomePage} and {@code BlankPage}.
|
* {@code WelcomePage} and {@code BlankPage}.
|
||||||
|
|
Loading…
Reference in New Issue