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

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