From 046b06e436bc4e7835415f033f3f32883df0e403 Mon Sep 17 00:00:00 2001 From: Ritwik Heda Date: Wed, 1 Aug 2018 11:41:54 -0500 Subject: [PATCH] added recent list --- conference.js | 19 + css/_navigate_section_list.scss | 43 +++ css/main.scss | 3 +- interface_config.js | 9 +- lang/main.json | 9 +- package.json | 1 + react/features/base/i18n/dateUtil.js | 16 +- react/features/base/react/Types.js | 77 ++++ .../react/components/NavigateSectionList.js | 229 ++++++++++++ react/features/base/react/components/index.js | 1 + .../components/native/NavigateSectionList.js | 346 ------------------ .../NavigateSectionListEmptyComponent.js | 51 +++ .../native/NavigateSectionListItem.js | 145 ++++++++ .../NavigateSectionListSectionHeader.js | 40 ++ .../react/components/native/SectionList.js | 91 +++++ .../base/react/components/native/index.js | 8 +- .../web/NavigateSectionListEmptyComponent.js | 0 .../components/web/NavigateSectionListItem.js | 73 ++++ .../web/NavigateSectionListSectionHeader.js | 35 ++ .../base/react/components/web/SectionList.js | 92 +++++ .../base/react/components/web/index.js | 7 + react/features/base/storage/middleware.js | 5 + .../recent-list/components/RecentList.js | 109 ++++++ .../components/RecentList.native.js | 234 ------------ .../recent-list/featureFlag.native.js | 7 + react/features/recent-list/featureFlag.web.js | 10 + react/features/recent-list/functions.all.js | 81 ++++ .../features/recent-list/functions.native.js | 62 ++++ react/features/recent-list/functions.web.js | 34 ++ react/features/recent-list/middleware.js | 43 ++- react/features/recent-list/reducer.js | 24 +- .../welcome/components/WelcomePage.web.js | 4 +- 32 files changed, 1301 insertions(+), 607 deletions(-) create mode 100644 css/_navigate_section_list.scss create mode 100644 react/features/base/react/Types.js create mode 100644 react/features/base/react/components/NavigateSectionList.js delete mode 100644 react/features/base/react/components/native/NavigateSectionList.js create mode 100644 react/features/base/react/components/native/NavigateSectionListEmptyComponent.js create mode 100644 react/features/base/react/components/native/NavigateSectionListItem.js create mode 100644 react/features/base/react/components/native/NavigateSectionListSectionHeader.js create mode 100644 react/features/base/react/components/native/SectionList.js create mode 100644 react/features/base/react/components/web/NavigateSectionListEmptyComponent.js create mode 100644 react/features/base/react/components/web/NavigateSectionListItem.js create mode 100644 react/features/base/react/components/web/NavigateSectionListSectionHeader.js create mode 100644 react/features/base/react/components/web/SectionList.js create mode 100644 react/features/recent-list/components/RecentList.js delete mode 100644 react/features/recent-list/components/RecentList.native.js create mode 100644 react/features/recent-list/featureFlag.native.js create mode 100644 react/features/recent-list/featureFlag.web.js create mode 100644 react/features/recent-list/functions.all.js create mode 100644 react/features/recent-list/functions.native.js create mode 100644 react/features/recent-list/functions.web.js diff --git a/conference.js b/conference.js index abf9223fe..69e6e4f4a 100644 --- a/conference.js +++ b/conference.js @@ -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(); diff --git a/css/_navigate_section_list.scss b/css/_navigate_section_list.scss new file mode 100644 index 000000000..94092cc11 --- /dev/null +++ b/css/_navigate_section_list.scss @@ -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; +} diff --git a/css/main.scss b/css/main.scss index cfd1988b0..0732309f2 100644 --- a/css/main.scss +++ b/css/main.scss @@ -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'; diff --git a/interface_config.js b/interface_config.js index a7617a4b7..b9d17b1c5 100644 --- a/interface_config.js +++ b/interface_config.js @@ -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. diff --git a/lang/main.json b/lang/main.json index 60d2f4ff4..d4c5a3346 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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", diff --git a/package.json b/package.json index 8d32b8eb9..2f625232f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/react/features/base/i18n/dateUtil.js b/react/features/base/i18n/dateUtil.js index 8964ff568..8fa0ced91 100644 --- a/react/features/base/i18n/dateUtil.js +++ b/react/features/base/i18n/dateUtil.js @@ -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 }); } /** diff --git a/react/features/base/react/Types.js b/react/features/base/react/Types.js new file mode 100644 index 000000000..99338a2d9 --- /dev/null +++ b/react/features/base/react/Types.js @@ -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 +} + +/** + * 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, + + /** + * Optional properties added only to fix some flow errors thrown by React + * SectionList + */ + ItemSeparatorComponent?: ?ComponentType, + + keyExtractor?: (item: Object) => string, + + renderItem?: ?(info: Object) => ?Element + +} + +/** + * 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 +} diff --git a/react/features/base/react/components/NavigateSectionList.js b/react/features/base/react/components/NavigateSectionList.js new file mode 100644 index 000000000..5910f0290 --- /dev/null +++ b/react/features/base/react/components/NavigateSectionList.js @@ -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
+}; + +/** + * 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 { + /** + * 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 ( + + ); + } + + _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 ( + + ); + } + + _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 ( + + ); + } + + return null; + } + + _renderSectionHeader: Object => Object; + + /** + * Renders a section header. + * + * @param {Object} section - The section being rendered. + * @private + * @returns {React$Node} + */ + _renderSectionHeader(section) { + return ( + + ); + } +} + +export default translate(NavigateSectionList); diff --git a/react/features/base/react/components/index.js b/react/features/base/react/components/index.js index cda61441e..bf512d974 100644 --- a/react/features/base/react/components/index.js +++ b/react/features/base/react/components/index.js @@ -1 +1,2 @@ export * from './_'; +export { default as NavigateSectionList } from './NavigateSectionList'; diff --git a/react/features/base/react/components/native/NavigateSectionList.js b/react/features/base/react/components/native/NavigateSectionList.js deleted file mode 100644 index 96a5967ff..000000000 --- a/react/features/base/react/components/native/NavigateSectionList.js +++ /dev/null @@ -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 <- additional lines to be rendered - * } - * ] - * } - * ] - */ - sections: Array -}; - -/** - * 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 { - /** - * 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 ( - - - - ); - } - - _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 ( - - - - - - { title.substr(0, 1).toUpperCase() } - - - - - - { title } - - { this._renderItemLines(lines) } - - - - ); - } - - _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 ( - - { line } - - ); - } - - _renderItemLines: Array => Array; - - /** - * Renders the additional item lines, if any. - * - * @param {Array} lines - The lines to render. - * @private - * @returns {Array} - */ - _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 ( - - - { t('sectionList.pullToRefresh') } - - - - ); - } - - return null; - } - - _renderSection: Object => Object; - - /** - * Renders a section title. - * - * @param {Object} section - The section being rendered. - * @private - * @returns {React$Node} - */ - _renderSection(section) { - return ( - - - { section.section.title } - - - ); - } -} - -export default translate(NavigateSectionList); diff --git a/react/features/base/react/components/native/NavigateSectionListEmptyComponent.js b/react/features/base/react/components/native/NavigateSectionListEmptyComponent.js new file mode 100644 index 000000000..70159d4c1 --- /dev/null +++ b/react/features/base/react/components/native/NavigateSectionListEmptyComponent.js @@ -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 { + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { t } = this.props; + + return ( + + + { t('sectionList.pullToRefresh') } + + + + ); + } +} + +export default translate(NavigateSectionListEmptyComponent); diff --git a/react/features/base/react/components/native/NavigateSectionListItem.js b/react/features/base/react/components/native/NavigateSectionListItem.js new file mode 100644 index 000000000..a9fb9ff23 --- /dev/null +++ b/react/features/base/react/components/native/NavigateSectionListItem.js @@ -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 { + /** + * 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 ( + + {line} + + ); + } + + _renderItemLines: Array => Array; + + /** + * Renders the additional item lines, if any. + * + * @param {Array} lines - The lines to render. + * @private + * @returns {Array} + */ + _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 ( + + + + + + {title.substr(0, 1).toUpperCase()} + + + + + + {title} + + {this._renderItemLines(lines)} + + + + ); + } +} diff --git a/react/features/base/react/components/native/NavigateSectionListSectionHeader.js b/react/features/base/react/components/native/NavigateSectionListSectionHeader.js new file mode 100644 index 000000000..dbadf7e8d --- /dev/null +++ b/react/features/base/react/components/native/NavigateSectionListSectionHeader.js @@ -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 { + /** + * Renders the content of this component. + * + * @returns {ReactElement} + */ + render() { + const { section } = this.props.section; + + return ( + + + { section.title } + + + ); + } +} diff --git a/react/features/base/react/components/native/SectionList.js b/react/features/base/react/components/native/SectionList.js new file mode 100644 index 000000000..028c2d1a5 --- /dev/null +++ b/react/features/base/react/components/native/SectionList.js @@ -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
+}; + +/** + * 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 { + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + return ( + + + + ); + } +} diff --git a/react/features/base/react/components/native/index.js b/react/features/base/react/components/native/index.js index 3041a8073..d0df15f25 100644 --- a/react/features/base/react/components/native/index.js +++ b/react/features/base/react/components/native/index.js @@ -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'; diff --git a/react/features/base/react/components/web/NavigateSectionListEmptyComponent.js b/react/features/base/react/components/web/NavigateSectionListEmptyComponent.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/base/react/components/web/NavigateSectionListItem.js b/react/features/base/react/components/web/NavigateSectionListItem.js new file mode 100644 index 000000000..d66d82251 --- /dev/null +++ b/react/features/base/react/components/web/NavigateSectionListItem.js @@ -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 { + /** + * 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 ( + + + { title } + + + { date } + + + { duration } + + + ); + } +} diff --git a/react/features/base/react/components/web/NavigateSectionListSectionHeader.js b/react/features/base/react/components/web/NavigateSectionListSectionHeader.js new file mode 100644 index 000000000..65f9a9b9a --- /dev/null +++ b/react/features/base/react/components/web/NavigateSectionListSectionHeader.js @@ -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 { + /** + * Renders the content of this component. + * + * @returns {ReactElement} + */ + render() { + return ( + + { this.props.section.title } + + ); + } +} diff --git a/react/features/base/react/components/web/SectionList.js b/react/features/base/react/components/web/SectionList.js new file mode 100644 index 000000000..625489b33 --- /dev/null +++ b/react/features/base/react/components/web/SectionList.js @@ -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
, + + /** + * 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 { + /** + * 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 */ + + { + sections.map((section, sectionIndex) => ( + + { renderSectionHeader(section) } + { section.data + .map((item, listIndex) => { + const listItem = { + item + }; + + return renderItem(listItem, + keyExtractor(section, + listIndex)); + }) } + + ) + ) + } + + /* eslint-enable no-extra-parens */ + ); + } + + return null; + } +} diff --git a/react/features/base/react/components/web/index.js b/react/features/base/react/components/web/index.js index 21916624a..a6ab98d66 100644 --- a/react/features/base/react/components/web/index.js +++ b/react/features/base/react/components/web/index.js @@ -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'; diff --git a/react/features/base/storage/middleware.js b/react/features/base/storage/middleware.js index 41b0ed536..e9b99360d 100644 --- a/react/features/base/storage/middleware.js +++ b/react/features/base/storage/middleware.js @@ -37,3 +37,8 @@ MiddlewareRegistry.register(store => next => action => { return result; }); + +window.addEventListener('beforeunload', () => { + // Stop the LogCollector + throttledPersistState.flush(); +}); diff --git a/react/features/recent-list/components/RecentList.js b/react/features/recent-list/components/RecentList.js new file mode 100644 index 000000000..e233d7fd9 --- /dev/null +++ b/react/features/recent-list/components/RecentList.js @@ -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
+}; + +/** + * The cross platform container rendering the list of the recently joined rooms. + * + */ +class RecentList extends Component { + /** + * 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 ( + + ); + } + + _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)); diff --git a/react/features/recent-list/components/RecentList.native.js b/react/features/recent-list/components/RecentList.native.js deleted file mode 100644 index 590ce95d0..000000000 --- a/react/features/recent-list/components/RecentList.native.js +++ /dev/null @@ -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 -}; - -/** - * The native container rendering the list of the recently joined rooms. - * - */ -class RecentList extends Component { - /** - * 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 ( - - ); - } - - _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; - - /** - * Transforms the history list to a displayable list - * with sections. - * - * @private - * @returns {Array} - */ - _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)); diff --git a/react/features/recent-list/featureFlag.native.js b/react/features/recent-list/featureFlag.native.js new file mode 100644 index 000000000..6667c1234 --- /dev/null +++ b/react/features/recent-list/featureFlag.native.js @@ -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; diff --git a/react/features/recent-list/featureFlag.web.js b/react/features/recent-list/featureFlag.web.js new file mode 100644 index 000000000..1cc9a9c74 --- /dev/null +++ b/react/features/recent-list/featureFlag.web.js @@ -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; diff --git a/react/features/recent-list/functions.all.js b/react/features/recent-list/functions.all.js new file mode 100644 index 000000000..8e256c80a --- /dev/null +++ b/react/features/recent-list/functions.all.js @@ -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'); +} diff --git a/react/features/recent-list/functions.native.js b/react/features/recent-list/functions.native.js new file mode 100644 index 000000000..386b8dd13 --- /dev/null +++ b/react/features/recent-list/functions.native.js @@ -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} recentList - The recent list form the redux store. + * @param {Function} t - The translate function. + * @param {string} defaultServerURL - The default server URL. + * @returns {Array} + */ +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; +} + diff --git a/react/features/recent-list/functions.web.js b/react/features/recent-list/functions.web.js new file mode 100644 index 000000000..bd1c9e710 --- /dev/null +++ b/react/features/recent-list/functions.web.js @@ -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} recentList - The recent list form the redux store. + * @param {Function} t - The translate function. + * @param {string} defaultServerURL - The default server URL. + * @returns {Array} + */ +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; +} + diff --git a/react/features/recent-list/middleware.js b/react/features/recent-list/middleware.js index 81dfb1e09..f092a80c5 100644 --- a/react/features/recent-list/middleware.js +++ b/react/features/recent-list/middleware.js @@ -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,15 +24,17 @@ import { _storeCurrentConference, _updateConferenceDuration } from './actions'; * @returns {Function} */ MiddlewareRegistry.register(store => next => action => { - switch (action.type) { - case APP_WILL_MOUNT: - return _appWillMount(store, next, action); + if (RECENT_LIST_ENABLED) { + switch (action.type) { + case APP_WILL_MOUNT: + return _appWillMount(store, next, action); - case CONFERENCE_WILL_LEAVE: - return _conferenceWillLeave(store, next, action); + case CONFERENCE_WILL_LEAVE: + return _conferenceWillLeave(store, next, action); - case SET_ROOM: - return _setRoom(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); } diff --git a/react/features/recent-list/reducer.js b/react/features/recent-list/reducer.js index de262a039..1c8bfb414 100644 --- a/react/features/recent-list/reducer.js +++ b/react/features/recent-list/reducer.js @@ -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,17 +50,19 @@ PersistenceRegistry.register(STORE_NAME); ReducerRegistry.register( STORE_NAME, (state = _getLegacyRecentRoomList(), action) => { - switch (action.type) { - case APP_WILL_MOUNT: - return _appWillMount(state); + if (RECENT_LIST_ENABLED) { + switch (action.type) { + case APP_WILL_MOUNT: + return _appWillMount(state); + case _STORE_CURRENT_CONFERENCE: + return _storeCurrentConference(state, action); - case _STORE_CURRENT_CONFERENCE: - return _storeCurrentConference(state, action); - - case _UPDATE_CONFERENCE_DURATION: - return _updateConferenceDuration(state, action); - - default: + case _UPDATE_CONFERENCE_DURATION: + return _updateConferenceDuration(state, action); + default: + return state; + } + } else { return state; } }); diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index 315a702af..e004ed528 100644 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -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') } + { RECENT_LIST_ENABLED ? : null } { showAdditionalContent ?