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 6.3KB

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