added recent list
This commit is contained in:
parent
af7c69a1aa
commit
046b06e436
|
@ -35,8 +35,10 @@ import {
|
||||||
conferenceJoined,
|
conferenceJoined,
|
||||||
conferenceLeft,
|
conferenceLeft,
|
||||||
conferenceWillJoin,
|
conferenceWillJoin,
|
||||||
|
conferenceWillLeave,
|
||||||
dataChannelOpened,
|
dataChannelOpened,
|
||||||
EMAIL_COMMAND,
|
EMAIL_COMMAND,
|
||||||
|
getCurrentConference,
|
||||||
lockStateChanged,
|
lockStateChanged,
|
||||||
onStartMutedPolicyChanged,
|
onStartMutedPolicyChanged,
|
||||||
p2pStatusChanged,
|
p2pStatusChanged,
|
||||||
|
@ -305,6 +307,10 @@ class ConferenceConnector {
|
||||||
_onConferenceFailed(err, ...params) {
|
_onConferenceFailed(err, ...params) {
|
||||||
APP.store.dispatch(conferenceFailed(room, err, ...params));
|
APP.store.dispatch(conferenceFailed(room, err, ...params));
|
||||||
logger.error('CONFERENCE FAILED:', 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) {
|
switch (err) {
|
||||||
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
case JitsiConferenceErrors.CONNECTION_ERROR: {
|
||||||
|
@ -375,6 +381,7 @@ class ConferenceConnector {
|
||||||
// FIXME the conference should be stopped by the library and not by
|
// FIXME the conference should be stopped by the library and not by
|
||||||
// the app. Both the errors above are unrecoverable from the library
|
// the app. Both the errors above are unrecoverable from the library
|
||||||
// perspective.
|
// perspective.
|
||||||
|
APP.store.dispatch(conferenceWillLeave(conference));
|
||||||
room.leave().then(() => connection.disconnect());
|
room.leave().then(() => connection.disconnect());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -468,6 +475,12 @@ function _connectionFailedHandler(error) {
|
||||||
JitsiConnectionEvents.CONNECTION_FAILED,
|
JitsiConnectionEvents.CONNECTION_FAILED,
|
||||||
_connectionFailedHandler);
|
_connectionFailedHandler);
|
||||||
if (room) {
|
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();
|
room.leave();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2465,6 +2478,12 @@ export default {
|
||||||
* requested
|
* requested
|
||||||
*/
|
*/
|
||||||
hangup(requestFeedback = false) {
|
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);
|
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
|
||||||
APP.UI.removeLocalMedia();
|
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 'modals/invite/add-people';
|
||||||
@import 'deep-linking/main';
|
@import 'deep-linking/main';
|
||||||
@import 'transcription-subtitles';
|
@import 'transcription-subtitles';
|
||||||
/* Modules END */
|
@import 'navigate_section_list';
|
||||||
|
@import 'transcription-subtitles';
|
||||||
|
|
|
@ -163,7 +163,14 @@ var interfaceConfig = {
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @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.
|
* 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."
|
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
|
||||||
},
|
},
|
||||||
"recentList": {
|
"recentList": {
|
||||||
"today": "Today",
|
"joinPastMeeting": "Join A Past Meeting"
|
||||||
"yesterday": "Yesterday",
|
|
||||||
"earlier": "Earlier"
|
|
||||||
},
|
},
|
||||||
"sectionList": {
|
"sectionList": {
|
||||||
"pullToRefresh": "Pull to refresh"
|
"pullToRefresh": "Pull to refresh"
|
||||||
|
@ -656,6 +654,11 @@
|
||||||
"ignored": "Ignored",
|
"ignored": "Ignored",
|
||||||
"expired": "Expired"
|
"expired": "Expired"
|
||||||
},
|
},
|
||||||
|
"dateUtils": {
|
||||||
|
"today": "Today",
|
||||||
|
"yesterday": "Yesterday",
|
||||||
|
"earlier": "Earlier"
|
||||||
|
},
|
||||||
"incomingCall": {
|
"incomingCall": {
|
||||||
"answer": "Answer",
|
"answer": "Answer",
|
||||||
"audioCallTitle": "Incoming call",
|
"audioCallTitle": "Incoming call",
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
||||||
"lodash": "4.17.4",
|
"lodash": "4.17.4",
|
||||||
"moment": "2.19.4",
|
"moment": "2.19.4",
|
||||||
|
"moment-duration-format": "2.2.2",
|
||||||
"postis": "2.2.0",
|
"postis": "2.2.0",
|
||||||
"prop-types": "15.6.0",
|
"prop-types": "15.6.0",
|
||||||
"react": "16.3.1",
|
"react": "16.3.1",
|
||||||
|
|
|
@ -4,6 +4,9 @@ import moment from 'moment';
|
||||||
|
|
||||||
import i18next from './i18next';
|
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
|
// 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
|
// language selection in the app we need to load all bundles that we support in
|
||||||
// the app.
|
// the app.
|
||||||
|
@ -55,8 +58,19 @@ export function getLocalizedDurationFormatter(duration: number) {
|
||||||
// states v2.19 so maybe locale on moment's duration was introduced in
|
// states v2.19 so maybe locale on moment's duration was introduced in
|
||||||
// between?
|
// 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
|
// $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 * 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 Container } from './Container';
|
||||||
export { default as Header } from './Header';
|
export { default as Header } from './Header';
|
||||||
export { default as NavigateSectionList } from './NavigateSectionList';
|
|
||||||
export { default as Link } from './Link';
|
export { default as Link } from './Link';
|
||||||
export { default as LoadingIndicator } from './LoadingIndicator';
|
export { default as LoadingIndicator } from './LoadingIndicator';
|
||||||
|
export { default as NavigateSectionListEmptyComponent } from
|
||||||
|
'./NavigateSectionListEmptyComponent';
|
||||||
|
export { default as NavigateSectionListItem }
|
||||||
|
from './NavigateSectionListItem';
|
||||||
|
export { default as NavigateSectionListSectionHeader }
|
||||||
|
from './NavigateSectionListSectionHeader';
|
||||||
export { default as PagedList } from './PagedList';
|
export { default as PagedList } from './PagedList';
|
||||||
export { default as Pressable } from './Pressable';
|
export { default as Pressable } from './Pressable';
|
||||||
export { default as SideBar } from './SideBar';
|
export { default as SideBar } from './SideBar';
|
||||||
export { default as Text } from './Text';
|
export { default as Text } from './Text';
|
||||||
|
export { default as SectionList } from './SectionList';
|
||||||
export { default as TintedView } from './TintedView';
|
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 Container } from './Container';
|
||||||
export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';
|
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 Text } from './Text';
|
||||||
export { default as Watermarks } from './Watermarks';
|
export { default as Watermarks } from './Watermarks';
|
||||||
|
|
|
@ -37,3 +37,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
|
|
||||||
return result;
|
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 { APP_WILL_MOUNT } from '../base/app';
|
||||||
import { CONFERENCE_WILL_LEAVE, SET_ROOM } from '../base/conference';
|
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 { addKnownDomains } from '../base/known-domains';
|
||||||
import { MiddlewareRegistry } from '../base/redux';
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
import { parseURIString } from '../base/util';
|
import { parseURIString } from '../base/util';
|
||||||
|
import { RECENT_LIST_ENABLED } from './featureFlag';
|
||||||
|
|
||||||
import { _storeCurrentConference, _updateConferenceDuration } from './actions';
|
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
|
* Middleware that captures joined rooms so they can be saved into
|
||||||
* {@code window.localStorage}.
|
* {@code window.localStorage}.
|
||||||
|
@ -16,15 +24,17 @@ import { _storeCurrentConference, _updateConferenceDuration } from './actions';
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
MiddlewareRegistry.register(store => next => action => {
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
switch (action.type) {
|
if (RECENT_LIST_ENABLED) {
|
||||||
case APP_WILL_MOUNT:
|
switch (action.type) {
|
||||||
return _appWillMount(store, next, action);
|
case APP_WILL_MOUNT:
|
||||||
|
return _appWillMount(store, next, action);
|
||||||
|
|
||||||
case CONFERENCE_WILL_LEAVE:
|
case CONFERENCE_WILL_LEAVE:
|
||||||
return _conferenceWillLeave(store, next, action);
|
return _conferenceWillLeave(store, next, action);
|
||||||
|
|
||||||
case SET_ROOM:
|
case SET_ROOM:
|
||||||
return _setRoom(store, next, action);
|
return _setRoom(store, next, action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
@ -77,9 +87,26 @@ function _appWillMount({ dispatch, getState }, next, action) {
|
||||||
* @returns {*} The result returned by {@code next(action)}.
|
* @returns {*} The result returned by {@code next(action)}.
|
||||||
*/
|
*/
|
||||||
function _conferenceWillLeave({ dispatch, getState }, 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(
|
dispatch(
|
||||||
_updateConferenceDuration(
|
_updateConferenceDuration(
|
||||||
getState()['features/base/connection'].locationURL));
|
locationURL));
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { APP_WILL_MOUNT } from '../base/app';
|
import { APP_WILL_MOUNT } from '../base/app';
|
||||||
import { getURLWithoutParamsNormalized } from '../base/connection';
|
import { getURLWithoutParamsNormalized } from '../base/connection';
|
||||||
import { ReducerRegistry } from '../base/redux';
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
@ -9,6 +8,7 @@ import {
|
||||||
_STORE_CURRENT_CONFERENCE,
|
_STORE_CURRENT_CONFERENCE,
|
||||||
_UPDATE_CONFERENCE_DURATION
|
_UPDATE_CONFERENCE_DURATION
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
import { RECENT_LIST_ENABLED } from './featureFlag';
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
|
@ -50,17 +50,19 @@ PersistenceRegistry.register(STORE_NAME);
|
||||||
ReducerRegistry.register(
|
ReducerRegistry.register(
|
||||||
STORE_NAME,
|
STORE_NAME,
|
||||||
(state = _getLegacyRecentRoomList(), action) => {
|
(state = _getLegacyRecentRoomList(), action) => {
|
||||||
switch (action.type) {
|
if (RECENT_LIST_ENABLED) {
|
||||||
case APP_WILL_MOUNT:
|
switch (action.type) {
|
||||||
return _appWillMount(state);
|
case APP_WILL_MOUNT:
|
||||||
|
return _appWillMount(state);
|
||||||
|
case _STORE_CURRENT_CONFERENCE:
|
||||||
|
return _storeCurrentConference(state, action);
|
||||||
|
|
||||||
case _STORE_CURRENT_CONFERENCE:
|
case _UPDATE_CONFERENCE_DURATION:
|
||||||
return _storeCurrentConference(state, action);
|
return _updateConferenceDuration(state, action);
|
||||||
|
default:
|
||||||
case _UPDATE_CONFERENCE_DURATION:
|
return state;
|
||||||
return _updateConferenceDuration(state, action);
|
}
|
||||||
|
} else {
|
||||||
default:
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { Watermarks } from '../../base/react';
|
import { Watermarks } from '../../base/react';
|
||||||
|
import { RecentList } from '../../recent-list';
|
||||||
|
|
||||||
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
||||||
|
|
||||||
|
@ -99,7 +100,7 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const { APP_NAME } = interfaceConfig;
|
const { APP_NAME, RECENT_LIST_ENABLED } = interfaceConfig;
|
||||||
const showAdditionalContent = this._shouldShowAdditionalContent();
|
const showAdditionalContent = this._shouldShowAdditionalContent();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -146,6 +147,7 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
{ t('welcomepage.go') }
|
{ t('welcomepage.go') }
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{ RECENT_LIST_ENABLED ? <RecentList /> : null }
|
||||||
</div>
|
</div>
|
||||||
{ showAdditionalContent
|
{ showAdditionalContent
|
||||||
? <div
|
? <div
|
||||||
|
|
Loading…
Reference in New Issue