From b30008e3a5895b3e78a76abab13c84ae024ee0e2 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Mon, 22 Oct 2018 13:49:18 -0500 Subject: [PATCH] feat(welcome-page): Redesign. (#3559) * feat(welcome-page): Redesign. * Style adjustments. --- css/_meetings_list.scss | 117 ++++++++++ css/_variables.scss | 4 +- css/_welcome_page.scss | 104 +++++++-- css/main.scss | 1 + lang/main.json | 1 + .../base/react/components/web/MeetingsList.js | 201 ++++++++++++++++++ .../base/react/components/web/index.js | 1 + .../components/AddMeetingUrlButton.web.js | 11 +- .../components/CalendarList.native.js | 2 +- .../components/CalendarList.web.js | 33 ++- ...ntent.js => CalendarListContent.native.js} | 32 +-- .../components/CalendarListContent.web.js | 177 +++++++++++++++ .../components/JoinButton.web.js | 12 +- .../components/AbstractRecentList.js | 102 +++++++++ .../{RecentList.js => RecentList.native.js} | 85 ++------ .../recent-list/components/RecentList.web.js | 99 +++++++++ .../{styles.js => styles.native.js} | 0 .../recent-list/components/styles.web.js | 1 + react/features/recent-list/functions.any.js | 80 ------- .../features/recent-list/functions.native.js | 80 ++++++- react/features/recent-list/functions.web.js | 40 ++-- react/features/welcome/components/Tab.js | 76 +++++++ react/features/welcome/components/Tabs.js | 63 ++++++ .../welcome/components/WelcomePage.web.js | 111 +++++----- 24 files changed, 1114 insertions(+), 319 deletions(-) create mode 100644 css/_meetings_list.scss create mode 100644 react/features/base/react/components/web/MeetingsList.js rename react/features/calendar-sync/components/{CalendarListContent.js => CalendarListContent.native.js} (87%) create mode 100644 react/features/calendar-sync/components/CalendarListContent.web.js create mode 100644 react/features/recent-list/components/AbstractRecentList.js rename react/features/recent-list/components/{RecentList.js => RecentList.native.js} (56%) create mode 100644 react/features/recent-list/components/RecentList.web.js rename react/features/recent-list/components/{styles.js => styles.native.js} (100%) create mode 100644 react/features/recent-list/components/styles.web.js delete mode 100644 react/features/recent-list/functions.any.js create mode 100644 react/features/welcome/components/Tab.js create mode 100644 react/features/welcome/components/Tabs.js diff --git a/css/_meetings_list.scss b/css/_meetings_list.scss new file mode 100644 index 000000000..bef7e4e3b --- /dev/null +++ b/css/_meetings_list.scss @@ -0,0 +1,117 @@ +.meetings-list { + font-size: 14px; + color: #253858; + line-height: 20px; + text-align: left; + text-overflow: ellipsis; + display: flex; + flex-direction: column; + position: relative; + width: 100%; + height: 100%; + overflow: auto; + + .meetings-list-empty { + text-align: center; + align-items: center; + justify-content: center; + display: flex; + flex-grow: 1; + flex-direction: column; + + .description { + font-size: 16px; + padding: 20px; + } + } + + .button { + background: #0074E0; + border-radius: 4px; + color: #FFFFFF; + display: flex; + justify-content: center; + align-items: center; + padding: 5px 10px; + cursor: pointer; + } + + .item { + background: rgba(255,255,255,0.50); + box-sizing: border-box; + display: inline-flex; + margin-top: 5px; + min-height: 92px; + width: 100%; + word-break: break-word; + display: flex; + flex-direction: row; + text-align: left; + + &:first-child { + margin-top: 0px; + } + + .left-column { + display: flex; + flex-direction: column; + width: 140px; + flex-grow: 0; + padding-left: 30px; + padding-top: 25px; + + .date { + font-weight: bold; + padding-bottom: 5px; + } + } + + .right-column { + display: flex; + flex-direction: column; + flex-grow: 1; + padding-left: 30px; + padding-top: 25px; + + .title { + font-size: 16px; + font-weight: bold; + padding-bottom: 5px; + } + } + + .actions { + display: flex; + align-items: center; + justify-content: center; + flex-grow: 0; + padding-right: 30px; + } + + &.with-click-handler { + cursor: pointer; + } + + &.with-click-handler:hover { + background-color: #75A7E7; + } + + .add-button { + width: 30px; + height: 30px; + padding: 0px; + } + + i { + cursor: inherit; + } + + .join-button { + display: none; + } + + &:hover .join-button { + display: block + } + } +} diff --git a/css/_variables.scss b/css/_variables.scss index 342d2cc0d..ae614aa14 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -144,7 +144,7 @@ $watermarkHeight: 74px; /** * Welcome page variables. */ -$welcomePageDescriptionColor: #E6EDFA; +$welcomePageDescriptionColor: #fff; $welcomePageFontFamily: inherit; -$welcomePageHeaderBackground: #1D69D4; +$welcomePageHeaderBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%); $welcomePageTitleColor: #fff; diff --git a/css/_welcome_page.scss b/css/_welcome_page.scss index 37cbda8c8..5be60c46d 100644 --- a/css/_welcome_page.scss +++ b/css/_welcome_page.scss @@ -4,7 +4,7 @@ body.welcome-page { } .welcome { - background-color: $welcomePageHeaderBackground; + background-image: $welcomePageHeaderBackground; display: flex; flex-direction: column; font-family: $welcomePageFontFamily; @@ -24,8 +24,8 @@ body.welcome-page { .header-text { display: flex; flex-direction: column; - margin-top: $watermarkHeight + 80; - margin-bottom: 36px; + margin-top: $watermarkHeight + 35; + margin-bottom: 35px; max-width: calc(100% - 40px); width: 650px; z-index: $zindex2; @@ -35,7 +35,6 @@ body.welcome-page { color: $welcomePageTitleColor; font-size: 2.5rem; font-weight: 500; - letter-spacing: 0; line-height: 1.18; margin-bottom: 16px; } @@ -49,41 +48,118 @@ body.welcome-page { } #enter_room { - align-items: center; display: flex; + align-items: center; max-width: calc(100% - 40px); - margin-bottom: 20px; - position: relative; - width: 650px; + width: 680px; z-index: $zindex2; + background-color: #fff; + padding: 25px 30px; - .enter-room-input { - display: inline-block; - margin-right: 8px; + .enter-room-input-container { width: 100%; + padding-right: 8px; + padding-bottom: 5px; + text-align: left; + color: #253858; + height: fit-content; + border-width: 0px 0px 2px 0px; + border-style: solid; + border-image: linear-gradient(to right, #dee1e6, #fff) 1; + + .enter-room-title { + font-size: 18px; + font-weight: bold; + padding-bottom: 5px; + } + + .enter-room-input { + border: none; + display: inline-block; + width: 100%; + font-size: 14px; + } + + ::placeholder { + color: #253858; + } } + } .tab-container { font-size: 16px; position: relative; text-align: left; - width: 650px; + min-height: 354px; + width: 710px; + background: #75A7E7; + display: flex; + flex-direction: column; + + .tab-content{ + margin: 5px 0px; + overflow: hidden; + flex-grow: 1; + position: relative; + + > * { + position: absolute; + } + } + + .tab-buttons { + font-size: 18px; + color: #FFFFFF; + display: flex; + flex-grow: 0; + flex-direction: row; + min-height: 54px; + width: 100%; + + .tab { + text-align: center; + background: rgba(9,30,66,0.37); + height: 55px; + line-height: 54px; + flex-grow: 1; + cursor: pointer; + + &.selected, &:hover { + background: rgba(9,30,66,0.71); + } + + &:last-child { + margin-left: 1px; + } + } + } } } .welcome-page-button { - font-size: 16px; + width: 51px; + height: 35px; + font-size: 14px; + background: #0074E0; + border-radius: 4px; + color: #FFFFFF; + text-align: center; + vertical-align: middle; + line-height: 35px; + cursor: pointer; } .welcome-page-settings { color: $welcomePageDescriptionColor; position: absolute; - right: 10px; + top: 32px; + right: 32px; z-index: $zindex2; * { cursor: pointer; + font-size: 32px; } } diff --git a/css/main.scss b/css/main.scss index 24fbec86f..4cf6f0fda 100644 --- a/css/main.scss +++ b/css/main.scss @@ -76,6 +76,7 @@ @import 'modals/invite/add-people'; @import 'deep-linking/main'; @import 'transcription-subtitles'; +@import '_meetings_list.scss'; @import 'navigate_section_list'; @import 'third-party-branding/google'; @import 'third-party-branding/microsoft'; diff --git a/lang/main.json b/lang/main.json index b7b09e93e..966f90429 100644 --- a/lang/main.json +++ b/lang/main.json @@ -59,6 +59,7 @@ "calendar": "Calendar", "connectCalendarText": "Connect your calendar to view all your meetings in __app__. Plus, add __app__ meetings to your calendar and start them with one click.", "connectCalendarButton": "Connect your calendar", + "enterRoomTitle": "Start a new meeting", "go": "GO", "join": "JOIN", "privacy": "Privacy", diff --git a/react/features/base/react/components/web/MeetingsList.js b/react/features/base/react/components/web/MeetingsList.js new file mode 100644 index 000000000..1474328e2 --- /dev/null +++ b/react/features/base/react/components/web/MeetingsList.js @@ -0,0 +1,201 @@ +// @flow + +import React, { Component } from 'react'; + +import { + getLocalizedDateFormatter, + getLocalizedDurationFormatter +} from '../../../i18n'; + +import Container from './Container'; +import Text from './Text'; + +type Props = { + + /** + * Indicates if the list is disabled or not. + */ + disabled: boolean, + + /** + * Indicates if the URL should be hidden or not. + */ + hideURL: boolean, + + /** + * Function to be invoked when an item is pressed. The item's URL is passed. + */ + onPress: Function, + + /** + * Rendered when the list is empty. Should be a rendered element. + */ + listEmptyComponent: Object, + + /** + * An array of meetings. + */ + meetings: Array, + + /** + * Defines what happens when an item in the section list is clicked + */ + onItemClick: Function +}; + +/** + * Generates a date string for a given date. + * + * @param {Object} date - The date. + * @private + * @returns {string} + */ +function _toDateString(date) { + return getLocalizedDateFormatter(date).format('MMM Do, YYYY'); +} + + +/** + * Generates a time (interval) string for a given times. + * + * @param {Array} times - Array of times. + * @private + * @returns {string} + */ +function _toTimeString(times) { + if (times && times.length > 0) { + return ( + times + .map(time => getLocalizedDateFormatter(time).format('LT')) + .join(' - ')); + } + + return undefined; +} + +/** + * Implements a React/Web {@link Component} for displaying a list with + * meetings. + * + * @extends Component + */ +export default class MeetingsList extends Component { + /** + * Constructor of the MeetingsList component. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._onPress = this._onPress.bind(this); + this._renderItem = this._renderItem.bind(this); + } + + /** + * Renders the content of this component. + * + * @returns {React.ReactNode} + */ + render() { + const { listEmptyComponent, meetings } = this.props; + + /** + * If there are no recent meetings we don't want to display anything + */ + if (meetings) { + return ( + + { + meetings.length === 0 + ? listEmptyComponent + : meetings.map(this._renderItem) + } + + ); + } + + return null; + } + + _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) { + const { disabled, onPress } = this.props; + + if (!disabled && url && typeof onPress === 'function') { + return () => onPress(url); + } + + return null; + } + + _renderItem: (Object, number) => React$Node; + + /** + * Renders an item for the list. + * + * @param {Object} meeting - Information about the meeting. + * @param {number} index - The index of the item. + * @returns {Node} + */ + _renderItem(meeting, index) { + const { + date, + duration, + elementAfter, + time, + title, + url + } = meeting; + const { hideURL = false } = this.props; + const onPress = this._onPress(url); + const rootClassName + = `item ${ + onPress ? 'with-click-handler' : 'without-click-handler'}`; + + return ( + + + + { _toDateString(date) } + + + { _toTimeString(time) } + + + + + { title } + + { + hideURL || !url ? null : ( + + { url } + ) + } + { + typeof duration === 'number' ? ( + + { getLocalizedDurationFormatter(duration) } + ) : null + } + + + { elementAfter || null } + + + ); + } +} diff --git a/react/features/base/react/components/web/index.js b/react/features/base/react/components/web/index.js index e9d62334d..cad7935b5 100644 --- a/react/features/base/react/components/web/index.js +++ b/react/features/base/react/components/web/index.js @@ -1,5 +1,6 @@ export { default as Container } from './Container'; export { default as LoadingIndicator } from './LoadingIndicator'; +export { default as MeetingsList } from './MeetingsList'; export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete'; export { default as NavigateSectionListEmptyComponent } from './NavigateSectionListEmptyComponent'; diff --git a/react/features/calendar-sync/components/AddMeetingUrlButton.web.js b/react/features/calendar-sync/components/AddMeetingUrlButton.web.js index 74130f0d1..5645a987e 100644 --- a/react/features/calendar-sync/components/AddMeetingUrlButton.web.js +++ b/react/features/calendar-sync/components/AddMeetingUrlButton.web.js @@ -1,6 +1,5 @@ // @flow -import Button from '@atlaskit/button'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import Tooltip from '@atlaskit/tooltip'; @@ -65,12 +64,11 @@ class AddMeetingUrlButton extends Component { render() { return ( - + ); } @@ -92,4 +90,3 @@ class AddMeetingUrlButton extends Component { } export default translate(connect()(AddMeetingUrlButton)); - diff --git a/react/features/calendar-sync/components/CalendarList.native.js b/react/features/calendar-sync/components/CalendarList.native.js index 45ca25b1c..afd267dbb 100644 --- a/react/features/calendar-sync/components/CalendarList.native.js +++ b/react/features/calendar-sync/components/CalendarList.native.js @@ -79,7 +79,7 @@ class CalendarList extends AbstractPage { CalendarListContent ? : null ); diff --git a/react/features/calendar-sync/components/CalendarList.web.js b/react/features/calendar-sync/components/CalendarList.web.js index acc48dc67..4e959d341 100644 --- a/react/features/calendar-sync/components/CalendarList.web.js +++ b/react/features/calendar-sync/components/CalendarList.web.js @@ -1,6 +1,5 @@ // @flow -import Button from '@atlaskit/button'; import Spinner from '@atlaskit/spinner'; import React from 'react'; import { connect } from 'react-redux'; @@ -82,7 +81,7 @@ class CalendarList extends AbstractPage { CalendarListContent ? : null ); @@ -102,21 +101,18 @@ class CalendarList extends AbstractPage { if (_hasIntegrationSelected && _hasLoadedEvents) { return ( -
+
{ t('calendarSync.noEvents') }
- +
); } else if (_hasIntegrationSelected && !_hasLoadedEvents) { return ( -
+
{ } return ( -
-

+

+

{ t('welcomepage.connectCalendarText', { app: interfaceConfig.APP_NAME }) }

- +
); } diff --git a/react/features/calendar-sync/components/CalendarListContent.js b/react/features/calendar-sync/components/CalendarListContent.native.js similarity index 87% rename from react/features/calendar-sync/components/CalendarListContent.js rename to react/features/calendar-sync/components/CalendarListContent.native.js index bd496d3c5..6bf00a06b 100644 --- a/react/features/calendar-sync/components/CalendarListContent.js +++ b/react/features/calendar-sync/components/CalendarListContent.native.js @@ -15,8 +15,6 @@ import { NavigateSectionList } from '../../base/react'; import { refreshCalendar, openUpdateCalendarEventDialog } from '../actions'; import { isCalendarEnabled } from '../functions'; -import AddMeetingUrlButton from './AddMeetingUrlButton'; -import JoinButton from './JoinButton'; /** * The type of the React {@code Component} props of @@ -42,7 +40,7 @@ type Props = { /** * */ - renderListEmptyComponent: Function, + listEmptyComponent: React$Node, /** * The translate function. @@ -70,7 +68,6 @@ class CalendarListContent extends Component { super(props); // Bind event handlers so they are only bound once per instance. - this._onJoinPress = this._onJoinPress.bind(this); this._onPress = this._onPress.bind(this); this._onRefresh = this._onRefresh.bind(this); this._onSecondaryAction = this._onSecondaryAction.bind(this); @@ -97,7 +94,7 @@ class CalendarListContent extends Component { * @inheritdoc */ render() { - const { disabled, renderListEmptyComponent } = this.props; + const { disabled, listEmptyComponent } = this.props; return ( { onRefresh = { this._onRefresh } onSecondaryAction = { this._onSecondaryAction } renderListEmptyComponent - = { renderListEmptyComponent } + = { listEmptyComponent } sections = { this._toDisplayableList() } /> ); } - _onJoinPress: (Object, string) => Function; - - /** - * Handles the list's navigate action. - * - * @private - * @param {Object} event - The click event. - * @param {string} url - The url string to navigate to. - * @returns {void} - */ - _onJoinPress(event, url) { - event.stopPropagation(); - - this._onPress(url, 'calendar.meeting.join'); - } - _onPress: (string, string) => Function; /** @@ -197,13 +178,6 @@ class CalendarListContent extends Component { */ _toDisplayableItem(event) { return { - elementAfter: event.url - ? - : (), id: event.id, key: `${event.id}-${event.startDate}`, lines: [ diff --git a/react/features/calendar-sync/components/CalendarListContent.web.js b/react/features/calendar-sync/components/CalendarListContent.web.js new file mode 100644 index 000000000..e7ecbeb6c --- /dev/null +++ b/react/features/calendar-sync/components/CalendarListContent.web.js @@ -0,0 +1,177 @@ +// @flow + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import { appNavigate } from '../../app'; +import { + createCalendarClickedEvent, + createCalendarSelectedEvent, + sendAnalytics +} from '../../analytics'; +import { MeetingsList } from '../../base/react'; + +import { isCalendarEnabled } from '../functions'; + +import AddMeetingUrlButton from './AddMeetingUrlButton'; +import JoinButton from './JoinButton'; + +/** + * The type of the React {@code Component} props of + * {@link CalendarListContent}. + */ +type Props = { + + /** + * The calendar event list. + */ + _eventList: Array, + + /** + * Indicates if the list is disabled or not. + */ + disabled: boolean, + + /** + * The Redux dispatch function. + */ + dispatch: Function, + + /** + * + */ + listEmptyComponent: React$Node, +}; + +/** + * Component to display a list of events from a connected calendar. + */ +class CalendarListContent extends Component { + /** + * Default values for the component's props. + */ + static defaultProps = { + _eventList: [] + }; + + /** + * Initializes a new {@code CalendarListContent} instance. + * + * @inheritdoc + */ + constructor(props) { + super(props); + + // Bind event handlers so they are only bound once per instance. + this._onJoinPress = this._onJoinPress.bind(this); + this._onPress = this._onPress.bind(this); + this._toDisplayableItem = this._toDisplayableItem.bind(this); + } + + /** + * Implements React's {@link Component#componentDidMount()}. Invoked + * immediately after this component is mounted. + * + * @inheritdoc + * @returns {void} + */ + componentDidMount() { + sendAnalytics(createCalendarSelectedEvent()); + } + + /** + * Implements React's {@link Component#render}. + * + * @inheritdoc + */ + render() { + const { disabled, listEmptyComponent } = this.props; + const { _eventList = [] } = this.props; + const meetings = _eventList.map(this._toDisplayableItem); + + return ( + + ); + } + + _onJoinPress: (Object, string) => Function; + + /** + * Handles the list's navigate action. + * + * @private + * @param {Object} event - The click event. + * @param {string} url - The url string to navigate to. + * @returns {void} + */ + _onJoinPress(event, url) { + event.stopPropagation(); + + this._onPress(url, 'calendar.meeting.join'); + } + + _onPress: (string, string) => Function; + + /** + * Handles the list's navigate action. + * + * @private + * @param {string} url - The url string to navigate to. + * @param {string} analyticsEventName - Тhe name of the analytics event. + * associated with this action. + * @returns {void} + */ + _onPress(url, analyticsEventName = 'calendar.meeting.tile') { + sendAnalytics(createCalendarClickedEvent(analyticsEventName)); + + this.props.dispatch(appNavigate(url)); + } + + _toDisplayableItem: Object => Object; + + /** + * Creates a displayable object from an event. + * + * @param {Object} event - The calendar event. + * @private + * @returns {Object} + */ + _toDisplayableItem(event) { + return { + elementAfter: event.url + ? + : (), + date: event.startDate, + time: [ event.startDate, event.endDate ], + description: event.url, + title: event.title, + url: event.url + }; + } +} + +/** + * Maps redux state to component props. + * + * @param {Object} state - The redux state. + * @returns {{ + * _eventList: Array + * }} + */ +function _mapStateToProps(state: Object) { + return { + _eventList: state['features/calendar-sync'].events + }; +} + +export default isCalendarEnabled() + ? connect(_mapStateToProps)(CalendarListContent) + : undefined; diff --git a/react/features/calendar-sync/components/JoinButton.web.js b/react/features/calendar-sync/components/JoinButton.web.js index c99b6264e..07063524b 100644 --- a/react/features/calendar-sync/components/JoinButton.web.js +++ b/react/features/calendar-sync/components/JoinButton.web.js @@ -1,6 +1,5 @@ // @flow -import Button from '@atlaskit/button'; import React, { Component } from 'react'; import Tooltip from '@atlaskit/tooltip'; @@ -58,13 +57,11 @@ class JoinButton extends Component { return ( - + ); } @@ -84,4 +81,3 @@ class JoinButton extends Component { } export default translate(JoinButton); - diff --git a/react/features/recent-list/components/AbstractRecentList.js b/react/features/recent-list/components/AbstractRecentList.js new file mode 100644 index 000000000..fdf9180a4 --- /dev/null +++ b/react/features/recent-list/components/AbstractRecentList.js @@ -0,0 +1,102 @@ +// @flow +import React from 'react'; + +import { + createRecentClickedEvent, + createRecentSelectedEvent, + sendAnalytics +} from '../../analytics'; +import { appNavigate } from '../../app'; +import { + AbstractPage, + Container, + Text +} from '../../base/react'; + +import styles from './styles'; + +/** + * The type of the React {@code Component} props of {@link AbstractRecentList} + */ +type Props = { + + /** + * The redux store's {@code dispatch} function. + */ + dispatch: Dispatch<*>, + + /** + * The translate function. + */ + t: Function +}; + +/** + * An abstract component for the recent list. + * + */ +export default class AbstractRecentList extends AbstractPage

{ + /** + * Initializes a new {@code RecentList} instance. + * + * @inheritdoc + */ + constructor(props: P) { + super(props); + + this._onPress = this._onPress.bind(this); + } + + /** + * Implements React's {@link Component#componentDidMount()}. Invoked + * immediately after this component is mounted. + * + * @inheritdoc + * @returns {void} + */ + componentDidMount() { + sendAnalytics(createRecentSelectedEvent()); + } + + _getRenderListEmptyComponent: () => React$Node; + + /** + * Returns a list empty component if a custom one has to be rendered instead + * of the default one in the {@link NavigateSectionList}. + * + * @private + * @returns {React$Component} + */ + _getRenderListEmptyComponent() { + const { t } = this.props; + + return ( + + + { t('welcomepage.recentListEmpty') } + + + ); + } + + _onPress: string => {}; + + /** + * Handles the list's navigate action. + * + * @private + * @param {string} url - The url string to navigate to. + * @returns {void} + */ + _onPress(url) { + const { dispatch } = this.props; + + sendAnalytics(createRecentClickedEvent('recent.meeting.tile')); + + dispatch(appNavigate(url)); + } +} diff --git a/react/features/recent-list/components/RecentList.js b/react/features/recent-list/components/RecentList.native.js similarity index 56% rename from react/features/recent-list/components/RecentList.js rename to react/features/recent-list/components/RecentList.native.js index eed501db2..614cd51a2 100644 --- a/react/features/recent-list/components/RecentList.js +++ b/react/features/recent-list/components/RecentList.native.js @@ -2,25 +2,14 @@ import React from 'react'; import { connect } from 'react-redux'; -import { - createRecentClickedEvent, - createRecentSelectedEvent, - sendAnalytics -} from '../../analytics'; -import { appNavigate, getDefaultURL } from '../../app'; +import { getDefaultURL } from '../../app'; import { translate } from '../../base/i18n'; -import { - AbstractPage, - Container, - NavigateSectionList, - Text -} from '../../base/react'; +import { NavigateSectionList } from '../../base/react'; import type { Section } from '../../base/react'; import { deleteRecentListEntry } from '../actions'; import { isRecentListEnabled, toDisplayableList } from '../functions'; - -import styles from './styles'; +import AbstractRecentList from './AbstractRecentList'; /** * The type of the React {@code Component} props of {@link RecentList} @@ -54,10 +43,13 @@ type Props = { }; /** - * The cross platform container rendering the list of the recently joined rooms. + * A class that renders the list of the recently joined rooms. * */ -class RecentList extends AbstractPage { +class RecentList extends AbstractRecentList { + _getRenderListEmptyComponent: () => React$Node; + _onPress: string => {}; + /** * Initializes a new {@code RecentList} instance. * @@ -67,18 +59,6 @@ class RecentList extends AbstractPage { super(props); this._onDelete = this._onDelete.bind(this); - this._onPress = this._onPress.bind(this); - } - - /** - * Implements React's {@link Component#componentDidMount()}. Invoked - * immediately after this component is mounted. - * - * @inheritdoc - * @returns {void} - */ - componentDidMount() { - sendAnalytics(createRecentSelectedEvent()); } /** @@ -90,7 +70,12 @@ class RecentList extends AbstractPage { if (!isRecentListEnabled()) { return null; } - const { disabled, t, _defaultServerURL, _recentList } = this.props; + const { + disabled, + t, + _defaultServerURL, + _recentList + } = this.props; const recentList = toDisplayableList(_recentList, t, _defaultServerURL); const slideActions = [ { backgroundColor: 'red', @@ -109,31 +94,6 @@ class RecentList extends AbstractPage { ); } - _getRenderListEmptyComponent: () => Object; - - /** - * Returns a list empty component if a custom one has to be rendered instead - * of the default one in the {@link NavigateSectionList}. - * - * @private - * @returns {React$Component} - */ - _getRenderListEmptyComponent() { - const { t } = this.props; - - return ( - - - { t('welcomepage.recentListEmpty') } - - - ); - } - _onDelete: Object => void /** @@ -146,23 +106,6 @@ class RecentList extends AbstractPage { _onDelete(itemId) { this.props.dispatch(deleteRecentListEntry(itemId)); } - - _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; - - sendAnalytics(createRecentClickedEvent('recent.meeting.tile')); - - dispatch(appNavigate(url)); - } } /** diff --git a/react/features/recent-list/components/RecentList.web.js b/react/features/recent-list/components/RecentList.web.js new file mode 100644 index 000000000..c353dd5d4 --- /dev/null +++ b/react/features/recent-list/components/RecentList.web.js @@ -0,0 +1,99 @@ +// @flow +import React from 'react'; +import { connect } from 'react-redux'; + +import { translate } from '../../base/i18n'; +import { MeetingsList } from '../../base/react'; + +import AbstractRecentList from './AbstractRecentList'; +import { isRecentListEnabled, 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 recent list from the Redux store. + */ + _recentList: Array +}; + +/** + * The cross platform container rendering the list of the recently joined rooms. + * + */ +class RecentList extends AbstractRecentList { + _getRenderListEmptyComponent: () => React$Node; + _onPress: string => {}; + + /** + * Initializes a new {@code RecentList} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._getRenderListEmptyComponent + = this._getRenderListEmptyComponent.bind(this); + this._onPress = this._onPress.bind(this); + } + + /** + * Implements the React Components's render method. + * + * @inheritdoc + */ + render() { + if (!isRecentListEnabled()) { + return null; + } + const { + disabled, + _recentList + } = this.props; + const recentList = toDisplayableList(_recentList); + + return ( + + ); + } +} + +/** + * Maps redux state to component props. + * + * @param {Object} state - The redux state. + * @returns {{ + * _defaultServerURL: string, + * _recentList: Array + * }} + */ +export function _mapStateToProps(state: Object) { + return { + _recentList: state['features/recent-list'] + }; +} + +export default translate(connect(_mapStateToProps)(RecentList)); diff --git a/react/features/recent-list/components/styles.js b/react/features/recent-list/components/styles.native.js similarity index 100% rename from react/features/recent-list/components/styles.js rename to react/features/recent-list/components/styles.native.js diff --git a/react/features/recent-list/components/styles.web.js b/react/features/recent-list/components/styles.web.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/react/features/recent-list/components/styles.web.js @@ -0,0 +1 @@ +export default {}; diff --git a/react/features/recent-list/functions.any.js b/react/features/recent-list/functions.any.js deleted file mode 100644 index ee85ec410..000000000 --- a/react/features/recent-list/functions.any.js +++ /dev/null @@ -1,80 +0,0 @@ -import { - getLocalizedDateFormatter, - getLocalizedDurationFormatter -} from '../base/i18n'; -import { parseURIString } from '../base/util'; - -/** - * 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 location = parseURIString(item.conference); - const baseURL = `${location.protocol}//${location.host}`; - const serverName = baseURL === defaultServerURL ? null : location.host; - - return { - colorBase: serverName, - id: { - date: item.date, - url: item.conference - }, - 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 m = getLocalizedDateFormatter(itemDate); - 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 - - if (dateInMs >= todayInMs) { - return m.fromNow(); - } else if (dateInMs >= yesterdayInMs) { - return t('dateUtils.yesterday'); - } 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'); - } - - 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 index 498772afd..49730066f 100644 --- a/react/features/recent-list/functions.native.js +++ b/react/features/recent-list/functions.native.js @@ -1,6 +1,84 @@ +import { + getLocalizedDateFormatter, + getLocalizedDurationFormatter +} from '../base/i18n'; import { NavigateSectionList } from '../base/react'; +import { parseURIString } from '../base/util'; -import { toDisplayableItem } from './functions.any'; +/** + * 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} + */ +function toDisplayableItem(item, defaultServerURL, t) { + const location = parseURIString(item.conference); + const baseURL = `${location.protocol}//${location.host}`; + const serverName = baseURL === defaultServerURL ? null : location.host; + + return { + colorBase: serverName, + id: { + date: item.date, + url: item.conference + }, + 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} + */ +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} + */ +function _toDateString(itemDate, t) { + const m = getLocalizedDateFormatter(itemDate); + 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 + + if (dateInMs >= todayInMs) { + return m.fromNow(); + } else if (dateInMs >= yesterdayInMs) { + return t('dateUtils.yesterday'); + } 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'); + } + + return m.format('ddd, MMMM DD h:mm A'); +} /** * Transforms the history list to a displayable list diff --git a/react/features/recent-list/functions.web.js b/react/features/recent-list/functions.web.js index b60dbf88b..608fc48bc 100644 --- a/react/features/recent-list/functions.web.js +++ b/react/features/recent-list/functions.web.js @@ -1,39 +1,27 @@ /* global interfaceConfig */ -import { NavigateSectionList } from '../base/react'; - -import { toDisplayableItem } from './functions.any'; +import { parseURIString } from '../base/util'; /** - * Transforms the history list to a displayable list - * with sections. + * Transforms the history list to a displayable list. * * @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'), 'joinPastMeeting'); - - // 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); - } - const displayableList = []; - - if (section.data.length) { - section.data.reverse(); - displayableList.push(section); - } - - return displayableList; +export function toDisplayableList(recentList) { + return ( + recentList.slice(-3).reverse() + .map(item => { + return { + date: item.date, + duration: item.duration, + time: [ item.date ], + title: parseURIString(item.conference).room, + url: item.conference + }; + })); } /** diff --git a/react/features/welcome/components/Tab.js b/react/features/welcome/components/Tab.js new file mode 100644 index 000000000..0e7e6efd7 --- /dev/null +++ b/react/features/welcome/components/Tab.js @@ -0,0 +1,76 @@ +// @flow +import React, { Component } from 'react'; + +/** + * The type of the React {@code Component} props of {@link Tab} + */ +type Props = { + + /** + * The index of the tab. + */ + index: number, + + /** + * Indicates if the tab is selected or not. + */ + isSelected: boolean, + + /** + * The label of the tab. + */ + label: string, + + /** + * Handler for selecting the tab. + */ + onSelect: Function +} + +/** + * A React component that implements tabs. + * + */ +export default class Tab extends Component { + /** + * Initializes a new {@code Tab} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._onSelect = this._onSelect.bind(this); + } + + _onSelect: () => {}; + + /** + * Selects a tab. + * + * @returns {void} + */ + _onSelect() { + const { index, onSelect } = this.props; + + onSelect(index); + } + + /** + * Implements the React Components's render method. + * + * @inheritdoc + */ + render() { + const { index, isSelected, label } = this.props; + const className = `tab${isSelected ? ' selected' : ''}`; + + return ( +
+ { label } +
); + } +} diff --git a/react/features/welcome/components/Tabs.js b/react/features/welcome/components/Tabs.js new file mode 100644 index 000000000..67e1b2cda --- /dev/null +++ b/react/features/welcome/components/Tabs.js @@ -0,0 +1,63 @@ +// @flow +import React, { Component } from 'react'; + +import Tab from './Tab'; + +/** + * The type of the React {@code Component} props of {@link Tabs} + */ +type Props = { + + /** + * Handler for selecting the tab. + */ + onSelect: Function, + + /** + * The index of the selected tab. + */ + selected: number, + + /** + * Tabs information. + */ + tabs: Object +}; + +/** + * A React component that implements tabs. + * + */ +export default class Tabs extends Component { + /** + * Implements the React Components's render method. + * + * @inheritdoc + */ + render() { + const { onSelect, selected, tabs } = this.props; + const { content } = tabs[selected]; + + return ( +
+
+ { content } +
+ { tabs.length > 1 ? ( +
+ { + tabs.map((tab, index) => ( + + )) + } +
) : null + } +
+ ); + } +} diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index d321b2b81..8c94d6ec0 100644 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -1,9 +1,5 @@ /* global interfaceConfig */ -import Button from '@atlaskit/button'; -import { FieldTextStateless } from '@atlaskit/field-text'; -import Tabs from '@atlaskit/tabs'; -import { AtlasKitThemeProvider } from '@atlaskit/theme'; import React from 'react'; import { connect } from 'react-redux'; @@ -14,6 +10,7 @@ import { RecentList } from '../../recent-list'; import { SettingsButton, SETTINGS_TABS } from '../../settings'; import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage'; +import Tabs from './Tabs'; /** * The Web container rendering the welcome page. @@ -118,58 +115,60 @@ class WelcomePage extends AbstractWelcomePage { const showAdditionalContent = this._shouldShowAdditionalContent(); return ( - -
-
- +
+
+ +
+
+
+
-
-
-
-

- { t('welcomepage.title') } -

-

- { t('welcomepage.appDescription', - { app: APP_NAME }) } -

-
-
-
- +
+

+ { t('welcomepage.title') } +

+

+ { t('welcomepage.appDescription', + { app: APP_NAME }) } +

+
+
+
+
+ { t('welcomepage.enterRoomTitle') } +
+ + -
- { this._renderTabs() } +
+ { t('welcomepage.go') } +
- { showAdditionalContent - ?
- : null } + { this._renderTabs() }
- + { showAdditionalContent + ?
+ : null } +
); } @@ -203,14 +202,12 @@ class WelcomePage extends AbstractWelcomePage { /** * Callback invoked when the desired tab to display should be changed. * - * @param {Object} tab - The configuration passed into atlaskit tabs to - * describe how to display the selected tab. * @param {number} tabIndex - The index of the tab within the array of * displayed tabs. * @private * @returns {void} */ - _onTabSelected(tab, tabIndex) { // eslint-disable-line no-unused-vars + _onTabSelected(tabIndex) { this.setState({ selectedTab: tabIndex }); } @@ -241,20 +238,14 @@ class WelcomePage extends AbstractWelcomePage { tabs.push({ label: t('welcomepage.recentList'), - content: , - defaultSelected: !CalendarList + content: }); return ( -
-
- -
- -
); + ); } /**