parent
d62974b433
commit
f2cb15ba44
|
@ -13,20 +13,31 @@
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
.navigate-section-list-tile {
|
.navigate-section-list-tile {
|
||||||
height: 90px;
|
|
||||||
width: 260px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #1754A9;
|
background-color: #1754A9;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
height: 100px;
|
||||||
|
margin-bottom: 8px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: inline-block;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
|
||||||
|
&.with-click-handler {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.with-click-handler:hover {
|
||||||
|
background-color: #1a5dbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
.navigate-section-tile-body {
|
.navigate-section-tile-body {
|
||||||
@extend %navigate-section-list-tile-text;
|
@extend %navigate-section-list-tile-text;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
.navigate-section-list-tile-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
.navigate-section-tile-title {
|
.navigate-section-tile-title {
|
||||||
@extend %navigate-section-list-tile-text;
|
@extend %navigate-section-list-tile-text;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -40,4 +51,8 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 36px;
|
margin-top: 36px;
|
||||||
margin-bottom: 36px;
|
margin-bottom: 36px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.navigate-section-list-empty {
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ body.welcome-page {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#enter_room {
|
#enter_room {
|
||||||
|
@ -62,12 +63,30 @@ body.welcome-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-container {
|
||||||
|
font-size: 16px;
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
width: 650px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-page-button {
|
.welcome-page-button {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.welcome-page-settings {
|
||||||
|
color: $welcomePageDescriptionColor;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
z-index: $zindex2;
|
||||||
|
|
||||||
|
* {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.welcome-watermark {
|
.welcome-watermark {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -52,7 +52,7 @@ var interfaceConfig = {
|
||||||
'tileview'
|
'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
|
// 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
|
// screen, 'height' would fit the original video height to the height of the
|
||||||
|
|
|
@ -57,6 +57,8 @@
|
||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"calendar": "Calendar",
|
"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",
|
"go": "GO",
|
||||||
"join": "JOIN",
|
"join": "JOIN",
|
||||||
"privacy": "Privacy",
|
"privacy": "Privacy",
|
||||||
|
@ -639,13 +641,14 @@
|
||||||
"startWithVideoMuted": "Start with video muted"
|
"startWithVideoMuted": "Start with video muted"
|
||||||
},
|
},
|
||||||
"calendarSync": {
|
"calendarSync": {
|
||||||
"later": "Later",
|
"addMeetingURL": "Add a meeting link",
|
||||||
"next": "Upcoming",
|
"today": "Today",
|
||||||
"nextMeeting": "next meeting",
|
"nextMeeting": "next meeting",
|
||||||
"now": "Now",
|
"noEvents": "There are no upcoming events scheduled.",
|
||||||
"ongoingMeeting": "ongoing meeting",
|
"ongoingMeeting": "ongoing meeting",
|
||||||
"permissionButton": "Open settings",
|
"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": {
|
"recentList": {
|
||||||
"joinPastMeeting": "Join A Past Meeting"
|
"joinPastMeeting": "Join A Past Meeting"
|
||||||
|
|
|
@ -12,6 +12,11 @@ export type Item = {
|
||||||
*/
|
*/
|
||||||
colorBase: string,
|
colorBase: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional react element to append to the end of the Item.
|
||||||
|
*/
|
||||||
|
elementAfter?: ?ComponentType<any>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item title
|
* Item title
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -87,7 +87,7 @@ class NavigateSectionList extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
renderListEmptyComponent = this._renderListEmptyComponent,
|
renderListEmptyComponent = this._renderListEmptyComponent(),
|
||||||
sections
|
sections
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -128,11 +128,13 @@ class NavigateSectionList extends Component<Props> {
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
_onPress(url) {
|
_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;
|
_onRefresh: () => void;
|
||||||
|
|
|
@ -6,18 +6,37 @@ import Container from './Container';
|
||||||
import Text from './Text';
|
import Text from './Text';
|
||||||
import type { Item } from '../../Types';
|
import type { Item } from '../../Types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@code Component} props of
|
||||||
|
* {@link NavigateSectionListItem}.
|
||||||
|
*/
|
||||||
type Props = {
|
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.
|
* 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
|
* A item containing data to be rendered
|
||||||
*/
|
*/
|
||||||
item: Item
|
item: Item
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React/Web {@link Component} for displaying an item in a
|
* Implements a React/Web {@link Component} for displaying an item in a
|
||||||
|
@ -25,14 +44,16 @@ type Props = {
|
||||||
*
|
*
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
export default class NavigateSectionListItem extends Component<Props> {
|
export default class NavigateSectionListItem<P: Props>
|
||||||
|
extends Component<P> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the content of this component.
|
* Renders the content of this component.
|
||||||
*
|
*
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { lines, title } = this.props.item;
|
const { elementAfter, lines, title } = this.props.item;
|
||||||
const { onPress } = this.props;
|
const { onPress } = this.props;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,10 +73,14 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||||
duration = lines[1];
|
duration = lines[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rootClassName = `navigate-section-list-tile ${
|
||||||
|
onPress ? 'with-click-handler' : 'without-click-handler'}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className = 'navigate-section-list-tile'
|
className = { rootClassName }
|
||||||
onClick = { onPress }>
|
onClick = { onPress }>
|
||||||
|
<Container className = 'navigate-section-list-tile-info'>
|
||||||
<Text
|
<Text
|
||||||
className = 'navigate-section-tile-title'>
|
className = 'navigate-section-tile-title'>
|
||||||
{ title }
|
{ title }
|
||||||
|
@ -69,6 +94,8 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||||
{ duration }
|
{ duration }
|
||||||
</Text>
|
</Text>
|
||||||
</Container>
|
</Container>
|
||||||
|
{ elementAfter || null }
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,11 @@ import type { Section } from '../../Types';
|
||||||
|
|
||||||
type Props = {
|
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.
|
* 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.
|
* 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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
ListEmptyComponent,
|
||||||
renderSectionHeader,
|
renderSectionHeader,
|
||||||
renderItem,
|
renderItem,
|
||||||
sections,
|
sections,
|
||||||
|
@ -56,14 +62,16 @@ export default class SectionList extends Component<Props> {
|
||||||
} = this.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) {
|
if (sections) {
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className = 'navigate-section-list'>
|
className = 'navigate-section-list'>
|
||||||
{
|
{
|
||||||
sections.map((section, sectionIndex) => (
|
sections.length === 0
|
||||||
|
? ListEmptyComponent
|
||||||
|
: sections.map((section, sectionIndex) => (
|
||||||
<Container
|
<Container
|
||||||
key = { sectionIndex }>
|
key = { sectionIndex }>
|
||||||
{ renderSectionHeader(section) }
|
{ renderSectionHeader(section) }
|
||||||
|
|
|
@ -74,3 +74,16 @@ export const SET_CALENDAR_AUTH_STATE = Symbol('SET_CALENDAR_AUTH_STATE');
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const SET_CALENDAR_PROFILE_EMAIL = Symbol('SET_CALENDAR_PROFILE_EMAIL');
|
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');
|
||||||
|
|
|
@ -9,7 +9,8 @@ import {
|
||||||
SET_CALENDAR_AUTHORIZATION,
|
SET_CALENDAR_AUTHORIZATION,
|
||||||
SET_CALENDAR_EVENTS,
|
SET_CALENDAR_EVENTS,
|
||||||
SET_CALENDAR_INTEGRATION,
|
SET_CALENDAR_INTEGRATION,
|
||||||
SET_CALENDAR_PROFILE_EMAIL
|
SET_CALENDAR_PROFILE_EMAIL,
|
||||||
|
SET_LOADING_CALENDAR_EVENTS
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { _getCalendarIntegration, isCalendarEnabled } from './functions';
|
import { _getCalendarIntegration, isCalendarEnabled } from './functions';
|
||||||
import { generateRoomWithoutSeparator } from '../welcome';
|
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
|
* Sets the calendar integration type to be used by web and signals that the
|
||||||
* integration is ready to be used.
|
* integration is ready to be used.
|
||||||
|
@ -211,6 +229,7 @@ export function signIn(calendarType: string): Function {
|
||||||
.then(() => dispatch(integration.signIn()))
|
.then(() => dispatch(integration.signIn()))
|
||||||
.then(() => dispatch(setIntegrationReady(calendarType)))
|
.then(() => dispatch(setIntegrationReady(calendarType)))
|
||||||
.then(() => dispatch(updateProfile(calendarType)))
|
.then(() => dispatch(updateProfile(calendarType)))
|
||||||
|
.then(() => dispatch(refreshCalendar()))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Error occurred while signing into calendar integration',
|
'Error occurred while signing into calendar integration',
|
||||||
|
|
|
@ -1,28 +1,23 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Text, TouchableOpacity, View } from 'react-native';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { appNavigate } from '../../app';
|
import { appNavigate } from '../../app';
|
||||||
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
||||||
import { NavigateSectionList } from '../../base/react';
|
import { NavigateSectionList } from '../../base/react';
|
||||||
import { openSettings } from '../../mobile/permissions';
|
|
||||||
|
|
||||||
import { refreshCalendar } from '../actions';
|
import { refreshCalendar } from '../actions';
|
||||||
import { isCalendarEnabled } from '../functions';
|
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 = {
|
type Props = {
|
||||||
|
|
||||||
/**
|
|
||||||
* The current state of the calendar access permission.
|
|
||||||
*/
|
|
||||||
_authorization: ?string,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The calendar event list.
|
* The calendar event list.
|
||||||
*/
|
*/
|
||||||
|
@ -38,6 +33,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
dispatch: Function,
|
dispatch: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
renderListEmptyComponent: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The translate 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.
|
* 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
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -83,13 +83,12 @@ class MeetingList extends Component<Props> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once per instance.
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._getRenderListEmptyComponent
|
|
||||||
= this._getRenderListEmptyComponent.bind(this);
|
|
||||||
this._onPress = this._onPress.bind(this);
|
this._onPress = this._onPress.bind(this);
|
||||||
this._onRefresh = this._onRefresh.bind(this);
|
this._onRefresh = this._onRefresh.bind(this);
|
||||||
this._toDateString = this._toDateString.bind(this);
|
this._toDateString = this._toDateString.bind(this);
|
||||||
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
||||||
this._toDisplayableList = this._toDisplayableList.bind(this);
|
this._toDisplayableList = this._toDisplayableList.bind(this);
|
||||||
|
this._toTimeString = this._toTimeString.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,7 +97,7 @@ class MeetingList extends Component<Props> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { disabled } = this.props;
|
const { disabled, renderListEmptyComponent } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigateSectionList
|
<NavigateSectionList
|
||||||
|
@ -106,46 +105,11 @@ class MeetingList extends Component<Props> {
|
||||||
onPress = { this._onPress }
|
onPress = { this._onPress }
|
||||||
onRefresh = { this._onRefresh }
|
onRefresh = { this._onRefresh }
|
||||||
renderListEmptyComponent
|
renderListEmptyComponent
|
||||||
= { this._getRenderListEmptyComponent() }
|
= { renderListEmptyComponent }
|
||||||
sections = { this._toDisplayableList() } />
|
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;
|
_onPress: string => Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -174,7 +138,7 @@ class MeetingList extends Component<Props> {
|
||||||
_toDateString: Object => string;
|
_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.
|
* @param {Object} event - The event.
|
||||||
* @private
|
* @private
|
||||||
|
@ -182,11 +146,9 @@ class MeetingList extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
_toDateString(event) {
|
_toDateString(event) {
|
||||||
const startDateTime
|
const startDateTime
|
||||||
= getLocalizedDateFormatter(event.startDate).format('lll');
|
= getLocalizedDateFormatter(event.startDate).format('MMM Do, YYYY');
|
||||||
const endTime
|
|
||||||
= getLocalizedDateFormatter(event.endDate).format('LT');
|
|
||||||
|
|
||||||
return `${startDateTime} - ${endTime}`;
|
return `${startDateTime}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_toDisplayableItem: Object => Object;
|
_toDisplayableItem: Object => Object;
|
||||||
|
@ -200,10 +162,15 @@ class MeetingList extends Component<Props> {
|
||||||
*/
|
*/
|
||||||
_toDisplayableItem(event) {
|
_toDisplayableItem(event) {
|
||||||
return {
|
return {
|
||||||
|
elementAfter: event.url ? undefined : (
|
||||||
|
<AddMeetingUrlButton
|
||||||
|
calendarId = { event.calendarId }
|
||||||
|
eventId = { event.id } />
|
||||||
|
),
|
||||||
key: `${event.id}-${event.startDate}`,
|
key: `${event.id}-${event.startDate}`,
|
||||||
lines: [
|
lines: [
|
||||||
event.url,
|
event.url,
|
||||||
this._toDateString(event)
|
this._toTimeString(event)
|
||||||
],
|
],
|
||||||
title: event.title,
|
title: event.title,
|
||||||
url: event.url
|
url: event.url
|
||||||
|
@ -221,39 +188,60 @@ class MeetingList extends Component<Props> {
|
||||||
_toDisplayableList() {
|
_toDisplayableList() {
|
||||||
const { _eventList, t } = this.props;
|
const { _eventList, t } = this.props;
|
||||||
|
|
||||||
const now = Date.now();
|
const now = new Date();
|
||||||
|
|
||||||
const { createSection } = NavigateSectionList;
|
const { createSection } = NavigateSectionList;
|
||||||
const nowSection = createSection(t('calendarSync.now'), 'now');
|
const TODAY_SECTION = 'today';
|
||||||
const nextSection = createSection(t('calendarSync.next'), 'next');
|
const sectionMap = new Map();
|
||||||
const laterSection = createSection(t('calendarSync.later'), 'later');
|
|
||||||
|
|
||||||
for (const event of _eventList) {
|
for (const event of _eventList) {
|
||||||
const displayableEvent = this._toDisplayableItem(event);
|
const displayableEvent = this._toDisplayableItem(event);
|
||||||
|
const startDate = new Date(event.startDate).getDate();
|
||||||
|
|
||||||
if (event.startDate < now && event.endDate > now) {
|
if (startDate === now.getDate()) {
|
||||||
nowSection.data.push(displayableEvent);
|
let todaySection = sectionMap.get(TODAY_SECTION);
|
||||||
} else if (event.startDate > now) {
|
|
||||||
if (nextSection.data.length
|
if (!todaySection) {
|
||||||
&& nextSection.data[0].startDate !== event.startDate) {
|
todaySection
|
||||||
laterSection.data.push(displayableEvent);
|
= 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 {
|
} else {
|
||||||
nextSection.data.push(displayableEvent);
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sectionList;
|
_toTimeString: Object => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* @param {Object} state - The redux state.
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* _authorization: ?string,
|
|
||||||
* _eventList: Array<Object>
|
* _eventList: Array<Object>
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state: Object) {
|
function _mapStateToProps(state: Object) {
|
||||||
const { authorization, events } = state['features/calendar-sync'];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_authorization: authorization,
|
_eventList: state['features/calendar-sync'].events
|
||||||
_eventList: events
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default isCalendarEnabled()
|
export default isCalendarEnabled()
|
||||||
? translate(connect(_mapStateToProps)(MeetingList))
|
? translate(connect(_mapStateToProps)(AbstractCalendarList))
|
||||||
: undefined;
|
: undefined;
|
|
@ -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;
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,3 +1,3 @@
|
||||||
export { default as ConferenceNotification } from './ConferenceNotification';
|
export { default as ConferenceNotification } from './ConferenceNotification';
|
||||||
export { default as MeetingList } from './MeetingList';
|
export { default as CalendarList } from './CalendarList';
|
||||||
export { default as MicrosoftSignInButton } from './MicrosoftSignInButton';
|
export { default as MicrosoftSignInButton } from './MicrosoftSignInButton';
|
||||||
|
|
|
@ -4,7 +4,7 @@ const NOTIFICATION_SIZE = 55;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The styles of the React {@code Component}s of the feature meeting-list i.e.
|
* The styles of the React {@code Component}s of the feature meeting-list i.e.
|
||||||
* {@code MeetingList}.
|
* {@code CalendarList}.
|
||||||
*/
|
*/
|
||||||
export default createStyleSheet({
|
export default createStyleSheet({
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { setLoadingCalendarEvents } from './actions';
|
||||||
export * from './functions.any';
|
export * from './functions.any';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -56,6 +57,8 @@ export function _fetchCalendarEntries(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch(setLoadingCalendarEvents(true));
|
||||||
|
|
||||||
dispatch(integration.load())
|
dispatch(integration.load())
|
||||||
.then(() => dispatch(integration._isSignedIn()))
|
.then(() => dispatch(integration._isSignedIn()))
|
||||||
.then(signedIn => {
|
.then(signedIn => {
|
||||||
|
@ -72,7 +75,8 @@ export function _fetchCalendarEntries(
|
||||||
getState
|
getState
|
||||||
}, events))
|
}, events))
|
||||||
.catch(error =>
|
.catch(error =>
|
||||||
logger.error('Error fetching calendar.', error));
|
logger.error('Error fetching calendar.', error))
|
||||||
|
.then(() => dispatch(setLoadingCalendarEvents(false)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,7 +10,8 @@ import {
|
||||||
SET_CALENDAR_AUTHORIZATION,
|
SET_CALENDAR_AUTHORIZATION,
|
||||||
SET_CALENDAR_EVENTS,
|
SET_CALENDAR_EVENTS,
|
||||||
SET_CALENDAR_INTEGRATION,
|
SET_CALENDAR_INTEGRATION,
|
||||||
SET_CALENDAR_PROFILE_EMAIL
|
SET_CALENDAR_PROFILE_EMAIL,
|
||||||
|
SET_LOADING_CALENDAR_EVENTS
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
import { isCalendarEnabled } from './functions';
|
import { isCalendarEnabled } from './functions';
|
||||||
|
|
||||||
|
@ -96,6 +97,9 @@ isCalendarEnabled()
|
||||||
|
|
||||||
case SET_CALENDAR_PROFILE_EMAIL:
|
case SET_CALENDAR_PROFILE_EMAIL:
|
||||||
return set(state, 'profileEmail', action.email);
|
return set(state, 'profileEmail', action.email);
|
||||||
|
|
||||||
|
case SET_LOADING_CALENDAR_EVENTS:
|
||||||
|
return set(state, 'isLoadingEvents', action.isLoadingEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -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'
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -2,15 +2,17 @@
|
||||||
|
|
||||||
import Button from '@atlaskit/button';
|
import Button from '@atlaskit/button';
|
||||||
import { FieldTextStateless } from '@atlaskit/field-text';
|
import { FieldTextStateless } from '@atlaskit/field-text';
|
||||||
|
import Tabs from '@atlaskit/tabs';
|
||||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { DialogContainer } from '../../base/dialog';
|
import { DialogContainer } from '../../base/dialog';
|
||||||
import { translate } from '../../base/i18n';
|
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 { RecentList } from '../../recent-list';
|
||||||
import { openSettingsDialog } from '../../settings';
|
import { SettingsButton } from '../../settings';
|
||||||
|
|
||||||
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
|
||||||
|
|
||||||
|
@ -66,7 +68,6 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
|
|
||||||
// Bind event handlers so they are only bound once per instance.
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._onFormSubmit = this._onFormSubmit.bind(this);
|
this._onFormSubmit = this._onFormSubmit.bind(this);
|
||||||
this._onOpenSettings = this._onOpenSettings.bind(this);
|
|
||||||
this._onRoomChange = this._onRoomChange.bind(this);
|
this._onRoomChange = this._onRoomChange.bind(this);
|
||||||
this._setAdditionalContentRef
|
this._setAdditionalContentRef
|
||||||
= this._setAdditionalContentRef.bind(this);
|
= this._setAdditionalContentRef.bind(this);
|
||||||
|
@ -159,7 +160,7 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
{ t('welcomepage.go') }
|
{ t('welcomepage.go') }
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<RecentList />
|
{ this._renderTabs() }
|
||||||
</div>
|
</div>
|
||||||
{ showAdditionalContent
|
{ showAdditionalContent
|
||||||
? <div
|
? <div
|
||||||
|
@ -187,16 +188,6 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
this._onJoin();
|
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
|
* Overrides the super to account for the differences in the argument types
|
||||||
* provided by HTML and React Native text inputs.
|
* provided by HTML and React Native text inputs.
|
||||||
|
@ -211,6 +202,47 @@ class WelcomePage extends AbstractWelcomePage {
|
||||||
super._onRoomChange(event.target.value);
|
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
|
* Sets the internal reference to the HTMLDivElement used to hold the
|
||||||
* welcome page content.
|
* welcome page content.
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { PagedList } from '../../base/react';
|
import { PagedList } from '../../base/react';
|
||||||
import { MeetingList } from '../../calendar-sync';
|
import { CalendarList } from '../../calendar-sync';
|
||||||
import { RecentList } from '../../recent-list';
|
import { RecentList } from '../../recent-list';
|
||||||
|
|
||||||
import { setWelcomePageListsDefaultPage } from '../actions';
|
import { setWelcomePageListsDefaultPage } from '../actions';
|
||||||
|
@ -82,7 +82,7 @@ class WelcomePageLists extends Component<Props> {
|
||||||
title: t('welcomepage.recentList')
|
title: t('welcomepage.recentList')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: MeetingList,
|
component: CalendarList,
|
||||||
icon: android ? 'event_note' : IOS_CALENDAR_ICON,
|
icon: android ? 'event_note' : IOS_CALENDAR_ICON,
|
||||||
title: t('welcomepage.calendar')
|
title: t('welcomepage.calendar')
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue