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:
parent
79bd5cce00
commit
31cc63b757
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -79,7 +79,9 @@ export default class NavigateSectionListItem<P: Props>
|
|||
{ duration }
|
||||
</Text>
|
||||
</Container>
|
||||
{ elementAfter || null }
|
||||
<Container className = { 'element-after' }>
|
||||
{ elementAfter || null }
|
||||
</Container>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue