選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

actions.ts 20KB

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