added recent list

This commit is contained in:
Ritwik Heda 2018-08-01 11:41:54 -05:00 committed by yanas
parent af7c69a1aa
commit 046b06e436
32 changed files with 1301 additions and 607 deletions

View File

@ -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();

View File

@ -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;
}

View File

@ -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';

View File

@ -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.

View File

@ -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",

View File

@ -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",

View File

@ -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 });
}
/**

View File

@ -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
}

View File

@ -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);

View File

@ -1 +1,2 @@
export * from './_';
export { default as NavigateSectionList } from './NavigateSectionList';

View File

@ -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);

View File

@ -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);

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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';

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -37,3 +37,8 @@ MiddlewareRegistry.register(store => next => action => {
return result;
});
window.addEventListener('beforeunload', () => {
// Stop the LogCollector
throttledPersistState.flush();
});

View File

@ -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));

View File

@ -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));

View File

@ -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;

View File

@ -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;

View File

@ -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');
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}
});
/**

View File

@ -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