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

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