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

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