@twentyhq/call-recorder 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/.env.example +5 -0
  2. package/.nvmrc +1 -0
  3. package/.oxlintrc.json +20 -0
  4. package/.yarnrc.yml +1 -0
  5. package/AGENTS.md +67 -0
  6. package/CLAUDE.md +67 -0
  7. package/README.md +24 -0
  8. package/SETUP.md +95 -0
  9. package/package.json +42 -0
  10. package/public/gallery/call-recorder-cover.png +0 -0
  11. package/public/logo.svg +5 -0
  12. package/src/__tests__/global-setup.ts +100 -0
  13. package/src/__tests__/schema.integration-test.ts +104 -0
  14. package/src/application-config.ts +96 -0
  15. package/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts +19 -0
  16. package/src/constants/app-description.ts +2 -0
  17. package/src/constants/app-display-name.ts +1 -0
  18. package/src/constants/application-universal-identifier.ts +2 -0
  19. package/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts +2 -0
  20. package/src/constants/calendar-event-record-page-layout-universal-identifier.ts +2 -0
  21. package/src/constants/calendar-event-recording-front-component-universal-identifier.ts +2 -0
  22. package/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts +2 -0
  23. package/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts +2 -0
  24. package/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts +2 -0
  25. package/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts +2 -0
  26. package/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts +2 -0
  27. package/src/constants/call-recorder-name-app-variable-universal-identifier.ts +2 -0
  28. package/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts +2 -0
  29. package/src/constants/call-recorder-preference-off-option-id.ts +2 -0
  30. package/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts +2 -0
  31. package/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts +2 -0
  32. package/src/constants/call-recorder-preference-on-option-id.ts +2 -0
  33. package/src/constants/call-recorder-preference.ts +4 -0
  34. package/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts +2 -0
  35. package/src/constants/call-recording-audio-field-universal-identifier.ts +2 -0
  36. package/src/constants/call-recording-video-field-universal-identifier.ts +2 -0
  37. package/src/constants/default-role-universal-identifier.ts +2 -0
  38. package/src/constants/process-recall-webhook-logic-function-universal-identifier.ts +2 -0
  39. package/src/constants/recall-webhook-logic-function-universal-identifier.ts +2 -0
  40. package/src/constants/stale-bot-state-logic-function-universal-identifier.ts +2 -0
  41. package/src/default-role.ts +69 -0
  42. package/src/fields/call-recorder-failure-reason-on-call-recording.field.ts +22 -0
  43. package/src/fields/call-recorder-preference-on-calendar-event.field.ts +41 -0
  44. package/src/front-components/calendar-event-recording.front-component.tsx +13 -0
  45. package/src/front-components/components/CalendarEventRecording.tsx +39 -0
  46. package/src/front-components/components/CalendarEventRecordingBody.tsx +96 -0
  47. package/src/front-components/components/CalendarEventRecordingContent.tsx +111 -0
  48. package/src/front-components/components/RecordingTranscript.tsx +92 -0
  49. package/src/front-components/components/RecordingVideoPlayer.tsx +52 -0
  50. package/src/front-components/components/TranscriptEntryList.tsx +61 -0
  51. package/src/front-components/components/TranscriptEntryListItem.tsx +115 -0
  52. package/src/front-components/components/TranscriptErrorBox.tsx +48 -0
  53. package/src/front-components/components/TranscriptSpeakerAvatar.tsx +141 -0
  54. package/src/front-components/components/TranscriptSpeakerChip.tsx +51 -0
  55. package/src/front-components/constants/recording-theme-css-variables.ts +40 -0
  56. package/src/front-components/hooks/use-calendar-event-participants.ts +172 -0
  57. package/src/front-components/hooks/use-calendar-event-recording.ts +155 -0
  58. package/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts +6 -0
  59. package/src/front-components/types/calendar-event-recording-participant.type.ts +7 -0
  60. package/src/front-components/types/transcript-entry.type.ts +13 -0
  61. package/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts +66 -0
  62. package/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts +29 -0
  63. package/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts +22 -0
  64. package/src/front-components/utils/__tests__/parse-transcript-entries.test.ts +162 -0
  65. package/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts +45 -0
  66. package/src/front-components/utils/find-active-transcript-entry-index.util.ts +77 -0
  67. package/src/front-components/utils/format-transcript-timestamp.util.ts +16 -0
  68. package/src/front-components/utils/get-absolute-avatar-url.util.ts +48 -0
  69. package/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts +24 -0
  70. package/src/front-components/utils/get-speaker-name-match-keys.util.ts +64 -0
  71. package/src/front-components/utils/get-video-file-extension.util.ts +23 -0
  72. package/src/front-components/utils/parse-transcript-entries.util.ts +85 -0
  73. package/src/logic-functions/__tests__/process-recall-webhook.test.ts +62 -0
  74. package/src/logic-functions/__tests__/recall-webhook.test.ts +180 -0
  75. package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts +2 -0
  76. package/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts +1 -0
  77. package/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts +2 -0
  78. package/src/logic-functions/constants/call-recorder-name-env-var-name.ts +1 -0
  79. package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts +2 -0
  80. package/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts +1 -0
  81. package/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts +2 -0
  82. package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts +2 -0
  83. package/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts +1 -0
  84. package/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts +1 -0
  85. package/src/logic-functions/constants/call-recording-request-status.ts +5 -0
  86. package/src/logic-functions/constants/call-recording-status.ts +9 -0
  87. package/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts +1 -0
  88. package/src/logic-functions/constants/default-call-recorder-name.ts +1 -0
  89. package/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts +2 -0
  90. package/src/logic-functions/constants/default-recall-region.ts +1 -0
  91. package/src/logic-functions/constants/milliseconds-per-minute.ts +1 -0
  92. package/src/logic-functions/constants/non-terminal-call-recording-statuses.ts +8 -0
  93. package/src/logic-functions/constants/recall-api-key-env-var-name.ts +1 -0
  94. package/src/logic-functions/constants/recall-api-max-attempts.ts +1 -0
  95. package/src/logic-functions/constants/recall-api-retry-delay-ms.ts +1 -0
  96. package/src/logic-functions/constants/recall-bot-automatic-leave.ts +74 -0
  97. package/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts +1 -0
  98. package/src/logic-functions/constants/recall-bot-recording-config.ts +34 -0
  99. package/src/logic-functions/constants/recall-region-env-var-name.ts +1 -0
  100. package/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts +1 -0
  101. package/src/logic-functions/constants/restricted-field-placeholder.ts +3 -0
  102. package/src/logic-functions/constants/stale-bot-state-cron-pattern.ts +1 -0
  103. package/src/logic-functions/constants/twenty-page-size.ts +1 -0
  104. package/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts +55 -0
  105. package/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts +43 -0
  106. package/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts +38 -0
  107. package/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts +22 -0
  108. package/src/logic-functions/data/complete-call-recording-ingestion.util.ts +24 -0
  109. package/src/logic-functions/data/create-call-recording.util.ts +41 -0
  110. package/src/logic-functions/data/fetch-all-nodes.util.ts +44 -0
  111. package/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts +80 -0
  112. package/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts +20 -0
  113. package/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts +19 -0
  114. package/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts +17 -0
  115. package/src/logic-functions/data/find-call-recordings-by-filter.util.ts +102 -0
  116. package/src/logic-functions/data/find-call-recordings-by-ids.util.ts +17 -0
  117. package/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts +14 -0
  118. package/src/logic-functions/data/get-current-workspace-id.util.ts +36 -0
  119. package/src/logic-functions/data/strip-restricted-field-value.util.ts +6 -0
  120. package/src/logic-functions/data/update-call-recording.util.ts +24 -0
  121. package/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts +47 -0
  122. package/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts +71 -0
  123. package/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts +37 -0
  124. package/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts +88 -0
  125. package/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts +59 -0
  126. package/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts +37 -0
  127. package/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts +120 -0
  128. package/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts +102 -0
  129. package/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts +42 -0
  130. package/src/logic-functions/domain/build-call-recorder-policy-result.util.ts +53 -0
  131. package/src/logic-functions/domain/build-failed-transcript-marker.util.ts +13 -0
  132. package/src/logic-functions/domain/build-pending-transcript-marker.util.ts +13 -0
  133. package/src/logic-functions/domain/build-recall-routing-metadata.util.ts +12 -0
  134. package/src/logic-functions/domain/build-transcript-failure-reason.util.ts +7 -0
  135. package/src/logic-functions/domain/compute-call-recording-charge.util.ts +41 -0
  136. package/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts +16 -0
  137. package/src/logic-functions/domain/compute-real-meeting-key.util.ts +48 -0
  138. package/src/logic-functions/domain/compute-recall-bot-join-at.util.ts +34 -0
  139. package/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts +19 -0
  140. package/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts +37 -0
  141. package/src/logic-functions/domain/is-recall-recording-done-signal.util.ts +13 -0
  142. package/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts +26 -0
  143. package/src/logic-functions/domain/parse-transcript-marker.util.ts +29 -0
  144. package/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts +72 -0
  145. package/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts +32 -0
  146. package/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts +45 -0
  147. package/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts +61 -0
  148. package/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts +727 -0
  149. package/src/logic-functions/flows/__tests__/download-transcript.test.ts +74 -0
  150. package/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts +1301 -0
  151. package/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts +225 -0
  152. package/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts +153 -0
  153. package/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts +425 -0
  154. package/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts +1007 -0
  155. package/src/logic-functions/flows/cancel-call-recording-request.util.ts +46 -0
  156. package/src/logic-functions/flows/charge-completed-call-recording.util.ts +31 -0
  157. package/src/logic-functions/flows/complete-and-charge-call-recording.util.ts +29 -0
  158. package/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts +8 -0
  159. package/src/logic-functions/flows/converge-diverged-call-recordings.util.ts +447 -0
  160. package/src/logic-functions/flows/download-transcript.util.ts +67 -0
  161. package/src/logic-functions/flows/ensure-call-recorder.util.ts +73 -0
  162. package/src/logic-functions/flows/handle-recall-webhook.util.ts +672 -0
  163. package/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts +82 -0
  164. package/src/logic-functions/flows/ingest-call-recording-media.util.ts +128 -0
  165. package/src/logic-functions/flows/persist-call-recording-progress.util.ts +58 -0
  166. package/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts +183 -0
  167. package/src/logic-functions/flows/reconcile-call-recorder.util.ts +495 -0
  168. package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts +11 -0
  169. package/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts +182 -0
  170. package/src/logic-functions/flows/reschedule-call-recording-bot.util.ts +69 -0
  171. package/src/logic-functions/process-recall-webhook.ts +23 -0
  172. package/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts +153 -0
  173. package/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts +67 -0
  174. package/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts +744 -0
  175. package/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts +122 -0
  176. package/src/logic-functions/recall-api/cancel-recall-bot.util.ts +28 -0
  177. package/src/logic-functions/recall-api/create-async-recall-transcript.util.ts +47 -0
  178. package/src/logic-functions/recall-api/eject-recall-bot.util.ts +28 -0
  179. package/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts +149 -0
  180. package/src/logic-functions/recall-api/extract-recall-bot-id.util.ts +10 -0
  181. package/src/logic-functions/recall-api/extract-recall-media-urls.util.ts +30 -0
  182. package/src/logic-functions/recall-api/extract-twenty-workspace-id-from-recall-webhook.util.ts +8 -0
  183. package/src/logic-functions/recall-api/get-recall-api-config.util.ts +59 -0
  184. package/src/logic-functions/recall-api/get-recall-bot.util.ts +42 -0
  185. package/src/logic-functions/recall-api/get-recall-recording.util.ts +31 -0
  186. package/src/logic-functions/recall-api/get-recall-webhook-bot-metadata.util.ts +18 -0
  187. package/src/logic-functions/recall-api/list-recall-transcripts.util.ts +141 -0
  188. package/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts +106 -0
  189. package/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts +14 -0
  190. package/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts +88 -0
  191. package/src/logic-functions/recall-api/recall-bot-api-request.util.ts +165 -0
  192. package/src/logic-functions/recall-api/recall-transcript-summary.type.ts +5 -0
  193. package/src/logic-functions/recall-api/reschedule-recall-bot.util.ts +56 -0
  194. package/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts +71 -0
  195. package/src/logic-functions/recall-api/schedule-recall-bot.util.ts +68 -0
  196. package/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts +109 -0
  197. package/src/logic-functions/recall-webhook.ts +90 -0
  198. package/src/logic-functions/reconcile-call-recorder-calendar-event.ts +178 -0
  199. package/src/logic-functions/reconcile-stale-bot-state.ts +106 -0
  200. package/src/logic-functions/types/calendar-event-record.type.ts +5 -0
  201. package/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts +10 -0
  202. package/src/logic-functions/types/call-recorder-policy-input.type.ts +9 -0
  203. package/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts +5 -0
  204. package/src/logic-functions/types/call-recorder-policy-required-reason.type.ts +1 -0
  205. package/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts +9 -0
  206. package/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts +6 -0
  207. package/src/logic-functions/types/call-recorder-policy-result.type.ts +12 -0
  208. package/src/logic-functions/types/call-recorder-reconciliation-result.type.ts +16 -0
  209. package/src/logic-functions/types/call-recording-media-file.type.ts +1 -0
  210. package/src/logic-functions/types/call-recording-record.type.ts +15 -0
  211. package/src/logic-functions/types/call-recording-update-fields.type.ts +20 -0
  212. package/src/logic-functions/types/files-field-value.type.ts +1 -0
  213. package/src/logic-functions/types/meeting-recording.type.ts +7 -0
  214. package/src/logic-functions/types/recall-bot-operation-result.type.ts +19 -0
  215. package/src/logic-functions/types/recall-routing-metadata.type.ts +4 -0
  216. package/src/logic-functions/types/removed-call-recorder-occurrence.type.ts +6 -0
  217. package/src/logic-functions/types/transcript-marker.type.ts +6 -0
  218. package/src/logic-functions/utils/as-record.util.ts +6 -0
  219. package/src/logic-functions/utils/get-application-variable-value.util.ts +3 -0
  220. package/src/logic-functions/utils/get-record-at-path.util.ts +10 -0
  221. package/src/logic-functions/utils/get-string.util.ts +4 -0
  222. package/src/logic-functions/utils/get-unique-sorted-ids.util.ts +8 -0
  223. package/src/logic-functions/utils/is-non-empty-string.util.ts +5 -0
  224. package/src/page-layouts/calendar-event-recording-tab.ts +33 -0
  225. package/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts +27 -0
  226. package/tsconfig.json +42 -0
  227. package/tsconfig.spec.json +9 -0
  228. package/vitest.config.ts +31 -0
  229. package/vitest.unit.config.ts +14 -0
@@ -0,0 +1,19 @@
1
+ import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recording-audio-field-universal-identifier';
5
+ import { CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recording-video-field-universal-identifier';
6
+
7
+ // This test is nothing more than a sanity check to ensure that the universal identifiers for the call recording media fields are correct.
8
+ describe('call recording field universal identifiers', () => {
9
+ it('matches the standard CallRecording media field identifiers', () => {
10
+ expect(CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER).toBe(
11
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.callRecording.fields.audio
12
+ .universalIdentifier,
13
+ );
14
+ expect(CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER).toBe(
15
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.callRecording.fields.video
16
+ .universalIdentifier,
17
+ );
18
+ });
19
+ });
@@ -0,0 +1,2 @@
1
+ export const APP_DESCRIPTION =
2
+ 'Capture every customer conversation automatically. A call recorder joins eligible meetings and records calls for you.';
@@ -0,0 +1 @@
1
+ export const APP_DISPLAY_NAME = 'Call Recorder';
@@ -0,0 +1,2 @@
1
+ export const APPLICATION_UNIVERSAL_IDENTIFIER =
2
+ '8da4b8b5-5edf-4880-b51f-ab6e679ec617';
@@ -0,0 +1,2 @@
1
+ export const CALENDAR_EVENT_RECONCILIATION_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
2
+ '1f28c477-6423-4911-85bf-2296ef112be9';
@@ -0,0 +1,2 @@
1
+ export const CALENDAR_EVENT_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER =
2
+ 'b9b10e40-9ce2-4704-8ac6-c6e92e2563c1';
@@ -0,0 +1,2 @@
1
+ export const CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER =
2
+ '445d742e-1c27-46ee-a9ab-4a1dde65adcf';
@@ -0,0 +1,2 @@
1
+ export const CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER =
2
+ '10c2c22d-952b-42ea-81fe-2e93e7d30f86';
@@ -0,0 +1,2 @@
1
+ export const CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER =
2
+ '29c2315b-2ee6-41df-91b7-42f58f6c1a53';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
2
+ 'c866ddd4-fb7b-4cb4-8ad1-5599755e495c';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_FAILURE_REASON_ON_CALL_RECORDING_FIELD_UNIVERSAL_IDENTIFIER =
2
+ '33a577e4-02f5-48bd-8bed-d365949caa72';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
2
+ '0568ebb2-3f64-47de-8c0d-d367dfbb7462';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
2
+ 'c54cbacd-ad10-40b4-9056-7aaf23846d64';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
2
+ '241180e8-d864-4160-ad02-db44a9e8d395';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_PREFERENCE_OFF_OPTION_ID =
2
+ 'cc7de62a-08b6-46c8-aa69-f8117e7dd722';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_PREFERENCE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER =
2
+ '8ee9444a-2437-4def-8e61-6e493862a4fd';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_PREFERENCE_ON_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
2
+ 'e8c7e9c5-2b4f-4a75-ae46-ea331809106c';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_PREFERENCE_ON_OPTION_ID =
2
+ '72431216-49c4-47c8-99af-de4c3831b0be';
@@ -0,0 +1,4 @@
1
+ export enum CallRecorderPreference {
2
+ ON = 'ON',
3
+ OFF = 'OFF',
4
+ }
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER =
2
+ '12e4e14d-d539-4d07-b477-4773539dd20b';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER =
2
+ '2eafc2d0-8fec-430c-a939-65ca5fbc0f08';
@@ -0,0 +1,2 @@
1
+ export const CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER =
2
+ 'bb9523d3-457e-4f4b-8c79-27a77afb87da';
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
2
+ '5fcf4d3a-0aca-42d9-9beb-7387f43ec180';
@@ -0,0 +1,2 @@
1
+ export const PROCESS_RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
2
+ '13d9c427-447e-494a-8d3c-1af5d0bacb82';
@@ -0,0 +1,2 @@
1
+ export const RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
2
+ '9215afe6-1497-4149-a49d-e608e239bbaf';
@@ -0,0 +1,2 @@
1
+ export const STALE_BOT_STATE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
2
+ 'e362aa9b-52c6-4b7e-bb20-927e0e8d7cbe';
@@ -0,0 +1,69 @@
1
+ import {
2
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
3
+ SystemPermissionFlag,
4
+ defineApplicationRole,
5
+ } from 'twenty-sdk/define';
6
+
7
+ import { APP_DISPLAY_NAME } from 'src/constants/app-display-name';
8
+ import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/constants/default-role-universal-identifier';
9
+
10
+ export default defineApplicationRole({
11
+ universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
12
+ label: `${APP_DISPLAY_NAME} default role`,
13
+ description:
14
+ 'Reads calendar events to decide whether the call recorder should attend a meeting; writes the resulting CallRecording records, uploads recording media, and fills transcripts.',
15
+ canReadAllObjectRecords: false,
16
+ canUpdateAllObjectRecords: false,
17
+ canSoftDeleteAllObjectRecords: false,
18
+ canDestroyAllObjectRecords: false,
19
+ canUpdateAllSettings: false,
20
+ canBeAssignedToAgents: false,
21
+ canBeAssignedToUsers: false,
22
+ canBeAssignedToApiKeys: false,
23
+ objectPermissions: [
24
+ {
25
+ objectUniversalIdentifier:
26
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEvent.universalIdentifier,
27
+ canReadObjectRecords: true,
28
+ canUpdateObjectRecords: false,
29
+ canSoftDeleteObjectRecords: false,
30
+ canDestroyObjectRecords: false,
31
+ },
32
+ {
33
+ objectUniversalIdentifier:
34
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEventParticipant
35
+ .universalIdentifier,
36
+ canReadObjectRecords: true,
37
+ canUpdateObjectRecords: false,
38
+ canSoftDeleteObjectRecords: false,
39
+ canDestroyObjectRecords: false,
40
+ },
41
+ {
42
+ objectUniversalIdentifier:
43
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.callRecording.universalIdentifier,
44
+ canReadObjectRecords: true,
45
+ canUpdateObjectRecords: true,
46
+ canSoftDeleteObjectRecords: false,
47
+ canDestroyObjectRecords: false,
48
+ },
49
+ {
50
+ objectUniversalIdentifier:
51
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
52
+ canReadObjectRecords: true,
53
+ canUpdateObjectRecords: false,
54
+ canSoftDeleteObjectRecords: false,
55
+ canDestroyObjectRecords: false,
56
+ },
57
+ {
58
+ objectUniversalIdentifier:
59
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember
60
+ .universalIdentifier,
61
+ canReadObjectRecords: true,
62
+ canUpdateObjectRecords: false,
63
+ canSoftDeleteObjectRecords: false,
64
+ canDestroyObjectRecords: false,
65
+ },
66
+ ],
67
+ fieldPermissions: [],
68
+ permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE],
69
+ });
@@ -0,0 +1,22 @@
1
+ import {
2
+ defineField,
3
+ FieldType,
4
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
5
+ } from 'twenty-sdk/define';
6
+
7
+ import { CALL_RECORDER_FAILURE_REASON_ON_CALL_RECORDING_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier';
8
+
9
+ export default defineField({
10
+ universalIdentifier:
11
+ CALL_RECORDER_FAILURE_REASON_ON_CALL_RECORDING_FIELD_UNIVERSAL_IDENTIFIER,
12
+ objectUniversalIdentifier:
13
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.callRecording.universalIdentifier,
14
+ type: FieldType.TEXT,
15
+ name: 'callRecorderFailureReason',
16
+ label: 'Call Recorder Failure Reason',
17
+ description:
18
+ 'Provider-specific reason the call recorder could not produce a recording.',
19
+ icon: 'IconAlertTriangle',
20
+ isNullable: true,
21
+ isUIEditable: false,
22
+ });
@@ -0,0 +1,41 @@
1
+ import {
2
+ defineField,
3
+ FieldType,
4
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
5
+ } from 'twenty-sdk/define';
6
+
7
+ import { CallRecorderPreference } from 'src/constants/call-recorder-preference';
8
+ import { CALL_RECORDER_PREFERENCE_OFF_OPTION_ID } from 'src/constants/call-recorder-preference-off-option-id';
9
+ import { CALL_RECORDER_PREFERENCE_ON_OPTION_ID } from 'src/constants/call-recorder-preference-on-option-id';
10
+ import { CALL_RECORDER_PREFERENCE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier';
11
+
12
+ export default defineField({
13
+ universalIdentifier:
14
+ CALL_RECORDER_PREFERENCE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER,
15
+ objectUniversalIdentifier:
16
+ STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEvent.universalIdentifier,
17
+ type: FieldType.SELECT,
18
+ name: 'callRecorderPreference',
19
+ label: 'Recording Bot',
20
+ description:
21
+ 'Call recording is on by default when the app is installed. Turn it off for this event when needed.',
22
+ icon: 'IconRobot',
23
+ isNullable: false,
24
+ defaultValue: `'${CallRecorderPreference.ON}'`,
25
+ options: [
26
+ {
27
+ id: CALL_RECORDER_PREFERENCE_ON_OPTION_ID,
28
+ value: CallRecorderPreference.ON,
29
+ label: 'On',
30
+ position: 0,
31
+ color: 'green',
32
+ },
33
+ {
34
+ id: CALL_RECORDER_PREFERENCE_OFF_OPTION_ID,
35
+ value: CallRecorderPreference.OFF,
36
+ label: 'Off',
37
+ position: 1,
38
+ color: 'red',
39
+ },
40
+ ],
41
+ });
@@ -0,0 +1,13 @@
1
+ import { defineFrontComponent } from 'twenty-sdk/define';
2
+
3
+ import { CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-recording-front-component-universal-identifier';
4
+ import { CalendarEventRecording } from 'src/front-components/components/CalendarEventRecording';
5
+
6
+ export default defineFrontComponent({
7
+ universalIdentifier:
8
+ CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
9
+ name: 'calendar-event-recording',
10
+ description:
11
+ 'Read-only recording viewer with synced transcript for the calendar event record page.',
12
+ component: CalendarEventRecording,
13
+ });
@@ -0,0 +1,39 @@
1
+ import styled from '@emotion/styled';
2
+ import { isUndefined } from '@sniptt/guards';
3
+ import { useSelectedRecordIds } from 'twenty-sdk/front-component';
4
+
5
+ import { CalendarEventRecordingContent } from 'src/front-components/components/CalendarEventRecordingContent';
6
+ import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
7
+
8
+ const StyledCenteredState = styled.div`
9
+ align-items: center;
10
+ box-sizing: border-box;
11
+ color: ${recordingThemeCssVariables.font.colorTertiary};
12
+ display: flex;
13
+ font-family: ${recordingThemeCssVariables.font.family};
14
+ font-size: ${recordingThemeCssVariables.font.sizeSm};
15
+ height: 100%;
16
+ justify-content: center;
17
+ padding: ${recordingThemeCssVariables.spacing[4]};
18
+ `;
19
+
20
+ export const CalendarEventRecording = () => {
21
+ const selectedRecordIds = useSelectedRecordIds();
22
+ const calendarEventId =
23
+ selectedRecordIds.length === 1 ? selectedRecordIds[0] : undefined;
24
+
25
+ if (isUndefined(calendarEventId)) {
26
+ return (
27
+ <StyledCenteredState>
28
+ Open a calendar event to see its recording.
29
+ </StyledCenteredState>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <CalendarEventRecordingContent
35
+ key={calendarEventId}
36
+ calendarEventId={calendarEventId}
37
+ />
38
+ );
39
+ };
@@ -0,0 +1,96 @@
1
+ import styled from '@emotion/styled';
2
+ import { isUndefined } from '@sniptt/guards';
3
+
4
+ import { RecordingTranscript } from 'src/front-components/components/RecordingTranscript';
5
+ import { RecordingVideoPlayer } from 'src/front-components/components/RecordingVideoPlayer';
6
+ import { TranscriptErrorBox } from 'src/front-components/components/TranscriptErrorBox';
7
+ import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
8
+ import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type';
9
+
10
+ const StyledCenteredState = styled.div`
11
+ align-items: center;
12
+ box-sizing: border-box;
13
+ color: ${recordingThemeCssVariables.font.colorTertiary};
14
+ display: flex;
15
+ font-family: ${recordingThemeCssVariables.font.family};
16
+ font-size: ${recordingThemeCssVariables.font.sizeSm};
17
+ justify-content: center;
18
+ min-height: 240px;
19
+ padding: ${recordingThemeCssVariables.spacing[4]};
20
+ `;
21
+
22
+ const StyledRecordingContainer = styled.div<{
23
+ $hasVideo?: boolean;
24
+ }>`
25
+ display: grid;
26
+ gap: ${recordingThemeCssVariables.spacing[2]};
27
+ grid-template-rows: ${({ $hasVideo }) =>
28
+ $hasVideo ? 'auto minmax(0, 1fr)' : 'minmax(0, 1fr)'};
29
+ min-height: 0;
30
+ `;
31
+
32
+ type CalendarEventRecordingBodyProps = {
33
+ transcript: unknown;
34
+ videoFileUrl: string | undefined;
35
+ isCalendarEventRecordingQueryLoading: boolean;
36
+ errorMessage: string | undefined;
37
+ currentTimeSeconds: number;
38
+ calendarEventParticipants: CalendarEventRecordingParticipant[];
39
+ onVideoTimeUpdate: (videoCurrentTimeSeconds: number) => void;
40
+ };
41
+
42
+ export const CalendarEventRecordingBody = ({
43
+ transcript,
44
+ videoFileUrl,
45
+ isCalendarEventRecordingQueryLoading,
46
+ errorMessage,
47
+ currentTimeSeconds,
48
+ calendarEventParticipants,
49
+ onVideoTimeUpdate,
50
+ }: CalendarEventRecordingBodyProps) => {
51
+ const hasVideo = !isUndefined(videoFileUrl);
52
+
53
+ if (!isUndefined(errorMessage)) {
54
+ return (
55
+ <TranscriptErrorBox
56
+ title="Failed to load the recording"
57
+ description={errorMessage}
58
+ />
59
+ );
60
+ }
61
+
62
+ if (isCalendarEventRecordingQueryLoading) {
63
+ return (
64
+ <StyledRecordingContainer $hasVideo={false}>
65
+ <RecordingVideoPlayer
66
+ src={undefined}
67
+ onTimeUpdate={onVideoTimeUpdate}
68
+ />
69
+ </StyledRecordingContainer>
70
+ );
71
+ }
72
+
73
+ if (isUndefined(transcript) && !hasVideo) {
74
+ return (
75
+ <StyledCenteredState>
76
+ No recording for this calendar event yet.
77
+ </StyledCenteredState>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <StyledRecordingContainer $hasVideo={hasVideo}>
83
+ {hasVideo && (
84
+ <RecordingVideoPlayer
85
+ src={videoFileUrl}
86
+ onTimeUpdate={onVideoTimeUpdate}
87
+ />
88
+ )}
89
+ <RecordingTranscript
90
+ transcript={transcript}
91
+ currentTimeSeconds={currentTimeSeconds}
92
+ calendarEventParticipants={calendarEventParticipants}
93
+ />
94
+ </StyledRecordingContainer>
95
+ );
96
+ };
@@ -0,0 +1,111 @@
1
+ import styled from '@emotion/styled';
2
+ import { useCallback, useState } from 'react';
3
+
4
+ import { CalendarEventRecordingBody } from 'src/front-components/components/CalendarEventRecordingBody';
5
+ import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
6
+ import { useCalendarEventParticipants } from 'src/front-components/hooks/use-calendar-event-participants';
7
+ import { useCalendarEventRecording } from 'src/front-components/hooks/use-calendar-event-recording';
8
+
9
+ const TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS = 0.25;
10
+
11
+ const StyledRecordingShell = styled.div`
12
+ background: ${recordingThemeCssVariables.background.primary};
13
+ border: 1px solid transparent;
14
+ border-bottom: 1px solid transparent;
15
+ border-radius: ${recordingThemeCssVariables.border.radiusMd};
16
+ box-sizing: border-box;
17
+ font-family: ${recordingThemeCssVariables.font.family};
18
+ padding: ${recordingThemeCssVariables.spacing[4]};
19
+ position: relative;
20
+ width: 100%;
21
+ `;
22
+
23
+ const StyledRecordingHeader = styled.div`
24
+ align-items: center;
25
+ box-sizing: border-box;
26
+ display: flex;
27
+ height: ${recordingThemeCssVariables.spacing[6]};
28
+ `;
29
+
30
+ const StyledRecordingTitle = styled.h2`
31
+ color: ${recordingThemeCssVariables.font.colorPrimary};
32
+ flex: 1;
33
+ font-size: ${recordingThemeCssVariables.font.sizeMd};
34
+ font-weight: ${recordingThemeCssVariables.font.weightMedium};
35
+ margin: 0;
36
+ overflow: hidden;
37
+ padding-inline: ${recordingThemeCssVariables.spacing[1]};
38
+ user-select: none;
39
+ `;
40
+
41
+ const StyledRecordingBody = styled.div`
42
+ box-sizing: border-box;
43
+ margin-top: ${recordingThemeCssVariables.spacing[2]};
44
+ `;
45
+
46
+ const StyledRecordingContentFrame = styled.div`
47
+ background-color: ${recordingThemeCssVariables.background.secondary};
48
+ border: 1px solid ${recordingThemeCssVariables.border.colorMedium};
49
+ border-radius: ${recordingThemeCssVariables.border.radiusMd};
50
+ box-sizing: border-box;
51
+ padding: ${recordingThemeCssVariables.spacing[2]};
52
+ `;
53
+
54
+ type CalendarEventRecordingContentProps = {
55
+ calendarEventId: string;
56
+ };
57
+
58
+ export const CalendarEventRecordingContent = ({
59
+ calendarEventId,
60
+ }: CalendarEventRecordingContentProps) => {
61
+ const [currentTimeSeconds, setCurrentTimeSeconds] = useState(0);
62
+ const updateCurrentTimeSeconds = useCallback(
63
+ (videoCurrentTimeSeconds: number) => {
64
+ const nextCurrentTimeSeconds =
65
+ Math.floor(
66
+ videoCurrentTimeSeconds / TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS,
67
+ ) * TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS;
68
+
69
+ setCurrentTimeSeconds((previousCurrentTimeSeconds) =>
70
+ previousCurrentTimeSeconds === nextCurrentTimeSeconds
71
+ ? previousCurrentTimeSeconds
72
+ : nextCurrentTimeSeconds,
73
+ );
74
+ },
75
+ [],
76
+ );
77
+
78
+ const {
79
+ transcript,
80
+ videoFile,
81
+ isCalendarEventRecordingQueryLoading,
82
+ errorMessage,
83
+ } = useCalendarEventRecording(calendarEventId);
84
+ const { calendarEventParticipants } =
85
+ useCalendarEventParticipants(calendarEventId);
86
+
87
+ const videoFileUrl = videoFile?.url ?? undefined;
88
+
89
+ return (
90
+ <StyledRecordingShell>
91
+ <StyledRecordingHeader>
92
+ <StyledRecordingTitle>Recording and Transcript</StyledRecordingTitle>
93
+ </StyledRecordingHeader>
94
+ <StyledRecordingBody>
95
+ <StyledRecordingContentFrame>
96
+ <CalendarEventRecordingBody
97
+ transcript={transcript}
98
+ videoFileUrl={videoFileUrl}
99
+ isCalendarEventRecordingQueryLoading={
100
+ isCalendarEventRecordingQueryLoading
101
+ }
102
+ errorMessage={errorMessage}
103
+ currentTimeSeconds={currentTimeSeconds}
104
+ calendarEventParticipants={calendarEventParticipants}
105
+ onVideoTimeUpdate={updateCurrentTimeSeconds}
106
+ />
107
+ </StyledRecordingContentFrame>
108
+ </StyledRecordingBody>
109
+ </StyledRecordingShell>
110
+ );
111
+ };
@@ -0,0 +1,92 @@
1
+ import styled from '@emotion/styled';
2
+ import { isUndefined } from '@sniptt/guards';
3
+ import { useMemo } from 'react';
4
+
5
+ import { TranscriptEntryList } from 'src/front-components/components/TranscriptEntryList';
6
+ import { TranscriptErrorBox } from 'src/front-components/components/TranscriptErrorBox';
7
+ import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables';
8
+ import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type';
9
+ import { parseTranscriptEntries } from 'src/front-components/utils/parse-transcript-entries.util';
10
+ import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util';
11
+ import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util';
12
+
13
+ const StyledTranscriptCenteredState = styled.div`
14
+ align-items: center;
15
+ color: ${recordingThemeCssVariables.font.colorTertiary};
16
+ display: flex;
17
+ flex: 1;
18
+ font-size: ${recordingThemeCssVariables.font.sizeSm};
19
+ justify-content: center;
20
+ `;
21
+
22
+ type RecordingTranscriptProps = {
23
+ transcript: unknown;
24
+ currentTimeSeconds: number;
25
+ calendarEventParticipants: CalendarEventRecordingParticipant[];
26
+ };
27
+
28
+ export const RecordingTranscript = ({
29
+ transcript,
30
+ currentTimeSeconds,
31
+ calendarEventParticipants,
32
+ }: RecordingTranscriptProps) => {
33
+ const marker = useMemo(() => parseTranscriptMarker(transcript), [transcript]);
34
+ const entries = useMemo(
35
+ () => parseTranscriptEntries(transcript),
36
+ [transcript],
37
+ );
38
+
39
+ if (isUndefined(transcript)) {
40
+ return (
41
+ <StyledTranscriptCenteredState>
42
+ No transcript for this calendar event yet.
43
+ </StyledTranscriptCenteredState>
44
+ );
45
+ }
46
+
47
+ if (marker?.status === 'PENDING') {
48
+ return (
49
+ <StyledTranscriptCenteredState>
50
+ The transcript is being generated. Check back in a few minutes.
51
+ </StyledTranscriptCenteredState>
52
+ );
53
+ }
54
+
55
+ if (marker?.status === 'FAILED') {
56
+ return (
57
+ <TranscriptErrorBox
58
+ title="Transcription failed"
59
+ description={
60
+ isNonEmptyString(marker.subCode)
61
+ ? `The transcription provider could not process this recording (${marker.subCode}).`
62
+ : 'The transcription provider could not process this recording.'
63
+ }
64
+ />
65
+ );
66
+ }
67
+
68
+ if (isUndefined(entries)) {
69
+ return (
70
+ <TranscriptErrorBox
71
+ title="Unrecognized transcript format"
72
+ description="The stored transcript does not match the expected diarized format."
73
+ />
74
+ );
75
+ }
76
+
77
+ if (entries.length === 0) {
78
+ return (
79
+ <StyledTranscriptCenteredState>
80
+ The transcript is empty.
81
+ </StyledTranscriptCenteredState>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <TranscriptEntryList
87
+ entries={entries}
88
+ currentTimeSeconds={currentTimeSeconds}
89
+ calendarEventParticipants={calendarEventParticipants}
90
+ />
91
+ );
92
+ };