[WiP] Calendar integration ui (#3395)

Calendar integration ui
This commit is contained in:
yanas 2018-08-27 10:13:59 -05:00 committed by GitHub
parent d62974b433
commit f2cb15ba44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 719 additions and 314 deletions

View File

@ -13,20 +13,31 @@
float: left;
}
.navigate-section-list-tile {
height: 90px;
width: 260px;
border-radius: 4px;
background-color: #1754A9;
border-radius: 4px;
box-sizing: border-box;
display: inline-flex;
height: 100px;
margin-bottom: 8px;
margin-right: 8px;
padding: 16px;
display: inline-block;
box-sizing: border-box;
cursor: pointer;
width: 100%;
&.with-click-handler {
cursor: pointer;
}
&.with-click-handler:hover {
background-color: #1a5dbb;
}
}
.navigate-section-tile-body {
@extend %navigate-section-list-tile-text;
font-weight: normal;
}
.navigate-section-list-tile-info {
flex: 1;
}
.navigate-section-tile-title {
@extend %navigate-section-list-tile-text;
font-weight: bold;
@ -40,4 +51,8 @@
position: relative;
margin-top: 36px;
margin-bottom: 36px;
width: 100%;
}
.navigate-section-list-empty {
text-align: center;
}

View File

@ -45,6 +45,7 @@ body.welcome-page {
font-size: 1rem;
font-weight: 400;
line-height: 24px;
margin-bottom: 20px;
}
#enter_room {
@ -62,12 +63,30 @@ body.welcome-page {
width: 100%;
}
}
.tab-container {
font-size: 16px;
position: relative;
text-align: left;
width: 650px;
}
}
.welcome-page-button {
font-size: 16px;
}
.welcome-page-settings {
color: $welcomePageDescriptionColor;
position: absolute;
right: 10px;
z-index: $zindex2;
* {
cursor: pointer;
}
}
.welcome-watermark {
position: absolute;
width: 100%;

View File

@ -52,7 +52,7 @@ var interfaceConfig = {
'tileview'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
// Determines how the video would fit the screen. 'both' would fit the whole
// screen, 'height' would fit the original video height to the height of the

View File

@ -57,6 +57,8 @@
"video": "Video"
},
"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",
"go": "GO",
"join": "JOIN",
"privacy": "Privacy",
@ -639,13 +641,14 @@
"startWithVideoMuted": "Start with video muted"
},
"calendarSync": {
"later": "Later",
"next": "Upcoming",
"addMeetingURL": "Add a meeting link",
"today": "Today",
"nextMeeting": "next meeting",
"now": "Now",
"noEvents": "There are no upcoming events scheduled.",
"ongoingMeeting": "ongoing meeting",
"permissionButton": "Open settings",
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
"permissionMessage": "The Calendar permission is required to see your meetings in the app.",
"refresh": "Refresh calendar"
},
"recentList": {
"joinPastMeeting": "Join A Past Meeting"

View File

@ -12,6 +12,11 @@ export type Item = {
*/
colorBase: string,
/**
* An optional react element to append to the end of the Item.
*/
elementAfter?: ?ComponentType<any>,
/**
* Item title
*/

View File

@ -87,7 +87,7 @@ class NavigateSectionList extends Component<Props> {
*/
render() {
const {
renderListEmptyComponent = this._renderListEmptyComponent,
renderListEmptyComponent = this._renderListEmptyComponent(),
sections
} = this.props;
@ -128,11 +128,13 @@ class NavigateSectionList extends Component<Props> {
* @returns {Function}
*/
_onPress(url) {
return () => {
const { disabled, onPress } = this.props;
const { disabled, onPress } = this.props;
!disabled && url && typeof onPress === 'function' && onPress(url);
};
if (!disabled && url && typeof onPress === 'function') {
return () => onPress(url);
}
return null;
}
_onRefresh: () => void;

View File

@ -6,18 +6,37 @@ import Container from './Container';
import Text from './Text';
import type { Item } from '../../Types';
/**
* The type of the React {@code Component} props of
* {@link NavigateSectionListItem}.
*/
type Props = {
/**
* The icon to use for the action button.
*/
actionIconName: string,
/**
* The function to call when the action button is clicked.
*/
actionOnClick: ?Function,
/**
* The tooltip to attach to the action button of this list item.
*/
actionTooltip: string,
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
onPress: Function,
onPress: ?Function,
/**
* A item containing data to be rendered
*/
item: Item
}
};
/**
* Implements a React/Web {@link Component} for displaying an item in a
@ -25,14 +44,16 @@ type Props = {
*
* @extends Component
*/
export default class NavigateSectionListItem extends Component<Props> {
export default class NavigateSectionListItem<P: Props>
extends Component<P> {
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
const { lines, title } = this.props.item;
const { elementAfter, lines, title } = this.props.item;
const { onPress } = this.props;
/**
@ -52,22 +73,28 @@ export default class NavigateSectionListItem extends Component<Props> {
duration = lines[1];
}
const rootClassName = `navigate-section-list-tile ${
onPress ? 'with-click-handler' : 'without-click-handler'}`;
return (
<Container
className = 'navigate-section-list-tile'
className = { rootClassName }
onClick = { onPress }>
<Text
className = 'navigate-section-tile-title'>
{ title }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ date }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ duration }
</Text>
<Container className = 'navigate-section-list-tile-info'>
<Text
className = 'navigate-section-tile-title'>
{ title }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ date }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ duration }
</Text>
</Container>
{ elementAfter || null }
</Container>
);
}

View File

@ -7,6 +7,11 @@ import type { Section } from '../../Types';
type Props = {
/**
* Rendered when the list is empty. Should be a rendered element.
*/
ListEmptyComponent: Object,
/**
* Used to extract a unique key for a given item at the specified index.
* Key is used for caching and as the react key to track item re-ordering.
@ -49,6 +54,7 @@ export default class SectionList extends Component<Props> {
*/
render() {
const {
ListEmptyComponent,
renderSectionHeader,
renderItem,
sections,
@ -56,30 +62,32 @@ export default class SectionList extends Component<Props> {
} = this.props;
/**
* If there are no recent items we dont want to display anything
* If there are no recent items we don't want to display anything
*/
if (sections) {
return (
<Container
className = 'navigate-section-list'>
{
sections.map((section, sectionIndex) => (
<Container
key = { sectionIndex }>
{ renderSectionHeader(section) }
{ section.data
.map((item, listIndex) => {
const listItem = {
item
};
sections.length === 0
? ListEmptyComponent
: sections.map((section, sectionIndex) => (
<Container
key = { sectionIndex }>
{ renderSectionHeader(section) }
{ section.data
.map((item, listIndex) => {
const listItem = {
item
};
return renderItem(listItem,
keyExtractor(section,
listIndex));
}) }
</Container>
)
)
return renderItem(listItem,
keyExtractor(section,
listIndex));
}) }
</Container>
)
)
}
</Container>
);

View File

@ -74,3 +74,16 @@ export const SET_CALENDAR_AUTH_STATE = Symbol('SET_CALENDAR_AUTH_STATE');
* @public
*/
export const SET_CALENDAR_PROFILE_EMAIL = Symbol('SET_CALENDAR_PROFILE_EMAIL');
/**
* The type of Redux action which denotes whether a request is in flight to get
* updated calendar events.
*
* {
* type: SET_LOADING_CALENDAR_EVENTS,
* isLoadingEvents: string
* }
* @public
*/
export const SET_LOADING_CALENDAR_EVENTS
= Symbol('SET_LOADING_CALENDAR_EVENTS');

View File

@ -9,7 +9,8 @@ import {
SET_CALENDAR_AUTHORIZATION,
SET_CALENDAR_EVENTS,
SET_CALENDAR_INTEGRATION,
SET_CALENDAR_PROFILE_EMAIL
SET_CALENDAR_PROFILE_EMAIL,
SET_LOADING_CALENDAR_EVENTS
} from './actionTypes';
import { _getCalendarIntegration, isCalendarEnabled } from './functions';
import { generateRoomWithoutSeparator } from '../welcome';
@ -173,6 +174,23 @@ export function setCalendarProfileEmail(newEmail: ?string) {
};
}
/**
* Sends an to denote a request in is flight to get calendar events.
*
* @param {boolean} isLoadingEvents - Whether or not calendar events are being
* fetched.
* @returns {{
* type: SET_LOADING_CALENDAR_EVENTS,
* isLoadingEvents: boolean
* }}
*/
export function setLoadingCalendarEvents(isLoadingEvents: boolean) {
return {
type: SET_LOADING_CALENDAR_EVENTS,
isLoadingEvents
};
}
/**
* Sets the calendar integration type to be used by web and signals that the
* integration is ready to be used.
@ -211,6 +229,7 @@ export function signIn(calendarType: string): Function {
.then(() => dispatch(integration.signIn()))
.then(() => dispatch(setIntegrationReady(calendarType)))
.then(() => dispatch(updateProfile(calendarType)))
.then(() => dispatch(refreshCalendar()))
.catch(error => {
logger.error(
'Error occurred while signing into calendar integration',

View File

@ -1,28 +1,23 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { appNavigate } from '../../app';
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { openSettings } from '../../mobile/permissions';
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import styles from './styles';
import AddMeetingUrlButton from './AddMeetingUrlButton';
/**
* The tyoe of the React {@code Component} props of {@link MeetingList}.
* The type of the React {@code Component} props of
* {@link AbstractCalendarList}.
*/
type Props = {
/**
* The current state of the calendar access permission.
*/
_authorization: ?string,
/**
* The calendar event list.
*/
@ -38,6 +33,11 @@ type Props = {
*/
dispatch: Function,
/**
*
*/
renderListEmptyComponent: Function,
/**
* The translate function.
*/
@ -45,9 +45,9 @@ type Props = {
};
/**
* Component to display a list of events from the (mobile) user's calendar.
* Component to display a list of events from a connected calendar.
*/
class MeetingList extends Component<Props> {
class AbstractCalendarList extends Component<Props> {
/**
* Default values for the component's props.
*/
@ -75,7 +75,7 @@ class MeetingList extends Component<Props> {
}
/**
* Initializes a new {@code MeetingList} instance.
* Initializes a new {@code CalendarList} instance.
*
* @inheritdoc
*/
@ -83,13 +83,12 @@ class MeetingList extends Component<Props> {
super(props);
// Bind event handlers so they are only bound once per instance.
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._toDateString = this._toDateString.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
this._toDisplayableList = this._toDisplayableList.bind(this);
this._toTimeString = this._toTimeString.bind(this);
}
/**
@ -98,7 +97,7 @@ class MeetingList extends Component<Props> {
* @inheritdoc
*/
render() {
const { disabled } = this.props;
const { disabled, renderListEmptyComponent } = this.props;
return (
<NavigateSectionList
@ -106,46 +105,11 @@ class MeetingList extends Component<Props> {
onPress = { this._onPress }
onRefresh = { this._onRefresh }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() }
= { renderListEmptyComponent }
sections = { this._toDisplayableList() } />
);
}
_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 { _authorization, t } = this.props;
// If we don't provide a list specific renderListEmptyComponent, then
// the default empty component of the NavigateSectionList will be
// rendered, which (atm) is a simple "Pull to refresh" message.
if (_authorization !== 'denied') {
return undefined;
}
return (
<View style = { styles.noPermissionMessageView }>
<Text style = { styles.noPermissionMessageText }>
{ t('calendarSync.permissionMessage') }
</Text>
<TouchableOpacity
onPress = { openSettings }
style = { styles.noPermissionMessageButton } >
<Text style = { styles.noPermissionMessageButtonText }>
{ t('calendarSync.permissionButton') }
</Text>
</TouchableOpacity>
</View>
);
}
_onPress: string => Function;
/**
@ -174,7 +138,7 @@ class MeetingList extends Component<Props> {
_toDateString: Object => string;
/**
* Generates a date (interval) string for a given event.
* Generates a date string for a given event.
*
* @param {Object} event - The event.
* @private
@ -182,11 +146,9 @@ class MeetingList extends Component<Props> {
*/
_toDateString(event) {
const startDateTime
= getLocalizedDateFormatter(event.startDate).format('lll');
const endTime
= getLocalizedDateFormatter(event.endDate).format('LT');
= getLocalizedDateFormatter(event.startDate).format('MMM Do, YYYY');
return `${startDateTime} - ${endTime}`;
return `${startDateTime}`;
}
_toDisplayableItem: Object => Object;
@ -200,10 +162,15 @@ class MeetingList extends Component<Props> {
*/
_toDisplayableItem(event) {
return {
elementAfter: event.url ? undefined : (
<AddMeetingUrlButton
calendarId = { event.calendarId }
eventId = { event.id } />
),
key: `${event.id}-${event.startDate}`,
lines: [
event.url,
this._toDateString(event)
this._toTimeString(event)
],
title: event.title,
url: event.url
@ -221,39 +188,60 @@ class MeetingList extends Component<Props> {
_toDisplayableList() {
const { _eventList, t } = this.props;
const now = Date.now();
const now = new Date();
const { createSection } = NavigateSectionList;
const nowSection = createSection(t('calendarSync.now'), 'now');
const nextSection = createSection(t('calendarSync.next'), 'next');
const laterSection = createSection(t('calendarSync.later'), 'later');
const TODAY_SECTION = 'today';
const sectionMap = new Map();
for (const event of _eventList) {
const displayableEvent = this._toDisplayableItem(event);
const startDate = new Date(event.startDate).getDate();
if (event.startDate < now && event.endDate > now) {
nowSection.data.push(displayableEvent);
} else if (event.startDate > now) {
if (nextSection.data.length
&& nextSection.data[0].startDate !== event.startDate) {
laterSection.data.push(displayableEvent);
} else {
nextSection.data.push(displayableEvent);
if (startDate === now.getDate()) {
let todaySection = sectionMap.get(TODAY_SECTION);
if (!todaySection) {
todaySection
= createSection(t('calendarSync.today'), TODAY_SECTION);
sectionMap.set(TODAY_SECTION, todaySection);
}
todaySection.data.push(displayableEvent);
} else if (sectionMap.has(startDate)) {
const section = sectionMap.get(startDate);
if (section) {
section.data.push(displayableEvent);
}
} else {
const newSection
= createSection(this._toDateString(event), startDate);
sectionMap.set(startDate, newSection);
newSection.data.push(displayableEvent);
}
}
const sectionList = [];
return Array.from(sectionMap.values());
}
for (const section of [
nowSection,
nextSection,
laterSection
]) {
section.data.length && sectionList.push(section);
}
_toTimeString: Object => string;
return sectionList;
/**
* Generates a time (interval) string for a given event.
*
* @param {Object} event - The event.
* @private
* @returns {string}
*/
_toTimeString(event) {
const startDateTime
= getLocalizedDateFormatter(event.startDate).format('lll');
const endTime
= getLocalizedDateFormatter(event.endDate).format('LT');
return `${startDateTime} - ${endTime}`;
}
}
@ -262,19 +250,15 @@ class MeetingList extends Component<Props> {
*
* @param {Object} state - The redux state.
* @returns {{
* _authorization: ?string,
* _eventList: Array<Object>
* }}
*/
function _mapStateToProps(state: Object) {
const { authorization, events } = state['features/calendar-sync'];
return {
_authorization: authorization,
_eventList: events
_eventList: state['features/calendar-sync'].events
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(MeetingList))
? translate(connect(_mapStateToProps)(AbstractCalendarList))
: undefined;

View File

@ -0,0 +1,23 @@
// @flow
import { Component } from 'react';
/**
* A React Component for adding a meeting URL to an existing calendar meeting.
*
* @extends Component
*/
class AddMeetingUrlButton extends Component<*> {
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
// Not yet implemented.
return null;
}
}
export default AddMeetingUrlButton;

View File

@ -0,0 +1,86 @@
// @flow
import Button from '@atlaskit/button';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { updateCalendarEvent } from '../actions';
/**
* The type of the React {@code Component} props of {@link AddMeetingUrlButton}.
*/
type Props = {
/**
* The calendar ID associated with the calendar event.
*/
calendarId: string,
/**
* Invoked to add a meeting URL to a calendar event.
*/
dispatch: Dispatch<*>,
/**
* The ID of the calendar event that will have a meeting URL added on click.
*/
eventId: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* A React Component for adding a meeting URL to an existing calendar event.
*
* @extends Component
*/
class AddMeetingUrlButton extends Component<Props> {
/**
* Initializes a new {@code AddMeetingUrlButton} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
return (
<Button
appearance = 'primary'
onClick = { this._onClick }
type = 'button'>
{ this.props.t('calendarSync.addMeetingURL') }
</Button>
);
}
_onClick: () => void;
/**
* Dispatches an action to adding a meeting URL to a calendar event.
*
* @returns {void}
*/
_onClick() {
const { calendarId, dispatch, eventId } = this.props;
dispatch(updateCalendarEvent(eventId, calendarId));
}
}
export default translate(connect()(AddMeetingUrlButton));

View File

@ -0,0 +1,126 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { openSettings } from '../../mobile/permissions';
import { translate } from '../../base/i18n';
import { isCalendarEnabled } from '../functions';
import styles from './styles';
import AbstractCalendarList from './AbstractCalendarList';
/**
* The tyoe of the React {@code Component} props of {@link CalendarList}.
*/
type Props = {
/**
* The current state of the calendar access permission.
*/
_authorization: ?string,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The translate function.
*/
t: Function
};
/**
* Component to display a list of events from the (mobile) user's calendar.
*/
class CalendarList extends Component<Props> {
/**
* Initializes a new {@code CalendarList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
return (
AbstractCalendarList
? <AbstractCalendarList
disabled = { disabled }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() } />
: null
);
}
_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 { _authorization, t } = this.props;
// If we don't provide a list specific renderListEmptyComponent, then
// the default empty component of the NavigateSectionList will be
// rendered, which (atm) is a simple "Pull to refresh" message.
if (_authorization !== 'denied') {
return undefined;
}
return (
<View style = { styles.noPermissionMessageView }>
<Text style = { styles.noPermissionMessageText }>
{ t('calendarSync.permissionMessage') }
</Text>
<TouchableOpacity
onPress = { openSettings }
style = { styles.noPermissionMessageButton } >
<Text style = { styles.noPermissionMessageButtonText }>
{ t('calendarSync.permissionButton') }
</Text>
</TouchableOpacity>
</View>
);
}
}
/**
* Maps redux state to component props.
*
* @param {Object} state - The redux state.
* @returns {{
* _authorization: ?string,
* _eventList: Array<Object>
* }}
*/
function _mapStateToProps(state: Object) {
const { authorization } = state['features/calendar-sync'];
return {
_authorization: authorization
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(CalendarList))
: undefined;

View File

@ -0,0 +1,194 @@
// @flow
import Button from '@atlaskit/button';
import Spinner from '@atlaskit/spinner';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { openSettingsDialog, SETTINGS_TABS } from '../../settings';
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import AbstractCalendarList from './AbstractCalendarList';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link CalendarList}.
*/
type Props = {
/**
* Whether or not a calendar may be connected for fetching calendar events.
*/
_hasIntegrationSelected: boolean,
/**
* Whether or not events have been fetched from a calendar.
*/
_hasLoadedEvents: boolean,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The translate function.
*/
t: Function
};
/**
* Component to display a list of events from the user's calendar.
*/
class CalendarList extends Component<Props> {
/**
* Initializes a new {@code CalendarList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
this._onOpenSettings = this._onOpenSettings.bind(this);
this._onRefreshEvents = this._onRefreshEvents.bind(this);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
return (
AbstractCalendarList
? <AbstractCalendarList
disabled = { disabled }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() } />
: null
);
}
_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 { _hasIntegrationSelected, _hasLoadedEvents, t } = this.props;
if (_hasIntegrationSelected && _hasLoadedEvents) {
return (
<div className = 'navigate-section-list-empty'>
<div>{ t('calendarSync.noEvents') }</div>
<Button
appearance = 'primary'
className = 'calendar-button'
id = 'connect_calendar_button'
onClick = { this._onRefreshEvents }
type = 'button'>
{ t('calendarSync.refresh') }
</Button>
</div>
);
} else if (_hasIntegrationSelected && !_hasLoadedEvents) {
return (
<div className = 'navigate-section-list-empty'>
<Spinner
invertColor = { true }
isCompleting = { false }
size = 'medium' />
</div>
);
}
return (
<div className = 'navigate-section-list-empty'>
<p className = 'header-text-description'>
{ t('welcomepage.connectCalendarText', {
app: interfaceConfig.APP_NAME
}) }
</p>
<Button
appearance = 'primary'
className = 'calendar-button'
id = 'connect_calendar_button'
onClick = { this._onOpenSettings }
type = 'button'>
{ t('welcomepage.connectCalendarButton') }
</Button>
</div>
);
}
_onOpenSettings: () => void;
/**
* Opens {@code SettingsDialog}.
*
* @private
* @returns {void}
*/
_onOpenSettings() {
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
}
_onRefreshEvents: () => void;
/**
* Gets an updated list of calendar events.
*
* @private
* @returns {void}
*/
_onRefreshEvents() {
this.props.dispatch(refreshCalendar(true));
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code CalendarList} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _hasIntegrationSelected: boolean,
* _hasLoadedEvents: boolean
* }}
*/
function _mapStateToProps(state) {
const {
events,
integrationType,
isLoadingEvents
} = state['features/calendar-sync'];
return {
_hasIntegrationSelected: Boolean(integrationType),
_hasLoadedEvents: Boolean(events) || !isLoadingEvents
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(CalendarList))
: undefined;

View File

@ -1,3 +1,3 @@
export { default as ConferenceNotification } from './ConferenceNotification';
export { default as MeetingList } from './MeetingList';
export { default as CalendarList } from './CalendarList';
export { default as MicrosoftSignInButton } from './MicrosoftSignInButton';

View File

@ -4,7 +4,7 @@ const NOTIFICATION_SIZE = 55;
/**
* The styles of the React {@code Component}s of the feature meeting-list i.e.
* {@code MeetingList}.
* {@code CalendarList}.
*/
export default createStyleSheet({

View File

@ -1,5 +1,6 @@
// @flow
import { setLoadingCalendarEvents } from './actions';
export * from './functions.any';
import {
@ -56,6 +57,8 @@ export function _fetchCalendarEntries(
return;
}
dispatch(setLoadingCalendarEvents(true));
dispatch(integration.load())
.then(() => dispatch(integration._isSignedIn()))
.then(signedIn => {
@ -72,7 +75,8 @@ export function _fetchCalendarEntries(
getState
}, events))
.catch(error =>
logger.error('Error fetching calendar.', error));
logger.error('Error fetching calendar.', error))
.then(() => dispatch(setLoadingCalendarEvents(false)));
}
/**

View File

@ -10,7 +10,8 @@ import {
SET_CALENDAR_AUTHORIZATION,
SET_CALENDAR_EVENTS,
SET_CALENDAR_INTEGRATION,
SET_CALENDAR_PROFILE_EMAIL
SET_CALENDAR_PROFILE_EMAIL,
SET_LOADING_CALENDAR_EVENTS
} from './actionTypes';
import { isCalendarEnabled } from './functions';
@ -96,6 +97,9 @@ isCalendarEnabled()
case SET_CALENDAR_PROFILE_EMAIL:
return set(state, 'profileEmail', action.email);
case SET_LOADING_CALENDAR_EVENTS:
return set(state, 'isLoadingEvents', action.isLoadingEvents);
}
return state;

View File

@ -1,159 +0,0 @@
import { createStyleSheet, BoxModel } from '../../base/styles';
const AVATAR_OPACITY = 0.4;
const AVATAR_SIZE = 65;
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
/**
* The styles of the React {@code Component}s of the feature recent-list i.e.
* {@code RecentList}.
*/
export default createStyleSheet({
/**
* The style of the actual avatar.
*/
avatar: {
alignItems: 'center',
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
borderRadius: AVATAR_SIZE,
height: AVATAR_SIZE,
justifyContent: 'center',
width: AVATAR_SIZE
},
/**
* The style of the avatar container that makes the avatar rounded.
*/
avatarContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-around',
paddingTop: 5
},
/**
* Simple {@code Text} content of the avatar (the actual initials).
*/
avatarContent: {
backgroundColor: 'rgba(0, 0, 0, 0)',
color: OVERLAY_FONT_COLOR,
fontSize: 32,
fontWeight: '100',
textAlign: 'center'
},
/**
* List of styles of the avatar of a remote meeting (not the default
* server). The number of colors are limited because they should match
* nicely.
*/
avatarRemoteServer1: {
backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})`
},
avatarRemoteServer2: {
backgroundColor: `rgba(255, 198, 115, ${AVATAR_OPACITY})`
},
avatarRemoteServer3: {
backgroundColor: `rgba(128, 128, 255, ${AVATAR_OPACITY})`
},
avatarRemoteServer4: {
backgroundColor: `rgba(105, 232, 194, ${AVATAR_OPACITY})`
},
avatarRemoteServer5: {
backgroundColor: `rgba(234, 255, 128, ${AVATAR_OPACITY})`
},
/**
* The style of the conference length (if rendered).
*/
confLength: {
color: OVERLAY_FONT_COLOR,
fontWeight: 'normal'
},
/**
* The top level container style of the list.
*/
container: {
flex: 1
},
/**
* Shows the container disabled.
*/
containerDisabled: {
opacity: 0.2
},
/**
* Second line of the list (date). May be extended with server name later.
*/
date: {
color: OVERLAY_FONT_COLOR
},
/**
* The style of the details container (right side) of the list.
*/
detailsContainer: {
alignItems: 'flex-start',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
marginLeft: 2 * BoxModel.margin
},
/**
* The container for an info line with an inline icon.
*/
infoWithIcon: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'flex-start'
},
/**
* The style of an inline icon in an info line.
*/
inlineIcon: {
color: OVERLAY_FONT_COLOR,
marginRight: 5
},
/**
* First line of the list (room name).
*/
roomName: {
color: OVERLAY_FONT_COLOR,
fontSize: 18,
fontWeight: 'bold'
},
/**
* The style of one single row in the list.
*/
row: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
padding: 8,
paddingBottom: 0
},
/**
* The style of the server name component (if rendered).
*/
serverName: {
color: OVERLAY_FONT_COLOR,
fontWeight: 'normal'
}
});

View File

@ -2,15 +2,17 @@
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';
import { DialogContainer } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { Watermarks } from '../../base/react';
import { Platform, Watermarks } from '../../base/react';
import { CalendarList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
import { openSettingsDialog } from '../../settings';
import { SettingsButton } from '../../settings';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
@ -66,7 +68,6 @@ class WelcomePage extends AbstractWelcomePage {
// Bind event handlers so they are only bound once per instance.
this._onFormSubmit = this._onFormSubmit.bind(this);
this._onOpenSettings = this._onOpenSettings.bind(this);
this._onRoomChange = this._onRoomChange.bind(this);
this._setAdditionalContentRef
= this._setAdditionalContentRef.bind(this);
@ -159,7 +160,7 @@ class WelcomePage extends AbstractWelcomePage {
{ t('welcomepage.go') }
</Button>
</div>
<RecentList />
{ this._renderTabs() }
</div>
{ showAdditionalContent
? <div
@ -187,16 +188,6 @@ class WelcomePage extends AbstractWelcomePage {
this._onJoin();
}
/**
* Opens {@code SettingsDialog}.
*
* @private
* @returns {void}
*/
_onOpenSettings() {
this.props.dispatch(openSettingsDialog());
}
/**
* Overrides the super to account for the differences in the argument types
* provided by HTML and React Native text inputs.
@ -211,6 +202,47 @@ class WelcomePage extends AbstractWelcomePage {
super._onRoomChange(event.target.value);
}
/**
* Renders tabs to show previous meetings and upcoming calendar events. The
* tabs are purposefully hidden on mobile browsers.
*
* @returns {ReactElement|null}
*/
_renderTabs() {
const isMobileBrowser
= Platform.OS === 'android' || Platform.OS === 'ios';
if (isMobileBrowser) {
return null;
}
const { t } = this.props;
const tabs = [];
if (CalendarList) {
tabs.push({
label: t('welcomepage.calendar'),
content: <CalendarList />,
defaultSelected: true
});
}
tabs.push({
label: t('welcomepage.recentList'),
content: <RecentList />,
defaultSelected: !CalendarList
});
return (
<div className = 'tab-container' >
<div className = 'welcome-page-settings'>
<SettingsButton />
</div>
<Tabs tabs = { tabs } />
</div>);
}
/**
* Sets the internal reference to the HTMLDivElement used to hold the
* welcome page content.

View File

@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { PagedList } from '../../base/react';
import { MeetingList } from '../../calendar-sync';
import { CalendarList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
import { setWelcomePageListsDefaultPage } from '../actions';
@ -82,7 +82,7 @@ class WelcomePageLists extends Component<Props> {
title: t('welcomepage.recentList')
},
{
component: MeetingList,
component: CalendarList,
icon: android ? 'event_note' : IOS_CALENDAR_ICON,
title: t('welcomepage.calendar')
}