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 { i {
cursor: inherit; 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 { .navigate-section-tile-body {
@extend %navigate-section-list-tile-text; @extend %navigate-section-list-tile-text;
font-weight: normal; font-weight: normal;
line-height: 24px;
} }
.navigate-section-list-tile-info { .navigate-section-list-tile-info {
flex: 1; flex: 1;
@ -45,6 +60,7 @@
.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;
line-height: 24px;
} }
.navigate-section-section-header { .navigate-section-section-header {
@extend %navigate-section-list-text; @extend %navigate-section-list-text;

View File

@ -62,7 +62,8 @@
"go": "GO", "go": "GO",
"join": "JOIN", "join": "JOIN",
"privacy": "Privacy", "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", "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.", "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", "sendFeedback": "Send feedback",
@ -642,16 +643,18 @@
}, },
"calendarSync": { "calendarSync": {
"addMeetingURL": "Add a meeting link", "addMeetingURL": "Add a meeting link",
"today": "Today", "join": "Join",
"joinTooltip": "Join the meeting",
"nextMeeting": "next meeting", "nextMeeting": "next meeting",
"noEvents": "There are no upcoming events scheduled.", "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" "refresh": "Refresh calendar",
"today": "Today"
}, },
"recentList": { "recentList": {
"joinPastMeeting": "Join A Past Meeting" "joinPastMeeting": "Join a past meeting"
}, },
"sectionList": { "sectionList": {
"pullToRefresh": "Pull to refresh" "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. * Creates an event for an action on the deep linking page.
* *

View File

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

View File

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

View File

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

View File

@ -4,6 +4,11 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { appNavigate } from '../../app'; import { appNavigate } from '../../app';
import {
createCalendarClickedEvent,
createCalendarSelectedEvent,
sendAnalytics
} from '../../analytics';
import { getLocalizedDateFormatter, translate } from '../../base/i18n'; import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react'; import { NavigateSectionList } from '../../base/react';
@ -12,6 +17,7 @@ import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions'; import { isCalendarEnabled } from '../functions';
import AddMeetingUrlButton from './AddMeetingUrlButton'; import AddMeetingUrlButton from './AddMeetingUrlButton';
import JoinButton from './JoinButton';
/** /**
* The type of the React {@code Component} props of * The type of the React {@code Component} props of
@ -84,6 +90,7 @@ class BaseCalendarList 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._onJoinPress = this._onJoinPress.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);
@ -92,6 +99,17 @@ class BaseCalendarList extends Component<Props> {
this._toTimeString = this._toTimeString.bind(this); 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}. * 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. * Handles the list's navigate action.
* *
* @private * @private
* @param {string} url - The url string to navigate to. * @param {string} url - The url string to navigate to.
* @param {string} analyticsEventName - Тhe name of the analytics event.
* associated with this action.
* @returns {void} * @returns {void}
*/ */
_onPress(url) { _onPress(url, analyticsEventName = 'calendar.meeting.tile') {
sendAnalytics(createCalendarClickedEvent(analyticsEventName));
this.props.dispatch(appNavigate(url)); this.props.dispatch(appNavigate(url));
} }
@ -163,11 +201,11 @@ class BaseCalendarList extends Component<Props> {
*/ */
_toDisplayableItem(event) { _toDisplayableItem(event) {
return { return {
elementAfter: event.url ? undefined : ( elementAfter: event.url
<AddMeetingUrlButton ? <JoinButton onPress = { this._onJoinPress } />
: (<AddMeetingUrlButton
calendarId = { event.calendarId } calendarId = { event.calendarId }
eventId = { event.id } /> eventId = { event.id } />),
),
key: `${event.id}-${event.startDate}`, key: `${event.id}-${event.startDate}`,
lines: [ lines: [
event.url, event.url,

View File

@ -7,6 +7,10 @@ import { connect } from 'react-redux';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { openSettingsDialog, SETTINGS_TABS } from '../../settings'; import { openSettingsDialog, SETTINGS_TABS } from '../../settings';
import {
createCalendarClickedEvent,
sendAnalytics
} from '../../analytics';
import { refreshCalendar } from '../actions'; import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions'; import { isCalendarEnabled } from '../functions';
@ -148,6 +152,8 @@ class CalendarList extends Component<Props> {
* @returns {void} * @returns {void}
*/ */
_onOpenSettings() { _onOpenSettings() {
sendAnalytics(createCalendarClickedEvent('calendar.connect'));
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR)); 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 React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import {
createRecentClickedEvent,
createRecentSelectedEvent,
sendAnalytics
} from '../../analytics';
import { appNavigate, getDefaultURL } from '../../app'; import { appNavigate, getDefaultURL } from '../../app';
import { translate } from '../../base/i18n'; import { translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react'; import { Container, NavigateSectionList, Text } from '../../base/react';
import type { Section } from '../../base/react'; import type { Section } from '../../base/react';
import { isRecentListEnabled, toDisplayableList } from '../functions'; import { isRecentListEnabled, toDisplayableList } from '../functions';
import styles from './styles';
/** /**
* The type of the React {@code Component} props of {@link RecentList} * 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); 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. * Implements the React Components's render method.
* *
@ -72,10 +90,37 @@ class RecentList extends Component<Props> {
<NavigateSectionList <NavigateSectionList
disabled = { disabled } disabled = { disabled }
onPress = { this._onPress } onPress = { this._onPress }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() }
sections = { recentList } /> 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; _onPress: string => Function;
/** /**
@ -88,9 +133,10 @@ class RecentList extends Component<Props> {
_onPress(url) { _onPress(url) {
const { dispatch } = this.props; const { dispatch } = this.props;
sendAnalytics(createRecentClickedEvent('recent.meeting.tile'));
dispatch(appNavigate(url)); 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
}
});