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.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../../notifications';
  2. import { set } from '../redux';
  3. import {
  4. DOMINANT_SPEAKER_CHANGED,
  5. HIDDEN_PARTICIPANT_JOINED,
  6. HIDDEN_PARTICIPANT_LEFT,
  7. GRANT_MODERATOR,
  8. KICK_PARTICIPANT,
  9. LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
  10. LOCAL_PARTICIPANT_RAISE_HAND,
  11. MUTE_REMOTE_PARTICIPANT,
  12. PARTICIPANT_ID_CHANGED,
  13. PARTICIPANT_JOINED,
  14. PARTICIPANT_KICKED,
  15. PARTICIPANT_LEFT,
  16. PARTICIPANT_UPDATED,
  17. PIN_PARTICIPANT,
  18. SCREENSHARE_PARTICIPANT_NAME_CHANGED,
  19. SET_LOADABLE_AVATAR_URL,
  20. RAISE_HAND_UPDATED
  21. } from './actionTypes';
  22. import {
  23. DISCO_REMOTE_CONTROL_FEATURE
  24. } from './constants';
  25. import {
  26. getLocalParticipant,
  27. getVirtualScreenshareParticipantOwnerId,
  28. getNormalizedDisplayName,
  29. getParticipantDisplayName,
  30. getParticipantById
  31. } from './functions';
  32. import logger from './logger';
  33. /**
  34. * Create an action for when dominant speaker changes.
  35. *
  36. * @param {string} dominantSpeaker - Participant ID of the dominant speaker.
  37. * @param {Array<string>} previousSpeakers - Participant IDs of the previous speakers.
  38. * @param {JitsiConference} conference - The {@code JitsiConference} associated
  39. * with the participant identified by the specified {@code id}. Only the local
  40. * participant is allowed to not specify an associated {@code JitsiConference}
  41. * instance.
  42. * @returns {{
  43. * type: DOMINANT_SPEAKER_CHANGED,
  44. * participant: {
  45. * conference: JitsiConference,
  46. * id: string,
  47. * previousSpeakers: Array<string>
  48. * }
  49. * }}
  50. */
  51. export function dominantSpeakerChanged(dominantSpeaker, previousSpeakers, conference) {
  52. return {
  53. type: DOMINANT_SPEAKER_CHANGED,
  54. participant: {
  55. conference,
  56. id: dominantSpeaker,
  57. previousSpeakers
  58. }
  59. };
  60. }
  61. /**
  62. * Create an action for granting moderator to a participant.
  63. *
  64. * @param {string} id - Participant's ID.
  65. * @returns {{
  66. * type: GRANT_MODERATOR,
  67. * id: string
  68. * }}
  69. */
  70. export function grantModerator(id) {
  71. return {
  72. type: GRANT_MODERATOR,
  73. id
  74. };
  75. }
  76. /**
  77. * Create an action for removing a participant from the conference.
  78. *
  79. * @param {string} id - Participant's ID.
  80. * @returns {{
  81. * type: KICK_PARTICIPANT,
  82. * id: string
  83. * }}
  84. */
  85. export function kickParticipant(id) {
  86. return {
  87. type: KICK_PARTICIPANT,
  88. id
  89. };
  90. }
  91. /**
  92. * Creates an action to signal the connection status of the local participant
  93. * has changed.
  94. *
  95. * @param {string} connectionStatus - The current connection status of the local
  96. * participant, as enumerated by the library's participantConnectionStatus
  97. * constants.
  98. * @returns {Function}
  99. */
  100. export function localParticipantConnectionStatusChanged(connectionStatus) {
  101. return (dispatch, getState) => {
  102. const participant = getLocalParticipant(getState);
  103. if (participant) {
  104. return dispatch(participantConnectionStatusChanged(
  105. participant.id,
  106. connectionStatus));
  107. }
  108. };
  109. }
  110. /**
  111. * Action to signal that the ID of local participant has changed. It happens
  112. * when the local participant joins a new conference or leaves an existing
  113. * conference.
  114. *
  115. * @param {string} id - New ID for local participant.
  116. * @returns {Function}
  117. */
  118. export function localParticipantIdChanged(id) {
  119. return (dispatch, getState) => {
  120. const participant = getLocalParticipant(getState);
  121. if (participant) {
  122. return dispatch({
  123. type: PARTICIPANT_ID_CHANGED,
  124. // XXX A participant is identified by an id-conference pair.
  125. // Only the local participant is with an undefined conference.
  126. conference: undefined,
  127. newValue: id,
  128. oldValue: participant.id
  129. });
  130. }
  131. };
  132. }
  133. /**
  134. * Action to signal that a local participant has joined.
  135. *
  136. * @param {Participant} participant={} - Information about participant.
  137. * @returns {{
  138. * type: PARTICIPANT_JOINED,
  139. * participant: Participant
  140. * }}
  141. */
  142. export function localParticipantJoined(participant = {}) {
  143. return participantJoined(set(participant, 'local', true));
  144. }
  145. /**
  146. * Action to remove a local participant.
  147. *
  148. * @returns {Function}
  149. */
  150. export function localParticipantLeft() {
  151. return (dispatch, getState) => {
  152. const participant = getLocalParticipant(getState);
  153. if (participant) {
  154. return (
  155. dispatch(
  156. participantLeft(
  157. participant.id,
  158. // XXX Only the local participant is allowed to leave
  159. // without stating the JitsiConference instance because
  160. // the local participant is uniquely identified by the
  161. // very fact that there is only one local participant
  162. // (and the fact that the local participant "joins" at
  163. // the beginning of the app and "leaves" at the end of
  164. // the app).
  165. undefined)));
  166. }
  167. };
  168. }
  169. /**
  170. * Action to signal the role of the local participant has changed. It can happen
  171. * when the participant has joined a conference, even before a non-default local
  172. * id has been set, or after a moderator leaves.
  173. *
  174. * @param {string} role - The new role of the local participant.
  175. * @returns {Function}
  176. */
  177. export function localParticipantRoleChanged(role) {
  178. return (dispatch, getState) => {
  179. const participant = getLocalParticipant(getState);
  180. if (participant) {
  181. return dispatch(participantRoleChanged(participant.id, role));
  182. }
  183. };
  184. }
  185. /**
  186. * Create an action for muting another participant in the conference.
  187. *
  188. * @param {string} id - Participant's ID.
  189. * @param {MEDIA_TYPE} mediaType - The media to mute.
  190. * @returns {{
  191. * type: MUTE_REMOTE_PARTICIPANT,
  192. * id: string,
  193. * mediaType: MEDIA_TYPE
  194. * }}
  195. */
  196. export function muteRemoteParticipant(id, mediaType) {
  197. return {
  198. type: MUTE_REMOTE_PARTICIPANT,
  199. id,
  200. mediaType
  201. };
  202. }
  203. /**
  204. * Action to update a participant's connection status.
  205. *
  206. * @param {string} id - Participant's ID.
  207. * @param {string} connectionStatus - The new connection status of the
  208. * participant.
  209. * @returns {{
  210. * type: PARTICIPANT_UPDATED,
  211. * participant: {
  212. * connectionStatus: string,
  213. * id: string
  214. * }
  215. * }}
  216. */
  217. export function participantConnectionStatusChanged(id, connectionStatus) {
  218. return {
  219. type: PARTICIPANT_UPDATED,
  220. participant: {
  221. connectionStatus,
  222. id
  223. }
  224. };
  225. }
  226. /**
  227. * Action to signal that a participant has joined.
  228. *
  229. * @param {Participant} participant - Information about participant.
  230. * @returns {{
  231. * type: PARTICIPANT_JOINED,
  232. * participant: Participant
  233. * }}
  234. */
  235. export function participantJoined(participant) {
  236. // Only the local participant is not identified with an id-conference pair.
  237. if (participant.local) {
  238. return {
  239. type: PARTICIPANT_JOINED,
  240. participant
  241. };
  242. }
  243. // In other words, a remote participant is identified with an id-conference
  244. // pair.
  245. const { conference } = participant;
  246. if (!conference) {
  247. throw Error(
  248. 'A remote participant must be associated with a JitsiConference!');
  249. }
  250. return (dispatch, getState) => {
  251. // A remote participant is only expected to join in a joined or joining
  252. // conference. The following check is really necessary because a
  253. // JitsiConference may have moved into leaving but may still manage to
  254. // sneak a PARTICIPANT_JOINED in if its leave is delayed for any purpose
  255. // (which is not outragous given that leaving involves network
  256. // requests.)
  257. const stateFeaturesBaseConference
  258. = getState()['features/base/conference'];
  259. if (conference === stateFeaturesBaseConference.conference
  260. || conference === stateFeaturesBaseConference.joining) {
  261. return dispatch({
  262. type: PARTICIPANT_JOINED,
  263. participant
  264. });
  265. }
  266. };
  267. }
  268. /**
  269. * Updates the features of a remote participant.
  270. *
  271. * @param {JitsiParticipant} jitsiParticipant - The ID of the participant.
  272. * @returns {{
  273. * type: PARTICIPANT_UPDATED,
  274. * participant: Participant
  275. * }}
  276. */
  277. export function updateRemoteParticipantFeatures(jitsiParticipant) {
  278. return (dispatch, getState) => {
  279. if (!jitsiParticipant) {
  280. return;
  281. }
  282. const id = jitsiParticipant.getId();
  283. jitsiParticipant.getFeatures()
  284. .then(features => {
  285. const supportsRemoteControl = features.has(DISCO_REMOTE_CONTROL_FEATURE);
  286. const participant = getParticipantById(getState(), id);
  287. if (!participant || participant.local) {
  288. return;
  289. }
  290. if (participant?.supportsRemoteControl !== supportsRemoteControl) {
  291. return dispatch({
  292. type: PARTICIPANT_UPDATED,
  293. participant: {
  294. id,
  295. supportsRemoteControl
  296. }
  297. });
  298. }
  299. })
  300. .catch(error => {
  301. logger.error(`Failed to get participant features for ${id}!`, error);
  302. });
  303. };
  304. }
  305. /**
  306. * Action to signal that a hidden participant has joined the conference.
  307. *
  308. * @param {string} id - The id of the participant.
  309. * @param {string} displayName - The display name, or undefined when
  310. * unknown.
  311. * @returns {{
  312. * type: HIDDEN_PARTICIPANT_JOINED,
  313. * displayName: string,
  314. * id: string
  315. * }}
  316. */
  317. export function hiddenParticipantJoined(id, displayName) {
  318. return {
  319. type: HIDDEN_PARTICIPANT_JOINED,
  320. id,
  321. displayName
  322. };
  323. }
  324. /**
  325. * Action to signal that a hidden participant has left the conference.
  326. *
  327. * @param {string} id - The id of the participant.
  328. * @returns {{
  329. * type: HIDDEN_PARTICIPANT_LEFT,
  330. * id: string
  331. * }}
  332. */
  333. export function hiddenParticipantLeft(id) {
  334. return {
  335. type: HIDDEN_PARTICIPANT_LEFT,
  336. id
  337. };
  338. }
  339. /**
  340. * Action to signal that a participant has left.
  341. *
  342. * @param {string} id - Participant's ID.
  343. * @param {JitsiConference} conference - The {@code JitsiConference} associated
  344. * with the participant identified by the specified {@code id}. Only the local
  345. * participant is allowed to not specify an associated {@code JitsiConference}
  346. * instance.
  347. * @param {boolean} isReplaced - Whether the participant is to be replaced in the meeting.
  348. * @returns {{
  349. * type: PARTICIPANT_LEFT,
  350. * participant: {
  351. * conference: JitsiConference,
  352. * id: string
  353. * }
  354. * }}
  355. */
  356. export function participantLeft(id, conference, isReplaced) {
  357. return {
  358. type: PARTICIPANT_LEFT,
  359. participant: {
  360. conference,
  361. id,
  362. isReplaced
  363. }
  364. };
  365. }
  366. /**
  367. * Action to signal that a participant's presence status has changed.
  368. *
  369. * @param {string} id - Participant's ID.
  370. * @param {string} presence - Participant's new presence status.
  371. * @returns {{
  372. * type: PARTICIPANT_UPDATED,
  373. * participant: {
  374. * id: string,
  375. * presence: string
  376. * }
  377. * }}
  378. */
  379. export function participantPresenceChanged(id, presence) {
  380. return participantUpdated({
  381. id,
  382. presence
  383. });
  384. }
  385. /**
  386. * Action to signal that a participant's role has changed.
  387. *
  388. * @param {string} id - Participant's ID.
  389. * @param {PARTICIPANT_ROLE} role - Participant's new role.
  390. * @returns {{
  391. * type: PARTICIPANT_UPDATED,
  392. * participant: {
  393. * id: string,
  394. * role: PARTICIPANT_ROLE
  395. * }
  396. * }}
  397. */
  398. export function participantRoleChanged(id, role) {
  399. return participantUpdated({
  400. id,
  401. role
  402. });
  403. }
  404. /**
  405. * Action to signal that a participant's display name has changed.
  406. *
  407. * @param {string} id - Screenshare participant's ID.
  408. * @param {name} name - The new display name of the screenshare participant's owner.
  409. * @returns {{
  410. * type: SCREENSHARE_PARTICIPANT_NAME_CHANGED,
  411. * id: string,
  412. * name: string
  413. * }}
  414. */
  415. export function screenshareParticipantDisplayNameChanged(id, name) {
  416. return {
  417. type: SCREENSHARE_PARTICIPANT_NAME_CHANGED,
  418. id,
  419. name
  420. };
  421. }
  422. /**
  423. * Action to signal that some of participant properties has been changed.
  424. *
  425. * @param {Participant} participant={} - Information about participant. To
  426. * identify the participant the object should contain either property id with
  427. * value the id of the participant or property local with value true (if the
  428. * local participant hasn't joined the conference yet).
  429. * @returns {{
  430. * type: PARTICIPANT_UPDATED,
  431. * participant: Participant
  432. * }}
  433. */
  434. export function participantUpdated(participant = {}) {
  435. const participantToUpdate = {
  436. ...participant
  437. };
  438. if (participant.name) {
  439. participantToUpdate.name = getNormalizedDisplayName(participant.name);
  440. }
  441. return {
  442. type: PARTICIPANT_UPDATED,
  443. participant: participantToUpdate
  444. };
  445. }
  446. /**
  447. * Action to signal that a participant has muted us.
  448. *
  449. * @param {JitsiParticipant} participant - Information about participant.
  450. * @param {JitsiLocalTrack} track - Information about the track that has been muted.
  451. * @returns {Promise}
  452. */
  453. export function participantMutedUs(participant, track) {
  454. return (dispatch, getState) => {
  455. if (!participant) {
  456. return;
  457. }
  458. const isAudio = track.isAudioTrack();
  459. dispatch(showNotification({
  460. titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
  461. titleArguments: {
  462. participantDisplayName: getParticipantDisplayName(getState, participant.getId())
  463. }
  464. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  465. };
  466. }
  467. /**
  468. * Action to create a virtual screenshare participant.
  469. *
  470. * @param {(string)} sourceName - JitsiTrack instance.
  471. * @param {(boolean)} local - JitsiTrack instance.
  472. * @returns {Function}
  473. */
  474. export function createVirtualScreenshareParticipant(sourceName, local) {
  475. return (dispatch, getState) => {
  476. const state = getState();
  477. const ownerId = getVirtualScreenshareParticipantOwnerId(sourceName);
  478. const owner = getParticipantById(state, ownerId);
  479. const ownerName = owner.name;
  480. if (!ownerName) {
  481. logger.error(`Failed to create a screenshare participant for sourceName: ${sourceName}`);
  482. return;
  483. }
  484. dispatch(participantJoined({
  485. conference: state['features/base/conference'].conference,
  486. id: sourceName,
  487. isVirtualScreenshareParticipant: true,
  488. isLocalScreenShare: local,
  489. name: ownerName
  490. }));
  491. };
  492. }
  493. /**
  494. * Action to signal that a participant had been kicked.
  495. *
  496. * @param {JitsiParticipant} kicker - Information about participant performing the kick.
  497. * @param {JitsiParticipant} kicked - Information about participant that was kicked.
  498. * @returns {Promise}
  499. */
  500. export function participantKicked(kicker, kicked) {
  501. return (dispatch, getState) => {
  502. dispatch({
  503. type: PARTICIPANT_KICKED,
  504. kicked: kicked.getId(),
  505. kicker: kicker?.getId()
  506. });
  507. if (kicked.isReplaced && kicked.isReplaced()) {
  508. return;
  509. }
  510. dispatch(showNotification({
  511. titleArguments: {
  512. kicked:
  513. getParticipantDisplayName(getState, kicked.getId()),
  514. kicker:
  515. getParticipantDisplayName(getState, kicker.getId())
  516. },
  517. titleKey: 'notify.kickParticipant'
  518. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  519. };
  520. }
  521. /**
  522. * Create an action which pins a conference participant.
  523. *
  524. * @param {string|null} id - The ID of the conference participant to pin or null
  525. * if none of the conference's participants are to be pinned.
  526. * @returns {{
  527. * type: PIN_PARTICIPANT,
  528. * participant: {
  529. * id: string
  530. * }
  531. * }}
  532. */
  533. export function pinParticipant(id) {
  534. return {
  535. type: PIN_PARTICIPANT,
  536. participant: {
  537. id
  538. }
  539. };
  540. }
  541. /**
  542. * Creates an action which notifies the app that the loadable URL of the avatar of a participant got updated.
  543. *
  544. * @param {string} participantId - The ID of the participant.
  545. * @param {string} url - The new URL.
  546. * @param {boolean} useCORS - Indicates whether we need to use CORS for this URL.
  547. * @returns {{
  548. * type: SET_LOADABLE_AVATAR_URL,
  549. * participant: {
  550. * id: string,
  551. * loadableAvatarUrl: string,
  552. * loadableAvatarUrlUseCORS: boolean
  553. * }
  554. * }}
  555. */
  556. export function setLoadableAvatarUrl(participantId, url, useCORS) {
  557. return {
  558. type: SET_LOADABLE_AVATAR_URL,
  559. participant: {
  560. id: participantId,
  561. loadableAvatarUrl: url,
  562. loadableAvatarUrlUseCORS: useCORS
  563. }
  564. };
  565. }
  566. /**
  567. * Raise hand for the local participant.
  568. *
  569. * @param {boolean} enabled - Raise or lower hand.
  570. * @returns {{
  571. * type: LOCAL_PARTICIPANT_RAISE_HAND,
  572. * raisedHandTimestamp: number
  573. * }}
  574. */
  575. export function raiseHand(enabled) {
  576. return {
  577. type: LOCAL_PARTICIPANT_RAISE_HAND,
  578. raisedHandTimestamp: enabled ? Date.now() : 0
  579. };
  580. }
  581. /**
  582. * Update raise hand queue of participants.
  583. *
  584. * @param {Object} participant - Participant that updated raised hand.
  585. * @returns {{
  586. * type: RAISE_HAND_UPDATED,
  587. * participant: Object
  588. * }}
  589. */
  590. export function raiseHandUpdateQueue(participant) {
  591. return {
  592. type: RAISE_HAND_UPDATED,
  593. participant
  594. };
  595. }
  596. /**
  597. * Notifies if the local participant audio level has changed.
  598. *
  599. * @param {number} level - The audio level.
  600. * @returns {{
  601. * type: LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
  602. * level: number
  603. * }}
  604. */
  605. export function localParticipantAudioLevelChanged(level) {
  606. return {
  607. type: LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
  608. level
  609. };
  610. }