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:
parent
dba7f2d429
commit
7267f386dc
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
*
|
||||
* {
|
||||
* type: BEGIN_SHARE_ROOM,
|
||||
* roomURL: string
|
||||
* roomURL: string,
|
||||
* includeDialInfo: boolean
|
||||
* }
|
||||
*/
|
||||
export const BEGIN_SHARE_ROOM = Symbol('BEGIN_SHARE_ROOM');
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in New Issue