feat(welcome-page): Redesign. (#3559)

* feat(welcome-page): Redesign.

* Style adjustments.
This commit is contained in:
Hristo Terezov 2018-10-22 13:49:18 -05:00 committed by yanas
parent 62b6737a3f
commit b30008e3a5
24 changed files with 1114 additions and 319 deletions

117
css/_meetings_list.scss Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Object>,
/**
* 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<Date>} 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<Props> {
/**
* 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 (
<Container
className = 'meetings-list'>
{
meetings.length === 0
? listEmptyComponent
: meetings.map(this._renderItem)
}
</Container>
);
}
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 (
<Container
className = { rootClassName }
key = { index }
onClick = { onPress }>
<Container className = 'left-column'>
<Text className = 'date'>
{ _toDateString(date) }
</Text>
<Text>
{ _toTimeString(time) }
</Text>
</Container>
<Container className = 'right-column'>
<Text className = 'title'>
{ title }
</Text>
{
hideURL || !url ? null : (
<Text>
{ url }
</Text>)
}
{
typeof duration === 'number' ? (
<Text>
{ getLocalizedDurationFormatter(duration) }
</Text>) : null
}
</Container>
<Container className = 'actions'>
{ elementAfter || null }
</Container>
</Container>
);
}
}

View File

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

View File

@ -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<Props> {
render() {
return (
<Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
<Button
appearance = 'primary'
onClick = { this._onClick }
type = 'button'>
<div
className = 'button add-button'
onClick = { this._onClick }>
<i className = { 'icon-add' } />
</Button>
</div>
</Tooltip>
);
}
@ -92,4 +90,3 @@ class AddMeetingUrlButton extends Component<Props> {
}
export default translate(connect()(AddMeetingUrlButton));

View File

@ -79,7 +79,7 @@ class CalendarList extends AbstractPage<Props> {
CalendarListContent
? <CalendarListContent
disabled = { disabled }
renderListEmptyComponent
listEmptyComponent
= { this._getRenderListEmptyComponent() } />
: null
);

View File

@ -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<Props> {
CalendarListContent
? <CalendarListContent
disabled = { disabled }
renderListEmptyComponent
listEmptyComponent
= { this._getRenderListEmptyComponent() } />
: null
);
@ -102,21 +101,18 @@ class CalendarList extends AbstractPage<Props> {
if (_hasIntegrationSelected && _hasLoadedEvents) {
return (
<div className = 'navigate-section-list-empty'>
<div className = 'meetings-list-empty'>
<div>{ t('calendarSync.noEvents') }</div>
<Button
appearance = 'primary'
className = 'calendar-button'
id = 'connect_calendar_button'
onClick = { this._onRefreshEvents }
type = 'button'>
<div
className = 'button'
onClick = { this._onRefreshEvents }>
{ t('calendarSync.refresh') }
</Button>
</div>
</div>
);
} else if (_hasIntegrationSelected && !_hasLoadedEvents) {
return (
<div className = 'navigate-section-list-empty'>
<div className = 'meetings-list-empty'>
<Spinner
invertColor = { true }
isCompleting = { false }
@ -126,20 +122,17 @@ class CalendarList extends AbstractPage<Props> {
}
return (
<div className = 'navigate-section-list-empty'>
<p className = 'header-text-description'>
<div className = 'meetings-list-empty'>
<p className = 'description'>
{ t('welcomepage.connectCalendarText', {
app: interfaceConfig.APP_NAME
}) }
</p>
<Button
appearance = 'primary'
className = 'calendar-button'
id = 'connect_calendar_button'
onClick = { this._onOpenSettings }
type = 'button'>
<div
className = 'button'
onClick = { this._onOpenSettings }>
{ t('welcomepage.connectCalendarButton') }
</Button>
</div>
</div>
);
}

View File

@ -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<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._onRefresh = this._onRefresh.bind(this);
this._onSecondaryAction = this._onSecondaryAction.bind(this);
@ -97,7 +94,7 @@ class CalendarListContent extends Component<Props> {
* @inheritdoc
*/
render() {
const { disabled, renderListEmptyComponent } = this.props;
const { disabled, listEmptyComponent } = this.props;
return (
<NavigateSectionList
@ -106,27 +103,11 @@ class CalendarListContent extends Component<Props> {
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<Props> {
*/
_toDisplayableItem(event) {
return {
elementAfter: event.url
? <JoinButton
onPress = { this._onJoinPress }
url = { event.url } />
: (<AddMeetingUrlButton
calendarId = { event.calendarId }
eventId = { event.id } />),
id: event.id,
key: `${event.id}-${event.startDate}`,
lines: [

View File

@ -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<Object>,
/**
* 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<Props> {
/**
* 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 (
<MeetingsList
disabled = { disabled }
listEmptyComponent = { listEmptyComponent }
meetings = { meetings }
onPress = { this._onPress } />
);
}
_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
? <JoinButton
onPress = { this._onJoinPress }
url = { event.url } />
: (<AddMeetingUrlButton
calendarId = { event.calendarId }
eventId = { event.id } />),
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<Object>
* }}
*/
function _mapStateToProps(state: Object) {
return {
_eventList: state['features/calendar-sync'].events
};
}
export default isCalendarEnabled()
? connect(_mapStateToProps)(CalendarListContent)
: undefined;

View File

@ -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<Props> {
return (
<Tooltip
content = { t('calendarSync.joinTooltip') }>
<Button
appearance = 'primary'
className = 'join-button'
onClick = { this._onClick }
type = 'button'>
<div
className = 'button join-button'
onClick = { this._onClick }>
{ t('calendarSync.join') }
</Button>
</div>
</Tooltip>
);
}
@ -84,4 +81,3 @@ class JoinButton extends Component<Props> {
}
export default translate(JoinButton);

View File

@ -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<P: Props> extends AbstractPage<P> {
/**
* 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 (
<Container
className = 'meetings-list-empty'
style = { styles.emptyListContainer }>
<Text
className = 'description'
style = { styles.emptyListText }>
{ t('welcomepage.recentListEmpty') }
</Text>
</Container>
);
}
_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));
}
}

View File

@ -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<Props> {
class RecentList extends AbstractRecentList<Props> {
_getRenderListEmptyComponent: () => React$Node;
_onPress: string => {};
/**
* Initializes a new {@code RecentList} instance.
*
@ -67,18 +59,6 @@ class RecentList extends AbstractPage<Props> {
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<Props> {
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<Props> {
);
}
_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 (
<Container
className = 'navigate-section-list-empty'
style = { styles.emptyListContainer }>
<Text
className = 'header-text-description'
style = { styles.emptyListText }>
{ t('welcomepage.recentListEmpty') }
</Text>
</Container>
);
}
_onDelete: Object => void
/**
@ -146,23 +106,6 @@ class RecentList extends AbstractPage<Props> {
_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));
}
}
/**

View File

@ -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<Object>
};
/**
* The cross platform container rendering the list of the recently joined rooms.
*
*/
class RecentList extends AbstractRecentList<Props> {
_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 (
<MeetingsList
disabled = { disabled }
hideURL = { true }
listEmptyComponent = { this._getRenderListEmptyComponent() }
meetings = { recentList }
onPress = { this._onPress } />
);
}
}
/**
* 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));

View File

@ -0,0 +1 @@
export default {};

View File

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

View File

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

View File

@ -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<Object>} recentList - The recent list form the redux store.
* @param {Function} t - The translate function.
* @param {string} defaultServerURL - The default server URL.
* @returns {Array<Object>}
*/
export function toDisplayableList(recentList, t, defaultServerURL) {
const { createSection } = NavigateSectionList;
const section
= createSection(t('recentList.joinPastMeeting'), '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
};
}));
}
/**

View File

@ -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<Props> {
/**
* 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 (
<div
className = { className }
key = { index }
onClick = { this._onSelect }>
{ label }
</div>);
}
}

View File

@ -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<Props> {
/**
* Implements the React Components's render method.
*
* @inheritdoc
*/
render() {
const { onSelect, selected, tabs } = this.props;
const { content } = tabs[selected];
return (
<div className = 'tab-container'>
<div className = 'tab-content'>
{ content }
</div>
{ tabs.length > 1 ? (
<div className = 'tab-buttons'>
{
tabs.map((tab, index) => (
<Tab
index = { index }
isSelected = { index === selected }
key = { index }
label = { tab.label }
onSelect = { onSelect } />
))
}
</div>) : null
}
</div>
);
}
}

View File

@ -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 (
<AtlasKitThemeProvider mode = 'light'>
<div
className = { `welcome ${showAdditionalContent
? 'with-content' : 'without-content'}` }
id = 'welcome_page'>
<div className = 'welcome-watermark'>
<Watermarks />
<div
className = { `welcome ${showAdditionalContent
? 'with-content' : 'without-content'}` }
id = 'welcome_page'>
<div className = 'welcome-watermark'>
<Watermarks />
</div>
<div className = 'header'>
<div className = 'welcome-page-settings'>
<SettingsButton
defaultTab = { SETTINGS_TABS.CALENDAR } />
</div>
<div className = 'header'>
<div className = 'header-image' />
<div className = 'header-text'>
<h1 className = 'header-text-title'>
{ t('welcomepage.title') }
</h1>
<p className = 'header-text-description'>
{ t('welcomepage.appDescription',
{ app: APP_NAME }) }
</p>
</div>
<div id = 'enter_room'>
<form
className = 'enter-room-input'
onSubmit = { this._onFormSubmit }>
<FieldTextStateless
<div className = 'header-image' />
<div className = 'header-text'>
<h1 className = 'header-text-title'>
{ t('welcomepage.title') }
</h1>
<p className = 'header-text-description'>
{ t('welcomepage.appDescription',
{ app: APP_NAME }) }
</p>
</div>
<div id = 'enter_room'>
<div className = 'enter-room-input-container'>
<div className = 'enter-room-title'>
{ t('welcomepage.enterRoomTitle') }
</div>
<form onSubmit = { this._onFormSubmit }>
<input
autoFocus = { true }
className = 'enter-room-input'
id = 'enter_room_field'
isLabelHidden = { true }
label = 'enter_room_field'
onChange = { this._onRoomChange }
placeholder = { this.state.roomPlaceholder }
shouldFitContainer = { true }
placeholder
= { this.state.roomPlaceholder }
type = 'text'
value = { this.state.room } />
</form>
<Button
appearance = 'primary'
className = 'welcome-page-button'
id = 'enter_room_button'
onClick = { this._onJoin }
type = 'button'>
{ t('welcomepage.go') }
</Button>
</div>
{ this._renderTabs() }
<div
className = 'welcome-page-button'
id = 'enter_room_button'
onClick = { this._onJoin }>
{ t('welcomepage.go') }
</div>
</div>
{ showAdditionalContent
? <div
className = 'welcome-page-content'
ref = { this._setAdditionalContentRef } />
: null }
{ this._renderTabs() }
</div>
</AtlasKitThemeProvider>
{ showAdditionalContent
? <div
className = 'welcome-page-content'
ref = { this._setAdditionalContentRef } />
: null }
</div>
);
}
@ -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: <RecentList />,
defaultSelected: !CalendarList
content: <RecentList />
});
return (
<div className = 'tab-container' >
<div className = 'welcome-page-settings'>
<SettingsButton defaultTab = { SETTINGS_TABS.CALENDAR } />
</div>
<Tabs
onSelect = { this._onTabSelected }
selected = { this.state.selectedTab }
tabs = { tabs } />
</div>);
<Tabs
onSelect = { this._onTabSelected }
selected = { this.state.selectedTab }
tabs = { tabs } />);
}
/**