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.

actions.any.ts 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. import { IStore } from '../app/types';
  2. import { getMeetingRegion, getRecordingSharingUrl } from '../base/config/functions';
  3. import { isJwtFeatureEnabled } from '../base/jwt/functions';
  4. import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
  5. import {
  6. getLocalParticipant,
  7. getParticipantDisplayName,
  8. isLocalParticipantModerator
  9. } from '../base/participants/functions';
  10. import { BUTTON_TYPES } from '../base/ui/constants.any';
  11. import { copyText } from '../base/util/copyText';
  12. import { getVpaasTenant, isVpaasMeeting } from '../jaas/functions';
  13. import {
  14. hideNotification,
  15. showErrorNotification,
  16. showNotification,
  17. showWarningNotification
  18. } from '../notifications/actions';
  19. import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
  20. import { INotificationProps } from '../notifications/types';
  21. import { setRequestingSubtitles } from '../subtitles/actions.any';
  22. import { isRecorderTranscriptionsRunning } from '../transcribing/functions';
  23. import {
  24. CLEAR_RECORDING_SESSIONS,
  25. RECORDING_SESSION_UPDATED,
  26. SET_MEETING_HIGHLIGHT_BUTTON_STATE,
  27. SET_PENDING_RECORDING_NOTIFICATION_UID,
  28. SET_SELECTED_RECORDING_SERVICE,
  29. SET_START_RECORDING_NOTIFICATION_SHOWN,
  30. SET_STREAM_KEY,
  31. START_LOCAL_RECORDING,
  32. STOP_LOCAL_RECORDING
  33. } from './actionTypes';
  34. import { START_RECORDING_NOTIFICATION_ID } from './constants';
  35. import {
  36. getRecordButtonProps,
  37. getRecordingLink,
  38. getResourceId,
  39. isRecordingRunning,
  40. isRecordingSharingEnabled,
  41. isSavingRecordingOnDropbox,
  42. sendMeetingHighlight,
  43. shouldAutoTranscribeOnRecord
  44. } from './functions';
  45. import logger from './logger';
  46. /**
  47. * Clears the data of every recording sessions.
  48. *
  49. * @returns {{
  50. * type: CLEAR_RECORDING_SESSIONS
  51. * }}
  52. */
  53. export function clearRecordingSessions() {
  54. return {
  55. type: CLEAR_RECORDING_SESSIONS
  56. };
  57. }
  58. /**
  59. * Marks the start recording notification as shown.
  60. *
  61. * @returns {{
  62. * type: SET_START_RECORDING_NOTIFICATION_SHOWN
  63. * }}
  64. */
  65. export function setStartRecordingNotificationShown() {
  66. return {
  67. type: SET_START_RECORDING_NOTIFICATION_SHOWN
  68. };
  69. }
  70. /**
  71. * Sets the meeting highlight button disable state.
  72. *
  73. * @param {boolean} disabled - The disabled state value.
  74. * @returns {{
  75. * type: CLEAR_RECORDING_SESSIONS
  76. * }}
  77. */
  78. export function setHighlightMomentButtonState(disabled: boolean) {
  79. return {
  80. type: SET_MEETING_HIGHLIGHT_BUTTON_STATE,
  81. disabled
  82. };
  83. }
  84. /**
  85. * Signals that the pending recording notification should be removed from the
  86. * screen.
  87. *
  88. * @param {string} streamType - The type of the stream ({@code 'file'} or
  89. * {@code 'stream'}).
  90. * @returns {Function}
  91. */
  92. export function hidePendingRecordingNotification(streamType: string) {
  93. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  94. const { pendingNotificationUids } = getState()['features/recording'];
  95. const pendingNotificationUid = pendingNotificationUids[streamType];
  96. if (pendingNotificationUid) {
  97. dispatch(hideNotification(pendingNotificationUid));
  98. dispatch(
  99. _setPendingRecordingNotificationUid(
  100. undefined, streamType));
  101. }
  102. };
  103. }
  104. /**
  105. * Sets the stream key last used by the user for later reuse.
  106. *
  107. * @param {string} streamKey - The stream key to set.
  108. * @returns {{
  109. * type: SET_STREAM_KEY,
  110. * streamKey: string
  111. * }}
  112. */
  113. export function setLiveStreamKey(streamKey: string) {
  114. return {
  115. type: SET_STREAM_KEY,
  116. streamKey
  117. };
  118. }
  119. /**
  120. * Signals that the pending recording notification should be shown on the
  121. * screen.
  122. *
  123. * @param {string} streamType - The type of the stream ({@code file} or
  124. * {@code stream}).
  125. * @returns {Function}
  126. */
  127. export function showPendingRecordingNotification(streamType: string) {
  128. return async (dispatch: IStore['dispatch']) => {
  129. const isLiveStreaming
  130. = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
  131. const dialogProps = isLiveStreaming ? {
  132. descriptionKey: 'liveStreaming.pending',
  133. titleKey: 'dialog.liveStreaming'
  134. } : {
  135. descriptionKey: 'recording.pending',
  136. titleKey: 'dialog.recording'
  137. };
  138. const notification = await dispatch(showNotification({
  139. ...dialogProps
  140. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  141. if (notification) {
  142. dispatch(_setPendingRecordingNotificationUid(notification.uid, streamType));
  143. }
  144. };
  145. }
  146. /**
  147. * Highlights a meeting moment.
  148. *
  149. * {@code stream}).
  150. *
  151. * @returns {Function}
  152. */
  153. export function highlightMeetingMoment() {
  154. return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  155. dispatch(setHighlightMomentButtonState(true));
  156. const success = await sendMeetingHighlight(getState());
  157. if (success) {
  158. dispatch(showNotification({
  159. descriptionKey: 'recording.highlightMomentSucessDescription',
  160. titleKey: 'recording.highlightMomentSuccess'
  161. }, NOTIFICATION_TIMEOUT_TYPE.SHORT));
  162. }
  163. dispatch(setHighlightMomentButtonState(false));
  164. };
  165. }
  166. /**
  167. * Signals that the recording error notification should be shown.
  168. *
  169. * @param {Object} props - The Props needed to render the notification.
  170. * @returns {showErrorNotification}
  171. */
  172. export function showRecordingError(props: Object) {
  173. return showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.LONG);
  174. }
  175. /**
  176. * Signals that the recording warning notification should be shown.
  177. *
  178. * @param {Object} props - The Props needed to render the notification.
  179. * @returns {showWarningNotification}
  180. */
  181. export function showRecordingWarning(props: Object) {
  182. return showWarningNotification(props);
  183. }
  184. /**
  185. * Signals that the stopped recording notification should be shown on the
  186. * screen for a given period.
  187. *
  188. * @param {string} streamType - The type of the stream ({@code file} or
  189. * {@code stream}).
  190. * @param {string?} participantName - The participant name stopping the recording.
  191. * @returns {showNotification}
  192. */
  193. export function showStoppedRecordingNotification(streamType: string, participantName?: string) {
  194. const isLiveStreaming
  195. = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
  196. const descriptionArguments = { name: participantName };
  197. const dialogProps = isLiveStreaming ? {
  198. descriptionKey: participantName ? 'liveStreaming.offBy' : 'liveStreaming.off',
  199. descriptionArguments,
  200. titleKey: 'dialog.liveStreaming'
  201. } : {
  202. descriptionKey: participantName ? 'recording.offBy' : 'recording.off',
  203. descriptionArguments,
  204. titleKey: 'dialog.recording'
  205. };
  206. return showNotification(dialogProps, NOTIFICATION_TIMEOUT_TYPE.SHORT);
  207. }
  208. /**
  209. * Signals that a started recording notification should be shown on the
  210. * screen for a given period.
  211. *
  212. * @param {string} mode - The type of the recording: Stream of File.
  213. * @param {string | Object } initiator - The participant who started recording.
  214. * @param {string} sessionId - The recording session id.
  215. * @returns {Function}
  216. */
  217. export function showStartedRecordingNotification(
  218. mode: string,
  219. initiator: { getId: Function; } | string,
  220. sessionId: string) {
  221. return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  222. const state = getState();
  223. const initiatorId = getResourceId(initiator);
  224. const participantName = getParticipantDisplayName(state, initiatorId);
  225. const notifyProps: {
  226. dialogProps: INotificationProps;
  227. type: string;
  228. } = {
  229. dialogProps: {
  230. descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
  231. descriptionArguments: { name: participantName },
  232. titleKey: 'dialog.liveStreaming'
  233. },
  234. type: NOTIFICATION_TIMEOUT_TYPE.SHORT
  235. };
  236. if (mode !== JitsiMeetJS.constants.recording.mode.STREAM) {
  237. const recordingSharingUrl = getRecordingSharingUrl(state);
  238. const iAmRecordingInitiator = getLocalParticipant(state)?.id === initiatorId;
  239. notifyProps.dialogProps = {
  240. customActionHandler: undefined,
  241. customActionNameKey: undefined,
  242. descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
  243. descriptionArguments: { name: participantName },
  244. titleKey: 'dialog.recording'
  245. };
  246. // fetch the recording link from the server for recording initiators in jaas meetings
  247. if (recordingSharingUrl
  248. && isVpaasMeeting(state)
  249. && iAmRecordingInitiator
  250. && !isSavingRecordingOnDropbox(state)) {
  251. const region = getMeetingRegion(state);
  252. const tenant = getVpaasTenant(state);
  253. try {
  254. const response = await getRecordingLink(recordingSharingUrl, sessionId, region, tenant);
  255. const { url: link, urlExpirationTimeMillis: ttl } = response;
  256. if (typeof APP === 'object') {
  257. APP.API.notifyRecordingLinkAvailable(link, ttl);
  258. }
  259. // add the option to copy recording link
  260. notifyProps.dialogProps = {
  261. ...notifyProps.dialogProps,
  262. customActionNameKey: [ 'recording.copyLink' ],
  263. customActionHandler: [ () => copyText(link) ],
  264. titleKey: 'recording.on',
  265. descriptionKey: 'recording.linkGenerated'
  266. };
  267. notifyProps.type = NOTIFICATION_TIMEOUT_TYPE.STICKY;
  268. } catch (err) {
  269. dispatch(showErrorNotification({
  270. titleKey: 'recording.errorFetchingLink'
  271. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  272. return logger.error('Could not fetch recording link', err);
  273. }
  274. }
  275. }
  276. dispatch(showNotification(notifyProps.dialogProps, notifyProps.type));
  277. };
  278. }
  279. /**
  280. * Updates the known state for a given recording session.
  281. *
  282. * @param {Object} session - The new state to merge with the existing state in
  283. * redux.
  284. * @returns {{
  285. * type: RECORDING_SESSION_UPDATED,
  286. * sessionData: Object
  287. * }}
  288. */
  289. export function updateRecordingSessionData(session: any) {
  290. const status = session.getStatus();
  291. const timestamp
  292. = status === JitsiRecordingConstants.status.ON
  293. ? Date.now() / 1000
  294. : undefined;
  295. return {
  296. type: RECORDING_SESSION_UPDATED,
  297. sessionData: {
  298. error: session.getError(),
  299. id: session.getID(),
  300. initiator: session.getInitiator(),
  301. liveStreamViewURL: session.getLiveStreamViewURL(),
  302. mode: session.getMode(),
  303. status,
  304. terminator: session.getTerminator(),
  305. timestamp
  306. }
  307. };
  308. }
  309. /**
  310. * Sets the selected recording service.
  311. *
  312. * @param {string} selectedRecordingService - The new selected recording service.
  313. * @returns {Object}
  314. */
  315. export function setSelectedRecordingService(selectedRecordingService: string) {
  316. return {
  317. type: SET_SELECTED_RECORDING_SERVICE,
  318. selectedRecordingService
  319. };
  320. }
  321. /**
  322. * Sets UID of the the pending streaming notification to use it when hinding
  323. * the notification is necessary, or unsets it when undefined (or no param) is
  324. * passed.
  325. *
  326. * @param {?number} uid - The UID of the notification.
  327. * @param {string} streamType - The type of the stream ({@code file} or
  328. * {@code stream}).
  329. * @returns {{
  330. * type: SET_PENDING_RECORDING_NOTIFICATION_UID,
  331. * streamType: string,
  332. * uid: number
  333. * }}
  334. */
  335. function _setPendingRecordingNotificationUid(uid: string | undefined, streamType: string) {
  336. return {
  337. type: SET_PENDING_RECORDING_NOTIFICATION_UID,
  338. streamType,
  339. uid
  340. };
  341. }
  342. /**
  343. * Starts local recording.
  344. *
  345. * @param {boolean} onlySelf - Whether to only record the local streams.
  346. * @returns {Object}
  347. */
  348. export function startLocalVideoRecording(onlySelf?: boolean) {
  349. return {
  350. type: START_LOCAL_RECORDING,
  351. onlySelf
  352. };
  353. }
  354. /**
  355. * Stops local recording.
  356. *
  357. * @returns {Object}
  358. */
  359. export function stopLocalVideoRecording() {
  360. return {
  361. type: STOP_LOCAL_RECORDING
  362. };
  363. }
  364. /**
  365. * Displays the notification suggesting to start the recording.
  366. *
  367. * @param {Function} openRecordingDialog - The callback to open the recording dialog.
  368. * @returns {void}
  369. */
  370. export function showStartRecordingNotificationWithCallback(openRecordingDialog: Function) {
  371. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  372. let state = getState();
  373. const { recordings } = state['features/base/config'];
  374. const { suggestRecording } = recordings || {};
  375. const recordButtonProps = getRecordButtonProps(state);
  376. const isAlreadyRecording = isRecordingRunning(state) || isRecorderTranscriptionsRunning(state);
  377. const wasNotificationShown = state['features/recording'].wasStartRecordingSuggested;
  378. if (!suggestRecording
  379. || isAlreadyRecording
  380. || !recordButtonProps.visible
  381. || recordButtonProps.disabled
  382. || wasNotificationShown) {
  383. return;
  384. }
  385. dispatch(setStartRecordingNotificationShown());
  386. dispatch(showNotification({
  387. titleKey: 'notify.suggestRecordingTitle',
  388. descriptionKey: 'notify.suggestRecordingDescription',
  389. uid: START_RECORDING_NOTIFICATION_ID,
  390. customActionType: [ BUTTON_TYPES.PRIMARY ],
  391. customActionNameKey: [ 'notify.suggestRecordingAction' ],
  392. customActionHandler: [ () => {
  393. state = getState();
  394. const isModerator = isLocalParticipantModerator(state);
  395. const { recordingService } = state['features/base/config'];
  396. const canBypassDialog = isModerator
  397. && recordingService?.enabled
  398. && isJwtFeatureEnabled(state, 'recording', true);
  399. if (canBypassDialog) {
  400. const options = {
  401. 'file_recording_metadata': {
  402. share: isRecordingSharingEnabled(state)
  403. }
  404. };
  405. const { conference } = state['features/base/conference'];
  406. const autoTranscribeOnRecord = shouldAutoTranscribeOnRecord(state);
  407. conference?.startRecording({
  408. mode: JitsiRecordingConstants.mode.FILE,
  409. appData: JSON.stringify(options)
  410. });
  411. if (autoTranscribeOnRecord) {
  412. dispatch(setRequestingSubtitles(true, false, null));
  413. }
  414. } else {
  415. openRecordingDialog();
  416. }
  417. dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
  418. } ],
  419. appearance: NOTIFICATION_TYPE.NORMAL
  420. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  421. };
  422. }