Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

middleware.js 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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. This
  220. * function also dedupes the list of entries based on exact match for title, URL
  221. * and time of the day and then sorts by time and limits the list
  222. * to MAX_LIST_LENGTH.
  223. *
  224. * XXX The function's {@code this} is the redux store.
  225. *
  226. * @param {Array<CalendarEntry>} events - The new event list.
  227. * @private
  228. * @returns {void}
  229. */
  230. function _updateCalendarEntries(events) {
  231. if (events && events.length) {
  232. // eslint-disable-next-line no-invalid-this
  233. const { dispatch, getState } = this;
  234. const knownDomains = getState()['features/base/known-domains'];
  235. const now = Date.now();
  236. const entryMap = new Map();
  237. for (const event of events) {
  238. const calendarEntry = _parseCalendarEntry(event, knownDomains);
  239. if (calendarEntry && calendarEntry.endDate > now) {
  240. // This is the data structure we'll try to match all entries to.
  241. // The smaller the better, hence the single letter field names.
  242. const eventMatchHash = md5.hex(JSON.stringify({
  243. d: new Date(calendarEntry.startDate).toTimeString(),
  244. t: calendarEntry.title,
  245. u: calendarEntry.url
  246. }));
  247. const existingEntry = entryMap.get(eventMatchHash);
  248. // We need to dedupe the list based on title, URL and time of
  249. // the day, and only show the first occurence. So if there was
  250. // no matcing entry or there was, but its a later event, we
  251. // overwrite/save it in the map.
  252. if (!existingEntry
  253. || existingEntry.startDate > event.startDate) {
  254. entryMap.set(eventMatchHash, calendarEntry);
  255. }
  256. }
  257. }
  258. dispatch(
  259. setCalendarEvents(
  260. Array.from(entryMap.values())
  261. .sort((a, b) => a.startDate - b.startDate)
  262. .slice(0, MAX_LIST_LENGTH)
  263. ));
  264. }
  265. }