diff --git a/css/_navigate_section_list.scss b/css/_navigate_section_list.scss index 32ffd684d..bd468c9d8 100644 --- a/css/_navigate_section_list.scss +++ b/css/_navigate_section_list.scss @@ -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; diff --git a/lang/main.json b/lang/main.json index 20499ded3..6ca9a4c52 100644 --- a/lang/main.json +++ b/lang/main.json @@ -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" diff --git a/react/features/analytics/AnalyticsEvents.js b/react/features/analytics/AnalyticsEvents.js index cff8fb200..715764959 100644 --- a/react/features/analytics/AnalyticsEvents.js +++ b/react/features/analytics/AnalyticsEvents.js @@ -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. * diff --git a/react/features/base/react/components/web/NavigateSectionListItem.js b/react/features/base/react/components/web/NavigateSectionListItem.js index 3ac8b14fa..ad7ca11d4 100644 --- a/react/features/base/react/components/web/NavigateSectionListItem.js +++ b/react/features/base/react/components/web/NavigateSectionListItem.js @@ -79,7 +79,9 @@ export default class NavigateSectionListItem { duration } - { elementAfter || null } + + { elementAfter || null } + ); } diff --git a/react/features/calendar-sync/actions.js b/react/features/calendar-sync/actions.js index c68fa9b16..64269bf45 100644 --- a/react/features/calendar-sync/actions.js +++ b/react/features/calendar-sync/actions.js @@ -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', diff --git a/react/features/calendar-sync/components/AddMeetingUrlButton.web.js b/react/features/calendar-sync/components/AddMeetingUrlButton.web.js index 774a9a31c..74130f0d1 100644 --- a/react/features/calendar-sync/components/AddMeetingUrlButton.web.js +++ b/react/features/calendar-sync/components/AddMeetingUrlButton.web.js @@ -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 { _onClick() { const { calendarId, dispatch, eventId } = this.props; + sendAnalytics(createCalendarClickedEvent('calendar.add.url')); + dispatch(updateCalendarEvent(eventId, calendarId)); } } diff --git a/react/features/calendar-sync/components/BaseCalendarList.js b/react/features/calendar-sync/components/BaseCalendarList.js index e7305dc10..276430fa0 100644 --- a/react/features/calendar-sync/components/BaseCalendarList.js +++ b/react/features/calendar-sync/components/BaseCalendarList.js @@ -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 { 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 { 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 { ); } - _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 { */ _toDisplayableItem(event) { return { - elementAfter: event.url ? undefined : ( - + : ( - ), + eventId = { event.id } />), key: `${event.id}-${event.startDate}`, lines: [ event.url, diff --git a/react/features/calendar-sync/components/CalendarList.web.js b/react/features/calendar-sync/components/CalendarList.web.js index 8c32a14cd..40b56c529 100644 --- a/react/features/calendar-sync/components/CalendarList.web.js +++ b/react/features/calendar-sync/components/CalendarList.web.js @@ -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 { * @returns {void} */ _onOpenSettings() { + sendAnalytics(createCalendarClickedEvent('calendar.connect')); + this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR)); } diff --git a/react/features/calendar-sync/components/JoinButton.native.js b/react/features/calendar-sync/components/JoinButton.native.js new file mode 100644 index 000000000..16e82531a --- /dev/null +++ b/react/features/calendar-sync/components/JoinButton.native.js @@ -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; diff --git a/react/features/calendar-sync/components/JoinButton.web.js b/react/features/calendar-sync/components/JoinButton.web.js new file mode 100644 index 000000000..c39405401 --- /dev/null +++ b/react/features/calendar-sync/components/JoinButton.web.js @@ -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 { + + /** + * Implements React's {@link Component#render}. + * + * @inheritdoc + */ + render() { + const { onPress, t } = this.props; + + return ( + + + + ); + } +} + +export default translate(JoinButton); + diff --git a/react/features/recent-list/components/RecentList.js b/react/features/recent-list/components/RecentList.js index c017df54a..5d290cc5a 100644 --- a/react/features/recent-list/components/RecentList.js +++ b/react/features/recent-list/components/RecentList.js @@ -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 { 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 { ); } + _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 ( + + + { t('welcomepage.recentListEmpty') } + + + ); + } + _onPress: string => Function; /** @@ -88,9 +133,10 @@ class RecentList extends Component { _onPress(url) { const { dispatch } = this.props; + sendAnalytics(createRecentClickedEvent('recent.meeting.tile')); + dispatch(appNavigate(url)); } - } /** diff --git a/react/features/recent-list/components/styles.js b/react/features/recent-list/components/styles.js new file mode 100644 index 000000000..2465c18f9 --- /dev/null +++ b/react/features/recent-list/components/styles.js @@ -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 + } +});