feat(welcome-page): Redesign. (#3559)
* feat(welcome-page): Redesign. * Style adjustments.
This commit is contained in:
parent
62b6737a3f
commit
b30008e3a5
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ class CalendarList extends AbstractPage<Props> {
|
|||
CalendarListContent
|
||||
? <CalendarListContent
|
||||
disabled = { disabled }
|
||||
renderListEmptyComponent
|
||||
listEmptyComponent
|
||||
= { this._getRenderListEmptyComponent() } />
|
||||
: null
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: [
|
|
@ -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;
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
|
@ -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));
|
|
@ -0,0 +1 @@
|
|||
export default {};
|
|
@ -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');
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 } />);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue