You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

middleware.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. // @flow
  2. import md5 from 'js-md5';
  3. import RNCalendarEvents from 'react-native-calendar-events';
  4. import { APP_WILL_MOUNT } from '../app';
  5. import { ADD_KNOWN_DOMAINS, addKnownDomains } from '../base/known-domains';
  6. import { MiddlewareRegistry } from '../base/redux';
  7. import { APP_LINK_SCHEME, parseURIString } from '../base/util';
  8. import { APP_STATE_CHANGED } from '../mobile/background';
  9. import { setCalendarAuthorization, setCalendarEvents } from './actions';
  10. import { REFRESH_CALENDAR } from './actionTypes';
  11. import { CALENDAR_ENABLED } from './constants';
  12. const logger = require('jitsi-meet-logger').getLogger(__filename);
  13. /**
  14. * The number of days to fetch.
  15. */
  16. const FETCH_END_DAYS = 10;
  17. /**
  18. * The number of days to go back when fetching.
  19. */
  20. const FETCH_START_DAYS = -1;
  21. /**
  22. * The max number of events to fetch from the calendar.
  23. */
  24. const MAX_LIST_LENGTH = 10;
  25. CALENDAR_ENABLED
  26. && MiddlewareRegistry.register(store => next => action => {
  27. switch (action.type) {
  28. case ADD_KNOWN_DOMAINS: {
  29. // XXX Fetch new calendar entries only when an actual domain has
  30. // become known.
  31. const { getState } = store;
  32. const oldValue = getState()['features/base/known-domains'];
  33. const result = next(action);
  34. const newValue = getState()['features/base/known-domains'];
  35. oldValue === newValue || _fetchCalendarEntries(store, false, false);
  36. return result;
  37. }
  38. case APP_STATE_CHANGED: {
  39. const result = next(action);
  40. _maybeClearAccessStatus(store, action);
  41. return result;
  42. }
  43. case APP_WILL_MOUNT: {
  44. // For legacy purposes, we've allowed the deserialization of
  45. // knownDomains and now we're to translate it to base/known-domains.
  46. const state = store.getState()['features/calendar-sync'];
  47. if (state) {
  48. const { knownDomains } = state;
  49. Array.isArray(knownDomains)
  50. && knownDomains.length
  51. && store.dispatch(addKnownDomains(knownDomains));
  52. }
  53. _fetchCalendarEntries(store, false, false);
  54. return next(action);
  55. }
  56. case REFRESH_CALENDAR: {
  57. const result = next(action);
  58. _fetchCalendarEntries(store, true, action.forcePermission);
  59. return result;
  60. }
  61. }
  62. return next(action);
  63. });
  64. /**
  65. * Ensures calendar access if possible and resolves the promise if it's granted.
  66. *
  67. * @param {boolean} promptForPermission - Flag to tell the app if it should
  68. * prompt for a calendar permission if it wasn't granted yet.
  69. * @param {Function} dispatch - The Redux dispatch function.
  70. * @private
  71. * @returns {Promise}
  72. */
  73. function _ensureCalendarAccess(promptForPermission, dispatch) {
  74. return new Promise((resolve, reject) => {
  75. RNCalendarEvents.authorizationStatus()
  76. .then(status => {
  77. if (status === 'authorized') {
  78. resolve(true);
  79. } else if (promptForPermission) {
  80. RNCalendarEvents.authorizeEventStore()
  81. .then(result => {
  82. dispatch(setCalendarAuthorization(result));
  83. resolve(result === 'authorized');
  84. })
  85. .catch(reject);
  86. } else {
  87. resolve(false);
  88. }
  89. })
  90. .catch(reject);
  91. });
  92. }
  93. /**
  94. * Reads the user's calendar and updates the stored entries if need be.
  95. *
  96. * @param {Object} store - The redux store.
  97. * @param {boolean} maybePromptForPermission - Flag to tell the app if it should
  98. * prompt for a calendar permission if it wasn't granted yet.
  99. * @param {boolean|undefined} forcePermission - Whether to force to re-ask for
  100. * the permission or not.
  101. * @private
  102. * @returns {void}
  103. */
  104. function _fetchCalendarEntries(
  105. store,
  106. maybePromptForPermission,
  107. forcePermission) {
  108. const { dispatch, getState } = store;
  109. const promptForPermission
  110. = (maybePromptForPermission
  111. && !getState()['features/calendar-sync'].authorization)
  112. || forcePermission;
  113. _ensureCalendarAccess(promptForPermission, dispatch)
  114. .then(accessGranted => {
  115. if (accessGranted) {
  116. const startDate = new Date();
  117. const endDate = new Date();
  118. startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
  119. endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
  120. RNCalendarEvents.fetchAllEvents(
  121. startDate.getTime(),
  122. endDate.getTime(),
  123. [])
  124. .then(_updateCalendarEntries.bind(store))
  125. .catch(error =>
  126. logger.error('Error fetching calendar.', error));
  127. } else {
  128. logger.warn('Calendar access not granted.');
  129. }
  130. })
  131. .catch(reason => logger.error('Error accessing calendar.', reason));
  132. }
  133. /**
  134. * Retrieves a Jitsi Meet URL from an event if present.
  135. *
  136. * @param {Object} event - The event to parse.
  137. * @param {Array<string>} knownDomains - The known domain names.
  138. * @private
  139. * @returns {string}
  140. */
  141. function _getURLFromEvent(event, knownDomains) {
  142. const linkTerminatorPattern = '[^\\s<>$]';
  143. const urlRegExp
  144. = new RegExp(
  145. `http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`,
  146. 'gi');
  147. const schemeRegExp
  148. = new RegExp(`${APP_LINK_SCHEME}${linkTerminatorPattern}+`, 'gi');
  149. const fieldsToSearch = [
  150. event.title,
  151. event.url,
  152. event.location,
  153. event.notes,
  154. event.description
  155. ];
  156. for (const field of fieldsToSearch) {
  157. if (typeof field === 'string') {
  158. const matches = urlRegExp.exec(field) || schemeRegExp.exec(field);
  159. if (matches) {
  160. const url = parseURIString(matches[0]);
  161. if (url) {
  162. return url.toString();
  163. }
  164. }
  165. }
  166. }
  167. return null;
  168. }
  169. /**
  170. * Clears the calendar access status when the app comes back from the
  171. * background. This is needed as some users may never quit the app, but puts it
  172. * into the background and we need to try to request for a permission as often
  173. * as possible, but not annoyingly often.
  174. *
  175. * @param {Object} store - The redux store.
  176. * @param {Object} action - The Redux action.
  177. * @private
  178. * @returns {void}
  179. */
  180. function _maybeClearAccessStatus(store, { appState }) {
  181. appState === 'background'
  182. && store.dispatch(setCalendarAuthorization(undefined));
  183. }
  184. /**
  185. * Updates the calendar entries in Redux when new list is received.
  186. *
  187. * @param {Object} event - An event returned from the native calendar.
  188. * @param {Array<string>} knownDomains - The known domain list.
  189. * @private
  190. * @returns {CalendarEntry}
  191. */
  192. function _parseCalendarEntry(event, knownDomains) {
  193. if (event) {
  194. const url = _getURLFromEvent(event, knownDomains);
  195. if (url) {
  196. const startDate = Date.parse(event.startDate);
  197. const endDate = Date.parse(event.endDate);
  198. if (isNaN(startDate) || isNaN(endDate)) {
  199. logger.warn(
  200. 'Skipping invalid calendar event',
  201. event.title,
  202. event.startDate,
  203. event.endDate
  204. );
  205. } else {
  206. return {
  207. endDate,
  208. id: event.id,
  209. startDate,
  210. title: event.title,
  211. url
  212. };
  213. }
  214. }
  215. }
  216. return null;
  217. }
  218. /**
  219. * Updates the calendar entries in redux when new list is received. The feature
  220. * calendar-sync doesn't display all calendar events, it displays unique
  221. * title, URL, and start time tuples i.e. it doesn't display subsequent
  222. * occurrences of recurring events, and the repetitions of events coming from
  223. * multiple calendars.
  224. *
  225. * XXX The function's {@code this} is the redux store.
  226. *
  227. * @param {Array<CalendarEntry>} events - The new event list.
  228. * @private
  229. * @returns {void}
  230. */
  231. function _updateCalendarEntries(events) {
  232. if (!events || !events.length) {
  233. return;
  234. }
  235. // eslint-disable-next-line no-invalid-this
  236. const { dispatch, getState } = this;
  237. const knownDomains = getState()['features/base/known-domains'];
  238. const now = Date.now();
  239. const entryMap = new Map();
  240. for (const event of events) {
  241. const entry = _parseCalendarEntry(event, knownDomains);
  242. if (entry && entry.endDate > now) {
  243. // As was stated above, we don't display subsequent occurrences of
  244. // recurring events, and the repetitions of events coming from
  245. // multiple calendars.
  246. const key = md5.hex(JSON.stringify([
  247. // Obviously, we want to display different conference/meetings
  248. // URLs. URLs are the very reason why we implemented the feature
  249. // calendar-sync in the first place.
  250. entry.url,
  251. // We probably want to display one and the same URL to people if
  252. // they have it under different titles in their Calendar.
  253. // Because maybe they remember the title of the meeting, not the
  254. // URL so they expect to see the title without realizing that
  255. // they have the same URL already under a different title.
  256. entry.title,
  257. // XXX Eventually, given that the URL and the title are the
  258. // same, what sets one event apart from another is the start
  259. // time of the day (note the use of toTimeString() bellow)! The
  260. // day itself is not important because we don't want multiple
  261. // occurrences of a recurring event or repetitions of an even
  262. // from multiple calendars.
  263. new Date(entry.startDate).toTimeString()
  264. ]));
  265. const existingEntry = entryMap.get(key);
  266. // We want only the earliest occurrence (which hasn't ended in the
  267. // past, that is) of a recurring event.
  268. if (!existingEntry || existingEntry.startDate > entry.startDate) {
  269. entryMap.set(key, entry);
  270. }
  271. }
  272. }
  273. dispatch(
  274. setCalendarEvents(
  275. Array.from(entryMap.values())
  276. .sort((a, b) => a.startDate - b.startDate)
  277. .slice(0, MAX_LIST_LENGTH)));
  278. }