Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

middleware.js 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // @flow
  2. import RNCalendarEvents from 'react-native-calendar-events';
  3. import { APP_WILL_MOUNT } from '../app';
  4. import { ADD_KNOWN_DOMAINS, addKnownDomains } from '../base/known-domains';
  5. import { MiddlewareRegistry } from '../base/redux';
  6. import { APP_LINK_SCHEME, parseURIString } from '../base/util';
  7. import { APP_STATE_CHANGED } from '../mobile/background';
  8. import { setCalendarAuthorization, setCalendarEvents } from './actions';
  9. import { REFRESH_CALENDAR } from './actionTypes';
  10. import { CALENDAR_ENABLED } from './constants';
  11. const logger = require('jitsi-meet-logger').getLogger(__filename);
  12. /**
  13. * The number of days to fetch.
  14. */
  15. const FETCH_END_DAYS = 10;
  16. /**
  17. * The number of days to go back when fetching.
  18. */
  19. const FETCH_START_DAYS = -1;
  20. /**
  21. * The max number of events to fetch from the calendar.
  22. */
  23. const MAX_LIST_LENGTH = 10;
  24. CALENDAR_ENABLED
  25. && MiddlewareRegistry.register(store => next => action => {
  26. switch (action.type) {
  27. case ADD_KNOWN_DOMAINS: {
  28. // XXX Fetch new calendar entries only when an actual domain has
  29. // become known.
  30. const { getState } = store;
  31. const oldValue = getState()['features/base/known-domains'];
  32. const result = next(action);
  33. const newValue = getState()['features/base/known-domains'];
  34. oldValue === newValue || _fetchCalendarEntries(store, false, false);
  35. return result;
  36. }
  37. case APP_STATE_CHANGED: {
  38. const result = next(action);
  39. _maybeClearAccessStatus(store, action);
  40. return result;
  41. }
  42. case APP_WILL_MOUNT: {
  43. // For legacy purposes, we've allowed the deserialization of
  44. // knownDomains and now we're to translate it to base/known-domains.
  45. const state = store.getState()['features/calendar-sync'];
  46. if (state) {
  47. const { knownDomains } = state;
  48. Array.isArray(knownDomains)
  49. && knownDomains.length
  50. && store.dispatch(addKnownDomains(knownDomains));
  51. }
  52. _fetchCalendarEntries(store, false, false);
  53. return next(action);
  54. }
  55. case REFRESH_CALENDAR: {
  56. const result = next(action);
  57. _fetchCalendarEntries(store, true, action.forcePermission);
  58. return result;
  59. }
  60. }
  61. return next(action);
  62. });
  63. /**
  64. * Ensures calendar access if possible and resolves the promise if it's granted.
  65. *
  66. * @param {boolean} promptForPermission - Flag to tell the app if it should
  67. * prompt for a calendar permission if it wasn't granted yet.
  68. * @param {Function} dispatch - The Redux dispatch function.
  69. * @private
  70. * @returns {Promise}
  71. */
  72. function _ensureCalendarAccess(promptForPermission, dispatch) {
  73. return new Promise((resolve, reject) => {
  74. RNCalendarEvents.authorizationStatus()
  75. .then(status => {
  76. if (status === 'authorized') {
  77. resolve(true);
  78. } else if (promptForPermission) {
  79. RNCalendarEvents.authorizeEventStore()
  80. .then(result => {
  81. dispatch(setCalendarAuthorization(result));
  82. resolve(result === 'authorized');
  83. })
  84. .catch(reject);
  85. } else {
  86. resolve(false);
  87. }
  88. })
  89. .catch(reject);
  90. });
  91. }
  92. /**
  93. * Reads the user's calendar and updates the stored entries if need be.
  94. *
  95. * @param {Object} store - The redux store.
  96. * @param {boolean} maybePromptForPermission - Flag to tell the app if it should
  97. * prompt for a calendar permission if it wasn't granted yet.
  98. * @param {boolean|undefined} forcePermission - Whether to force to re-ask for
  99. * the permission or not.
  100. * @private
  101. * @returns {void}
  102. */
  103. function _fetchCalendarEntries(
  104. store,
  105. maybePromptForPermission,
  106. forcePermission) {
  107. const { dispatch, getState } = store;
  108. const promptForPermission
  109. = (maybePromptForPermission
  110. && !getState()['features/calendar-sync'].authorization)
  111. || forcePermission;
  112. _ensureCalendarAccess(promptForPermission, dispatch)
  113. .then(accessGranted => {
  114. if (accessGranted) {
  115. const startDate = new Date();
  116. const endDate = new Date();
  117. startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
  118. endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
  119. RNCalendarEvents.fetchAllEvents(
  120. startDate.getTime(),
  121. endDate.getTime(),
  122. [])
  123. .then(_updateCalendarEntries.bind(store))
  124. .catch(error =>
  125. logger.error('Error fetching calendar.', error));
  126. } else {
  127. logger.warn('Calendar access not granted.');
  128. }
  129. })
  130. .catch(reason => logger.error('Error accessing calendar.', reason));
  131. }
  132. /**
  133. * Retrieves a Jitsi Meet URL from an event if present.
  134. *
  135. * @param {Object} event - The event to parse.
  136. * @param {Array<string>} knownDomains - The known domain names.
  137. * @private
  138. * @returns {string}
  139. */
  140. function _getURLFromEvent(event, knownDomains) {
  141. const linkTerminatorPattern = '[^\\s<>$]';
  142. const urlRegExp
  143. = new RegExp(
  144. `http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`,
  145. 'gi');
  146. const schemeRegExp
  147. = new RegExp(`${APP_LINK_SCHEME}${linkTerminatorPattern}+`, 'gi');
  148. const fieldsToSearch = [
  149. event.title,
  150. event.url,
  151. event.location,
  152. event.notes,
  153. event.description
  154. ];
  155. for (const field of fieldsToSearch) {
  156. if (typeof field === 'string') {
  157. const matches = urlRegExp.exec(field) || schemeRegExp.exec(field);
  158. if (matches) {
  159. const url = parseURIString(matches[0]);
  160. if (url) {
  161. return url.toString();
  162. }
  163. }
  164. }
  165. }
  166. return null;
  167. }
  168. /**
  169. * Clears the calendar access status when the app comes back from the
  170. * background. This is needed as some users may never quit the app, but puts it
  171. * into the background and we need to try to request for a permission as often
  172. * as possible, but not annoyingly often.
  173. *
  174. * @param {Object} store - The redux store.
  175. * @param {Object} action - The Redux action.
  176. * @private
  177. * @returns {void}
  178. */
  179. function _maybeClearAccessStatus(store, { appState }) {
  180. appState === 'background'
  181. && store.dispatch(setCalendarAuthorization(undefined));
  182. }
  183. /**
  184. * Updates the calendar entries in Redux when new list is received.
  185. *
  186. * @param {Object} event - An event returned from the native calendar.
  187. * @param {Array<string>} knownDomains - The known domain list.
  188. * @private
  189. * @returns {CalendarEntry}
  190. */
  191. function _parseCalendarEntry(event, knownDomains) {
  192. if (event) {
  193. const url = _getURLFromEvent(event, knownDomains);
  194. if (url) {
  195. const startDate = Date.parse(event.startDate);
  196. const endDate = Date.parse(event.endDate);
  197. if (isNaN(startDate) || isNaN(endDate)) {
  198. logger.warn(
  199. 'Skipping invalid calendar event',
  200. event.title,
  201. event.startDate,
  202. event.endDate
  203. );
  204. } else {
  205. return {
  206. endDate,
  207. id: event.id,
  208. startDate,
  209. title: event.title,
  210. url
  211. };
  212. }
  213. }
  214. }
  215. return null;
  216. }
  217. /**
  218. * Updates the calendar entries in redux when new list is received.
  219. *
  220. * XXX The function's {@code this} is the redux store.
  221. *
  222. * @param {Array<CalendarEntry>} events - The new event list.
  223. * @private
  224. * @returns {void}
  225. */
  226. function _updateCalendarEntries(events) {
  227. if (events && events.length) {
  228. // eslint-disable-next-line no-invalid-this
  229. const { dispatch, getState } = this;
  230. const knownDomains = getState()['features/base/known-domains'];
  231. const eventList = [];
  232. const now = Date.now();
  233. for (const event of events) {
  234. const calendarEntry = _parseCalendarEntry(event, knownDomains);
  235. calendarEntry
  236. && calendarEntry.endDate > now
  237. && eventList.push(calendarEntry);
  238. }
  239. dispatch(
  240. setCalendarEvents(
  241. eventList
  242. .sort((a, b) => a.startDate - b.startDate)
  243. .slice(0, MAX_LIST_LENGTH)));
  244. }
  245. }