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

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