Add join button to calendar events. (#3408)

* Add joing button to the calendar events.

* Add space between calendar lines.

* Adjust recent list name.

* Fixes test failure.

* Restyle mobile recent list message.

* Add analytics events.

* Addressing PR review comments.
This commit is contained in:
yanas 2018-08-31 20:03:35 -05:00 committed by virtuacoplenny
parent 79bd5cce00
commit 31cc63b757
12 changed files with 327 additions and 13 deletions

View File

@ -34,10 +34,25 @@
i {
cursor: inherit;
}
.element-after {
display: flex;
align-items: center;
justify-content: center;
}
.join-button {
display: none;
}
&:hover .join-button {
display: block
}
}
.navigate-section-tile-body {
@extend %navigate-section-list-tile-text;
font-weight: normal;
line-height: 24px;
}
.navigate-section-list-tile-info {
flex: 1;
@ -45,6 +60,7 @@
.navigate-section-tile-title {
@extend %navigate-section-list-tile-text;
font-weight: bold;
line-height: 24px;
}
.navigate-section-section-header {
@extend %navigate-section-list-text;

View File

@ -62,7 +62,8 @@
"go": "GO",
"join": "JOIN",
"privacy": "Privacy",
"recentList": "History",
"recentList": "Recent",
"recentListEmpty": "Your recent list is currently empty. Chat with your team and you will find all your recent meetings here.",
"roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",
@ -642,16 +643,18 @@
},
"calendarSync": {
"addMeetingURL": "Add a meeting link",
"today": "Today",
"join": "Join",
"joinTooltip": "Join the meeting",
"nextMeeting": "next meeting",
"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.",
"refresh": "Refresh calendar"
"refresh": "Refresh calendar",
"today": "Today"
},
"recentList": {
"joinPastMeeting": "Join A Past Meeting"
"joinPastMeeting": "Join a past meeting"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"

View File

@ -113,6 +113,95 @@ export function createConnectionEvent(action, attributes = {}) {
};
}
/**
* Creates an event which indicates an action occurred in the calendar
* integration UI.
*
* @param {string} eventName - The name of the calendar UI event.
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createCalendarClickedEvent(eventName, attributes = {}) {
return {
action: 'clicked',
actionSubject: eventName,
attributes,
source: 'calendar',
type: TYPE_UI
};
}
/**
* Creates an event which indicates that the calendar container is shown and
* selected.
*
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createCalendarSelectedEvent(attributes = {}) {
return {
action: 'selected',
actionSubject: 'calendar.selected',
attributes,
source: 'calendar',
type: TYPE_UI
};
}
/**
* Creates an event indicating that a calendar has been connected.
*
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createCalendarConnectedEvent(attributes = {}) {
return {
action: 'calendar.connected',
actionSubject: 'calendar.connected',
attributes
};
}
/**
* Creates an event which indicates an action occurred in the recent list
* integration UI.
*
* @param {string} eventName - The name of the recent list UI event.
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRecentClickedEvent(eventName, attributes = {}) {
return {
action: 'clicked',
actionSubject: eventName,
attributes,
source: 'recent.list',
type: TYPE_UI
};
}
/**
* Creates an event which indicates that the recent list container is shown and
* selected.
*
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRecentSelectedEvent(attributes = {}) {
return {
action: 'selected',
actionSubject: 'recent.list.selected',
attributes,
source: 'recent.list',
type: TYPE_UI
};
}
/**
* Creates an event for an action on the deep linking page.
*

View File

@ -79,7 +79,9 @@ export default class NavigateSectionListItem<P: Props>
{ duration }
</Text>
</Container>
{ elementAfter || null }
<Container className = { 'element-after' }>
{ elementAfter || null }
</Container>
</Container>
);
}

View File

@ -2,6 +2,8 @@
import { loadGoogleAPI } from '../google-api';
import { createCalendarConnectedEvent, sendAnalytics } from '../analytics';
import {
CLEAR_CALENDAR_INTEGRATION,
REFRESH_CALENDAR,
@ -230,6 +232,7 @@ export function signIn(calendarType: string): Function {
.then(() => dispatch(setIntegrationReady(calendarType)))
.then(() => dispatch(updateProfile(calendarType)))
.then(() => dispatch(refreshCalendar()))
.then(() => sendAnalytics(createCalendarConnectedEvent()))
.catch(error => {
logger.error(
'Error occurred while signing into calendar integration',

View File

@ -5,6 +5,10 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import Tooltip from '@atlaskit/tooltip';
import {
createCalendarClickedEvent,
sendAnalytics
} from '../../analytics';
import { translate } from '../../base/i18n';
import { updateCalendarEvent } from '../actions';
@ -81,6 +85,8 @@ class AddMeetingUrlButton extends Component<Props> {
_onClick() {
const { calendarId, dispatch, eventId } = this.props;
sendAnalytics(createCalendarClickedEvent('calendar.add.url'));
dispatch(updateCalendarEvent(eventId, calendarId));
}
}

View File

@ -4,6 +4,11 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { appNavigate } from '../../app';
import {
createCalendarClickedEvent,
createCalendarSelectedEvent,
sendAnalytics
} from '../../analytics';
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
@ -12,6 +17,7 @@ import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import AddMeetingUrlButton from './AddMeetingUrlButton';
import JoinButton from './JoinButton';
/**
* The type of the React {@code Component} props of
@ -84,6 +90,7 @@ class BaseCalendarList extends Component<Props> {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onJoinPress = this._onJoinPress.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._toDateString = this._toDateString.bind(this);
@ -92,6 +99,17 @@ class BaseCalendarList extends Component<Props> {
this._toTimeString = this._toTimeString.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately after this component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
sendAnalytics(createCalendarSelectedEvent());
}
/**
* Implements React's {@link Component#render}.
*
@ -111,16 +129,36 @@ class BaseCalendarList extends Component<Props> {
);
}
_onPress: string => Function;
_onJoinPress: (Object, string) => Function;
/**
* Handles the list's navigate action.
*
* @private
* @param {Object} event - The click event.
* @param {string} url - The url string to navigate to.
* @returns {void}
*/
_onJoinPress(event, url) {
event.stopPropagation();
this._onPress(url, 'calendar.meeting.join');
}
_onPress: (string, string) => Function;
/**
* Handles the list's navigate action.
*
* @private
* @param {string} url - The url string to navigate to.
* @param {string} analyticsEventName - Тhe name of the analytics event.
* associated with this action.
* @returns {void}
*/
_onPress(url) {
_onPress(url, analyticsEventName = 'calendar.meeting.tile') {
sendAnalytics(createCalendarClickedEvent(analyticsEventName));
this.props.dispatch(appNavigate(url));
}
@ -163,11 +201,11 @@ class BaseCalendarList extends Component<Props> {
*/
_toDisplayableItem(event) {
return {
elementAfter: event.url ? undefined : (
<AddMeetingUrlButton
elementAfter: event.url
? <JoinButton onPress = { this._onJoinPress } />
: (<AddMeetingUrlButton
calendarId = { event.calendarId }
eventId = { event.id } />
),
eventId = { event.id } />),
key: `${event.id}-${event.startDate}`,
lines: [
event.url,

View File

@ -7,6 +7,10 @@ import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { openSettingsDialog, SETTINGS_TABS } from '../../settings';
import {
createCalendarClickedEvent,
sendAnalytics
} from '../../analytics';
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
@ -148,6 +152,8 @@ class CalendarList extends Component<Props> {
* @returns {void}
*/
_onOpenSettings() {
sendAnalytics(createCalendarClickedEvent('calendar.connect'));
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
}

View File

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

View File

@ -0,0 +1,56 @@
// @flow
import Button from '@atlaskit/button';
import React, { Component } from 'react';
import Tooltip from '@atlaskit/tooltip';
import { translate } from '../../base/i18n';
/**
* The type of the React {@code Component} props of {@link JoinButton}.
*/
type Props = {
/**
* The function called when the button is pressed.
*/
onPress: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* A React Component for joining an existing calendar meeting.
*
* @extends Component
*/
class JoinButton extends Component<Props> {
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { onPress, t } = this.props;
return (
<Tooltip
content = { t('calendarSync.joinTooltip') }>
<Button
appearance = 'primary'
className = 'join-button'
onClick = { onPress }
type = 'button'>
{ t('calendarSync.join') }
</Button>
</Tooltip>
);
}
}
export default translate(JoinButton);

View File

@ -2,13 +2,20 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
createRecentClickedEvent,
createRecentSelectedEvent,
sendAnalytics
} from '../../analytics';
import { appNavigate, getDefaultURL } from '../../app';
import { translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { Container, NavigateSectionList, Text } from '../../base/react';
import type { Section } from '../../base/react';
import { isRecentListEnabled, toDisplayableList } from '../functions';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link RecentList}
*/
@ -56,6 +63,17 @@ class RecentList extends Component<Props> {
this._onPress = this._onPress.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately after this component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
sendAnalytics(createRecentSelectedEvent());
}
/**
* Implements the React Components's render method.
*
@ -72,10 +90,37 @@ class RecentList extends Component<Props> {
<NavigateSectionList
disabled = { disabled }
onPress = { this._onPress }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() }
sections = { recentList } />
);
}
_getRenderListEmptyComponent: () => Object;
/**
* Returns a list empty component if a custom one has to be rendered instead
* of the default one in the {@link NavigateSectionList}.
*
* @private
* @returns {React$Component}
*/
_getRenderListEmptyComponent() {
const { t } = this.props;
return (
<Container
className = 'navigate-section-list-empty'
style = { styles.emptyListContainer }>
<Text
className = 'header-text-description'
style = { styles.emptyListText }>
{ t('welcomepage.recentListEmpty') }
</Text>
</Container>
);
}
_onPress: string => Function;
/**
@ -88,9 +133,10 @@ class RecentList extends Component<Props> {
_onPress(url) {
const { dispatch } = this.props;
sendAnalytics(createRecentClickedEvent('recent.meeting.tile'));
dispatch(appNavigate(url));
}
}
/**

View File

@ -0,0 +1,26 @@
import { createStyleSheet } from '../../base/styles';
/**
* The styles of the React {@code Component}s of the feature recent-list i.e.
* {@code CalendarList}.
*/
export default createStyleSheet({
/**
* Text style of the empty recent list message.
*/
emptyListText: {
backgroundColor: 'transparent',
color: 'rgba(255, 255, 255, 0.6)',
textAlign: 'center'
},
/**
* The style of the empty recent list container.
*/
emptyListContainer: {
alignItems: 'center',
justifyContent: 'center',
padding: 20
}
});