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.ts 19KB

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