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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. // @flow
  2. import i18next from 'i18next';
  3. import _ from 'lodash';
  4. import type { Dispatch } from 'redux';
  5. import { createBreakoutRoomsEvent, sendAnalytics } from '../analytics';
  6. import {
  7. conferenceLeft,
  8. conferenceWillLeave,
  9. createConference,
  10. getCurrentConference
  11. } from '../base/conference';
  12. import {
  13. MEDIA_TYPE,
  14. setAudioMuted,
  15. setVideoMuted
  16. } from '../base/media';
  17. import { getRemoteParticipants } from '../base/participants';
  18. import {
  19. getLocalTracks,
  20. isLocalCameraTrackMuted,
  21. isLocalTrackMuted
  22. } from '../base/tracks';
  23. import { createDesiredLocalTracks } from '../base/tracks/actions';
  24. import {
  25. NOTIFICATION_TIMEOUT_TYPE,
  26. clearNotifications,
  27. showNotification
  28. } from '../notifications';
  29. import { _RESET_BREAKOUT_ROOMS, _UPDATE_ROOM_COUNTER } from './actionTypes';
  30. import { FEATURE_KEY } from './constants';
  31. import {
  32. getBreakoutRooms,
  33. getMainRoom,
  34. getRoomByJid
  35. } from './functions';
  36. import logger from './logger';
  37. declare var APP: Object;
  38. /**
  39. * Action to create a breakout room.
  40. *
  41. * @param {string} name - Name / subject for the breakout room.
  42. * @returns {Function}
  43. */
  44. export function createBreakoutRoom(name?: string) {
  45. return (dispatch: Dispatch<any>, getState: Function) => {
  46. const state = getState();
  47. let { roomCounter } = state[FEATURE_KEY];
  48. const subject = name || i18next.t('breakoutRooms.defaultName', { index: ++roomCounter });
  49. sendAnalytics(createBreakoutRoomsEvent('create'));
  50. dispatch({
  51. type: _UPDATE_ROOM_COUNTER,
  52. roomCounter
  53. });
  54. // $FlowExpectedError
  55. getCurrentConference(state)?.getBreakoutRooms()
  56. ?.createBreakoutRoom(subject);
  57. };
  58. }
  59. /**
  60. * Action to close a room and send participants to the main room.
  61. *
  62. * @param {string} roomId - The id of the room to close.
  63. * @returns {Function}
  64. */
  65. export function closeBreakoutRoom(roomId: string) {
  66. return (dispatch: Dispatch<any>, getState: Function) => {
  67. const rooms = getBreakoutRooms(getState);
  68. const room = rooms[roomId];
  69. const mainRoom = getMainRoom(getState);
  70. sendAnalytics(createBreakoutRoomsEvent('close'));
  71. if (room && mainRoom) {
  72. Object.values(room.participants).forEach(p => {
  73. // $FlowExpectedError
  74. dispatch(sendParticipantToRoom(p.jid, mainRoom.id));
  75. });
  76. }
  77. };
  78. }
  79. /**
  80. * Action to remove a breakout room.
  81. *
  82. * @param {string} breakoutRoomJid - The jid of the breakout room to remove.
  83. * @returns {Function}
  84. */
  85. export function removeBreakoutRoom(breakoutRoomJid: string) {
  86. return (dispatch: Dispatch<any>, getState: Function) => {
  87. sendAnalytics(createBreakoutRoomsEvent('remove'));
  88. const room = getRoomByJid(getState, breakoutRoomJid);
  89. if (!room) {
  90. logger.error('The room to remove was not found.');
  91. return;
  92. }
  93. if (Object.keys(room.participants).length > 0) {
  94. dispatch(closeBreakoutRoom(room.id));
  95. }
  96. // $FlowExpectedError
  97. getCurrentConference(getState)?.getBreakoutRooms()
  98. ?.removeBreakoutRoom(breakoutRoomJid);
  99. };
  100. }
  101. /**
  102. * Action to auto-assign the participants to breakout rooms.
  103. *
  104. * @returns {Function}
  105. */
  106. export function autoAssignToBreakoutRooms() {
  107. return (dispatch: Dispatch<any>, getState: Function) => {
  108. const rooms = getBreakoutRooms(getState);
  109. const breakoutRooms = _.filter(rooms, (room: Object) => !room.isMainRoom);
  110. if (breakoutRooms) {
  111. sendAnalytics(createBreakoutRoomsEvent('auto.assign'));
  112. const participantIds = Array.from(getRemoteParticipants(getState).keys());
  113. const length = Math.ceil(participantIds.length / breakoutRooms.length);
  114. _.chunk(_.shuffle(participantIds), length).forEach((group, index) =>
  115. group.forEach(participantId => {
  116. dispatch(sendParticipantToRoom(participantId, breakoutRooms[index].id));
  117. })
  118. );
  119. }
  120. };
  121. }
  122. /**
  123. * Action to send a participant to a room.
  124. *
  125. * @param {string} participantId - The participant id.
  126. * @param {string} roomId - The room id.
  127. * @returns {Function}
  128. */
  129. export function sendParticipantToRoom(participantId: string, roomId: string) {
  130. return (dispatch: Dispatch<any>, getState: Function) => {
  131. const rooms = getBreakoutRooms(getState);
  132. const room = rooms[roomId];
  133. if (!room) {
  134. logger.warn(`Invalid room: ${roomId}`);
  135. return;
  136. }
  137. // Get the full JID of the participant. We could be getting the endpoint ID or
  138. // a participant JID. We want to find the connection JID.
  139. const participantJid = _findParticipantJid(getState, participantId);
  140. if (!participantJid) {
  141. logger.warn(`Could not find participant ${participantId}`);
  142. return;
  143. }
  144. // $FlowExpectedError
  145. getCurrentConference(getState)?.getBreakoutRooms()
  146. ?.sendParticipantToRoom(participantJid, room.jid);
  147. };
  148. }
  149. /**
  150. * Action to move to a room.
  151. *
  152. * @param {string} roomId - The room id to move to. If omitted move to the main room.
  153. * @returns {Function}
  154. */
  155. export function moveToRoom(roomId?: string) {
  156. return async (dispatch: Dispatch<any>, getState: Function) => {
  157. const mainRoomId = getMainRoom(getState)?.id;
  158. let _roomId = roomId || mainRoomId;
  159. // Check if we got a full JID.
  160. // $FlowExpectedError
  161. if (_roomId?.indexOf('@') !== -1) {
  162. // $FlowExpectedError
  163. const [ id, ...domainParts ] = _roomId.split('@');
  164. // On mobile we first store the room and the connection is created
  165. // later, so let's attach the domain to the room String object as
  166. // a little hack.
  167. // eslint-disable-next-line no-new-wrappers
  168. _roomId = new String(id);
  169. // $FlowExpectedError
  170. _roomId.domain = domainParts.join('@');
  171. }
  172. // $FlowExpectedError
  173. const roomIdStr = _roomId?.toString();
  174. const goToMainRoom = roomIdStr === mainRoomId;
  175. const rooms = getBreakoutRooms(getState);
  176. const targetRoom = rooms[roomIdStr];
  177. if (!targetRoom) {
  178. logger.warn(`Unknown room: ${targetRoom}`);
  179. return;
  180. }
  181. dispatch({
  182. type: _RESET_BREAKOUT_ROOMS
  183. });
  184. if (navigator.product === 'ReactNative') {
  185. const conference = getCurrentConference(getState);
  186. const { audio, video } = getState()['features/base/media'];
  187. dispatch(conferenceWillLeave(conference));
  188. try {
  189. await conference.leave();
  190. } catch (error) {
  191. logger.warn('JitsiConference.leave() rejected with:', error);
  192. dispatch(conferenceLeft(conference));
  193. }
  194. dispatch(clearNotifications());
  195. // dispatch(setRoom(_roomId));
  196. dispatch(createConference(_roomId));
  197. dispatch(setAudioMuted(audio.muted));
  198. dispatch(setVideoMuted(video.muted));
  199. dispatch(createDesiredLocalTracks());
  200. } else {
  201. const localTracks = getLocalTracks(getState()['features/base/tracks']);
  202. const isAudioMuted = isLocalTrackMuted(localTracks, MEDIA_TYPE.AUDIO);
  203. const isVideoMuted = isLocalCameraTrackMuted(localTracks);
  204. try {
  205. // all places we fire notifyConferenceLeft we pass the room name from APP.conference
  206. await APP.conference.leaveRoom(false /* doDisconnect */).then(
  207. () => APP.API.notifyConferenceLeft(APP.conference.roomName));
  208. } catch (error) {
  209. logger.warn('APP.conference.leaveRoom() rejected with:', error);
  210. // TODO: revisit why we don't dispatch CONFERENCE_LEFT here.
  211. }
  212. APP.conference.joinRoom(_roomId, {
  213. startWithAudioMuted: isAudioMuted,
  214. startWithVideoMuted: isVideoMuted
  215. });
  216. }
  217. if (goToMainRoom) {
  218. dispatch(showNotification({
  219. titleKey: 'breakoutRooms.notifications.joinedTitle',
  220. descriptionKey: 'breakoutRooms.notifications.joinedMainRoom',
  221. concatText: true,
  222. maxLines: 2
  223. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  224. } else {
  225. dispatch(showNotification({
  226. titleKey: 'breakoutRooms.notifications.joinedTitle',
  227. descriptionKey: 'breakoutRooms.notifications.joined',
  228. descriptionArguments: { name: targetRoom.name },
  229. concatText: true,
  230. maxLines: 2
  231. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  232. }
  233. };
  234. }
  235. /**
  236. * Finds a participant's connection JID given its ID.
  237. *
  238. * @param {Function} getState - The redux store state getter.
  239. * @param {string} participantId - ID of the given participant.
  240. * @returns {string|undefined} - The participant connection JID if found.
  241. */
  242. function _findParticipantJid(getState: Function, participantId: string) {
  243. const conference = getCurrentConference(getState);
  244. if (!conference) {
  245. return;
  246. }
  247. // Get the full JID of the participant. We could be getting the endpoint ID or
  248. // a participant JID. We want to find the connection JID.
  249. let _participantId = participantId;
  250. let participantJid;
  251. if (!participantId.includes('@')) {
  252. const p = conference.getParticipantById(participantId);
  253. // $FlowExpectedError
  254. _participantId = p?.getJid(); // This will be the room JID.
  255. }
  256. if (_participantId) {
  257. const rooms = getBreakoutRooms(getState);
  258. for (const room of Object.values(rooms)) {
  259. // $FlowExpectedError
  260. const participants = room.participants || {};
  261. const p = participants[_participantId]
  262. // $FlowExpectedError
  263. || Object.values(participants).find(item => item.jid === _participantId);
  264. if (p) {
  265. participantJid = p.jid;
  266. break;
  267. }
  268. }
  269. }
  270. return participantJid;
  271. }