ref(RecentList): Improvements after review.

This commit is contained in:
hristoterezov 2018-08-01 15:37:15 -05:00 committed by yanas
parent 046b06e436
commit fb75180632
25 changed files with 199 additions and 146 deletions

View File

@ -38,7 +38,6 @@ import {
conferenceWillLeave,
dataChannelOpened,
EMAIL_COMMAND,
getCurrentConference,
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
@ -307,10 +306,6 @@ 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: {
@ -378,10 +373,11 @@ class ConferenceConnector {
case JitsiConferenceErrors.FOCUS_LEFT:
case JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
APP.store.dispatch(conferenceWillLeave(room));
// 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;
@ -475,12 +471,7 @@ 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));
APP.store.dispatch(conferenceWillLeave(room));
room.leave();
}
}
@ -2478,12 +2469,6 @@ 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();
@ -2512,13 +2497,24 @@ export default {
// before all operations are done.
Promise.all([
requestFeedbackPromise,
room.leave().then(disconnect, disconnect)
this.leaveRoomAndDisconnect()
]).then(values => {
APP.API.notifyReadyToClose();
maybeRedirectToWelcomePage(values[0]);
});
},
/**
* Leaves the room and calls JitsiConnection.disconnect.
*
* @returns {Promise}
*/
leaveRoomAndDisconnect() {
APP.store.dispatch(conferenceWillLeave(room));
return room.leave().then(disconnect, disconnect);
},
/**
* Changes the email for the local user
* @param email {string} the new email

View File

@ -79,4 +79,4 @@
@import 'deep-linking/main';
@import 'transcription-subtitles';
@import 'navigate_section_list';
@import 'transcription-subtitles';
/* Modules END */

5
package-lock.json generated
View File

@ -10502,6 +10502,11 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.19.4.tgz",
"integrity": "sha512-1xFTAknSLfc47DIxHDUbnJWC+UwgWxATmymaxIPQpmMh7LBm7ZbwVEsuushqwL2GYZU0jie4xO+TK44hJPjNSQ=="
},
"moment-duration-format": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-2.2.2.tgz",
"integrity": "sha1-uVdhLeJgFsmtnrYIfAVFc+USd3k="
},
"morgan": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz",

View File

@ -24,12 +24,14 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
conferenceFailed,
conferenceLeft,
conferenceWillLeave,
createConference,
setLastN
} from './actions';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN,
@ -46,6 +48,11 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
declare var APP: Object;
/**
* Handler for before unload event.
*/
let beforeUnloadHandler;
/**
* Implements the middleware of the feature base/conference.
*
@ -66,6 +73,10 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_FAILED:
return _connectionFailed(store, next, action);
case CONFERENCE_WILL_LEAVE:
_conferenceWillLeave();
break;
case DATA_CHANNEL_OPENED:
return _syncReceiveVideoQuality(store, next, action);
@ -135,6 +146,11 @@ function _conferenceFailed(store, next, action) {
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
if (typeof APP !== 'undefined') {
if (typeof beforeUnloadHandler !== 'undefined') {
window.removeEventListener('beforeunload', beforeUnloadHandler);
beforeUnloadHandler = undefined;
}
return result;
}
@ -175,6 +191,16 @@ function _conferenceJoined({ dispatch, getState }, next, action) {
// and the LastN value needs to be synchronized here.
audioOnly && conference.getLastN() !== 0 && dispatch(setLastN(0));
// FIXME: Very dirty solution. This will work on web only.
// When the user closes the window or quits the browser, lib-jitsi-meet
// handles the process of leaving the conference. This is temporary solution
// that should cover the described use case as part of the effort to
// implement the conferenceWillLeave action for web.
beforeUnloadHandler = () => {
dispatch(conferenceWillLeave(conference));
};
window.addEventListener('beforeunload', beforeUnloadHandler);
return result;
}
@ -227,6 +253,11 @@ function _connectionFailed({ dispatch, getState }, next, action) {
const result = next(action);
if (typeof beforeUnloadHandler !== 'undefined') {
window.removeEventListener('beforeunload', beforeUnloadHandler);
beforeUnloadHandler = undefined;
}
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js and appropriate failure handlers
// are set there.
@ -266,6 +297,21 @@ function _connectionFailed({ dispatch, getState }, next, action) {
return result;
}
/**
* Notifies the feature base/conference that the action
* {@code CONFERENCE_WILL_LEAVE} is being dispatched within a specific redux
* store.
*
* @private
* @returns {void}
*/
function _conferenceWillLeave() {
if (typeof beforeUnloadHandler !== 'undefined') {
window.removeEventListener('beforeunload', beforeUnloadHandler);
beforeUnloadHandler = undefined;
}
}
/**
* Returns whether or not a CONNECTION_FAILED action is for a possible split
* brain error. A split brain error occurs when at least two users join a

View File

@ -114,6 +114,15 @@ function _visitNode(node, callback) {
global.addEventListener = () => {};
}
// removeEventListener
//
// Required by:
// - features/base/conference/middleware
if (typeof global.removeEventListener === 'undefined') {
// eslint-disable-next-line no-empty-function
global.removeEventListener = () => {};
}
// Array.prototype[@@iterator]
//
// Required by:

View File

@ -1,12 +1,10 @@
// @flow
/**
* item data for NavigateSectionList
*/
import type {
ComponentType,
Element
} from 'react';
import type { ComponentType, Element } from 'react';
/**
* Item data for <tt>NavigateSectionList</tt>.
*/
export type Item = {
/**

View File

@ -2,8 +2,8 @@
import React, { Component } from 'react';
import { translate } from '../../i18n';
// TODO: Maybe try to make all NavigateSectionList components to work for both
// mobile and web, and move them to NavigateSectionList component.
import {
NavigateSectionListEmptyComponent,
NavigateSectionListItem,
@ -19,11 +19,6 @@ type Props = {
*/
disabled: boolean,
/**
* The translate function.
*/
t: Function,
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
@ -59,7 +54,7 @@ class NavigateSectionList extends Component<Props> {
* @private
* @returns {Object}
*/
static createSection(title, key) {
static createSection(title: string, key: string) {
return {
data: [],
key,
@ -166,7 +161,7 @@ class NavigateSectionList extends Component<Props> {
* @private
* @returns {Component}
*/
_renderItem(listItem, key = '') {
_renderItem(listItem, key: string = '') {
const { item } = listItem;
const { url } = item;
@ -197,12 +192,11 @@ class NavigateSectionList extends Component<Props> {
* @returns {React$Node}
*/
_renderListEmptyComponent() {
const { t, onRefresh } = this.props;
const { onRefresh } = this.props;
if (typeof onRefresh === 'function') {
return (
<NavigateSectionListEmptyComponent
t = { t } />
<NavigateSectionListEmptyComponent />
);
}
@ -226,4 +220,4 @@ class NavigateSectionList extends Component<Props> {
}
}
export default translate(NavigateSectionList);
export default NavigateSectionList;

View File

@ -34,6 +34,7 @@ export default class Container extends AbstractContainer {
accessible,
onClick,
touchFeedback = onClick,
underlayColor,
visible = true,
...props
} = this.props;
@ -62,7 +63,8 @@ export default class Container extends AbstractContainer {
{
accessibilityLabel,
accessible,
onPress: onClick
onPress: onClick,
underlayColor
},
element);
}

View File

@ -1,13 +1,10 @@
// @flow
import React, { Component } from 'react';
import {
Text,
View
} from 'react-native';
import { Text, View } from 'react-native';
import { Icon } from '../../../font-icons/index';
import { translate } from '../../../i18n/index';
import { Icon } from '../../../font-icons';
import { translate } from '../../../i18n';
import styles from './styles';

View File

@ -1,12 +1,9 @@
// @flow
import React, { Component } from 'react';
import { TouchableHighlight } from 'react-native';
import {
Text,
Container
} from './index';
import Container from './Container';
import Text from './Text';
import styles, { UNDERLAY_COLOR } from './styles';
import type { Item } from '../../Types';
@ -110,36 +107,35 @@ export default class NavigateSectionListItem extends Component<Props> {
*/
render() {
const { colorBase, lines, title } = this.props.item;
const avatarStyles = {
...styles.avatar,
...this._getAvatarColor(colorBase)
};
return (
<TouchableHighlight
onPress = { this.props.onPress }
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
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}
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
<Text style = { styles.avatarContent }>
{title.substr(0, 1).toUpperCase()}
</Text>
{this._renderItemLines(lines)}
</Container>
</Container>
</TouchableHighlight>
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = {{
...styles.listItemText,
...styles.listItemTitle
}}>
{title}
</Text>
{this._renderItemLines(lines)}
</Container>
</Container>
);
}
}

View File

@ -2,8 +2,9 @@
import React, { Component } from 'react';
import { Text, Container } from './index';
import Container from './Container';
import styles from './styles';
import Text from './Text';
import type { SetionListSection } from '../../Types';
type Props = {

View File

@ -2,7 +2,8 @@
import React, { Component } from 'react';
import { Text, Container } from './index';
import Container from './Container';
import Text from './Text';
import type { Item } from '../../Types';
type Props = {

View File

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import { Text } from './index';
import Text from './Text';
import type { Section } from '../../Types';
type Props = {

View File

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import { Container } from './index';
import Container from './Container';
import type { Section } from '../../Types';
type Props = {

View File

@ -1,2 +1,3 @@
export * from './components';
export { default as Platform } from './Platform';
export * from './Types';

View File

@ -20,6 +20,15 @@ const throttledPersistState
state => PersistenceRegistry.persistState(state),
PERSIST_STATE_DELAY);
// Web only code.
// We need the <tt>if</tt> beacuse it appears that on mobile the polyfill is not
// executed yet.
if (typeof window.addEventListener === 'function') {
window.addEventListener('unload', () => {
throttledPersistState.flush();
});
}
/**
* A master MiddleWare to selectively persist state. Please use the
* {@link persisterconfig.json} to set which subtrees of the redux state should
@ -37,8 +46,3 @@ MiddlewareRegistry.register(store => next => action => {
return result;
});
window.addEventListener('beforeunload', () => {
// Stop the LogCollector
throttledPersistState.flush();
});

View File

@ -4,10 +4,10 @@ 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 type { Section } from '../../base/react';
import { toDisplayableList } from '../functions';
import { isRecentListEnabled, toDisplayableList } from '../functions';
/**
* The type of the React {@code Component} props of {@link RecentList}
@ -62,6 +62,9 @@ class RecentList extends Component<Props> {
* @inheritdoc
*/
render() {
if (!isRecentListEnabled()) {
return null;
}
const { disabled, t, _defaultServerURL, _recentList } = this.props;
const recentList = toDisplayableList(_recentList, t, _defaultServerURL);

View File

@ -1,7 +0,0 @@
/**
* 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

@ -1,10 +0,0 @@
// @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

@ -1,6 +1,8 @@
import { getLocalizedDateFormatter, getLocalizedDurationFormatter }
from '../base/i18n/index';
import { parseURIString } from '../base/util/index';
import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter
} from '../base/i18n';
import { parseURIString } from '../base/util';
/**
* Creates a displayable list item of a recent list entry.
@ -12,7 +14,6 @@ import { parseURIString } from '../base/util/index';
* @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;
@ -54,26 +55,20 @@ export function _toDurationString(duration) {
* @returns {string}
*/
export function _toDateString(itemDate, t) {
const date = new Date(itemDate);
const dateString = date.toDateString();
const m = getLocalizedDateFormatter(itemDate);
const yesterday = new Date();
const date = new Date(itemDate);
const dateInMs = date.getTime();
const now = new Date();
const todayInMs = (new Date()).setHours(0, 0, 0, 0);
const yesterdayInMs = todayInMs - 86400000; // 1 day = 86400000ms
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.
if (dateInMs >= todayInMs) {
return m.fromNow();
} else if (dateString === yesterdayString) {
} else if (dateInMs >= yesterdayInMs) {
return t('dateUtils.yesterday');
} else if (year !== currentYear) {
// we only want to include the year in the date if its not the current
// year
} else if (date.getFullYear() !== now.getFullYear()) {
// 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');
}

View File

@ -1,6 +1,6 @@
import { NavigateSectionList } from '../base/react/index';
import { NavigateSectionList } from '../base/react';
import { toDisplayableItem } from './functions.all';
import { toDisplayableItem } from './functions.any';
/**
* Transforms the history list to a displayable list
@ -60,3 +60,12 @@ export function toDisplayableList(recentList, t, defaultServerURL) {
return displayableList;
}
/**
* Returns <tt>true</tt> if recent list is enabled and <tt>false</tt> otherwise.
*
* @returns {boolean} <tt>true</tt> if recent list is enabled and <tt>false</tt>
* otherwise.
*/
export function isRecentListEnabled() {
return true;
}

View File

@ -1,6 +1,9 @@
import { NavigateSectionList } from '../base/react/index';
/* global interfaceConfig */
import { NavigateSectionList } from '../base/react';
import { toDisplayableItem } from './functions.any';
import { toDisplayableItem } from './functions.all';
/**
* Transforms the history list to a displayable list
@ -14,10 +17,11 @@ import { toDisplayableItem } from './functions.all';
*/
export function toDisplayableList(recentList, t, defaultServerURL) {
const { createSection } = NavigateSectionList;
const section = createSection(t('recentList.joinPastMeeting'), 'all');
const section
= createSection(t('recentList.joinPastMeeting'), 'joinPastMeeting');
// we only want the last three conferences we were in for web
for (const item of recentList.slice(1).slice(-3)) {
// We only want the last three conferences we were in for web.
for (const item of recentList.slice(-3)) {
const displayableItem = toDisplayableItem(item, defaultServerURL, t);
section.data.push(displayableItem);
@ -32,3 +36,12 @@ export function toDisplayableList(recentList, t, defaultServerURL) {
return displayableList;
}
/**
* Returns <tt>true</tt> if recent list is enabled and <tt>false</tt> otherwise.
*
* @returns {boolean} <tt>true</tt> if recent list is enabled and <tt>false</tt>
* otherwise.
*/
export function isRecentListEnabled() {
return interfaceConfig.RECENT_LIST_ENABLED;
}

View File

@ -1,19 +1,18 @@
// @flow
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 {
CONFERENCE_WILL_LEAVE,
SET_ROOM,
JITSI_CONFERENCE_URL_KEY
} from '../base/conference';
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';
import { isRecentListEnabled } from './functions';
/**
* 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;
/**
@ -24,7 +23,7 @@ declare var APP: Object;
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
if (RECENT_LIST_ENABLED) {
if (isRecentListEnabled()) {
switch (action.type) {
case APP_WILL_MOUNT:
return _appWillMount(store, next, action);
@ -89,7 +88,8 @@ function _appWillMount({ dispatch, getState }, next, action) {
function _conferenceWillLeave({ dispatch, getState }, next, action) {
let locationURL;
/** FIXME
/**
* 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

View File

@ -8,7 +8,7 @@ import {
_STORE_CURRENT_CONFERENCE,
_UPDATE_CONFERENCE_DURATION
} from './actionTypes';
import { RECENT_LIST_ENABLED } from './featureFlag';
import { isRecentListEnabled } from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@ -50,7 +50,7 @@ PersistenceRegistry.register(STORE_NAME);
ReducerRegistry.register(
STORE_NAME,
(state = _getLegacyRecentRoomList(), action) => {
if (RECENT_LIST_ENABLED) {
if (isRecentListEnabled()) {
switch (action.type) {
case APP_WILL_MOUNT:
return _appWillMount(state);
@ -62,9 +62,9 @@ ReducerRegistry.register(
default:
return state;
}
} else {
return state;
}
return state;
});
/**

View File

@ -100,7 +100,7 @@ class WelcomePage extends AbstractWelcomePage {
*/
render() {
const { t } = this.props;
const { APP_NAME, RECENT_LIST_ENABLED } = interfaceConfig;
const { APP_NAME } = interfaceConfig;
const showAdditionalContent = this._shouldShowAdditionalContent();
return (
@ -147,7 +147,7 @@ class WelcomePage extends AbstractWelcomePage {
{ t('welcomepage.go') }
</Button>
</div>
{ RECENT_LIST_ENABLED ? <RecentList /> : null }
<RecentList />
</div>
{ showAdditionalContent
? <div