Дамян Минков 7eda31315f Google & Microsoft calendar API integration (#3340)
* Refactor calendar-sync feature to be loaded on web.

For the web part it just adds new property to enable/disable calendar web integration, disabled by default.

* Initial implementation of retrieving google calendar events.

* Initial implementation of retrieving microsoft calendar events.

* Fixes comments.

* Rework to use the promise part of microsoft-graph-client api.

* Moves dispatching some actions, fixing comments.

* Makes sure we do not initializeClient google-api client multiple times.

* Do not try to login when fetching calendar entries.

The case where there is a calendar type google selected, but not logged in, trying to login on loading welcome page will show a warning that it tried to open a popup, which was denied by browser.

* Updates profile display data on sign in.

* Propagate google-api state to calendar-sync only if we use google cal.

* Adds sign out action.

* Clears the event listener when the popup closes.

* Clears calendarIntegrationInstance on signOut.

* WIP: UI for calendar settings, refactor auth flows

* Clean up some unused constants, functions and exports.

* break circular dependency of function and constant

* Exports only isCalendarEnabled from functions.

* Checks isSignedIn when doing fetchCalendarEntries on web.

* address comments

List microsoftApiApplicationClientID in undocument config.

remove unused SET_CALENDAR_TYPE action

use helper for calendar enabled in bootstrap

reorder actions

reorder imports

change order of signin -> set type -> update profile

add logging for signout error

reword setting dialog desc to avoid redundancy

add jsdoc to microsoft button props

reorder calendar constants

move default state to reducer (not reused anywhere)

update comment about calendar-sync due to removal of getCalendarState

update comment for getCalendarIntegration

remove vague comment

alpha order reducer, return default state on reset

alpha order persistence registry

remove unnecessary getType from apis

update comments in microsoftCalendar

alpha order google-api exports, use api.get in loadGoogleAPI

set jsdoc for google signin props

alpha order googleapi methods

fix calendartab docs

* Moves fetching calendar from APP_WILL_MOUNT to SET_CONFIG.

The web part needs configuration in order to refresh tokens (Microsoft).

* Fixes storing token expire time and refreshing tokens in Microsoft impl.

* Address comments

updateProfile changed to getCurrentEmail

rename result to results

stop storing integration in redux, store if ready for use

use existing helpers to parse redirect url

* update jsdocs, get google app id from redux

* clear integration instead of actual sign out
2018-08-15 13:11:54 -07:00

159 lines
5.2 KiB
JavaScript

// @flow
import md5 from 'js-md5';
import { setCalendarEvents } from './actions';
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
import { MAX_LIST_LENGTH } from './constants';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Updates the calendar entries in redux when new list is received. The feature
* calendar-sync doesn't display all calendar events, it displays unique
* title, URL, and start time tuples i.e. it doesn't display subsequent
* occurrences of recurring events, and the repetitions of events coming from
* multiple calendars.
*
* XXX The function's {@code this} is the redux store.
*
* @param {Array<CalendarEntry>} events - The new event list.
* @private
* @returns {void}
*/
export function _updateCalendarEntries(events: Array<Object>) {
if (!events || !events.length) {
return;
}
// eslint-disable-next-line no-invalid-this
const { dispatch, getState } = this;
const knownDomains = getState()['features/base/known-domains'];
const now = Date.now();
const entryMap = new Map();
for (const event of events) {
const entry = _parseCalendarEntry(event, knownDomains);
if (entry && entry.endDate > now) {
// As was stated above, we don't display subsequent occurrences of
// recurring events, and the repetitions of events coming from
// multiple calendars.
const key = md5.hex(JSON.stringify([
// Obviously, we want to display different conference/meetings
// URLs. URLs are the very reason why we implemented the feature
// calendar-sync in the first place.
entry.url,
// We probably want to display one and the same URL to people if
// they have it under different titles in their Calendar.
// Because maybe they remember the title of the meeting, not the
// URL so they expect to see the title without realizing that
// they have the same URL already under a different title.
entry.title,
// XXX Eventually, given that the URL and the title are the
// same, what sets one event apart from another is the start
// time of the day (note the use of toTimeString() bellow)! The
// day itself is not important because we don't want multiple
// occurrences of a recurring event or repetitions of an even
// from multiple calendars.
new Date(entry.startDate).toTimeString()
]));
const existingEntry = entryMap.get(key);
// We want only the earliest occurrence (which hasn't ended in the
// past, that is) of a recurring event.
if (!existingEntry || existingEntry.startDate > entry.startDate) {
entryMap.set(key, entry);
}
}
}
dispatch(
setCalendarEvents(
Array.from(entryMap.values())
.sort((a, b) => a.startDate - b.startDate)
.slice(0, MAX_LIST_LENGTH)));
}
/**
* Updates the calendar entries in Redux when new list is received.
*
* @param {Object} event - An event returned from the native calendar.
* @param {Array<string>} knownDomains - The known domain list.
* @private
* @returns {CalendarEntry}
*/
function _parseCalendarEntry(event, knownDomains) {
if (event) {
const url = _getURLFromEvent(event, knownDomains);
if (url) {
const startDate = Date.parse(event.startDate);
const endDate = Date.parse(event.endDate);
if (isNaN(startDate) || isNaN(endDate)) {
logger.warn(
'Skipping invalid calendar event',
event.title,
event.startDate,
event.endDate
);
} else {
return {
endDate,
id: event.id,
startDate,
title: event.title,
url
};
}
}
}
return null;
}
/**
* Retrieves a Jitsi Meet URL from an event if present.
*
* @param {Object} event - The event to parse.
* @param {Array<string>} knownDomains - The known domain names.
* @private
* @returns {string}
*/
function _getURLFromEvent(event, knownDomains) {
const linkTerminatorPattern = '[^\\s<>$]';
const urlRegExp
= new RegExp(
`http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`,
'gi');
const schemeRegExp
= new RegExp(`${APP_LINK_SCHEME}${linkTerminatorPattern}+`, 'gi');
const fieldsToSearch = [
event.title,
event.url,
event.location,
event.notes,
event.description
];
for (const field of fieldsToSearch) {
if (typeof field === 'string') {
const matches = urlRegExp.exec(field) || schemeRegExp.exec(field);
if (matches) {
const url = parseURIString(matches[0]);
if (url) {
return url.toString();
}
}
}
}
return null;
}