diff --git a/css/_navigate_section_list.scss b/css/_navigate_section_list.scss index 94092cc11..5c1d8bd15 100644 --- a/css/_navigate_section_list.scss +++ b/css/_navigate_section_list.scss @@ -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; } diff --git a/css/_welcome_page.scss b/css/_welcome_page.scss index f704ec5ae..37cbda8c8 100644 --- a/css/_welcome_page.scss +++ b/css/_welcome_page.scss @@ -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%; diff --git a/interface_config.js b/interface_config.js index 9b731e2b4..8505ad8ee 100644 --- a/interface_config.js +++ b/interface_config.js @@ -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 diff --git a/lang/main.json b/lang/main.json index 0305a159d..20499ded3 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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" diff --git a/react/features/base/react/Types.js b/react/features/base/react/Types.js index 3fcf0efd9..a10c6ba2f 100644 --- a/react/features/base/react/Types.js +++ b/react/features/base/react/Types.js @@ -12,6 +12,11 @@ export type Item = { */ colorBase: string, + /** + * An optional react element to append to the end of the Item. + */ + elementAfter?: ?ComponentType, + /** * Item title */ diff --git a/react/features/base/react/components/NavigateSectionList.js b/react/features/base/react/components/NavigateSectionList.js index 6fca09eaa..7b6eeb7f7 100644 --- a/react/features/base/react/components/NavigateSectionList.js +++ b/react/features/base/react/components/NavigateSectionList.js @@ -87,7 +87,7 @@ class NavigateSectionList extends Component { */ render() { const { - renderListEmptyComponent = this._renderListEmptyComponent, + renderListEmptyComponent = this._renderListEmptyComponent(), sections } = this.props; @@ -128,11 +128,13 @@ class NavigateSectionList extends Component { * @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; diff --git a/react/features/base/react/components/web/NavigateSectionListItem.js b/react/features/base/react/components/web/NavigateSectionListItem.js index 4fb99d3c2..4a045b336 100644 --- a/react/features/base/react/components/web/NavigateSectionListItem.js +++ b/react/features/base/react/components/web/NavigateSectionListItem.js @@ -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 { +export default class NavigateSectionListItem + extends Component

{ + /** * 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 { duration = lines[1]; } + const rootClassName = `navigate-section-list-tile ${ + onPress ? 'with-click-handler' : 'without-click-handler'}`; + return ( - - { title } - - - { date } - - - { duration } - + + + { title } + + + { date } + + + { duration } + + + { elementAfter || null } ); } diff --git a/react/features/base/react/components/web/SectionList.js b/react/features/base/react/components/web/SectionList.js index c34806ce7..864cac441 100644 --- a/react/features/base/react/components/web/SectionList.js +++ b/react/features/base/react/components/web/SectionList.js @@ -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 { */ render() { const { + ListEmptyComponent, renderSectionHeader, renderItem, sections, @@ -56,30 +62,32 @@ export default class SectionList extends Component { } = 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 ( { - sections.map((section, sectionIndex) => ( - - { renderSectionHeader(section) } - { section.data - .map((item, listIndex) => { - const listItem = { - item - }; + sections.length === 0 + ? ListEmptyComponent + : sections.map((section, sectionIndex) => ( + + { renderSectionHeader(section) } + { section.data + .map((item, listIndex) => { + const listItem = { + item + }; - return renderItem(listItem, - keyExtractor(section, - listIndex)); - }) } - - ) - ) + return renderItem(listItem, + keyExtractor(section, + listIndex)); + }) } + + ) + ) } ); diff --git a/react/features/calendar-sync/actionTypes.js b/react/features/calendar-sync/actionTypes.js index 50cb9f8f6..fa260e27d 100644 --- a/react/features/calendar-sync/actionTypes.js +++ b/react/features/calendar-sync/actionTypes.js @@ -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'); diff --git a/react/features/calendar-sync/actions.js b/react/features/calendar-sync/actions.js index 7dfb3ed77..c68fa9b16 100644 --- a/react/features/calendar-sync/actions.js +++ b/react/features/calendar-sync/actions.js @@ -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', diff --git a/react/features/calendar-sync/components/MeetingList.native.js b/react/features/calendar-sync/components/AbstractCalendarList.js similarity index 58% rename from react/features/calendar-sync/components/MeetingList.native.js rename to react/features/calendar-sync/components/AbstractCalendarList.js index a8448f7ea..b9e5e4605 100644 --- a/react/features/calendar-sync/components/MeetingList.native.js +++ b/react/features/calendar-sync/components/AbstractCalendarList.js @@ -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 { +class AbstractCalendarList extends Component { /** * Default values for the component's props. */ @@ -75,7 +75,7 @@ class MeetingList extends Component { } /** - * Initializes a new {@code MeetingList} instance. + * Initializes a new {@code CalendarList} instance. * * @inheritdoc */ @@ -83,13 +83,12 @@ class MeetingList extends Component { 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 { * @inheritdoc */ render() { - const { disabled } = this.props; + const { disabled, renderListEmptyComponent } = this.props; return ( { 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 ( - - - { t('calendarSync.permissionMessage') } - - - - { t('calendarSync.permissionButton') } - - - - ); - } - _onPress: string => Function; /** @@ -174,7 +138,7 @@ class MeetingList extends Component { _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 { */ _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 { */ _toDisplayableItem(event) { return { + elementAfter: event.url ? undefined : ( + + ), 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 { _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 { * * @param {Object} state - The redux state. * @returns {{ - * _authorization: ?string, * _eventList: Array * }} */ 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; diff --git a/react/features/calendar-sync/components/AddMeetingUrlButton.native.js b/react/features/calendar-sync/components/AddMeetingUrlButton.native.js new file mode 100644 index 000000000..ceb721497 --- /dev/null +++ b/react/features/calendar-sync/components/AddMeetingUrlButton.native.js @@ -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; diff --git a/react/features/calendar-sync/components/AddMeetingUrlButton.web.js b/react/features/calendar-sync/components/AddMeetingUrlButton.web.js new file mode 100644 index 000000000..818264695 --- /dev/null +++ b/react/features/calendar-sync/components/AddMeetingUrlButton.web.js @@ -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 { + /** + * 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 ( + + ); + } + + _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)); + diff --git a/react/features/calendar-sync/components/CalendarList.native.js b/react/features/calendar-sync/components/CalendarList.native.js new file mode 100644 index 000000000..012f4d648 --- /dev/null +++ b/react/features/calendar-sync/components/CalendarList.native.js @@ -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 { + /** + * 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 + ? + : 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 ( + + + { t('calendarSync.permissionMessage') } + + + + { t('calendarSync.permissionButton') } + + + + ); + } +} + +/** + * Maps redux state to component props. + * + * @param {Object} state - The redux state. + * @returns {{ + * _authorization: ?string, + * _eventList: Array + * }} + */ +function _mapStateToProps(state: Object) { + const { authorization } = state['features/calendar-sync']; + + return { + _authorization: authorization + }; +} + +export default isCalendarEnabled() + ? translate(connect(_mapStateToProps)(CalendarList)) + : undefined; diff --git a/react/features/calendar-sync/components/CalendarList.web.js b/react/features/calendar-sync/components/CalendarList.web.js new file mode 100644 index 000000000..3aa7785e2 --- /dev/null +++ b/react/features/calendar-sync/components/CalendarList.web.js @@ -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 { + /** + * 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 + ? + : 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 ( +
+
{ t('calendarSync.noEvents') }
+ +
+ ); + } else if (_hasIntegrationSelected && !_hasLoadedEvents) { + return ( +
+ +
+ ); + } + + return ( +
+

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

+ +
+ ); + } + + _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; diff --git a/react/features/calendar-sync/components/MeetingList.web.js b/react/features/calendar-sync/components/MeetingList.web.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/react/features/calendar-sync/components/index.js b/react/features/calendar-sync/components/index.js index af957a46e..1c2d8d5c5 100644 --- a/react/features/calendar-sync/components/index.js +++ b/react/features/calendar-sync/components/index.js @@ -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'; diff --git a/react/features/calendar-sync/components/styles.js b/react/features/calendar-sync/components/styles.js index 9481dd1a5..572baaf1e 100644 --- a/react/features/calendar-sync/components/styles.js +++ b/react/features/calendar-sync/components/styles.js @@ -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({ diff --git a/react/features/calendar-sync/functions.web.js b/react/features/calendar-sync/functions.web.js index 0035d6939..085a714ee 100644 --- a/react/features/calendar-sync/functions.web.js +++ b/react/features/calendar-sync/functions.web.js @@ -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))); } /** diff --git a/react/features/calendar-sync/reducer.js b/react/features/calendar-sync/reducer.js index 9b2cb4804..0e1918389 100644 --- a/react/features/calendar-sync/reducer.js +++ b/react/features/calendar-sync/reducer.js @@ -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; diff --git a/react/features/recent-list/components/styles.js b/react/features/recent-list/components/styles.js deleted file mode 100644 index 3130cc813..000000000 --- a/react/features/recent-list/components/styles.js +++ /dev/null @@ -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' - } -}); diff --git a/react/features/welcome/components/WelcomePage.web.js b/react/features/welcome/components/WelcomePage.web.js index 65877ece4..b3d8d3c9b 100644 --- a/react/features/welcome/components/WelcomePage.web.js +++ b/react/features/welcome/components/WelcomePage.web.js @@ -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') } - + { this._renderTabs() } { showAdditionalContent ?