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.
pull/3338/merge jitsi-meet_3297
Дамян Минков 7 years ago committed by virtuacoplenny
parent dba7f2d429
commit 7267f386dc
  1. 5
      lang/main.json
  2. 10
      react/features/base/util/uri.js
  3. 48
      react/features/calendar-sync/actions.js
  4. 21
      react/features/calendar-sync/functions.any.js
  5. 14
      react/features/calendar-sync/web/googleCalendar.js
  6. 68
      react/features/calendar-sync/web/microsoftCalendar.js
  7. 22
      react/features/google-api/actions.js
  8. 92
      react/features/google-api/googleApi.js
  9. 20
      react/features/invite/components/info-dialog/InfoDialog.web.js
  10. 62
      react/features/invite/functions.js
  11. 3
      react/features/share-room/actionTypes.js
  12. 4
      react/features/share-room/actions.js
  13. 13
      react/features/share-room/middleware.js

@ -424,6 +424,11 @@
], ],
"and": "and" "and": "and"
}, },
"share":
{
"mainText": "Click the following link to join the meeting:\n__roomUrl__",
"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__"
},
"connection": "connection":
{ {
"ERROR": "Error", "ERROR": "Error",

@ -300,7 +300,15 @@ export function parseStandardURIString(str: string) {
* references a Jitsi Meet resource (location). * references a Jitsi Meet resource (location).
* @public * @public
* @returns {{ * @returns {{
* room: (string|undefined) * contextRoot: string,
* hash: string,
* host: string,
* hostname: string,
* pathname: string,
* port: string,
* protocol: string,
* room: (string|undefined),
* search: string
* }} * }}
*/ */
export function parseURIString(uri: ?string) { export function parseURIString(uri: ?string) {

@ -12,6 +12,7 @@ import {
SET_CALENDAR_PROFILE_EMAIL SET_CALENDAR_PROFILE_EMAIL
} from './actionTypes'; } from './actionTypes';
import { _getCalendarIntegration, isCalendarEnabled } from './functions'; import { _getCalendarIntegration, isCalendarEnabled } from './functions';
import { generateRoomWithoutSeparator } from '../welcome';
const logger = require('jitsi-meet-logger').getLogger(__filename); const logger = require('jitsi-meet-logger').getLogger(__filename);
@ -242,3 +243,50 @@ export function updateProfile(calendarType: string): Function {
}); });
}; };
} }
/**
* Updates calendar event by generating new invite URL and editing the event
* adding some descriptive text and location.
*
* @param {string} id - The event id.
* @param {string} calendarId - The id of the calendar to use.
* @returns {Function}
*/
export function updateCalendarEvent(id: string, calendarId: string): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
const { integrationType } = getState()['features/calendar-sync'];
const integration = _getCalendarIntegration(integrationType);
if (!integration) {
return Promise.reject('No integration found');
}
const { locationURL } = getState()['features/base/connection'];
const newRoomName = generateRoomWithoutSeparator();
let href = locationURL.href;
href.endsWith('/') || (href += '/');
const roomURL = `${href}${newRoomName}`;
return dispatch(integration.updateCalendarEvent(
id, calendarId, roomURL))
.then(() => {
// make a copy of the array
const events
= getState()['features/calendar-sync'].events.slice(0);
const eventIx = events.findIndex(
e => e.id === id && e.calendarId === calendarId);
// clone the event we will modify
const newEvent = Object.assign({}, events[eventIx]);
newEvent.url = roomURL;
events[eventIx] = newEvent;
return dispatch(setCalendarEvents(events));
});
};
}

@ -90,19 +90,32 @@ function _parseCalendarEntry(event, knownDomains) {
if (event) { if (event) {
const url = _getURLFromEvent(event, knownDomains); const url = _getURLFromEvent(event, knownDomains);
if (url) { // we only filter events without url on mobile, this is temporary
// till we implement event edit on mobile
if (url || navigator.product !== 'ReactNative') {
const startDate = Date.parse(event.startDate); const startDate = Date.parse(event.startDate);
const endDate = Date.parse(event.endDate); const endDate = Date.parse(event.endDate);
if (isNaN(startDate) || isNaN(endDate)) { // we want to hide all events that
logger.warn( // - has no start or end date
// - for web, if there is no url and we cannot edit the event (has
// no calendarId)
if (isNaN(startDate)
|| isNaN(endDate)
|| (navigator.product !== 'ReactNative'
&& !url
&& !event.calendarId)) {
logger.debug(
'Skipping invalid calendar event', 'Skipping invalid calendar event',
event.title, event.title,
event.startDate, event.startDate,
event.endDate event.endDate,
url,
event.calendarId
); );
} else { } else {
return { return {
calendarId: event.calendarId,
endDate, endDate,
id: event.id, id: event.id,
startDate, startDate,

@ -5,6 +5,7 @@ import {
googleApi, googleApi,
loadGoogleAPI, loadGoogleAPI,
signIn, signIn,
updateCalendarEvent,
updateProfile updateProfile
} from '../../google-api'; } from '../../google-api';
@ -62,5 +63,16 @@ export const googleCalendarApi = {
*/ */
_isSignedIn() { _isSignedIn() {
return () => googleApi.isSignedIn(); return () => googleApi.isSignedIn();
} },
/**
* Updates calendar event by generating new invite URL and editing the event
* adding some descriptive text and location.
*
* @param {string} id - The event id.
* @param {string} calendarId - The id of the calendar to use.
* @param {string} location - The location to save to the event.
* @returns {function(Dispatch<*>): Promise<string|never>}
*/
updateCalendarEvent
}; };

@ -7,6 +7,7 @@ import { createDeferred } from '../../../../modules/util/helpers';
import parseURLParams from '../../base/config/parseURLParams'; import parseURLParams from '../../base/config/parseURLParams';
import { parseStandardURIString } from '../../base/util'; import { parseStandardURIString } from '../../base/util';
import { getShareInfoText } from '../../invite';
import { setCalendarAPIAuthState } from '../actions'; import { setCalendarAPIAuthState } from '../actions';
@ -31,7 +32,7 @@ const MS_API_CONFIGURATION = {
* *
* @type {string} * @type {string}
*/ */
MS_API_SCOPES: 'openid profile Calendars.Read', MS_API_SCOPES: 'openid profile Calendars.ReadWrite',
/** /**
* See https://docs.microsoft.com/en-us/azure/active-directory/develop/ * See https://docs.microsoft.com/en-us/azure/active-directory/develop/
@ -106,7 +107,7 @@ export const microsoftCalendarApi = {
// get .value of every element from the array of results, // get .value of every element from the array of results,
// which is an array of events and flatten it to one array // which is an array of events and flatten it to one array
// of events // of events
.then(result => [].concat(...result.map(en => en.value))) .then(result => [].concat(...result))
.then(entries => entries.map(e => formatCalendarEntry(e))); .then(entries => entries.map(e => formatCalendarEntry(e)));
}; };
}, },
@ -308,6 +309,59 @@ export const microsoftCalendarApi = {
})); }));
}); });
}; };
},
/**
* Updates calendar event by generating new invite URL and editing the event
* adding some descriptive text and location.
*
* @param {string} id - The event id.
* @param {string} calendarId - The id of the calendar to use.
* @param {string} location - The location to save to the event.
* @returns {function(Dispatch<*>): Promise<string|never>}
*/
updateCalendarEvent(id: string, calendarId: string, location: string) {
return (dispatch: Dispatch<*>, getState: Function): Promise<*> => {
const state = getState()['features/calendar-sync'] || {};
const token = state.msAuthState && state.msAuthState.accessToken;
if (!token) {
return Promise.reject('Not authorized, please sign in!');
}
const { dialInNumbersUrl } = getState()['features/base/config'];
const text = getShareInfoText(
location, dialInNumbersUrl !== undefined, true/* use html */);
const client = Client.init({
authProvider: done => done(null, token)
});
return client
.api(`/me/events/${id}`)
.get()
.then(description => {
const body = description.body;
if (description.bodyPreview) {
body.content = `${description.bodyPreview}<br><br>`;
}
// replace all new lines from the text with html <br>
// to make it pretty
body.content += text.split('\n').join('<br>');
return client
.api(`/me/calendar/events/${id}`)
.patch({
body,
location: {
'displayName': location
}
});
});
};
} }
}; };
@ -317,6 +371,7 @@ export const microsoftCalendarApi = {
* @param {Object} entry - The Microsoft calendar entry. * @param {Object} entry - The Microsoft calendar entry.
* @private * @private
* @returns {{ * @returns {{
* calendarId: string,
* description: string, * description: string,
* endDate: string, * endDate: string,
* id: string, * id: string,
@ -327,6 +382,7 @@ export const microsoftCalendarApi = {
*/ */
function formatCalendarEntry(entry) { function formatCalendarEntry(entry) {
return { return {
calendarId: entry.calendarId,
description: entry.body.content, description: entry.body.content,
endDate: entry.end.dateTime, endDate: entry.end.dateTime,
id: entry.id, id: entry.id,
@ -509,7 +565,13 @@ function requestCalendarEvents( // eslint-disable-line max-params
.filter(filter) .filter(filter)
.select('id,subject,start,end,location,body') .select('id,subject,start,end,location,body')
.orderby('createdDateTime DESC') .orderby('createdDateTime DESC')
.get(); .get()
.then(result => result.value.map(item => {
return {
...item,
calendarId
};
}));
} }
/** /**

@ -1,4 +1,5 @@
/* @flow */ /* @flow */
import { getShareInfoText } from '../invite';
import { import {
SET_GOOGLE_API_PROFILE, SET_GOOGLE_API_PROFILE,
@ -184,3 +185,24 @@ export function updateProfile() {
return profile.getEmail(); return profile.getEmail();
}); });
} }
/**
* Updates the calendar event and adds a location and text.
*
* @param {string} id - The event id to update.
* @param {string} calendarId - The calendar id to use.
* @param {string} location - The location to add to the event.
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function updateCalendarEvent(
id: string, calendarId: string, location: string) {
return (dispatch: Dispatch<*>, getState: Function) => {
const { dialInNumbersUrl } = getState()['features/base/config'];
const text = getShareInfoText(location, dialInNumbersUrl !== undefined);
return googleApi.get()
.then(() =>
googleApi._updateCalendarEntry(id, calendarId, location, text));
};
}

@ -204,22 +204,24 @@ const googleApi = {
* *
* @param {Object} entry - The google calendar entry. * @param {Object} entry - The google calendar entry.
* @returns {{ * @returns {{
* id: string, * calendarId: string,
* startDate: string, * description: string,
* endDate: string, * endDate: string,
* title: string, * id: string,
* location: string, * location: string,
* description: string}} * startDate: string,
* title: string}}
* @private * @private
*/ */
_convertCalendarEntry(entry) { _convertCalendarEntry(entry) {
return { return {
id: entry.id, calendarId: entry.calendarId,
startDate: entry.start.dateTime, description: entry.description,
endDate: entry.end.dateTime, endDate: entry.end.dateTime,
title: entry.summary, id: entry.id,
location: entry.location, location: entry.location,
description: entry.description startDate: entry.start.dateTime,
title: entry.summary
}; };
}, },
@ -240,6 +242,8 @@ const googleApi = {
return null; return null;
} }
// user can edit the events, so we want only those that
// can be edited
return this._getGoogleApiClient() return this._getGoogleApiClient()
.client.calendar.calendarList.list(); .client.calendar.calendarList.list();
}) })
@ -251,14 +255,20 @@ const googleApi = {
} }
const calendarIds const calendarIds
= calendarList.result.items.map(en => en.id); = calendarList.result.items.map(en => {
const promises = calendarIds.map(id => { return {
id: en.id,
accessRole: en.accessRole
};
});
const promises = calendarIds.map(({ id, accessRole }) => {
const startDate = new Date(); const startDate = new Date();
const endDate = new Date(); const endDate = new Date();
startDate.setDate(startDate.getDate() + fetchStartDays); startDate.setDate(startDate.getDate() + fetchStartDays);
endDate.setDate(endDate.getDate() + fetchEndDays); endDate.setDate(endDate.getDate() + fetchEndDays);
// retrieve the events and adds to the result the calendarId
return this._getGoogleApiClient() return this._getGoogleApiClient()
.client.calendar.events.list({ .client.calendar.events.list({
'calendarId': id, 'calendarId': id,
@ -267,17 +277,73 @@ const googleApi = {
'showDeleted': false, 'showDeleted': false,
'singleEvents': true, 'singleEvents': true,
'orderBy': 'startTime' 'orderBy': 'startTime'
}); })
.then(result => result.result.items
.map(item => {
const resultItem = { ...item };
// add the calendarId only for the events
// we can edit
if (accessRole === 'writer'
|| accessRole === 'owner') {
resultItem.calendarId = id;
}
return resultItem;
}));
}); });
return Promise.all(promises) return Promise.all(promises)
.then(results => .then(results => [].concat(...results))
[].concat(...results.map(rItem => rItem.result.items)))
.then(entries => .then(entries =>
entries.map(e => this._convertCalendarEntry(e))); entries.map(e => this._convertCalendarEntry(e)));
}); });
}, },
/* eslint-disable max-params */
/**
* Updates the calendar event and adds a location and text.
*
* @param {string} id - The event id to update.
* @param {string} calendarId - The calendar id to use.
* @param {string} location - The location to add to the event.
* @param {string} text - The description text to set/append.
* @returns {Promise<T | never>}
* @private
*/
_updateCalendarEntry(id, calendarId, location, text) {
return this.get()
.then(() => this.isSignedIn())
.then(isSignedIn => {
if (!isSignedIn) {
return null;
}
return this._getGoogleApiClient()
.client.calendar.events.get({
'calendarId': calendarId,
'eventId': id
}).then(event => {
let newDescription = text;
if (event.result.description) {
newDescription = `${event.result.description}\n\n${
text}`;
}
return this._getGoogleApiClient()
.client.calendar.events.patch({
'calendarId': calendarId,
'eventId': id,
'description': newDescription,
'location': location
});
});
});
},
/* eslint-enable max-params */
/** /**
* Returns the global Google API Client Library object. Direct use of this * Returns the global Google API Client Library object. Direct use of this
* method is discouraged; instead use the {@link get} method. * method is discouraged; instead use the {@link get} method.

@ -7,6 +7,7 @@ import { getInviteURL } from '../../../base/connection';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants'; import { isLocalParticipantModerator } from '../../../base/participants';
import { getDialInfoPageURL } from '../../functions';
import DialInNumber from './DialInNumber'; import DialInNumber from './DialInNumber';
import PasswordForm from './PasswordForm'; import PasswordForm from './PasswordForm';
@ -266,23 +267,8 @@ class InfoDialog extends Component {
* @returns {string} * @returns {string}
*/ */
_getDialInfoPageURL() { _getDialInfoPageURL() {
const origin = window.location.origin; return getDialInfoPageURL(
const encodedConferenceName encodeURIComponent(this.props._conferenceName));
= encodeURIComponent(this.props._conferenceName);
const pathParts = window.location.pathname.split('/');
pathParts.length = pathParts.length - 1;
const newPath = pathParts.reduce((accumulator, currentValue) => {
if (currentValue) {
return `${accumulator}/${currentValue}`;
}
return accumulator;
}, '');
return `${origin}${newPath}/static/dialInInfo.html?room=${
encodedConferenceName}`;
} }
/** /**

@ -1,8 +1,9 @@
// @flow // @flow
import { getAppProp } from '../base/app'; import { getAppProp } from '../base/app';
import { i18next } from '../base/i18n';
import { isLocalParticipantModerator } from '../base/participants'; import { isLocalParticipantModerator } from '../base/participants';
import { doGetJSON } from '../base/util'; import { doGetJSON, parseURIString } from '../base/util';
declare var $: Function; declare var $: Function;
declare var interfaceConfig: Object; declare var interfaceConfig: Object;
@ -397,3 +398,62 @@ export function searchDirectory( // eslint-disable-line max-params
return Promise.reject(error); return Promise.reject(error);
}); });
} }
/**
* Returns descriptive text that can be used to invite participants to a meeting
* (share via mobile or use it for calendar event description).
*
* @param {string} inviteUrl - The conference/location URL.
* @param {boolean} includeDialInfo - Whether to include or not the dialing
* information link.
* @param {boolean} useHtml - Whether to return html text.
* @returns {string}
*/
export function getShareInfoText(
inviteUrl: string, includeDialInfo: boolean, useHtml: ?boolean) {
let roomUrl = inviteUrl;
if (useHtml) {
roomUrl = `<a href="${roomUrl}">${roomUrl}</a>`;
}
let infoText = i18next.t('share.mainText', { roomUrl });
if (includeDialInfo) {
const { room } = parseURIString(inviteUrl);
let dialInfoPageUrl = getDialInfoPageURL(room);
if (useHtml) {
dialInfoPageUrl
= `<a href="${dialInfoPageUrl}">${dialInfoPageUrl}</a>`;
}
infoText += i18next.t('share.dialInfoText', { dialInfoPageUrl });
}
return infoText;
}
/**
* Generates the URL for the static dial in info page.
*
* @param {string} conferenceName - The conference name.
* @private
* @returns {string}
*/
export function getDialInfoPageURL(conferenceName: string) {
const origin = window.location.origin;
const pathParts = window.location.pathname.split('/');
pathParts.length = pathParts.length - 1;
const newPath = pathParts.reduce((accumulator, currentValue) => {
if (currentValue) {
return `${accumulator}/${currentValue}`;
}
return accumulator;
}, '');
return `${origin}${newPath}/static/dialInInfo.html?room=${conferenceName}`;
}

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

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

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

Loading…
Cancel
Save