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.

functions.any.js 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. // @flow
  2. import md5 from 'js-md5';
  3. import { setCalendarEvents } from './actions';
  4. import { APP_LINK_SCHEME, parseURIString } from '../base/util';
  5. import { MAX_LIST_LENGTH } from './constants';
  6. const logger = require('jitsi-meet-logger').getLogger(__filename);
  7. /**
  8. * Updates the calendar entries in redux when new list is received. The feature
  9. * calendar-sync doesn't display all calendar events, it displays unique
  10. * title, URL, and start time tuples i.e. it doesn't display subsequent
  11. * occurrences of recurring events, and the repetitions of events coming from
  12. * multiple calendars.
  13. *
  14. * XXX The function's {@code this} is the redux store.
  15. *
  16. * @param {Array<CalendarEntry>} events - The new event list.
  17. * @private
  18. * @returns {void}
  19. */
  20. export function _updateCalendarEntries(events: Array<Object>) {
  21. if (!events || !events.length) {
  22. return;
  23. }
  24. // eslint-disable-next-line no-invalid-this
  25. const { dispatch, getState } = this;
  26. const knownDomains = getState()['features/base/known-domains'];
  27. const now = Date.now();
  28. const entryMap = new Map();
  29. for (const event of events) {
  30. const entry = _parseCalendarEntry(event, knownDomains);
  31. if (entry && entry.endDate > now) {
  32. // As was stated above, we don't display subsequent occurrences of
  33. // recurring events, and the repetitions of events coming from
  34. // multiple calendars.
  35. const key = md5.hex(JSON.stringify([
  36. // Obviously, we want to display different conference/meetings
  37. // URLs. URLs are the very reason why we implemented the feature
  38. // calendar-sync in the first place.
  39. entry.url,
  40. // We probably want to display one and the same URL to people if
  41. // they have it under different titles in their Calendar.
  42. // Because maybe they remember the title of the meeting, not the
  43. // URL so they expect to see the title without realizing that
  44. // they have the same URL already under a different title.
  45. entry.title,
  46. // XXX Eventually, given that the URL and the title are the
  47. // same, what sets one event apart from another is the start
  48. // time of the day (note the use of toTimeString() bellow)! The
  49. // day itself is not important because we don't want multiple
  50. // occurrences of a recurring event or repetitions of an even
  51. // from multiple calendars.
  52. new Date(entry.startDate).toTimeString()
  53. ]));
  54. const existingEntry = entryMap.get(key);
  55. // We want only the earliest occurrence (which hasn't ended in the
  56. // past, that is) of a recurring event.
  57. if (!existingEntry || existingEntry.startDate > entry.startDate) {
  58. entryMap.set(key, entry);
  59. }
  60. }
  61. }
  62. dispatch(
  63. setCalendarEvents(
  64. Array.from(entryMap.values())
  65. .sort((a, b) => a.startDate - b.startDate)
  66. .slice(0, MAX_LIST_LENGTH)));
  67. }
  68. /**
  69. * Updates the calendar entries in Redux when new list is received.
  70. *
  71. * @param {Object} event - An event returned from the native calendar.
  72. * @param {Array<string>} knownDomains - The known domain list.
  73. * @private
  74. * @returns {CalendarEntry}
  75. */
  76. function _parseCalendarEntry(event, knownDomains) {
  77. if (event) {
  78. const url = _getURLFromEvent(event, knownDomains);
  79. // we only filter events without url on mobile, this is temporary
  80. // till we implement event edit on mobile
  81. if (url || navigator.product !== 'ReactNative') {
  82. const startDate = Date.parse(event.startDate);
  83. const endDate = Date.parse(event.endDate);
  84. // we want to hide all events that
  85. // - has no start or end date
  86. // - for web, if there is no url and we cannot edit the event (has
  87. // no calendarId)
  88. if (isNaN(startDate)
  89. || isNaN(endDate)
  90. || (navigator.product !== 'ReactNative'
  91. && !url
  92. && !event.calendarId)) {
  93. logger.debug(
  94. 'Skipping invalid calendar event',
  95. event.title,
  96. event.startDate,
  97. event.endDate,
  98. url,
  99. event.calendarId
  100. );
  101. } else {
  102. return {
  103. calendarId: event.calendarId,
  104. endDate,
  105. id: event.id,
  106. startDate,
  107. title: event.title,
  108. url
  109. };
  110. }
  111. }
  112. }
  113. return null;
  114. }
  115. /**
  116. * Retrieves a Jitsi Meet URL from an event if present.
  117. *
  118. * @param {Object} event - The event to parse.
  119. * @param {Array<string>} knownDomains - The known domain names.
  120. * @private
  121. * @returns {string}
  122. */
  123. function _getURLFromEvent(event, knownDomains) {
  124. const linkTerminatorPattern = '[^\\s<>$]';
  125. const urlRegExp
  126. = new RegExp(
  127. `http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`,
  128. 'gi');
  129. const schemeRegExp
  130. = new RegExp(`${APP_LINK_SCHEME}${linkTerminatorPattern}+`, 'gi');
  131. const fieldsToSearch = [
  132. event.title,
  133. event.url,
  134. event.location,
  135. event.notes,
  136. event.description
  137. ];
  138. for (const field of fieldsToSearch) {
  139. if (typeof field === 'string') {
  140. const matches = urlRegExp.exec(field) || schemeRegExp.exec(field);
  141. if (matches) {
  142. const url = parseURIString(matches[0]);
  143. if (url) {
  144. return url.toString();
  145. }
  146. }
  147. }
  148. }
  149. return null;
  150. }