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.

reducer.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. // @flow
  2. import {
  3. SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
  4. } from '../../video-layout/actionTypes';
  5. import { ReducerRegistry, set } from '../redux';
  6. import {
  7. DOMINANT_SPEAKER_CHANGED,
  8. PARTICIPANT_ID_CHANGED,
  9. PARTICIPANT_JOINED,
  10. PARTICIPANT_LEFT,
  11. PARTICIPANT_UPDATED,
  12. PIN_PARTICIPANT,
  13. RAISE_HAND_UPDATED,
  14. SET_LOADABLE_AVATAR_URL
  15. } from './actionTypes';
  16. import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
  17. import { isParticipantModerator } from './functions';
  18. /**
  19. * Participant object.
  20. *
  21. * @typedef {Object} Participant
  22. * @property {string} id - Participant ID.
  23. * @property {string} name - Participant name.
  24. * @property {string} avatar - Path to participant avatar if any.
  25. * @property {string} role - Participant role.
  26. * @property {boolean} local - If true, participant is local.
  27. * @property {boolean} pinned - If true, participant is currently a
  28. * "PINNED_ENDPOINT".
  29. * @property {boolean} dominantSpeaker - If this participant is the dominant
  30. * speaker in the (associated) conference, {@code true}; otherwise,
  31. * {@code false}.
  32. * @property {string} email - Participant email.
  33. */
  34. /**
  35. * The participant properties which cannot be updated through
  36. * {@link PARTICIPANT_UPDATED}. They either identify the participant or can only
  37. * be modified through property-dedicated actions.
  38. *
  39. * @type {string[]}
  40. */
  41. const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
  42. // The following properties identify the participant:
  43. 'conference',
  44. 'id',
  45. 'local',
  46. // The following properties can only be modified through property-dedicated
  47. // actions:
  48. 'dominantSpeaker',
  49. 'pinned'
  50. ];
  51. const DEFAULT_STATE = {
  52. dominantSpeaker: undefined,
  53. everyoneIsModerator: false,
  54. fakeParticipants: new Map(),
  55. haveParticipantWithScreenSharingFeature: false,
  56. local: undefined,
  57. localScreenShare: undefined,
  58. pinnedParticipant: undefined,
  59. raisedHandsQueue: [],
  60. remote: new Map(),
  61. sortedRemoteFakeScreenShareParticipants: new Map(),
  62. sortedRemoteParticipants: new Map(),
  63. sortedRemoteScreenshares: new Map(),
  64. speakersList: new Map()
  65. };
  66. /**
  67. * Listen for actions which add, remove, or update the set of participants in
  68. * the conference.
  69. *
  70. * @param {Participant[]} state - List of participants to be modified.
  71. * @param {Object} action - Action object.
  72. * @param {string} action.type - Type of action.
  73. * @param {Participant} action.participant - Information about participant to be
  74. * added/removed/modified.
  75. * @returns {Participant[]}
  76. */
  77. ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, action) => {
  78. switch (action.type) {
  79. case PARTICIPANT_ID_CHANGED: {
  80. const { local } = state;
  81. if (local) {
  82. if (action.newValue === 'local' && state.raisedHandsQueue.find(pid => pid.id === local.id)) {
  83. state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== local.id);
  84. }
  85. state.local = {
  86. ...local,
  87. id: action.newValue
  88. };
  89. return {
  90. ...state
  91. };
  92. }
  93. return state;
  94. }
  95. case DOMINANT_SPEAKER_CHANGED: {
  96. const { participant } = action;
  97. const { id, previousSpeakers = [] } = participant;
  98. const { dominantSpeaker, local } = state;
  99. const newSpeakers = [ id, ...previousSpeakers ];
  100. const sortedSpeakersList = [];
  101. for (const speaker of newSpeakers) {
  102. if (speaker !== local?.id) {
  103. const remoteParticipant = state.remote.get(speaker);
  104. remoteParticipant
  105. && sortedSpeakersList.push(
  106. [ speaker, _getDisplayName(state, remoteParticipant.name) ]
  107. );
  108. }
  109. }
  110. // Keep the remote speaker list sorted alphabetically.
  111. sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
  112. // Only one dominant speaker is allowed.
  113. if (dominantSpeaker) {
  114. _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
  115. }
  116. if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
  117. return {
  118. ...state,
  119. dominantSpeaker: id,
  120. speakersList: new Map(sortedSpeakersList)
  121. };
  122. }
  123. delete state.dominantSpeaker;
  124. return {
  125. ...state
  126. };
  127. }
  128. case PIN_PARTICIPANT: {
  129. const { participant } = action;
  130. const { id } = participant;
  131. const { pinnedParticipant } = state;
  132. // Only one pinned participant is allowed.
  133. if (pinnedParticipant) {
  134. _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
  135. }
  136. if (id && _updateParticipantProperty(state, id, 'pinned', true)) {
  137. return {
  138. ...state,
  139. pinnedParticipant: id
  140. };
  141. }
  142. delete state.pinnedParticipant;
  143. return {
  144. ...state
  145. };
  146. }
  147. case SET_LOADABLE_AVATAR_URL:
  148. case PARTICIPANT_UPDATED: {
  149. const { participant } = action;
  150. let { id } = participant;
  151. const { local } = participant;
  152. if (!id && local) {
  153. id = LOCAL_PARTICIPANT_DEFAULT_ID;
  154. }
  155. let newParticipant;
  156. if (state.remote.has(id)) {
  157. newParticipant = _participant(state.remote.get(id), action);
  158. state.remote.set(id, newParticipant);
  159. } else if (id === state.local?.id) {
  160. newParticipant = state.local = _participant(state.local, action);
  161. }
  162. if (newParticipant) {
  163. // everyoneIsModerator calculation:
  164. const isModerator = isParticipantModerator(newParticipant);
  165. if (state.everyoneIsModerator && !isModerator) {
  166. state.everyoneIsModerator = false;
  167. } else if (!state.everyoneIsModerator && isModerator) {
  168. state.everyoneIsModerator = _isEveryoneModerator(state);
  169. }
  170. // haveParticipantWithScreenSharingFeature calculation:
  171. const { features = {} } = participant;
  172. // Currently we use only PARTICIPANT_UPDATED to set a feature to enabled and we never disable it.
  173. if (String(features['screen-sharing']) === 'true') {
  174. state.haveParticipantWithScreenSharingFeature = true;
  175. }
  176. }
  177. return {
  178. ...state
  179. };
  180. }
  181. case PARTICIPANT_JOINED: {
  182. const participant = _participantJoined(action);
  183. const { id, isFakeParticipant, isFakeScreenShareParticipant, isLocalScreenShare, name, pinned } = participant;
  184. const { pinnedParticipant, dominantSpeaker } = state;
  185. if (pinned) {
  186. if (pinnedParticipant) {
  187. _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
  188. }
  189. state.pinnedParticipant = id;
  190. }
  191. if (participant.dominantSpeaker) {
  192. if (dominantSpeaker) {
  193. _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
  194. }
  195. state.dominantSpeaker = id;
  196. }
  197. const isModerator = isParticipantModerator(participant);
  198. const { local, remote } = state;
  199. if (state.everyoneIsModerator && !isModerator) {
  200. state.everyoneIsModerator = false;
  201. } else if (!local && remote.size === 0 && isModerator) {
  202. state.everyoneIsModerator = true;
  203. }
  204. if (participant.local) {
  205. return {
  206. ...state,
  207. local: participant
  208. };
  209. }
  210. if (isLocalScreenShare) {
  211. return {
  212. ...state,
  213. localScreenShare: participant
  214. };
  215. }
  216. state.remote.set(id, participant);
  217. // Insert the new participant.
  218. const displayName = _getDisplayName(state, name);
  219. const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
  220. sortedRemoteParticipants.push([ id, displayName ]);
  221. sortedRemoteParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  222. // The sort order of participants is preserved since Map remembers the original insertion order of the keys.
  223. state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
  224. if (isFakeScreenShareParticipant) {
  225. const sortedRemoteFakeScreenShareParticipants = [ ...state.sortedRemoteFakeScreenShareParticipants ];
  226. sortedRemoteFakeScreenShareParticipants.push([ id, name ]);
  227. sortedRemoteFakeScreenShareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  228. state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
  229. }
  230. if (isFakeParticipant) {
  231. state.fakeParticipants.set(id, participant);
  232. }
  233. return { ...state };
  234. }
  235. case PARTICIPANT_LEFT: {
  236. // XXX A remote participant is uniquely identified by their id in a
  237. // specific JitsiConference instance. The local participant is uniquely
  238. // identified by the very fact that there is only one local participant
  239. // (and the fact that the local participant "joins" at the beginning of
  240. // the app and "leaves" at the end of the app).
  241. const { conference, id } = action.participant;
  242. const {
  243. fakeParticipants,
  244. sortedRemoteFakeScreenShareParticipants,
  245. remote,
  246. local,
  247. localScreenShare,
  248. dominantSpeaker,
  249. pinnedParticipant
  250. } = state;
  251. let oldParticipant = remote.get(id);
  252. if (oldParticipant && oldParticipant.conference === conference) {
  253. remote.delete(id);
  254. } else if (local?.id === id) {
  255. oldParticipant = state.local;
  256. delete state.local;
  257. } else if (localScreenShare?.id === id) {
  258. oldParticipant = state.local;
  259. delete state.localScreenShare;
  260. } else {
  261. // no participant found
  262. return state;
  263. }
  264. state.sortedRemoteParticipants.delete(id);
  265. state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== id);
  266. if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
  267. state.everyoneIsModerator = _isEveryoneModerator(state);
  268. }
  269. const { features = {} } = oldParticipant || {};
  270. if (state.haveParticipantWithScreenSharingFeature && String(features['screen-sharing']) === 'true') {
  271. const { features: localFeatures = {} } = state.local || {};
  272. if (String(localFeatures['screen-sharing']) !== 'true') {
  273. state.haveParticipantWithScreenSharingFeature = false;
  274. // eslint-disable-next-line no-unused-vars
  275. for (const [ key, participant ] of state.remote) {
  276. const { features: f = {} } = participant;
  277. if (String(f['screen-sharing']) === 'true') {
  278. state.haveParticipantWithScreenSharingFeature = true;
  279. break;
  280. }
  281. }
  282. }
  283. }
  284. if (dominantSpeaker === id) {
  285. state.dominantSpeaker = undefined;
  286. }
  287. // Remove the participant from the list of speakers.
  288. state.speakersList.has(id) && state.speakersList.delete(id);
  289. if (pinnedParticipant === id) {
  290. state.pinnedParticipant = undefined;
  291. }
  292. if (fakeParticipants.has(id)) {
  293. fakeParticipants.delete(id);
  294. }
  295. if (sortedRemoteFakeScreenShareParticipants.has(id)) {
  296. sortedRemoteFakeScreenShareParticipants.delete(id);
  297. state.sortedRemoteFakeScreenShareParticipants = new Map(sortedRemoteFakeScreenShareParticipants);
  298. }
  299. return { ...state };
  300. }
  301. case RAISE_HAND_UPDATED: {
  302. return {
  303. ...state,
  304. raisedHandsQueue: action.queue
  305. };
  306. }
  307. case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: {
  308. const { participantIds } = action;
  309. const sortedSharesList = [];
  310. for (const participant of participantIds) {
  311. const remoteParticipant = state.remote.get(participant);
  312. if (remoteParticipant) {
  313. const displayName
  314. = _getDisplayName(state, remoteParticipant.name);
  315. sortedSharesList.push([ participant, displayName ]);
  316. }
  317. }
  318. // Keep the remote screen share list sorted alphabetically.
  319. sortedSharesList.length && sortedSharesList.sort((a, b) => a[1].localeCompare(b[1]));
  320. state.sortedRemoteScreenshares = new Map(sortedSharesList);
  321. return { ...state };
  322. }
  323. }
  324. return state;
  325. });
  326. /**
  327. * Returns the participant's display name, default string if display name is not set on the participant.
  328. *
  329. * @param {Object} state - The local participant redux state.
  330. * @param {string} name - The display name of the participant.
  331. * @returns {string}
  332. */
  333. function _getDisplayName(state: Object, name: string): string {
  334. const config = state['features/base/config'];
  335. return name ?? (config?.defaultRemoteDisplayName || 'Fellow Jitster');
  336. }
  337. /**
  338. * Loops trough the participants in the state in order to check if all participants are moderators.
  339. *
  340. * @param {Object} state - The local participant redux state.
  341. * @returns {boolean}
  342. */
  343. function _isEveryoneModerator(state) {
  344. if (isParticipantModerator(state.local)) {
  345. // eslint-disable-next-line no-unused-vars
  346. for (const [ k, p ] of state.remote) {
  347. if (!isParticipantModerator(p)) {
  348. return false;
  349. }
  350. }
  351. return true;
  352. }
  353. return false;
  354. }
  355. /**
  356. * Reducer function for a single participant.
  357. *
  358. * @param {Participant|undefined} state - Participant to be modified.
  359. * @param {Object} action - Action object.
  360. * @param {string} action.type - Type of action.
  361. * @param {Participant} action.participant - Information about participant to be
  362. * added/modified.
  363. * @param {JitsiConference} action.conference - Conference instance.
  364. * @private
  365. * @returns {Participant}
  366. */
  367. function _participant(state: Object = {}, action) {
  368. switch (action.type) {
  369. case SET_LOADABLE_AVATAR_URL:
  370. case PARTICIPANT_UPDATED: {
  371. const { participant } = action; // eslint-disable-line no-shadow
  372. const newState = { ...state };
  373. for (const key in participant) {
  374. if (participant.hasOwnProperty(key)
  375. && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
  376. === -1) {
  377. newState[key] = participant[key];
  378. }
  379. }
  380. return newState;
  381. }
  382. }
  383. return state;
  384. }
  385. /**
  386. * Reduces a specific redux action of type {@link PARTICIPANT_JOINED} in the
  387. * feature base/participants.
  388. *
  389. * @param {Action} action - The redux action of type {@code PARTICIPANT_JOINED}
  390. * to reduce.
  391. * @private
  392. * @returns {Object} The new participant derived from the payload of the
  393. * specified {@code action} to be added into the redux state of the feature
  394. * base/participants after the reduction of the specified
  395. * {@code action}.
  396. */
  397. function _participantJoined({ participant }) {
  398. const {
  399. avatarURL,
  400. botType,
  401. connectionStatus,
  402. dominantSpeaker,
  403. email,
  404. isFakeParticipant,
  405. isFakeScreenShareParticipant,
  406. isLocalScreenShare,
  407. isReplacing,
  408. isJigasi,
  409. loadableAvatarUrl,
  410. local,
  411. name,
  412. pinned,
  413. presence,
  414. role
  415. } = participant;
  416. let { conference, id } = participant;
  417. if (local) {
  418. // conference
  419. //
  420. // XXX The local participant is not identified in association with a
  421. // JitsiConference because it is identified by the very fact that it is
  422. // the local participant.
  423. conference = undefined;
  424. // id
  425. id || (id = LOCAL_PARTICIPANT_DEFAULT_ID);
  426. }
  427. return {
  428. avatarURL,
  429. botType,
  430. conference,
  431. connectionStatus,
  432. dominantSpeaker: dominantSpeaker || false,
  433. email,
  434. id,
  435. isFakeParticipant,
  436. isFakeScreenShareParticipant,
  437. isLocalScreenShare,
  438. isReplacing,
  439. isJigasi,
  440. loadableAvatarUrl,
  441. local: local || false,
  442. name,
  443. pinned: pinned || false,
  444. presence,
  445. role: role || PARTICIPANT_ROLE.NONE
  446. };
  447. }
  448. /**
  449. * Updates a specific property for a participant.
  450. *
  451. * @param {State} state - The redux state.
  452. * @param {string} id - The ID of the participant.
  453. * @param {string} property - The property to update.
  454. * @param {*} value - The new value.
  455. * @returns {boolean} - True if a participant was updated and false otherwise.
  456. */
  457. function _updateParticipantProperty(state, id, property, value) {
  458. const { remote, local, localScreenShare } = state;
  459. if (remote.has(id)) {
  460. remote.set(id, set(remote.get(id), property, value));
  461. return true;
  462. } else if (local?.id === id || local?.id === 'local') {
  463. // The local participant's ID can chance from something to "local" when
  464. // not in a conference.
  465. state.local = set(local, property, value);
  466. return true;
  467. } else if (localScreenShare?.id === id) {
  468. state.localScreenShare = set(localScreenShare, property, value);
  469. return true;
  470. }
  471. return false;
  472. }