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.
This commit is contained in:
Дамян Минков 2018-08-17 14:34:41 -05:00 committed by virtuacoplenny
parent dba7f2d429
commit 7267f386dc
13 changed files with 334 additions and 48 deletions

View File

@ -424,6 +424,11 @@
],
"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":
{
"ERROR": "Error",

View File

@ -300,7 +300,15 @@ export function parseStandardURIString(str: string) {
* references a Jitsi Meet resource (location).
* @public
* @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) {

View File

@ -12,6 +12,7 @@ import {
SET_CALENDAR_PROFILE_EMAIL
} from './actionTypes';
import { _getCalendarIntegration, isCalendarEnabled } from './functions';
import { generateRoomWithoutSeparator } from '../welcome';
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));
});
};
}

View File

@ -90,19 +90,32 @@ function _parseCalendarEntry(event, knownDomains) {
if (event) {
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 endDate = Date.parse(event.endDate);
if (isNaN(startDate) || isNaN(endDate)) {
logger.warn(
// we want to hide all events that
// - 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',
event.title,
event.startDate,
event.endDate
event.endDate,
url,
event.calendarId
);
} else {
return {
calendarId: event.calendarId,
endDate,
id: event.id,
startDate,

View File

@ -5,6 +5,7 @@ import {
googleApi,
loadGoogleAPI,
signIn,
updateCalendarEvent,
updateProfile
} from '../../google-api';
@ -62,5 +63,16 @@ export const googleCalendarApi = {
*/
_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
};

View File

@ -7,6 +7,7 @@ import { createDeferred } from '../../../../modules/util/helpers';
import parseURLParams from '../../base/config/parseURLParams';
import { parseStandardURIString } from '../../base/util';
import { getShareInfoText } from '../../invite';
import { setCalendarAPIAuthState } from '../actions';
@ -31,7 +32,7 @@ const MS_API_CONFIGURATION = {
*
* @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/
@ -106,7 +107,7 @@ export const microsoftCalendarApi = {
// get .value of every element from the array of results,
// which is an array of events and flatten it to one array
// of events
.then(result => [].concat(...result.map(en => en.value)))
.then(result => [].concat(...result))
.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.
* @private
* @returns {{
* calendarId: string,
* description: string,
* endDate: string,
* id: string,
@ -327,6 +382,7 @@ export const microsoftCalendarApi = {
*/
function formatCalendarEntry(entry) {
return {
calendarId: entry.calendarId,
description: entry.body.content,
endDate: entry.end.dateTime,
id: entry.id,
@ -509,7 +565,13 @@ function requestCalendarEvents( // eslint-disable-line max-params
.filter(filter)
.select('id,subject,start,end,location,body')
.orderby('createdDateTime DESC')
.get();
.get()
.then(result => result.value.map(item => {
return {
...item,
calendarId
};
}));
}
/**

View File

@ -1,4 +1,5 @@
/* @flow */
import { getShareInfoText } from '../invite';
import {
SET_GOOGLE_API_PROFILE,
@ -184,3 +185,24 @@ export function updateProfile() {
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));
};
}

View File

@ -204,22 +204,24 @@ const googleApi = {
*
* @param {Object} entry - The google calendar entry.
* @returns {{
* id: string,
* startDate: string,
* calendarId: string,
* description: string,
* endDate: string,
* title: string,
* id: string,
* location: string,
* description: string}}
* startDate: string,
* title: string}}
* @private
*/
_convertCalendarEntry(entry) {
return {
id: entry.id,
startDate: entry.start.dateTime,
calendarId: entry.calendarId,
description: entry.description,
endDate: entry.end.dateTime,
title: entry.summary,
id: entry.id,
location: entry.location,
description: entry.description
startDate: entry.start.dateTime,
title: entry.summary
};
},
@ -240,6 +242,8 @@ const googleApi = {
return null;
}
// user can edit the events, so we want only those that
// can be edited
return this._getGoogleApiClient()
.client.calendar.calendarList.list();
})
@ -251,14 +255,20 @@ const googleApi = {
}
const calendarIds
= calendarList.result.items.map(en => en.id);
const promises = calendarIds.map(id => {
= calendarList.result.items.map(en => {
return {
id: en.id,
accessRole: en.accessRole
};
});
const promises = calendarIds.map(({ id, accessRole }) => {
const startDate = new Date();
const endDate = new Date();
startDate.setDate(startDate.getDate() + fetchStartDays);
endDate.setDate(endDate.getDate() + fetchEndDays);
// retrieve the events and adds to the result the calendarId
return this._getGoogleApiClient()
.client.calendar.events.list({
'calendarId': id,
@ -267,17 +277,73 @@ const googleApi = {
'showDeleted': false,
'singleEvents': true,
'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)
.then(results =>
[].concat(...results.map(rItem => rItem.result.items)))
.then(results => [].concat(...results))
.then(entries =>
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
* method is discouraged; instead use the {@link get} method.

View File

@ -7,6 +7,7 @@ import { getInviteURL } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { getDialInfoPageURL } from '../../functions';
import DialInNumber from './DialInNumber';
import PasswordForm from './PasswordForm';
@ -266,23 +267,8 @@ class InfoDialog extends Component {
* @returns {string}
*/
_getDialInfoPageURL() {
const origin = window.location.origin;
const encodedConferenceName
= 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}`;
return getDialInfoPageURL(
encodeURIComponent(this.props._conferenceName));
}
/**

View File

@ -1,8 +1,9 @@
// @flow
import { getAppProp } from '../base/app';
import { i18next } from '../base/i18n';
import { isLocalParticipantModerator } from '../base/participants';
import { doGetJSON } from '../base/util';
import { doGetJSON, parseURIString } from '../base/util';
declare var $: Function;
declare var interfaceConfig: Object;
@ -397,3 +398,62 @@ export function searchDirectory( // eslint-disable-line max-params
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}`;
}

View File

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

View File

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

View File

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