Ver código fonte

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.
master
yanas 7 anos atrás
pai
commit
31cc63b757

+ 16
- 0
css/_navigate_section_list.scss Ver arquivo

@@ -34,10 +34,25 @@
34 34
     i {
35 35
         cursor: inherit;
36 36
     }
37
+
38
+    .element-after {
39
+        display: flex;
40
+        align-items: center;
41
+        justify-content: center;
42
+    }
43
+
44
+    .join-button {
45
+        display: none;
46
+    }
47
+
48
+    &:hover .join-button {
49
+        display: block
50
+    }
37 51
 }
38 52
 .navigate-section-tile-body {
39 53
     @extend %navigate-section-list-tile-text;
40 54
     font-weight: normal;
55
+    line-height: 24px;
41 56
 }
42 57
 .navigate-section-list-tile-info {
43 58
     flex: 1;
@@ -45,6 +60,7 @@
45 60
 .navigate-section-tile-title {
46 61
     @extend %navigate-section-list-tile-text;
47 62
     font-weight: bold;
63
+    line-height: 24px;
48 64
 }
49 65
 .navigate-section-section-header {
50 66
     @extend %navigate-section-list-text;

+ 7
- 4
lang/main.json Ver arquivo

@@ -62,7 +62,8 @@
62 62
         "go": "GO",
63 63
         "join": "JOIN",
64 64
         "privacy": "Privacy",
65
-        "recentList": "History",
65
+        "recentList": "Recent",
66
+        "recentListEmpty": "Your recent list is currently empty. Chat with your team and you will find all your recent meetings here.",
66 67
         "roomname": "Enter room name",
67 68
         "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.",
68 69
         "sendFeedback": "Send feedback",
@@ -642,16 +643,18 @@
642 643
     },
643 644
     "calendarSync": {
644 645
         "addMeetingURL": "Add a meeting link",
645
-        "today": "Today",
646
+        "join": "Join",
647
+        "joinTooltip": "Join the meeting",
646 648
         "nextMeeting": "next meeting",
647 649
         "noEvents": "There are no upcoming events scheduled.",
648 650
         "ongoingMeeting": "ongoing meeting",
649 651
         "permissionButton": "Open settings",
650 652
         "permissionMessage": "The Calendar permission is required to see your meetings in the app.",
651
-        "refresh": "Refresh calendar"
653
+        "refresh": "Refresh calendar",
654
+        "today": "Today"
652 655
     },
653 656
     "recentList": {
654
-        "joinPastMeeting": "Join A Past Meeting"
657
+        "joinPastMeeting": "Join a past meeting"
655 658
     },
656 659
     "sectionList": {
657 660
         "pullToRefresh": "Pull to refresh"

+ 89
- 0
react/features/analytics/AnalyticsEvents.js Ver arquivo

@@ -113,6 +113,95 @@ export function createConnectionEvent(action, attributes = {}) {
113 113
     };
114 114
 }
115 115
 
116
+/**
117
+ * Creates an event which indicates an action occurred in the calendar
118
+ * integration UI.
119
+ *
120
+ * @param {string} eventName - The name of the calendar UI event.
121
+ * @param {Object} attributes - Attributes to attach to the event.
122
+ * @returns {Object} The event in a format suitable for sending via
123
+ * sendAnalytics.
124
+ */
125
+export function createCalendarClickedEvent(eventName, attributes = {}) {
126
+    return {
127
+        action: 'clicked',
128
+        actionSubject: eventName,
129
+        attributes,
130
+        source: 'calendar',
131
+        type: TYPE_UI
132
+    };
133
+}
134
+
135
+/**
136
+ * Creates an event which indicates that the calendar container is shown and
137
+ * selected.
138
+ *
139
+ * @param {Object} attributes - Attributes to attach to the event.
140
+ * @returns {Object} The event in a format suitable for sending via
141
+ * sendAnalytics.
142
+ */
143
+export function createCalendarSelectedEvent(attributes = {}) {
144
+    return {
145
+        action: 'selected',
146
+        actionSubject: 'calendar.selected',
147
+        attributes,
148
+        source: 'calendar',
149
+        type: TYPE_UI
150
+    };
151
+}
152
+
153
+/**
154
+ * Creates an event indicating that a calendar has been connected.
155
+ *
156
+ * @param {boolean} attributes - Additional attributes to attach to the event.
157
+ * @returns {Object} The event in a format suitable for sending via
158
+ * sendAnalytics.
159
+ */
160
+export function createCalendarConnectedEvent(attributes = {}) {
161
+    return {
162
+        action: 'calendar.connected',
163
+        actionSubject: 'calendar.connected',
164
+        attributes
165
+    };
166
+}
167
+
168
+/**
169
+ * Creates an event which indicates an action occurred in the recent list
170
+ * integration UI.
171
+ *
172
+ * @param {string} eventName - The name of the recent list UI event.
173
+ * @param {Object} attributes - Attributes to attach to the event.
174
+ * @returns {Object} The event in a format suitable for sending via
175
+ * sendAnalytics.
176
+ */
177
+export function createRecentClickedEvent(eventName, attributes = {}) {
178
+    return {
179
+        action: 'clicked',
180
+        actionSubject: eventName,
181
+        attributes,
182
+        source: 'recent.list',
183
+        type: TYPE_UI
184
+    };
185
+}
186
+
187
+/**
188
+ * Creates an event which indicates that the recent list container is shown and
189
+ * selected.
190
+ *
191
+ * @param {Object} attributes - Attributes to attach to the event.
192
+ * @returns {Object} The event in a format suitable for sending via
193
+ * sendAnalytics.
194
+ */
195
+export function createRecentSelectedEvent(attributes = {}) {
196
+    return {
197
+        action: 'selected',
198
+        actionSubject: 'recent.list.selected',
199
+        attributes,
200
+        source: 'recent.list',
201
+        type: TYPE_UI
202
+    };
203
+}
204
+
116 205
 /**
117 206
  * Creates an event for an action on the deep linking page.
118 207
  *

+ 3
- 1
react/features/base/react/components/web/NavigateSectionListItem.js Ver arquivo

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

+ 3
- 0
react/features/calendar-sync/actions.js Ver arquivo

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

+ 6
- 0
react/features/calendar-sync/components/AddMeetingUrlButton.web.js Ver arquivo

@@ -5,6 +5,10 @@ import React, { Component } from 'react';
5 5
 import { connect } from 'react-redux';
6 6
 import Tooltip from '@atlaskit/tooltip';
7 7
 
8
+import {
9
+    createCalendarClickedEvent,
10
+    sendAnalytics
11
+} from '../../analytics';
8 12
 import { translate } from '../../base/i18n';
9 13
 
10 14
 import { updateCalendarEvent } from '../actions';
@@ -81,6 +85,8 @@ class AddMeetingUrlButton extends Component<Props> {
81 85
     _onClick() {
82 86
         const { calendarId, dispatch, eventId } = this.props;
83 87
 
88
+        sendAnalytics(createCalendarClickedEvent('calendar.add.url'));
89
+
84 90
         dispatch(updateCalendarEvent(eventId, calendarId));
85 91
     }
86 92
 }

+ 44
- 6
react/features/calendar-sync/components/BaseCalendarList.js Ver arquivo

@@ -4,6 +4,11 @@ import React, { Component } from 'react';
4 4
 import { connect } from 'react-redux';
5 5
 
6 6
 import { appNavigate } from '../../app';
7
+import {
8
+    createCalendarClickedEvent,
9
+    createCalendarSelectedEvent,
10
+    sendAnalytics
11
+} from '../../analytics';
7 12
 import { getLocalizedDateFormatter, translate } from '../../base/i18n';
8 13
 import { NavigateSectionList } from '../../base/react';
9 14
 
@@ -12,6 +17,7 @@ import { refreshCalendar } from '../actions';
12 17
 import { isCalendarEnabled } from '../functions';
13 18
 
14 19
 import AddMeetingUrlButton from './AddMeetingUrlButton';
20
+import JoinButton from './JoinButton';
15 21
 
16 22
 /**
17 23
  * The type of the React {@code Component} props of
@@ -84,6 +90,7 @@ class BaseCalendarList extends Component<Props> {
84 90
         super(props);
85 91
 
86 92
         // Bind event handlers so they are only bound once per instance.
93
+        this._onJoinPress = this._onJoinPress.bind(this);
87 94
         this._onPress = this._onPress.bind(this);
88 95
         this._onRefresh = this._onRefresh.bind(this);
89 96
         this._toDateString = this._toDateString.bind(this);
@@ -92,6 +99,17 @@ class BaseCalendarList extends Component<Props> {
92 99
         this._toTimeString = this._toTimeString.bind(this);
93 100
     }
94 101
 
102
+    /**
103
+     * Implements React's {@link Component#componentDidMount()}. Invoked
104
+     * immediately after this component is mounted.
105
+     *
106
+     * @inheritdoc
107
+     * @returns {void}
108
+     */
109
+    componentDidMount() {
110
+        sendAnalytics(createCalendarSelectedEvent());
111
+    }
112
+
95 113
     /**
96 114
      * Implements React's {@link Component#render}.
97 115
      *
@@ -111,16 +129,36 @@ class BaseCalendarList extends Component<Props> {
111 129
         );
112 130
     }
113 131
 
114
-    _onPress: string => Function;
132
+    _onJoinPress: (Object, string) => Function;
115 133
 
116 134
     /**
117 135
      * Handles the list's navigate action.
118 136
      *
119 137
      * @private
138
+     * @param {Object} event - The click event.
120 139
      * @param {string} url - The url string to navigate to.
121 140
      * @returns {void}
122 141
      */
123
-    _onPress(url) {
142
+    _onJoinPress(event, url) {
143
+        event.stopPropagation();
144
+
145
+        this._onPress(url, 'calendar.meeting.join');
146
+    }
147
+
148
+    _onPress: (string, string) => Function;
149
+
150
+    /**
151
+     * Handles the list's navigate action.
152
+     *
153
+     * @private
154
+     * @param {string} url - The url string to navigate to.
155
+     * @param {string} analyticsEventName - Тhe name of the analytics event.
156
+     * associated with this action.
157
+     * @returns {void}
158
+     */
159
+    _onPress(url, analyticsEventName = 'calendar.meeting.tile') {
160
+        sendAnalytics(createCalendarClickedEvent(analyticsEventName));
161
+
124 162
         this.props.dispatch(appNavigate(url));
125 163
     }
126 164
 
@@ -163,11 +201,11 @@ class BaseCalendarList extends Component<Props> {
163 201
      */
164 202
     _toDisplayableItem(event) {
165 203
         return {
166
-            elementAfter: event.url ? undefined : (
167
-                <AddMeetingUrlButton
204
+            elementAfter: event.url
205
+                ? <JoinButton onPress = { this._onJoinPress } />
206
+                : (<AddMeetingUrlButton
168 207
                     calendarId = { event.calendarId }
169
-                    eventId = { event.id } />
170
-            ),
208
+                    eventId = { event.id } />),
171 209
             key: `${event.id}-${event.startDate}`,
172 210
             lines: [
173 211
                 event.url,

+ 6
- 0
react/features/calendar-sync/components/CalendarList.web.js Ver arquivo

@@ -7,6 +7,10 @@ import { connect } from 'react-redux';
7 7
 
8 8
 import { translate } from '../../base/i18n';
9 9
 import { openSettingsDialog, SETTINGS_TABS } from '../../settings';
10
+import {
11
+    createCalendarClickedEvent,
12
+    sendAnalytics
13
+} from '../../analytics';
10 14
 
11 15
 import { refreshCalendar } from '../actions';
12 16
 import { isCalendarEnabled } from '../functions';
@@ -148,6 +152,8 @@ class CalendarList extends Component<Props> {
148 152
      * @returns {void}
149 153
      */
150 154
     _onOpenSettings() {
155
+        sendAnalytics(createCalendarClickedEvent('calendar.connect'));
156
+
151 157
         this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
152 158
     }
153 159
 

+ 23
- 0
react/features/calendar-sync/components/JoinButton.native.js Ver arquivo

@@ -0,0 +1,23 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+/**
6
+ * A React Component for joining an existing calendar meeting.
7
+ *
8
+ * @extends Component
9
+ */
10
+class JoinButton extends Component<*> {
11
+    /**
12
+     * Implements React's {@link Component#render}.
13
+     *
14
+     * @inheritdoc
15
+     */
16
+    render() {
17
+        // Not yet implemented.
18
+
19
+        return null;
20
+    }
21
+}
22
+
23
+export default JoinButton;

+ 56
- 0
react/features/calendar-sync/components/JoinButton.web.js Ver arquivo

@@ -0,0 +1,56 @@
1
+// @flow
2
+
3
+import Button from '@atlaskit/button';
4
+import React, { Component } from 'react';
5
+import Tooltip from '@atlaskit/tooltip';
6
+
7
+import { translate } from '../../base/i18n';
8
+
9
+/**
10
+ * The type of the React {@code Component} props of {@link JoinButton}.
11
+ */
12
+type Props = {
13
+
14
+    /**
15
+     * The function called when the button is pressed.
16
+     */
17
+    onPress: Function,
18
+
19
+    /**
20
+     * Invoked to obtain translated strings.
21
+     */
22
+    t: Function
23
+};
24
+
25
+/**
26
+ * A React Component for joining an existing calendar meeting.
27
+ *
28
+ * @extends Component
29
+ */
30
+class JoinButton extends Component<Props> {
31
+
32
+    /**
33
+     * Implements React's {@link Component#render}.
34
+     *
35
+     * @inheritdoc
36
+     */
37
+    render() {
38
+        const { onPress, t } = this.props;
39
+
40
+        return (
41
+            <Tooltip
42
+                content = { t('calendarSync.joinTooltip') }>
43
+                <Button
44
+                    appearance = 'primary'
45
+                    className = 'join-button'
46
+                    onClick = { onPress }
47
+                    type = 'button'>
48
+                    { t('calendarSync.join') }
49
+                </Button>
50
+            </Tooltip>
51
+        );
52
+    }
53
+}
54
+
55
+export default translate(JoinButton);
56
+

+ 48
- 2
react/features/recent-list/components/RecentList.js Ver arquivo

@@ -2,13 +2,20 @@
2 2
 import React, { Component } from 'react';
3 3
 import { connect } from 'react-redux';
4 4
 
5
+import {
6
+    createRecentClickedEvent,
7
+    createRecentSelectedEvent,
8
+    sendAnalytics
9
+} from '../../analytics';
5 10
 import { appNavigate, getDefaultURL } from '../../app';
6 11
 import { translate } from '../../base/i18n';
7
-import { NavigateSectionList } from '../../base/react';
12
+import { Container, NavigateSectionList, Text } from '../../base/react';
8 13
 import type { Section } from '../../base/react';
9 14
 
10 15
 import { isRecentListEnabled, toDisplayableList } from '../functions';
11 16
 
17
+import styles from './styles';
18
+
12 19
 /**
13 20
  * The type of the React {@code Component} props of {@link RecentList}
14 21
  */
@@ -56,6 +63,17 @@ class RecentList extends Component<Props> {
56 63
         this._onPress = this._onPress.bind(this);
57 64
     }
58 65
 
66
+    /**
67
+     * Implements React's {@link Component#componentDidMount()}. Invoked
68
+     * immediately after this component is mounted.
69
+     *
70
+     * @inheritdoc
71
+     * @returns {void}
72
+     */
73
+    componentDidMount() {
74
+        sendAnalytics(createRecentSelectedEvent());
75
+    }
76
+
59 77
     /**
60 78
      * Implements the React Components's render method.
61 79
      *
@@ -72,10 +90,37 @@ class RecentList extends Component<Props> {
72 90
             <NavigateSectionList
73 91
                 disabled = { disabled }
74 92
                 onPress = { this._onPress }
93
+                renderListEmptyComponent
94
+                    = { this._getRenderListEmptyComponent() }
75 95
                 sections = { recentList } />
76 96
         );
77 97
     }
78 98
 
99
+    _getRenderListEmptyComponent: () => Object;
100
+
101
+    /**
102
+     * Returns a list empty component if a custom one has to be rendered instead
103
+     * of the default one in the {@link NavigateSectionList}.
104
+     *
105
+     * @private
106
+     * @returns {React$Component}
107
+     */
108
+    _getRenderListEmptyComponent() {
109
+        const { t } = this.props;
110
+
111
+        return (
112
+            <Container
113
+                className = 'navigate-section-list-empty'
114
+                style = { styles.emptyListContainer }>
115
+                <Text
116
+                    className = 'header-text-description'
117
+                    style = { styles.emptyListText }>
118
+                    { t('welcomepage.recentListEmpty') }
119
+                </Text>
120
+            </Container>
121
+        );
122
+    }
123
+
79 124
     _onPress: string => Function;
80 125
 
81 126
     /**
@@ -88,9 +133,10 @@ class RecentList extends Component<Props> {
88 133
     _onPress(url) {
89 134
         const { dispatch } = this.props;
90 135
 
136
+        sendAnalytics(createRecentClickedEvent('recent.meeting.tile'));
137
+
91 138
         dispatch(appNavigate(url));
92 139
     }
93
-
94 140
 }
95 141
 
96 142
 /**

+ 26
- 0
react/features/recent-list/components/styles.js Ver arquivo

@@ -0,0 +1,26 @@
1
+import { createStyleSheet } from '../../base/styles';
2
+
3
+/**
4
+ * The styles of the React {@code Component}s of the feature recent-list i.e.
5
+ * {@code CalendarList}.
6
+ */
7
+export default createStyleSheet({
8
+
9
+    /**
10
+     * Text style of the empty recent list message.
11
+     */
12
+    emptyListText: {
13
+        backgroundColor: 'transparent',
14
+        color: 'rgba(255, 255, 255, 0.6)',
15
+        textAlign: 'center'
16
+    },
17
+
18
+    /**
19
+     * The style of the empty recent list container.
20
+     */
21
+    emptyListContainer: {
22
+        alignItems: 'center',
23
+        justifyContent: 'center',
24
+        padding: 20
25
+    }
26
+});

Carregando…
Cancelar
Salvar