瀏覽代碼

Implements calendar entries edit. (#3382)

* Implements calendar entries edit.

Share text generation between calendar-sync and the share-room feature.

* Fixing comments.

* Clone the event element we modify on update.
master
Дамян Минков 6 年之前
父節點
當前提交
7267f386dc

+ 5
- 0
lang/main.json 查看文件

@@ -424,6 +424,11 @@
424 424
         ],
425 425
         "and": "and"
426 426
     },
427
+    "share":
428
+    {
429
+        "mainText": "Click the following link to join the meeting:\n__roomUrl__",
430
+        "dialInfoText": "\n\n=====\n\nJust want to dial in on your phone?\n\nClick this link to see the dial in phone numbers for this meetings\n__dialInfoPageUrl__"
431
+    },
427 432
     "connection":
428 433
     {
429 434
         "ERROR": "Error",

+ 9
- 1
react/features/base/util/uri.js 查看文件

@@ -300,7 +300,15 @@ export function parseStandardURIString(str: string) {
300 300
  * references a Jitsi Meet resource (location).
301 301
  * @public
302 302
  * @returns {{
303
- *     room: (string|undefined)
303
+ *     contextRoot: string,
304
+ *     hash: string,
305
+ *     host: string,
306
+ *     hostname: string,
307
+ *     pathname: string,
308
+ *     port: string,
309
+ *     protocol: string,
310
+ *     room: (string|undefined),
311
+ *     search: string
304 312
  * }}
305 313
  */
306 314
 export function parseURIString(uri: ?string) {

+ 48
- 0
react/features/calendar-sync/actions.js 查看文件

@@ -12,6 +12,7 @@ import {
12 12
     SET_CALENDAR_PROFILE_EMAIL
13 13
 } from './actionTypes';
14 14
 import { _getCalendarIntegration, isCalendarEnabled } from './functions';
15
+import { generateRoomWithoutSeparator } from '../welcome';
15 16
 
16 17
 const logger = require('jitsi-meet-logger').getLogger(__filename);
17 18
 
@@ -242,3 +243,50 @@ export function updateProfile(calendarType: string): Function {
242 243
             });
243 244
     };
244 245
 }
246
+
247
+/**
248
+ * Updates calendar event by generating new invite URL and editing the event
249
+ * adding some descriptive text and location.
250
+ *
251
+ * @param {string} id - The event id.
252
+ * @param {string} calendarId - The id of the calendar to use.
253
+ * @returns {Function}
254
+ */
255
+export function updateCalendarEvent(id: string, calendarId: string): Function {
256
+    return (dispatch: Dispatch<*>, getState: Function) => {
257
+
258
+        const { integrationType } = getState()['features/calendar-sync'];
259
+        const integration = _getCalendarIntegration(integrationType);
260
+
261
+        if (!integration) {
262
+            return Promise.reject('No integration found');
263
+        }
264
+
265
+        const { locationURL } = getState()['features/base/connection'];
266
+        const newRoomName = generateRoomWithoutSeparator();
267
+        let href = locationURL.href;
268
+
269
+        href.endsWith('/') || (href += '/');
270
+
271
+        const roomURL = `${href}${newRoomName}`;
272
+
273
+        return dispatch(integration.updateCalendarEvent(
274
+                id, calendarId, roomURL))
275
+            .then(() => {
276
+                // make a copy of the array
277
+                const events
278
+                    = getState()['features/calendar-sync'].events.slice(0);
279
+
280
+                const eventIx = events.findIndex(
281
+                    e => e.id === id && e.calendarId === calendarId);
282
+
283
+                // clone the event we will modify
284
+                const newEvent = Object.assign({}, events[eventIx]);
285
+
286
+                newEvent.url = roomURL;
287
+                events[eventIx] = newEvent;
288
+
289
+                return dispatch(setCalendarEvents(events));
290
+            });
291
+    };
292
+}

+ 17
- 4
react/features/calendar-sync/functions.any.js 查看文件

@@ -90,19 +90,32 @@ function _parseCalendarEntry(event, knownDomains) {
90 90
     if (event) {
91 91
         const url = _getURLFromEvent(event, knownDomains);
92 92
 
93
-        if (url) {
93
+        // we only filter events without url on mobile, this is temporary
94
+        // till we implement event edit on mobile
95
+        if (url || navigator.product !== 'ReactNative') {
94 96
             const startDate = Date.parse(event.startDate);
95 97
             const endDate = Date.parse(event.endDate);
96 98
 
97
-            if (isNaN(startDate) || isNaN(endDate)) {
98
-                logger.warn(
99
+            // we want to hide all events that
100
+            // - has no start or end date
101
+            // - for web, if there is no url and we cannot edit the event (has
102
+            // no calendarId)
103
+            if (isNaN(startDate)
104
+                || isNaN(endDate)
105
+                || (navigator.product !== 'ReactNative'
106
+                        && !url
107
+                        && !event.calendarId)) {
108
+                logger.debug(
99 109
                     'Skipping invalid calendar event',
100 110
                     event.title,
101 111
                     event.startDate,
102
-                    event.endDate
112
+                    event.endDate,
113
+                    url,
114
+                    event.calendarId
103 115
                 );
104 116
             } else {
105 117
                 return {
118
+                    calendarId: event.calendarId,
106 119
                     endDate,
107 120
                     id: event.id,
108 121
                     startDate,

+ 13
- 1
react/features/calendar-sync/web/googleCalendar.js 查看文件

@@ -5,6 +5,7 @@ import {
5 5
     googleApi,
6 6
     loadGoogleAPI,
7 7
     signIn,
8
+    updateCalendarEvent,
8 9
     updateProfile
9 10
 } from '../../google-api';
10 11
 
@@ -62,5 +63,16 @@ export const googleCalendarApi = {
62 63
      */
63 64
     _isSignedIn() {
64 65
         return () => googleApi.isSignedIn();
65
-    }
66
+    },
67
+
68
+    /**
69
+     * Updates calendar event by generating new invite URL and editing the event
70
+     * adding some descriptive text and location.
71
+     *
72
+     * @param {string} id - The event id.
73
+     * @param {string} calendarId - The id of the calendar to use.
74
+     * @param {string} location - The location to save to the event.
75
+     * @returns {function(Dispatch<*>): Promise<string|never>}
76
+     */
77
+    updateCalendarEvent
66 78
 };

+ 65
- 3
react/features/calendar-sync/web/microsoftCalendar.js 查看文件

@@ -7,6 +7,7 @@ import { createDeferred } from '../../../../modules/util/helpers';
7 7
 
8 8
 import parseURLParams from '../../base/config/parseURLParams';
9 9
 import { parseStandardURIString } from '../../base/util';
10
+import { getShareInfoText } from '../../invite';
10 11
 
11 12
 import { setCalendarAPIAuthState } from '../actions';
12 13
 
@@ -31,7 +32,7 @@ const MS_API_CONFIGURATION = {
31 32
      *
32 33
      * @type {string}
33 34
      */
34
-    MS_API_SCOPES: 'openid profile Calendars.Read',
35
+    MS_API_SCOPES: 'openid profile Calendars.ReadWrite',
35 36
 
36 37
     /**
37 38
      * See https://docs.microsoft.com/en-us/azure/active-directory/develop/
@@ -106,7 +107,7 @@ export const microsoftCalendarApi = {
106 107
                 // get .value of every element from the array of results,
107 108
                 // which is an array of events and flatten it to one array
108 109
                 // of events
109
-                .then(result => [].concat(...result.map(en => en.value)))
110
+                .then(result => [].concat(...result))
110 111
                 .then(entries => entries.map(e => formatCalendarEntry(e)));
111 112
         };
112 113
     },
@@ -308,6 +309,59 @@ export const microsoftCalendarApi = {
308 309
                 }));
309 310
             });
310 311
         };
312
+    },
313
+
314
+    /**
315
+     * Updates calendar event by generating new invite URL and editing the event
316
+     * adding some descriptive text and location.
317
+     *
318
+     * @param {string} id - The event id.
319
+     * @param {string} calendarId - The id of the calendar to use.
320
+     * @param {string} location - The location to save to the event.
321
+     * @returns {function(Dispatch<*>): Promise<string|never>}
322
+     */
323
+    updateCalendarEvent(id: string, calendarId: string, location: string) {
324
+        return (dispatch: Dispatch<*>, getState: Function): Promise<*> => {
325
+            const state = getState()['features/calendar-sync'] || {};
326
+            const token = state.msAuthState && state.msAuthState.accessToken;
327
+
328
+            if (!token) {
329
+                return Promise.reject('Not authorized, please sign in!');
330
+            }
331
+
332
+            const { dialInNumbersUrl } = getState()['features/base/config'];
333
+            const text = getShareInfoText(
334
+                location, dialInNumbersUrl !== undefined, true/* use html */);
335
+
336
+
337
+            const client = Client.init({
338
+                authProvider: done => done(null, token)
339
+            });
340
+
341
+            return client
342
+                .api(`/me/events/${id}`)
343
+                .get()
344
+                .then(description => {
345
+                    const body = description.body;
346
+
347
+                    if (description.bodyPreview) {
348
+                        body.content = `${description.bodyPreview}<br><br>`;
349
+                    }
350
+
351
+                    // replace all new lines from the text with html <br>
352
+                    // to make it pretty
353
+                    body.content += text.split('\n').join('<br>');
354
+
355
+                    return client
356
+                        .api(`/me/calendar/events/${id}`)
357
+                        .patch({
358
+                            body,
359
+                            location: {
360
+                                'displayName': location
361
+                            }
362
+                        });
363
+                });
364
+        };
311 365
     }
312 366
 };
313 367
 
@@ -317,6 +371,7 @@ export const microsoftCalendarApi = {
317 371
  * @param {Object} entry - The Microsoft calendar entry.
318 372
  * @private
319 373
  * @returns {{
374
+ *     calendarId: string,
320 375
  *     description: string,
321 376
  *     endDate: string,
322 377
  *     id: string,
@@ -327,6 +382,7 @@ export const microsoftCalendarApi = {
327 382
  */
328 383
 function formatCalendarEntry(entry) {
329 384
     return {
385
+        calendarId: entry.calendarId,
330 386
         description: entry.body.content,
331 387
         endDate: entry.end.dateTime,
332 388
         id: entry.id,
@@ -509,7 +565,13 @@ function requestCalendarEvents( // eslint-disable-line max-params
509 565
         .filter(filter)
510 566
         .select('id,subject,start,end,location,body')
511 567
         .orderby('createdDateTime DESC')
512
-        .get();
568
+        .get()
569
+        .then(result => result.value.map(item => {
570
+            return {
571
+                ...item,
572
+                calendarId
573
+            };
574
+        }));
513 575
 }
514 576
 
515 577
 /**

+ 22
- 0
react/features/google-api/actions.js 查看文件

@@ -1,4 +1,5 @@
1 1
 /* @flow */
2
+import { getShareInfoText } from '../invite';
2 3
 
3 4
 import {
4 5
     SET_GOOGLE_API_PROFILE,
@@ -184,3 +185,24 @@ export function updateProfile() {
184 185
             return profile.getEmail();
185 186
         });
186 187
 }
188
+
189
+/**
190
+ * Updates the calendar event and adds a location and text.
191
+ *
192
+ * @param {string} id - The event id to update.
193
+ * @param {string} calendarId - The calendar id to use.
194
+ * @param {string} location - The location to add to the event.
195
+ * @returns {function(Dispatch<*>): Promise<string | never>}
196
+ */
197
+export function updateCalendarEvent(
198
+        id: string, calendarId: string, location: string) {
199
+    return (dispatch: Dispatch<*>, getState: Function) => {
200
+
201
+        const { dialInNumbersUrl } = getState()['features/base/config'];
202
+        const text = getShareInfoText(location, dialInNumbersUrl !== undefined);
203
+
204
+        return googleApi.get()
205
+            .then(() =>
206
+                googleApi._updateCalendarEntry(id, calendarId, location, text));
207
+    };
208
+}

+ 79
- 13
react/features/google-api/googleApi.js 查看文件

@@ -204,22 +204,24 @@ const googleApi = {
204 204
      *
205 205
      * @param {Object} entry - The google calendar entry.
206 206
      * @returns {{
207
-     *  id: string,
208
-     *  startDate: string,
207
+     *  calendarId: string,
208
+     *  description: string,
209 209
      *  endDate: string,
210
-     *  title: string,
210
+     *  id: string,
211 211
      *  location: string,
212
-     *  description: string}}
212
+     *  startDate: string,
213
+     *  title: string}}
213 214
      * @private
214 215
      */
215 216
     _convertCalendarEntry(entry) {
216 217
         return {
217
-            id: entry.id,
218
-            startDate: entry.start.dateTime,
218
+            calendarId: entry.calendarId,
219
+            description: entry.description,
219 220
             endDate: entry.end.dateTime,
220
-            title: entry.summary,
221
+            id: entry.id,
221 222
             location: entry.location,
222
-            description: entry.description
223
+            startDate: entry.start.dateTime,
224
+            title: entry.summary
223 225
         };
224 226
     },
225 227
 
@@ -240,6 +242,8 @@ const googleApi = {
240 242
                     return null;
241 243
                 }
242 244
 
245
+                // user can edit the events, so we want only those that
246
+                // can be edited
243 247
                 return this._getGoogleApiClient()
244 248
                     .client.calendar.calendarList.list();
245 249
             })
@@ -251,14 +255,20 @@ const googleApi = {
251 255
                 }
252 256
 
253 257
                 const calendarIds
254
-                    = calendarList.result.items.map(en => en.id);
255
-                const promises = calendarIds.map(id => {
258
+                    = calendarList.result.items.map(en => {
259
+                        return {
260
+                            id: en.id,
261
+                            accessRole: en.accessRole
262
+                        };
263
+                    });
264
+                const promises = calendarIds.map(({ id, accessRole }) => {
256 265
                     const startDate = new Date();
257 266
                     const endDate = new Date();
258 267
 
259 268
                     startDate.setDate(startDate.getDate() + fetchStartDays);
260 269
                     endDate.setDate(endDate.getDate() + fetchEndDays);
261 270
 
271
+                    // retrieve the events and adds to the result the calendarId
262 272
                     return this._getGoogleApiClient()
263 273
                         .client.calendar.events.list({
264 274
                             'calendarId': id,
@@ -267,17 +277,73 @@ const googleApi = {
267 277
                             'showDeleted': false,
268 278
                             'singleEvents': true,
269 279
                             'orderBy': 'startTime'
270
-                        });
280
+                        })
281
+                        .then(result => result.result.items
282
+                            .map(item => {
283
+                                const resultItem = { ...item };
284
+
285
+                                // add the calendarId only for the events
286
+                                // we can edit
287
+                                if (accessRole === 'writer'
288
+                                    || accessRole === 'owner') {
289
+                                    resultItem.calendarId = id;
290
+                                }
291
+
292
+                                return resultItem;
293
+                            }));
271 294
                 });
272 295
 
273 296
                 return Promise.all(promises)
274
-                    .then(results =>
275
-                        [].concat(...results.map(rItem => rItem.result.items)))
297
+                    .then(results => [].concat(...results))
276 298
                     .then(entries =>
277 299
                         entries.map(e => this._convertCalendarEntry(e)));
278 300
             });
279 301
     },
280 302
 
303
+    /* eslint-disable max-params */
304
+    /**
305
+     * Updates the calendar event and adds a location and text.
306
+     *
307
+     * @param {string} id - The event id to update.
308
+     * @param {string} calendarId - The calendar id to use.
309
+     * @param {string} location - The location to add to the event.
310
+     * @param {string} text - The description text to set/append.
311
+     * @returns {Promise<T | never>}
312
+     * @private
313
+     */
314
+    _updateCalendarEntry(id, calendarId, location, text) {
315
+        return this.get()
316
+            .then(() => this.isSignedIn())
317
+            .then(isSignedIn => {
318
+                if (!isSignedIn) {
319
+                    return null;
320
+                }
321
+
322
+                return this._getGoogleApiClient()
323
+                    .client.calendar.events.get({
324
+                        'calendarId': calendarId,
325
+                        'eventId': id
326
+                    }).then(event => {
327
+                        let newDescription = text;
328
+
329
+                        if (event.result.description) {
330
+                            newDescription = `${event.result.description}\n\n${
331
+                                text}`;
332
+                        }
333
+
334
+                        return this._getGoogleApiClient()
335
+                            .client.calendar.events.patch({
336
+                                'calendarId': calendarId,
337
+                                'eventId': id,
338
+                                'description': newDescription,
339
+                                'location': location
340
+                            });
341
+                    });
342
+
343
+            });
344
+    },
345
+    /* eslint-enable max-params */
346
+
281 347
     /**
282 348
      * Returns the global Google API Client Library object. Direct use of this
283 349
      * method is discouraged; instead use the {@link get} method.

+ 3
- 17
react/features/invite/components/info-dialog/InfoDialog.web.js 查看文件

@@ -7,6 +7,7 @@ import { getInviteURL } from '../../../base/connection';
7 7
 import { translate } from '../../../base/i18n';
8 8
 import { isLocalParticipantModerator } from '../../../base/participants';
9 9
 
10
+import { getDialInfoPageURL } from '../../functions';
10 11
 import DialInNumber from './DialInNumber';
11 12
 import PasswordForm from './PasswordForm';
12 13
 
@@ -266,23 +267,8 @@ class InfoDialog extends Component {
266 267
      * @returns {string}
267 268
      */
268 269
     _getDialInfoPageURL() {
269
-        const origin = window.location.origin;
270
-        const encodedConferenceName
271
-            = encodeURIComponent(this.props._conferenceName);
272
-        const pathParts = window.location.pathname.split('/');
273
-
274
-        pathParts.length = pathParts.length - 1;
275
-
276
-        const newPath = pathParts.reduce((accumulator, currentValue) => {
277
-            if (currentValue) {
278
-                return `${accumulator}/${currentValue}`;
279
-            }
280
-
281
-            return accumulator;
282
-        }, '');
283
-
284
-        return `${origin}${newPath}/static/dialInInfo.html?room=${
285
-            encodedConferenceName}`;
270
+        return getDialInfoPageURL(
271
+            encodeURIComponent(this.props._conferenceName));
286 272
     }
287 273
 
288 274
     /**

+ 61
- 1
react/features/invite/functions.js 查看文件

@@ -1,8 +1,9 @@
1 1
 // @flow
2 2
 
3 3
 import { getAppProp } from '../base/app';
4
+import { i18next } from '../base/i18n';
4 5
 import { isLocalParticipantModerator } from '../base/participants';
5
-import { doGetJSON } from '../base/util';
6
+import { doGetJSON, parseURIString } from '../base/util';
6 7
 
7 8
 declare var $: Function;
8 9
 declare var interfaceConfig: Object;
@@ -397,3 +398,62 @@ export function searchDirectory( // eslint-disable-line max-params
397 398
                 return Promise.reject(error);
398 399
             });
399 400
 }
401
+
402
+/**
403
+ * Returns descriptive text that can be used to invite participants to a meeting
404
+ * (share via mobile or use it for calendar event description).
405
+ *
406
+ * @param {string} inviteUrl - The conference/location URL.
407
+ * @param {boolean} includeDialInfo - Whether to include or not the dialing
408
+ * information link.
409
+ * @param {boolean} useHtml - Whether to return html text.
410
+ * @returns {string}
411
+ */
412
+export function getShareInfoText(
413
+        inviteUrl: string, includeDialInfo: boolean, useHtml: ?boolean) {
414
+    let roomUrl = inviteUrl;
415
+
416
+    if (useHtml) {
417
+        roomUrl = `<a href="${roomUrl}">${roomUrl}</a>`;
418
+    }
419
+
420
+    let infoText = i18next.t('share.mainText', { roomUrl });
421
+
422
+    if (includeDialInfo) {
423
+        const { room } = parseURIString(inviteUrl);
424
+        let dialInfoPageUrl = getDialInfoPageURL(room);
425
+
426
+        if (useHtml) {
427
+            dialInfoPageUrl
428
+                = `<a href="${dialInfoPageUrl}">${dialInfoPageUrl}</a>`;
429
+        }
430
+
431
+        infoText += i18next.t('share.dialInfoText', { dialInfoPageUrl });
432
+    }
433
+
434
+    return infoText;
435
+}
436
+
437
+/**
438
+ * Generates the URL for the static dial in info page.
439
+ *
440
+ * @param {string} conferenceName - The conference name.
441
+ * @private
442
+ * @returns {string}
443
+ */
444
+export function getDialInfoPageURL(conferenceName: string) {
445
+    const origin = window.location.origin;
446
+    const pathParts = window.location.pathname.split('/');
447
+
448
+    pathParts.length = pathParts.length - 1;
449
+
450
+    const newPath = pathParts.reduce((accumulator, currentValue) => {
451
+        if (currentValue) {
452
+            return `${accumulator}/${currentValue}`;
453
+        }
454
+
455
+        return accumulator;
456
+    }, '');
457
+
458
+    return `${origin}${newPath}/static/dialInInfo.html?room=${conferenceName}`;
459
+}

+ 2
- 1
react/features/share-room/actionTypes.js 查看文件

@@ -4,7 +4,8 @@
4 4
  *
5 5
  * {
6 6
  *     type: BEGIN_SHARE_ROOM,
7
- *     roomURL: string
7
+ *     roomURL: string,
8
+ *     includeDialInfo: boolean
8 9
  * }
9 10
  */
10 11
 export const BEGIN_SHARE_ROOM = Symbol('BEGIN_SHARE_ROOM');

+ 3
- 1
react/features/share-room/actions.js 查看文件

@@ -19,7 +19,9 @@ export function beginShareRoom(roomURL: ?string): Function {
19 19
         }
20 20
         roomURL && dispatch({
21 21
             type: BEGIN_SHARE_ROOM,
22
-            roomURL
22
+            roomURL,
23
+            includeDialInfo: getState()['features/base/config']
24
+                .dialInNumbersUrl !== undefined
23 25
         });
24 26
     };
25 27
 }

+ 7
- 6
react/features/share-room/middleware.js 查看文件

@@ -4,6 +4,7 @@ import { Share } from 'react-native';
4 4
 
5 5
 import { getName } from '../app';
6 6
 import { MiddlewareRegistry } from '../base/redux';
7
+import { getShareInfoText } from '../invite';
7 8
 
8 9
 import { endShareRoom } from './actions';
9 10
 import { BEGIN_SHARE_ROOM } from './actionTypes';
@@ -20,7 +21,7 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
20 21
 MiddlewareRegistry.register(store => next => action => {
21 22
     switch (action.type) {
22 23
     case BEGIN_SHARE_ROOM:
23
-        _shareRoom(action.roomURL, store.dispatch);
24
+        _shareRoom(action.roomURL, action.includeDialInfo, store.dispatch);
24 25
         break;
25 26
     }
26 27
 
@@ -31,15 +32,15 @@ MiddlewareRegistry.register(store => next => action => {
31 32
  * Open the native sheet for sharing a specific conference/room URL.
32 33
  *
33 34
  * @param {string} roomURL - The URL of the conference/room to be shared.
35
+ * @param {boolean} includeDialInfo - Whether to include or not the dialing
36
+ * information link.
34 37
  * @param {Dispatch} dispatch - The Redux dispatch function.
35 38
  * @private
36 39
  * @returns {void}
37 40
  */
38
-function _shareRoom(roomURL: string, dispatch: Function) {
39
-    // TODO The following display/human-readable strings were submitted for
40
-    // review before i18n was introduces in react/. However, I reviewed it
41
-    // afterwards. Translate the display/human-readable strings.
42
-    const message = `Click the following link to join the meeting: ${roomURL}`;
41
+function _shareRoom(
42
+        roomURL: string, includeDialInfo: boolean, dispatch: Function) {
43
+    const message = getShareInfoText(roomURL, includeDialInfo);
43 44
     const title = `${getName()} Conference`;
44 45
     const onFulfilled
45 46
         = (shared: boolean) => dispatch(endShareRoom(roomURL, shared));

Loading…
取消
儲存