added recent list
This commit is contained in:
parent
af7c69a1aa
commit
046b06e436
|
@ -35,8 +35,10 @@ import {
|
|||
conferenceJoined,
|
||||
conferenceLeft,
|
||||
conferenceWillJoin,
|
||||
conferenceWillLeave,
|
||||
dataChannelOpened,
|
||||
EMAIL_COMMAND,
|
||||
getCurrentConference,
|
||||
lockStateChanged,
|
||||
onStartMutedPolicyChanged,
|
||||
p2pStatusChanged,
|
||||
|
@ -305,6 +307,10 @@ class ConferenceConnector {
|
|||
_onConferenceFailed(err, ...params) {
|
||||
APP.store.dispatch(conferenceFailed(room, err, ...params));
|
||||
logger.error('CONFERENCE FAILED:', err, ...params);
|
||||
const state = APP.store.getState();
|
||||
|
||||
// The conference we have already joined or are joining.
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
switch (err) {
|
||||
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
||||
|
@ -375,6 +381,7 @@ class ConferenceConnector {
|
|||
// FIXME the conference should be stopped by the library and not by
|
||||
// the app. Both the errors above are unrecoverable from the library
|
||||
// perspective.
|
||||
APP.store.dispatch(conferenceWillLeave(conference));
|
||||
room.leave().then(() => connection.disconnect());
|
||||
break;
|
||||
|
||||
|
@ -468,6 +475,12 @@ function _connectionFailedHandler(error) {
|
|||
JitsiConnectionEvents.CONNECTION_FAILED,
|
||||
_connectionFailedHandler);
|
||||
if (room) {
|
||||
const state = APP.store.getState();
|
||||
|
||||
// The conference we have already joined or are joining.
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
APP.store.dispatch(conferenceWillLeave(conference));
|
||||
room.leave();
|
||||
}
|
||||
}
|
||||
|
@ -2465,6 +2478,12 @@ export default {
|
|||
* requested
|
||||
*/
|
||||
hangup(requestFeedback = false) {
|
||||
const state = APP.store.getState();
|
||||
|
||||
// The conference we have already joined or are joining.
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
APP.store.dispatch(conferenceWillLeave(conference));
|
||||
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
|
||||
APP.UI.removeLocalMedia();
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
%navigate-section-list-text {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: $welcomePageTitleColor;
|
||||
text-align: left;
|
||||
font-family: 'open_sanslight', Helvetica, sans-serif;
|
||||
}
|
||||
%navigate-section-list-tile-text {
|
||||
@extend %navigate-section-list-text;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
float: left;
|
||||
}
|
||||
.navigate-section-list-tile {
|
||||
height: 90px;
|
||||
width: 260px;
|
||||
border-radius: 4px;
|
||||
background-color: #1754A9;
|
||||
margin-right: 8px;
|
||||
padding: 16px;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
.navigate-section-tile-body {
|
||||
@extend %navigate-section-list-tile-text;
|
||||
font-weight: normal;
|
||||
}
|
||||
.navigate-section-tile-title {
|
||||
@extend %navigate-section-list-tile-text;
|
||||
font-weight: bold;
|
||||
}
|
||||
.navigate-section-section-header {
|
||||
@extend %navigate-section-list-text;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.navigate-section-list {
|
||||
position: relative;
|
||||
margin-top: 36px;
|
||||
margin-bottom: 36px;
|
||||
}
|
|
@ -78,4 +78,5 @@
|
|||
@import 'modals/invite/add-people';
|
||||
@import 'deep-linking/main';
|
||||
@import 'transcription-subtitles';
|
||||
/* Modules END */
|
||||
@import 'navigate_section_list';
|
||||
@import 'transcription-subtitles';
|
||||
|
|
|
@ -163,7 +163,14 @@ var interfaceConfig = {
|
|||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
VIDEO_QUALITY_LABEL_DISABLED: false
|
||||
VIDEO_QUALITY_LABEL_DISABLED: false,
|
||||
|
||||
/**
|
||||
* If true, will display recent list
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
RECENT_LIST_ENABLED: true
|
||||
|
||||
/**
|
||||
* Specify custom URL for downloading android mobile app.
|
||||
|
|
|
@ -626,9 +626,7 @@
|
|||
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
|
||||
},
|
||||
"recentList": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"earlier": "Earlier"
|
||||
"joinPastMeeting": "Join A Past Meeting"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Pull to refresh"
|
||||
|
@ -656,6 +654,11 @@
|
|||
"ignored": "Ignored",
|
||||
"expired": "Expired"
|
||||
},
|
||||
"dateUtils": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"earlier": "Earlier"
|
||||
},
|
||||
"incomingCall": {
|
||||
"answer": "Answer",
|
||||
"audioCallTitle": "Incoming call",
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
||||
"lodash": "4.17.4",
|
||||
"moment": "2.19.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"postis": "2.2.0",
|
||||
"prop-types": "15.6.0",
|
||||
"react": "16.3.1",
|
||||
|
|
|
@ -4,6 +4,9 @@ import moment from 'moment';
|
|||
|
||||
import i18next from './i18next';
|
||||
|
||||
// allows for moment durations to be formatted
|
||||
import 'moment-duration-format';
|
||||
|
||||
// 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.
|
||||
|
@ -55,8 +58,19 @@ export function getLocalizedDurationFormatter(duration: number) {
|
|||
// states v2.19 so maybe locale on moment's duration was introduced in
|
||||
// between?
|
||||
//
|
||||
|
||||
// If the conference is under an hour long we want to display it without
|
||||
// showing the hour and we want to include the hour if the conference is
|
||||
// more than an hour long
|
||||
|
||||
// $FlowFixMe
|
||||
return moment.duration(duration).locale(_getSupportedLocale());
|
||||
if (moment.duration(duration).format('h') !== '0') {
|
||||
// $FlowFixMe
|
||||
return moment.duration(duration).format('h:mm:ss');
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
return moment.duration(duration).format('mm:ss', { trim: false });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// @flow
|
||||
/**
|
||||
* item data for NavigateSectionList
|
||||
*/
|
||||
import type {
|
||||
ComponentType,
|
||||
Element
|
||||
} from 'react';
|
||||
|
||||
export type Item = {
|
||||
|
||||
/**
|
||||
* the color base of the avatar
|
||||
*/
|
||||
colorBase: string,
|
||||
|
||||
/**
|
||||
* Item title
|
||||
*/
|
||||
title: string,
|
||||
|
||||
/**
|
||||
* Item url
|
||||
*/
|
||||
url: string,
|
||||
|
||||
/**
|
||||
* lines[0] - date
|
||||
* lines[1] - duration
|
||||
* lines[2] - server name
|
||||
*/
|
||||
lines: Array<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* web implementation of section data for NavigateSectionList
|
||||
*/
|
||||
export type Section = {
|
||||
|
||||
/**
|
||||
* section title
|
||||
*/
|
||||
title: string,
|
||||
|
||||
/**
|
||||
* unique key for the section
|
||||
*/
|
||||
key?: string,
|
||||
|
||||
/**
|
||||
* Array of items in the section
|
||||
*/
|
||||
data: $ReadOnlyArray<Item>,
|
||||
|
||||
/**
|
||||
* Optional properties added only to fix some flow errors thrown by React
|
||||
* SectionList
|
||||
*/
|
||||
ItemSeparatorComponent?: ?ComponentType<any>,
|
||||
|
||||
keyExtractor?: (item: Object) => string,
|
||||
|
||||
renderItem?: ?(info: Object) => ?Element<any>
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* native implementation of section data for NavigateSectionList
|
||||
*
|
||||
* When react-native's SectionList component parses through an array of sections
|
||||
* it passes the section nested within the section property of another object
|
||||
* to the renderSection method (on web for our own implementation of SectionList
|
||||
* this nesting is not implemented as there is no need for nesting)
|
||||
*/
|
||||
export type SetionListSection = {
|
||||
section: Section
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../i18n';
|
||||
|
||||
import {
|
||||
NavigateSectionListEmptyComponent,
|
||||
NavigateSectionListItem,
|
||||
NavigateSectionListSectionHeader,
|
||||
SectionList
|
||||
} from './_';
|
||||
import type { Section } from '../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||
*/
|
||||
onPress: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when pull-to-refresh is performed.
|
||||
*/
|
||||
onRefresh: Function,
|
||||
|
||||
/**
|
||||
* Function to override the rendered default empty list component.
|
||||
*/
|
||||
renderListEmptyComponent: Function,
|
||||
|
||||
/**
|
||||
* An array of sections
|
||||
*/
|
||||
sections: Array<Section>
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class NavigateSectionList extends Component<Props> {
|
||||
/**
|
||||
* Creates an empty section object.
|
||||
*
|
||||
* @param {string} title - The title of the section.
|
||||
* @param {string} key - The key of the section. It must be unique.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
static createSection(title, key) {
|
||||
return {
|
||||
data: [],
|
||||
key,
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor of the NavigateSectionList component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this._getItemKey = this._getItemKey.bind(this);
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._onRefresh = this._onRefresh.bind(this);
|
||||
this._renderItem = this._renderItem.bind(this);
|
||||
this._renderListEmptyComponent
|
||||
= this._renderListEmptyComponent.bind(this);
|
||||
this._renderSectionHeader = this._renderSectionHeader.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's Component.render.
|
||||
* Note: we don't use the refreshing value yet, because refreshing of these
|
||||
* lists is super quick, no need to complicate the code - yet.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
renderListEmptyComponent = this._renderListEmptyComponent,
|
||||
sections
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
ListEmptyComponent = { renderListEmptyComponent }
|
||||
keyExtractor = { this._getItemKey }
|
||||
onItemClick = { this.props.onPress }
|
||||
onRefresh = { this._onRefresh }
|
||||
refreshing = { false }
|
||||
renderItem = { this._renderItem }
|
||||
renderSectionHeader = { this._renderSectionHeader }
|
||||
sections = { sections } />
|
||||
);
|
||||
}
|
||||
|
||||
_getItemKey: (Object, number) => string;
|
||||
|
||||
/**
|
||||
* Generates a unique id to every item.
|
||||
*
|
||||
* @param {Object} item - The item.
|
||||
* @param {number} index - The item index.
|
||||
* @private
|
||||
* @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.
|
||||
*
|
||||
* @param {string} url - The URL of the item to navigate to.
|
||||
* @private
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onPress(url) {
|
||||
return () => {
|
||||
const { disabled, onPress } = this.props;
|
||||
|
||||
!disabled && url && typeof onPress === 'function' && onPress(url);
|
||||
};
|
||||
}
|
||||
|
||||
_onRefresh: () => void;
|
||||
|
||||
/**
|
||||
* Invokes the onRefresh callback if present.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRefresh() {
|
||||
const { onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
onRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
_renderItem: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a single item in the list.
|
||||
*
|
||||
* @param {Object} listItem - The item to render.
|
||||
* @param {string} key - The item needed for rendering using map on web.
|
||||
* @private
|
||||
* @returns {Component}
|
||||
*/
|
||||
_renderItem(listItem, key = '') {
|
||||
const { item } = listItem;
|
||||
const { url } = item;
|
||||
|
||||
// XXX The value of title cannot be undefined; otherwise, react-native
|
||||
// will throw a TypeError: Cannot read property of undefined. While it's
|
||||
// difficult to get an undefined title and very likely requires the
|
||||
// execution of incorrect source code, it is undesirable to break the
|
||||
// whole app because of an undefined string.
|
||||
if (typeof item.title === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigateSectionListItem
|
||||
item = { item }
|
||||
key = { key }
|
||||
onPress = { this._onPress(url) } />
|
||||
);
|
||||
}
|
||||
|
||||
_renderListEmptyComponent: () => Object;
|
||||
|
||||
/**
|
||||
* Renders a component to display when the list is empty.
|
||||
*
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderListEmptyComponent() {
|
||||
const { t, onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
return (
|
||||
<NavigateSectionListEmptyComponent
|
||||
t = { t } />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_renderSectionHeader: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a section header.
|
||||
*
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderSectionHeader(section) {
|
||||
return (
|
||||
<NavigateSectionListSectionHeader
|
||||
section = { section } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(NavigateSectionList);
|
|
@ -1 +1,2 @@
|
|||
export * from './_';
|
||||
export { default as NavigateSectionList } from './NavigateSectionList';
|
||||
|
|
|
@ -1,346 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
SafeAreaView,
|
||||
SectionList,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { Icon } from '../../../font-icons';
|
||||
import { translate } from '../../../i18n';
|
||||
|
||||
import styles, { UNDERLAY_COLOR } from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||
*/
|
||||
onPress: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when pull-to-refresh is performed.
|
||||
*/
|
||||
onRefresh: Function,
|
||||
|
||||
/**
|
||||
* Function to override the rendered default empty list component.
|
||||
*/
|
||||
renderListEmptyComponent: 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.
|
||||
*/
|
||||
class NavigateSectionList extends Component<Props> {
|
||||
/**
|
||||
* Creates an empty section object.
|
||||
*
|
||||
* @param {string} title - The title of the section.
|
||||
* @param {string} key - The key of the section. It must be unique.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
static createSection(title, key) {
|
||||
return {
|
||||
data: [],
|
||||
key,
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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._onRefresh = this._onRefresh.bind(this);
|
||||
this._renderItem = this._renderItem.bind(this);
|
||||
this._renderItemLine = this._renderItemLine.bind(this);
|
||||
this._renderItemLines = this._renderItemLines.bind(this);
|
||||
this._renderListEmptyComponent
|
||||
= this._renderListEmptyComponent.bind(this);
|
||||
this._renderSection = this._renderSection.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's Component.render.
|
||||
* Note: we don't use the refreshing value yet, because refreshing of these
|
||||
* lists is super quick, no need to complicate the code - yet.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
renderListEmptyComponent = this._renderListEmptyComponent,
|
||||
sections
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style = { styles.container } >
|
||||
<SectionList
|
||||
ListEmptyComponent = { renderListEmptyComponent }
|
||||
keyExtractor = { this._getItemKey }
|
||||
onRefresh = { this._onRefresh }
|
||||
refreshing = { false }
|
||||
renderItem = { this._renderItem }
|
||||
renderSectionHeader = { this._renderSection }
|
||||
sections = { sections }
|
||||
style = { styles.list } />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
_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.
|
||||
*
|
||||
* @param {Object} item - The item.
|
||||
* @param {number} index - The item index.
|
||||
* @private
|
||||
* @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.
|
||||
*
|
||||
* @param {string} url - The URL of the item to navigate to.
|
||||
* @private
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onPress(url) {
|
||||
return () => {
|
||||
const { disabled, onPress } = this.props;
|
||||
|
||||
!disabled && url && typeof onPress === 'function' && onPress(url);
|
||||
};
|
||||
}
|
||||
|
||||
_onRefresh: () => void;
|
||||
|
||||
/**
|
||||
* Invokes the onRefresh callback if present.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRefresh() {
|
||||
const { onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
onRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
_renderItem: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a single item in the list.
|
||||
*
|
||||
* @param {Object} listItem - The item to render.
|
||||
* @private
|
||||
* @returns {Component}
|
||||
*/
|
||||
_renderItem(listItem) {
|
||||
const { item: { colorBase, lines, title, url } } = listItem;
|
||||
|
||||
// XXX The value of title cannot be undefined; otherwise, react-native
|
||||
// will throw a TypeError: Cannot read property of undefined. While it's
|
||||
// difficult to get an undefined title and very likely requires the
|
||||
// execution of incorrect source code, it is undesirable to break the
|
||||
// whole app because of an undefined string.
|
||||
if (typeof title === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress = { this._onPress(url) }
|
||||
underlayColor = { UNDERLAY_COLOR }>
|
||||
<View style = { styles.listItem }>
|
||||
<View style = { styles.avatarContainer } >
|
||||
<View
|
||||
style = { [
|
||||
styles.avatar,
|
||||
this._getAvatarColor(colorBase)
|
||||
] } >
|
||||
<Text style = { styles.avatarContent }>
|
||||
{ title.substr(0, 1).toUpperCase() }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style = { styles.listItemDetails }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { [
|
||||
styles.listItemText,
|
||||
styles.listItemTitle
|
||||
] }>
|
||||
{ title }
|
||||
</Text>
|
||||
{ this._renderItemLines(lines) }
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
_renderItemLine: (string, number) => React$Node;
|
||||
|
||||
/**
|
||||
* Renders a single line from the additional lines.
|
||||
*
|
||||
* @param {string} line - The line text.
|
||||
* @param {number} index - The index of the line.
|
||||
* @private
|
||||
* @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.
|
||||
*
|
||||
* @param {Array<string>} lines - The lines to render.
|
||||
* @private
|
||||
* @returns {Array<React$Node>}
|
||||
*/
|
||||
_renderItemLines(lines) {
|
||||
return lines && lines.length ? lines.map(this._renderItemLine) : null;
|
||||
}
|
||||
|
||||
_renderListEmptyComponent: () => Object;
|
||||
|
||||
/**
|
||||
* Renders a component to display when the list is empty.
|
||||
*
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderListEmptyComponent() {
|
||||
const { t, onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
return (
|
||||
<View style = { styles.pullToRefresh }>
|
||||
<Text style = { styles.pullToRefreshText }>
|
||||
{ t('sectionList.pullToRefresh') }
|
||||
</Text>
|
||||
<Icon
|
||||
name = 'menu-down'
|
||||
style = { styles.pullToRefreshIcon } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_renderSection: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a section title.
|
||||
*
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderSection(section) {
|
||||
return (
|
||||
<View style = { styles.listSection }>
|
||||
<Text style = { styles.listSectionText }>
|
||||
{ section.section.title }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(NavigateSectionList);
|
|
@ -0,0 +1,51 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { Icon } from '../../../font-icons/index';
|
||||
import { translate } from '../../../i18n/index';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React Native {@link Component} that is to be displayed when the
|
||||
* list is empty
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class NavigateSectionListEmptyComponent extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.pullToRefresh }>
|
||||
<Text style = { styles.pullToRefreshText }>
|
||||
{ t('sectionList.pullToRefresh') }
|
||||
</Text>
|
||||
<Icon
|
||||
name = 'menu-down'
|
||||
style = { styles.pullToRefreshIcon } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(NavigateSectionListEmptyComponent);
|
|
@ -0,0 +1,145 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { TouchableHighlight } from 'react-native';
|
||||
|
||||
import {
|
||||
Text,
|
||||
Container
|
||||
} from './index';
|
||||
import styles, { UNDERLAY_COLOR } from './styles';
|
||||
import type { Item } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* item containing data to be rendered
|
||||
*/
|
||||
item: Item,
|
||||
|
||||
/**
|
||||
* Function to be invoked when an Item is pressed. The Item's URL is passed.
|
||||
*/
|
||||
onPress: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React/Native {@link Component} that renders the Navigate Section
|
||||
* List Item
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListItem extends Component<Props> {
|
||||
/**
|
||||
* Constructor of the NavigateSectionList component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this._getAvatarColor = this._getAvatarColor.bind(this);
|
||||
this._renderItemLine = this._renderItemLine.bind(this);
|
||||
this._renderItemLines = this._renderItemLines.bind(this);
|
||||
}
|
||||
|
||||
_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}`];
|
||||
}
|
||||
|
||||
_renderItemLine: (string, number) => React$Node;
|
||||
|
||||
/**
|
||||
* Renders a single line from the additional lines.
|
||||
*
|
||||
* @param {string} line - The line text.
|
||||
* @param {number} index - The index of the line.
|
||||
* @private
|
||||
* @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.
|
||||
*
|
||||
* @param {Array<string>} lines - The lines to render.
|
||||
* @private
|
||||
* @returns {Array<React$Node>}
|
||||
*/
|
||||
_renderItemLines(lines) {
|
||||
return lines && lines.length ? lines.map(this._renderItemLine) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { colorBase, lines, title } = this.props.item;
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress = { this.props.onPress }
|
||||
underlayColor = { UNDERLAY_COLOR }>
|
||||
<Container style = { styles.listItem }>
|
||||
<Container style = { styles.avatarContainer }>
|
||||
<Container
|
||||
style = { [
|
||||
styles.avatar,
|
||||
this._getAvatarColor(colorBase)
|
||||
] }>
|
||||
<Text style = { styles.avatarContent }>
|
||||
{title.substr(0, 1).toUpperCase()}
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
<Container style = { styles.listItemDetails }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { [
|
||||
styles.listItemText,
|
||||
styles.listItemTitle
|
||||
] }>
|
||||
{title}
|
||||
</Text>
|
||||
{this._renderItemLines(lines)}
|
||||
</Container>
|
||||
</Container>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Text, Container } from './index';
|
||||
import styles from './styles';
|
||||
import type { SetionListSection } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* A section containing the data to be rendered
|
||||
*/
|
||||
section: SetionListSection
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React/Native {@link Component} that renders the section header
|
||||
* of the list
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListSectionHeader extends Component<Props> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { section } = this.props.section;
|
||||
|
||||
return (
|
||||
<Container style = { styles.listSection }>
|
||||
<Text style = { styles.listSectionText }>
|
||||
{ section.title }
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
SafeAreaView,
|
||||
SectionList as ReactNativeSectionList
|
||||
} from 'react-native';
|
||||
|
||||
import styles from './styles';
|
||||
import type { Section } from '../../Types';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SectionList}
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Rendered when the list is empty. Can be a React Component Class, a render
|
||||
* function, or a rendered element.
|
||||
*/
|
||||
ListEmptyComponent: Object,
|
||||
|
||||
/**
|
||||
*
|
||||
* Used to extract a unique key for a given item at the specified index.
|
||||
* Key is used for caching and as the react key to track item re-ordering.
|
||||
*/
|
||||
keyExtractor: Function,
|
||||
|
||||
/**
|
||||
*
|
||||
* Functions that defines what happens when the list is pulled for refresh
|
||||
*/
|
||||
onRefresh: Function,
|
||||
|
||||
/**
|
||||
*
|
||||
* A boolean that is set true while waiting for new data from a refresh.
|
||||
*/
|
||||
refreshing: ?boolean,
|
||||
|
||||
/**
|
||||
*
|
||||
* Default renderer for every item in every section.
|
||||
*/
|
||||
renderItem: Function,
|
||||
|
||||
/**
|
||||
*
|
||||
* A component rendered at the top of each section. These stick to the top
|
||||
* of the ScrollView by default on iOS.
|
||||
*/
|
||||
renderSectionHeader: Object,
|
||||
|
||||
/**
|
||||
* An array of sections
|
||||
*/
|
||||
sections: Array<Section>
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React Native {@link Component} that wraps the React Native
|
||||
* SectionList component in a SafeAreaView so that it renders the sectionlist
|
||||
* within the safe area of the device
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class SectionList extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<SafeAreaView
|
||||
style = { styles.container } >
|
||||
<ReactNativeSectionList
|
||||
ListEmptyComponent = { this.props.ListEmptyComponent }
|
||||
keyExtractor = { this.props.keyExtractor }
|
||||
onRefresh = { this.props.onRefresh }
|
||||
refreshing = { this.props.refreshing }
|
||||
renderItem = { this.props.renderItem }
|
||||
renderSectionHeader = { this.props.renderSectionHeader }
|
||||
sections = { this.props.sections }
|
||||
style = { styles.list } />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,16 @@
|
|||
export { default as Container } from './Container';
|
||||
export { default as Header } from './Header';
|
||||
export { default as NavigateSectionList } from './NavigateSectionList';
|
||||
export { default as Link } from './Link';
|
||||
export { default as LoadingIndicator } from './LoadingIndicator';
|
||||
export { default as NavigateSectionListEmptyComponent } from
|
||||
'./NavigateSectionListEmptyComponent';
|
||||
export { default as NavigateSectionListItem }
|
||||
from './NavigateSectionListItem';
|
||||
export { default as NavigateSectionListSectionHeader }
|
||||
from './NavigateSectionListSectionHeader';
|
||||
export { default as PagedList } from './PagedList';
|
||||
export { default as Pressable } from './Pressable';
|
||||
export { default as SideBar } from './SideBar';
|
||||
export { default as Text } from './Text';
|
||||
export { default as SectionList } from './SectionList';
|
||||
export { default as TintedView } from './TintedView';
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Text, Container } from './index';
|
||||
import type { Item } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||
*/
|
||||
onPress: Function,
|
||||
|
||||
/**
|
||||
* A item containing data to be rendered
|
||||
*/
|
||||
item: Item
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React/Web {@link Component} for displaying an item in a
|
||||
* NavigateSectionList
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListItem extends Component<Props> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { lines, title } = this.props.item;
|
||||
const { onPress } = this.props;
|
||||
|
||||
/**
|
||||
* Initiliazes the date and duration of the conference to the an empty
|
||||
* string in case for some reason there is an error where the item data
|
||||
* lines doesn't contain one or both of those values (even though this
|
||||
* unlikely the app shouldn't break because of it)
|
||||
* @type {string}
|
||||
*/
|
||||
let date = '';
|
||||
let duration = '';
|
||||
|
||||
if (lines[0]) {
|
||||
date = lines[0];
|
||||
}
|
||||
if (lines[1]) {
|
||||
duration = lines[1];
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
className = 'navigate-section-list-tile'
|
||||
onClick = { onPress }>
|
||||
<Text
|
||||
className = 'navigate-section-tile-title'>
|
||||
{ title }
|
||||
</Text>
|
||||
<Text
|
||||
className = 'navigate-section-tile-body'>
|
||||
{ date }
|
||||
</Text>
|
||||
<Text
|
||||
className = 'navigate-section-tile-body'>
|
||||
{ duration }
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Text } from './index';
|
||||
import type { Section } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* A section containing the data to be rendered
|
||||
*/
|
||||
section: Section
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React/Web {@link Component} that renders the section header of
|
||||
* the list
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListSectionHeader extends Component<Props> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Text className = 'navigate-section-section-header'>
|
||||
{ this.props.section.title }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Container } from './index';
|
||||
import type { Section } from '../../Types';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Used to extract a unique key for a given item at the specified index.
|
||||
* Key is used for caching and as the react key to track item re-ordering.
|
||||
*/
|
||||
keyExtractor: Function,
|
||||
|
||||
/**
|
||||
* Returns a React component that renders each Item in the list
|
||||
*/
|
||||
renderItem: Function,
|
||||
|
||||
/**
|
||||
* Returns a React component that renders the header for every section
|
||||
*/
|
||||
renderSectionHeader: Function,
|
||||
|
||||
/**
|
||||
* An array of sections
|
||||
*/
|
||||
sections: Array<Section>,
|
||||
|
||||
/**
|
||||
* defines what happens when an item in the section list is clicked
|
||||
*/
|
||||
onItemClick: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React/Web {@link Component} for displaying a list with
|
||||
* sections similar to React Native's {@code SectionList} in order to
|
||||
* faciliate cross-platform source code.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class SectionList extends Component<Props> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
renderSectionHeader,
|
||||
renderItem,
|
||||
sections,
|
||||
keyExtractor
|
||||
} = this.props;
|
||||
|
||||
/**
|
||||
* If there are no recent items we dont want to display anything
|
||||
*/
|
||||
if (sections) {
|
||||
return (
|
||||
/* eslint-disable no-extra-parens */
|
||||
<Container
|
||||
className = 'navigate-section-list'>
|
||||
{
|
||||
sections.map((section, sectionIndex) => (
|
||||
<Container
|
||||
key = { sectionIndex }>
|
||||
{ renderSectionHeader(section) }
|
||||
{ section.data
|
||||
.map((item, listIndex) => {
|
||||
const listItem = {
|
||||
item
|
||||
};
|
||||
|
||||
return renderItem(listItem,
|
||||
keyExtractor(section,
|
||||
listIndex));
|
||||
}) }
|
||||
</Container>
|
||||
)
|
||||
)
|
||||
}
|
||||
</Container>
|
||||
/* eslint-enable no-extra-parens */
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,11 @@
|
|||
export { default as Container } from './Container';
|
||||
export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';
|
||||
export { default as NavigateSectionListEmptyComponent } from
|
||||
'./NavigateSectionListEmptyComponent';
|
||||
export { default as NavigateSectionListItem } from
|
||||
'./NavigateSectionListItem';
|
||||
export { default as NavigateSectionListSectionHeader }
|
||||
from './NavigateSectionListSectionHeader';
|
||||
export { default as SectionList } from './SectionList';
|
||||
export { default as Text } from './Text';
|
||||
export { default as Watermarks } from './Watermarks';
|
||||
|
|
|
@ -37,3 +37,8 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
return result;
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
// Stop the LogCollector
|
||||
throttledPersistState.flush();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { appNavigate, getDefaultURL } from '../../app';
|
||||
import { translate } from '../../base/i18n';
|
||||
import type { Section } from '../../base/react/Types';
|
||||
import { NavigateSectionList } from '../../base/react';
|
||||
|
||||
import { toDisplayableList } from '../functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RecentList}
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Renders the list disabled.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* The redux store's {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Dispatch<*>,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The default server URL.
|
||||
*/
|
||||
_defaultServerURL: string,
|
||||
|
||||
/**
|
||||
* The recent list from the Redux store.
|
||||
*/
|
||||
_recentList: Array<Section>
|
||||
};
|
||||
|
||||
/**
|
||||
* The cross platform container rendering the list of the recently joined rooms.
|
||||
*
|
||||
*/
|
||||
class RecentList extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code RecentList} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onPress = this._onPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the React Components's render method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { disabled, t, _defaultServerURL, _recentList } = this.props;
|
||||
const recentList = toDisplayableList(_recentList, t, _defaultServerURL);
|
||||
|
||||
return (
|
||||
<NavigateSectionList
|
||||
disabled = { disabled }
|
||||
onPress = { this._onPress }
|
||||
sections = { recentList } />
|
||||
);
|
||||
}
|
||||
|
||||
_onPress: string => Function;
|
||||
|
||||
/**
|
||||
* Handles the list's navigate action.
|
||||
*
|
||||
* @private
|
||||
* @param {string} url - The url string to navigate to.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPress(url) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(appNavigate(url));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* _defaultServerURL: string,
|
||||
* _recentList: Array
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_defaultServerURL: getDefaultURL(state),
|
||||
_recentList: state['features/recent-list']
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RecentList));
|
|
@ -1,234 +0,0 @@
|
|||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { appNavigate, getDefaultURL } from '../../app';
|
||||
import {
|
||||
getLocalizedDateFormatter,
|
||||
getLocalizedDurationFormatter,
|
||||
translate
|
||||
} from '../../base/i18n';
|
||||
import { NavigateSectionList } from '../../base/react';
|
||||
import { parseURIString } from '../../base/util';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RecentList}
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Renders the list disabled.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* The redux store's {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Dispatch<*>,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* The default server URL.
|
||||
*/
|
||||
_defaultServerURL: string,
|
||||
|
||||
/**
|
||||
* The recent list from the Redux store.
|
||||
*/
|
||||
_recentList: Array<Object>
|
||||
};
|
||||
|
||||
/**
|
||||
* The native container rendering the list of the recently joined rooms.
|
||||
*
|
||||
*/
|
||||
class RecentList extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code RecentList} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._toDateString = this._toDateString.bind(this);
|
||||
this._toDurationString = this._toDurationString.bind(this);
|
||||
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
||||
this._toDisplayableList = this._toDisplayableList.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the React Components's render method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { disabled } = this.props;
|
||||
|
||||
return (
|
||||
<NavigateSectionList
|
||||
disabled = { disabled }
|
||||
onPress = { this._onPress }
|
||||
sections = { this._toDisplayableList() } />
|
||||
);
|
||||
}
|
||||
|
||||
_onPress: string => Function;
|
||||
|
||||
/**
|
||||
* Handles the list's navigate action.
|
||||
*
|
||||
* @private
|
||||
* @param {string} url - The url string to navigate to.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPress(url) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(appNavigate(url));
|
||||
}
|
||||
|
||||
_toDisplayableItem: Object => Object;
|
||||
|
||||
/**
|
||||
* Creates a displayable list item of a recent list entry.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item - The recent list entry.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_toDisplayableItem(item) {
|
||||
const { _defaultServerURL } = this.props;
|
||||
const location = parseURIString(item.conference);
|
||||
const baseURL = `${location.protocol}//${location.host}`;
|
||||
const serverName = baseURL === _defaultServerURL ? null : location.host;
|
||||
|
||||
return {
|
||||
colorBase: serverName,
|
||||
key: `key-${item.conference}-${item.date}`,
|
||||
lines: [
|
||||
this._toDateString(item.date),
|
||||
this._toDurationString(item.duration),
|
||||
serverName
|
||||
],
|
||||
title: location.room,
|
||||
url: item.conference
|
||||
};
|
||||
}
|
||||
|
||||
_toDisplayableList: () => Array<Object>;
|
||||
|
||||
/**
|
||||
* Transforms the history list to a displayable list
|
||||
* with sections.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_toDisplayableList() {
|
||||
const { _recentList, t } = this.props;
|
||||
const { createSection } = NavigateSectionList;
|
||||
const todaySection = createSection(t('recentList.today'), 'today');
|
||||
const yesterdaySection
|
||||
= createSection(t('recentList.yesterday'), 'yesterday');
|
||||
const earlierSection
|
||||
= 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;
|
||||
|
||||
/**
|
||||
* Generates a date string for the item.
|
||||
*
|
||||
* @private
|
||||
* @param {number} itemDate - The item's timestamp.
|
||||
* @returns {string}
|
||||
*/
|
||||
_toDateString(itemDate) {
|
||||
const date = new Date(itemDate);
|
||||
const m = getLocalizedDateFormatter(itemDate);
|
||||
|
||||
if (date.toDateString() === new Date().toDateString()) {
|
||||
// The date is today, we use fromNow format.
|
||||
return m.fromNow();
|
||||
}
|
||||
|
||||
return m.format('lll');
|
||||
}
|
||||
|
||||
_toDurationString: number => string;
|
||||
|
||||
/**
|
||||
* Generates a duration string for the item.
|
||||
*
|
||||
* @private
|
||||
* @param {number} duration - The item's duration.
|
||||
* @returns {string}
|
||||
*/
|
||||
_toDurationString(duration) {
|
||||
if (duration) {
|
||||
return getLocalizedDurationFormatter(duration).humanize();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* _defaultServerURL: string,
|
||||
* _recentList: Array
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_defaultServerURL: getDefaultURL(state),
|
||||
_recentList: state['features/recent-list']
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RecentList));
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Everything about recent list on web should be behind a feature flag and in
|
||||
* order to share code, this alias for the feature flag on mobile is always true
|
||||
* because we dont need a feature flag for recent list on mobile
|
||||
* @type {boolean}
|
||||
*/
|
||||
export const RECENT_LIST_ENABLED = true;
|
|
@ -0,0 +1,10 @@
|
|||
// @flow
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Everything about recent list on web should be behind a feature flag and in
|
||||
* order to share code, this alias for the feature flag on mobile is set to the
|
||||
* value defined in interface_config
|
||||
* @type {boolean}
|
||||
*/
|
||||
export const { RECENT_LIST_ENABLED } = interfaceConfig;
|
|
@ -0,0 +1,81 @@
|
|||
import { getLocalizedDateFormatter, getLocalizedDurationFormatter }
|
||||
from '../base/i18n/index';
|
||||
import { parseURIString } from '../base/util/index';
|
||||
|
||||
/**
|
||||
* Creates a displayable list item of a recent list entry.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item - The recent list entry.
|
||||
* @param {string} defaultServerURL - The default server URL.
|
||||
* @param {Function} t - The translate function.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function toDisplayableItem(item, defaultServerURL, t) {
|
||||
// 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: [
|
||||
_toDateString(item.date, t),
|
||||
_toDurationString(item.duration),
|
||||
serverName
|
||||
],
|
||||
title: location.room,
|
||||
url: item.conference
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a duration string for the item.
|
||||
*
|
||||
* @private
|
||||
* @param {number} duration - The item's duration.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function _toDurationString(duration) {
|
||||
if (duration) {
|
||||
return getLocalizedDurationFormatter(duration);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a date string for the item.
|
||||
*
|
||||
* @private
|
||||
* @param {number} itemDate - The item's timestamp.
|
||||
* @param {Function} t - The translate function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function _toDateString(itemDate, t) {
|
||||
const date = new Date(itemDate);
|
||||
const dateString = date.toDateString();
|
||||
const m = getLocalizedDateFormatter(itemDate);
|
||||
const yesterday = new Date();
|
||||
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayString = yesterday.toDateString();
|
||||
const today = new Date();
|
||||
const todayString = today.toDateString();
|
||||
const currentYear = today.getFullYear();
|
||||
const year = date.getFullYear();
|
||||
|
||||
if (dateString === todayString) {
|
||||
// The date is today, we use fromNow format.
|
||||
return m.fromNow();
|
||||
} else if (dateString === yesterdayString) {
|
||||
return t('dateUtils.yesterday');
|
||||
} else if (year !== currentYear) {
|
||||
// we only want to include the year in the date if its not the current
|
||||
// year
|
||||
return m.format('ddd, MMMM DD h:mm A, gggg');
|
||||
}
|
||||
|
||||
return m.format('ddd, MMMM DD h:mm A');
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { NavigateSectionList } from '../base/react/index';
|
||||
|
||||
import { toDisplayableItem } from './functions.all';
|
||||
|
||||
/**
|
||||
* Transforms the history list to a displayable list
|
||||
* with sections.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<Object>} recentList - The recent list form the redux store.
|
||||
* @param {Function} t - The translate function.
|
||||
* @param {string} defaultServerURL - The default server URL.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function toDisplayableList(recentList, t, defaultServerURL) {
|
||||
const { createSection } = NavigateSectionList;
|
||||
const todaySection = createSection(t('dateUtils.today'), 'today');
|
||||
const yesterdaySection
|
||||
= createSection(t('dateUtils.yesterday'), 'yesterday');
|
||||
const earlierSection
|
||||
= createSection(t('dateUtils.earlier'), 'earlier');
|
||||
const today = new Date();
|
||||
const todayString = today.toDateString();
|
||||
const yesterday = new Date();
|
||||
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayString = yesterday.toDateString();
|
||||
|
||||
for (const item of recentList) {
|
||||
const itemDateString = new Date(item.date).toDateString();
|
||||
const displayableItem = toDisplayableItem(item, defaultServerURL, t);
|
||||
|
||||
if (itemDateString === todayString) {
|
||||
todaySection.data.push(displayableItem);
|
||||
} else if (itemDateString === yesterdayString) {
|
||||
yesterdaySection.data.push(displayableItem);
|
||||
} else {
|
||||
earlierSection.data.push(displayableItem);
|
||||
}
|
||||
}
|
||||
const displayableList = [];
|
||||
|
||||
// the recent list in the redux store has the latest date in the last index
|
||||
// therefore all the sectionLists' data that was created by parsing through
|
||||
// the recent list is in reverse order and must be reversed for the most
|
||||
// item to show first
|
||||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { NavigateSectionList } from '../base/react/index';
|
||||
|
||||
import { toDisplayableItem } from './functions.all';
|
||||
|
||||
/**
|
||||
* Transforms the history list to a displayable list
|
||||
* with sections.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<Object>} recentList - The recent list form the redux store.
|
||||
* @param {Function} t - The translate function.
|
||||
* @param {string} defaultServerURL - The default server URL.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function toDisplayableList(recentList, t, defaultServerURL) {
|
||||
const { createSection } = NavigateSectionList;
|
||||
const section = createSection(t('recentList.joinPastMeeting'), 'all');
|
||||
|
||||
// we only want the last three conferences we were in for web
|
||||
for (const item of recentList.slice(1).slice(-3)) {
|
||||
const displayableItem = toDisplayableItem(item, defaultServerURL, t);
|
||||
|
||||
section.data.push(displayableItem);
|
||||
}
|
||||
const displayableList = [];
|
||||
|
||||
if (section.data.length) {
|
||||
section.data.reverse();
|
||||
displayableList.push(section);
|
||||
}
|
||||
|
||||
return displayableList;
|
||||
}
|
||||
|
|
@ -2,12 +2,20 @@
|
|||
|
||||
import { APP_WILL_MOUNT } from '../base/app';
|
||||
import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference';
|
||||
import { JITSI_CONFERENCE_URL_KEY } from '../base/conference/constants';
|
||||
import { addKnownDomains } from '../base/known-domains';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { parseURIString } from '../base/util';
|
||||
import { RECENT_LIST_ENABLED } from './featureFlag';
|
||||
|
||||
import { _storeCurrentConference, _updateConferenceDuration } from './actions';
|
||||
|
||||
/**
|
||||
* used in order to get the device because there is a different way to get the
|
||||
* location URL on web and on native
|
||||
*/
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Middleware that captures joined rooms so they can be saved into
|
||||
* {@code window.localStorage}.
|
||||
|
@ -16,6 +24,7 @@ import { _storeCurrentConference, _updateConferenceDuration } from './actions';
|
|||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
if (RECENT_LIST_ENABLED) {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(store, next, action);
|
||||
|
@ -26,6 +35,7 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
case SET_ROOM:
|
||||
return _setRoom(store, next, action);
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
@ -77,9 +87,26 @@ function _appWillMount({ dispatch, getState }, next, action) {
|
|||
* @returns {*} The result returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceWillLeave({ dispatch, getState }, next, action) {
|
||||
let locationURL;
|
||||
|
||||
/** FIXME
|
||||
* It is better to use action.conference[JITSI_CONFERENCE_URL_KEY]
|
||||
* in order to make sure we get the url the conference is leaving
|
||||
* from (i.e. the room we are leaving from) because if the order of events
|
||||
* is different, we cannot be guranteed that the location URL in base
|
||||
* connection is the url we are leaving from... not the one we are going to
|
||||
* (the latter happens on mobile -- if we use the web implementation);
|
||||
* however, the conference object on web does not have
|
||||
* JITSI_CONFERENCE_URL_KEY so we cannot call it and must use the other way
|
||||
*/
|
||||
if (typeof APP === 'undefined') {
|
||||
locationURL = action.conference[JITSI_CONFERENCE_URL_KEY];
|
||||
} else {
|
||||
locationURL = getState()['features/base/connection'].locationURL;
|
||||
}
|
||||
dispatch(
|
||||
_updateConferenceDuration(
|
||||
getState()['features/base/connection'].locationURL));
|
||||
locationURL));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// @flow
|
||||
|
||||
import { APP_WILL_MOUNT } from '../base/app';
|
||||
import { getURLWithoutParamsNormalized } from '../base/connection';
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
@ -9,6 +8,7 @@ import {
|
|||
_STORE_CURRENT_CONFERENCE,
|
||||
_UPDATE_CONFERENCE_DURATION
|
||||
} from './actionTypes';
|
||||
import { RECENT_LIST_ENABLED } from './featureFlag';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
|
@ -50,19 +50,21 @@ PersistenceRegistry.register(STORE_NAME);
|
|||
ReducerRegistry.register(
|
||||
STORE_NAME,
|
||||
(state = _getLegacyRecentRoomList(), action) => {
|
||||
if (RECENT_LIST_ENABLED) {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(state);
|
||||
|
||||
case _STORE_CURRENT_CONFERENCE:
|
||||
return _storeCurrentConference(state, action);
|
||||
|
||||
case _UPDATE_CONFERENCE_DURATION:
|
||||
return _updateConferenceDuration(state, action);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Watermarks } from '../../base/react';
|
||||
import { RecentList } from '../../recent-list';
|
||||
|
||||
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
||||
|
||||
|
@ -99,7 +100,7 @@ class WelcomePage extends AbstractWelcomePage {
|
|||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { APP_NAME } = interfaceConfig;
|
||||
const { APP_NAME, RECENT_LIST_ENABLED } = interfaceConfig;
|
||||
const showAdditionalContent = this._shouldShowAdditionalContent();
|
||||
|
||||
return (
|
||||
|
@ -146,6 +147,7 @@ class WelcomePage extends AbstractWelcomePage {
|
|||
{ t('welcomepage.go') }
|
||||
</Button>
|
||||
</div>
|
||||
{ RECENT_LIST_ENABLED ? <RecentList /> : null }
|
||||
</div>
|
||||
{ showAdditionalContent
|
||||
? <div
|
||||
|
|
Loading…
Reference in New Issue