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

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