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.ts 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. import { AnyAction } from 'redux';
  2. import { MEDIA_TYPE } from '../media/constants';
  3. import ReducerRegistry from '../redux/ReducerRegistry';
  4. import { set } from '../redux/functions';
  5. import {
  6. DOMINANT_SPEAKER_CHANGED,
  7. NOTIFIED_TO_SPEAK,
  8. OVERWRITE_PARTICIPANT_NAME,
  9. PARTICIPANT_ID_CHANGED,
  10. PARTICIPANT_JOINED,
  11. PARTICIPANT_LEFT,
  12. PARTICIPANT_SOURCES_UPDATED,
  13. PARTICIPANT_UPDATED,
  14. PIN_PARTICIPANT,
  15. RAISE_HAND_CLEAR,
  16. RAISE_HAND_UPDATED,
  17. SCREENSHARE_PARTICIPANT_NAME_CHANGED,
  18. SET_LOADABLE_AVATAR_URL
  19. } from './actionTypes';
  20. import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
  21. import {
  22. isLocalScreenshareParticipant,
  23. isParticipantModerator,
  24. isRemoteScreenshareParticipant,
  25. isScreenShareParticipant
  26. } from './functions';
  27. import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
  28. /**
  29. * Participant object.
  30. *
  31. * @typedef {Object} Participant
  32. * @property {string} id - Participant ID.
  33. * @property {string} name - Participant name.
  34. * @property {string} avatar - Path to participant avatar if any.
  35. * @property {string} role - Participant role.
  36. * @property {boolean} local - If true, participant is local.
  37. * @property {boolean} pinned - If true, participant is currently a
  38. * "PINNED_ENDPOINT".
  39. * @property {boolean} dominantSpeaker - If this participant is the dominant
  40. * speaker in the (associated) conference, {@code true}; otherwise,
  41. * {@code false}.
  42. * @property {string} email - Participant email.
  43. */
  44. /**
  45. * The participant properties which cannot be updated through
  46. * {@link PARTICIPANT_UPDATED}. They either identify the participant or can only
  47. * be modified through property-dedicated actions.
  48. *
  49. * @type {string[]}
  50. */
  51. const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
  52. // The following properties identify the participant:
  53. 'conference',
  54. 'id',
  55. 'local',
  56. // The following properties can only be modified through property-dedicated
  57. // actions:
  58. 'dominantSpeaker',
  59. 'pinned'
  60. ];
  61. const DEFAULT_STATE = {
  62. dominantSpeaker: undefined,
  63. fakeParticipants: new Map(),
  64. local: undefined,
  65. localScreenShare: undefined,
  66. numberOfNonModeratorParticipants: 0,
  67. numberOfParticipantsDisabledE2EE: 0,
  68. numberOfParticipantsNotSupportingE2EE: 0,
  69. overwrittenNameList: {},
  70. pinnedParticipant: undefined,
  71. raisedHandsQueue: [],
  72. remote: new Map(),
  73. remoteVideoSources: new Set<string>(),
  74. sortedRemoteVirtualScreenshareParticipants: new Map(),
  75. sortedRemoteParticipants: new Map(),
  76. speakersList: new Map()
  77. };
  78. export interface IParticipantsState {
  79. dominantSpeaker?: string;
  80. fakeParticipants: Map<string, IParticipant>;
  81. local?: ILocalParticipant;
  82. localScreenShare?: IParticipant;
  83. numberOfNonModeratorParticipants: number;
  84. numberOfParticipantsDisabledE2EE: number;
  85. numberOfParticipantsNotSupportingE2EE: number;
  86. overwrittenNameList: { [id: string]: string; };
  87. pinnedParticipant?: string;
  88. raisedHandsQueue: Array<{ hasBeenNotified?: boolean; id: string; raisedHandTimestamp: number; }>;
  89. remote: Map<string, IParticipant>;
  90. remoteVideoSources: Set<string>;
  91. sortedRemoteParticipants: Map<string, string>;
  92. sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
  93. speakersList: Map<string, string>;
  94. }
  95. /**
  96. * Listen for actions which add, remove, or update the set of participants in
  97. * the conference.
  98. *
  99. * @param {IParticipant[]} state - List of participants to be modified.
  100. * @param {Object} action - Action object.
  101. * @param {string} action.type - Type of action.
  102. * @param {IParticipant} action.participant - Information about participant to be
  103. * added/removed/modified.
  104. * @returns {IParticipant[]}
  105. */
  106. ReducerRegistry.register<IParticipantsState>('features/base/participants',
  107. (state = DEFAULT_STATE, action): IParticipantsState => {
  108. switch (action.type) {
  109. case NOTIFIED_TO_SPEAK: {
  110. return {
  111. ...state,
  112. raisedHandsQueue: state.raisedHandsQueue.map((item, index) => {
  113. if (index === 0) {
  114. return {
  115. ...item,
  116. hasBeenNotified: true
  117. };
  118. }
  119. return item;
  120. })
  121. };
  122. }
  123. case PARTICIPANT_ID_CHANGED: {
  124. const { local } = state;
  125. if (local) {
  126. if (action.newValue === 'local' && state.raisedHandsQueue.find(pid => pid.id === local.id)) {
  127. state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== local.id);
  128. }
  129. state.local = {
  130. ...local,
  131. id: action.newValue
  132. };
  133. return {
  134. ...state
  135. };
  136. }
  137. return state;
  138. }
  139. case DOMINANT_SPEAKER_CHANGED: {
  140. const { participant } = action;
  141. const { id, previousSpeakers = [] } = participant;
  142. const { dominantSpeaker, local } = state;
  143. const newSpeakers = [ id, ...previousSpeakers ];
  144. const sortedSpeakersList: Array<Array<string>> = [];
  145. for (const speaker of newSpeakers) {
  146. if (speaker !== local?.id) {
  147. const remoteParticipant = state.remote.get(speaker);
  148. remoteParticipant
  149. && sortedSpeakersList.push(
  150. [ speaker, _getDisplayName(state, remoteParticipant?.name) ]
  151. );
  152. }
  153. }
  154. // Keep the remote speaker list sorted alphabetically.
  155. sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
  156. // Only one dominant speaker is allowed.
  157. if (dominantSpeaker) {
  158. _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
  159. }
  160. if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
  161. return {
  162. ...state,
  163. dominantSpeaker: id, // @ts-ignore
  164. speakersList: new Map(sortedSpeakersList)
  165. };
  166. }
  167. delete state.dominantSpeaker;
  168. return {
  169. ...state
  170. };
  171. }
  172. case PIN_PARTICIPANT: {
  173. const { participant } = action;
  174. const { id } = participant;
  175. const { pinnedParticipant } = state;
  176. // Only one pinned participant is allowed.
  177. if (pinnedParticipant) {
  178. _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
  179. }
  180. if (id && _updateParticipantProperty(state, id, 'pinned', true)) {
  181. return {
  182. ...state,
  183. pinnedParticipant: id
  184. };
  185. }
  186. delete state.pinnedParticipant;
  187. return {
  188. ...state
  189. };
  190. }
  191. case SET_LOADABLE_AVATAR_URL:
  192. case PARTICIPANT_UPDATED: {
  193. const { participant } = action;
  194. let { id } = participant;
  195. const { local } = participant;
  196. if (!id && local) {
  197. id = LOCAL_PARTICIPANT_DEFAULT_ID;
  198. }
  199. let newParticipant: IParticipant | null = null;
  200. const oldParticipant = local || state.local?.id === id ? state.local : state.remote.get(id);
  201. if (state.remote.has(id)) {
  202. newParticipant = _participant(oldParticipant, action);
  203. state.remote.set(id, newParticipant);
  204. } else if (id === state.local?.id) {
  205. newParticipant = state.local = _participant(state.local, action);
  206. }
  207. if (oldParticipant && newParticipant && !newParticipant.fakeParticipant) {
  208. const isModerator = isParticipantModerator(newParticipant);
  209. if (isParticipantModerator(oldParticipant) !== isModerator) {
  210. state.numberOfNonModeratorParticipants += isModerator ? -1 : 1;
  211. }
  212. const e2eeEnabled = Boolean(newParticipant.e2eeEnabled);
  213. const e2eeSupported = Boolean(newParticipant.e2eeSupported);
  214. if (Boolean(oldParticipant.e2eeEnabled) !== e2eeEnabled) {
  215. state.numberOfParticipantsDisabledE2EE += e2eeEnabled ? -1 : 1;
  216. }
  217. if (!local && Boolean(oldParticipant.e2eeSupported) !== e2eeSupported) {
  218. state.numberOfParticipantsNotSupportingE2EE += e2eeSupported ? -1 : 1;
  219. }
  220. }
  221. return {
  222. ...state
  223. };
  224. }
  225. case SCREENSHARE_PARTICIPANT_NAME_CHANGED: {
  226. const { id, name } = action;
  227. if (state.sortedRemoteVirtualScreenshareParticipants.has(id)) {
  228. state.sortedRemoteVirtualScreenshareParticipants.delete(id);
  229. const sortedRemoteVirtualScreenshareParticipants = [ ...state.sortedRemoteVirtualScreenshareParticipants ];
  230. sortedRemoteVirtualScreenshareParticipants.push([ id, name ]);
  231. sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  232. state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
  233. }
  234. return { ...state };
  235. }
  236. case PARTICIPANT_JOINED: {
  237. const participant = _participantJoined(action);
  238. const {
  239. fakeParticipant,
  240. id,
  241. name,
  242. pinned,
  243. sources
  244. } = participant;
  245. const { pinnedParticipant, dominantSpeaker } = state;
  246. if (pinned) {
  247. if (pinnedParticipant) {
  248. _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
  249. }
  250. state.pinnedParticipant = id;
  251. }
  252. if (participant.dominantSpeaker) {
  253. if (dominantSpeaker) {
  254. _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
  255. }
  256. state.dominantSpeaker = id;
  257. }
  258. if (!fakeParticipant) {
  259. const isModerator = isParticipantModerator(participant);
  260. if (!isModerator) {
  261. state.numberOfNonModeratorParticipants += 1;
  262. }
  263. const { e2eeEnabled, e2eeSupported } = participant as IParticipant;
  264. if (!e2eeEnabled) {
  265. state.numberOfParticipantsDisabledE2EE += 1;
  266. }
  267. if (!participant.local && !e2eeSupported) {
  268. state.numberOfParticipantsNotSupportingE2EE += 1;
  269. }
  270. }
  271. if (participant.local) {
  272. return {
  273. ...state,
  274. local: participant
  275. };
  276. }
  277. if (isLocalScreenshareParticipant(participant)) {
  278. return {
  279. ...state,
  280. localScreenShare: participant
  281. };
  282. }
  283. state.remote.set(id, participant);
  284. if (sources?.size) {
  285. const videoSources: Map<string, ISourceInfo> | undefined = sources.get(MEDIA_TYPE.VIDEO);
  286. if (videoSources?.size) {
  287. const newRemoteVideoSources = new Set(state.remoteVideoSources);
  288. for (const source of videoSources.keys()) {
  289. newRemoteVideoSources.add(source);
  290. }
  291. state.remoteVideoSources = newRemoteVideoSources;
  292. }
  293. }
  294. // Insert the new participant.
  295. const displayName = _getDisplayName(state, name);
  296. const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
  297. sortedRemoteParticipants.push([ id, displayName ]);
  298. sortedRemoteParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  299. // The sort order of participants is preserved since Map remembers the original insertion order of the keys.
  300. state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
  301. if (isRemoteScreenshareParticipant(participant)) {
  302. const sortedRemoteVirtualScreenshareParticipants = [ ...state.sortedRemoteVirtualScreenshareParticipants ];
  303. sortedRemoteVirtualScreenshareParticipants.push([ id, name ?? '' ]);
  304. sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  305. state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
  306. }
  307. // Exclude the screenshare participant from the fake participant count to avoid duplicates.
  308. if (fakeParticipant && !isScreenShareParticipant(participant)) {
  309. state.fakeParticipants.set(id, participant);
  310. }
  311. return { ...state };
  312. }
  313. case PARTICIPANT_LEFT: {
  314. // XXX A remote participant is uniquely identified by their id in a
  315. // specific JitsiConference instance. The local participant is uniquely
  316. // identified by the very fact that there is only one local participant
  317. // (and the fact that the local participant "joins" at the beginning of
  318. // the app and "leaves" at the end of the app).
  319. const { conference, id } = action.participant;
  320. const {
  321. fakeParticipants,
  322. sortedRemoteVirtualScreenshareParticipants,
  323. remote,
  324. local,
  325. localScreenShare,
  326. dominantSpeaker,
  327. pinnedParticipant
  328. } = state;
  329. let oldParticipant = remote.get(id);
  330. let isLocalScreenShare = false;
  331. if (oldParticipant && oldParticipant.conference === conference) {
  332. remote.delete(id);
  333. } else if (local?.id === id) {
  334. oldParticipant = state.local;
  335. delete state.local;
  336. } else if (localScreenShare?.id === id) {
  337. isLocalScreenShare = true;
  338. oldParticipant = state.local;
  339. delete state.localScreenShare;
  340. } else {
  341. // no participant found
  342. return state;
  343. }
  344. if (oldParticipant?.sources?.size) {
  345. const videoSources: Map<string, ISourceInfo> | undefined = oldParticipant.sources.get(MEDIA_TYPE.VIDEO);
  346. if (videoSources?.size) {
  347. const newRemoteVideoSources = new Set(state.remoteVideoSources);
  348. for (const source of videoSources.keys()) {
  349. newRemoteVideoSources.delete(source);
  350. }
  351. state.remoteVideoSources = newRemoteVideoSources;
  352. }
  353. } else if (oldParticipant?.fakeParticipant === FakeParticipant.RemoteScreenShare) {
  354. const newRemoteVideoSources = new Set(state.remoteVideoSources);
  355. if (newRemoteVideoSources.delete(id)) {
  356. state.remoteVideoSources = newRemoteVideoSources;
  357. }
  358. }
  359. state.sortedRemoteParticipants.delete(id);
  360. state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== id);
  361. if (dominantSpeaker === id) {
  362. state.dominantSpeaker = undefined;
  363. }
  364. // Remove the participant from the list of speakers.
  365. state.speakersList.has(id) && state.speakersList.delete(id);
  366. if (pinnedParticipant === id) {
  367. state.pinnedParticipant = undefined;
  368. }
  369. if (fakeParticipants.has(id)) {
  370. fakeParticipants.delete(id);
  371. }
  372. if (sortedRemoteVirtualScreenshareParticipants.has(id)) {
  373. sortedRemoteVirtualScreenshareParticipants.delete(id);
  374. state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
  375. }
  376. if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {
  377. const { e2eeEnabled, e2eeSupported } = oldParticipant;
  378. if (!isParticipantModerator(oldParticipant)) {
  379. state.numberOfNonModeratorParticipants -= 1;
  380. }
  381. if (!e2eeEnabled) {
  382. state.numberOfParticipantsDisabledE2EE -= 1;
  383. }
  384. if (!oldParticipant.local && !e2eeSupported) {
  385. state.numberOfParticipantsNotSupportingE2EE -= 1;
  386. }
  387. }
  388. return { ...state };
  389. }
  390. case PARTICIPANT_SOURCES_UPDATED: {
  391. const { id, sources } = action.participant;
  392. const participant = state.remote.get(id);
  393. if (participant) {
  394. participant.sources = sources;
  395. const videoSources: Map<string, ISourceInfo> = sources.get(MEDIA_TYPE.VIDEO);
  396. if (videoSources?.size) {
  397. const newRemoteVideoSources = new Set(state.remoteVideoSources);
  398. for (const source of videoSources.keys()) {
  399. newRemoteVideoSources.add(source);
  400. }
  401. state.remoteVideoSources = newRemoteVideoSources;
  402. }
  403. }
  404. return { ...state };
  405. }
  406. case RAISE_HAND_CLEAR: {
  407. return {
  408. ...state,
  409. raisedHandsQueue: []
  410. };
  411. }
  412. case RAISE_HAND_UPDATED: {
  413. return {
  414. ...state,
  415. raisedHandsQueue: action.queue
  416. };
  417. }
  418. case OVERWRITE_PARTICIPANT_NAME: {
  419. const { id, name } = action;
  420. return {
  421. ...state,
  422. overwrittenNameList: {
  423. ...state.overwrittenNameList,
  424. [id]: name
  425. }
  426. };
  427. }
  428. }
  429. return state;
  430. });
  431. /**
  432. * Returns the participant's display name, default string if display name is not set on the participant.
  433. *
  434. * @param {Object} state - The local participant redux state.
  435. * @param {string} name - The display name of the participant.
  436. * @returns {string}
  437. */
  438. function _getDisplayName(state: Object, name?: string): string {
  439. // @ts-ignore
  440. const config = state['features/base/config'];
  441. return name ?? (config?.defaultRemoteDisplayName || 'Fellow Jitster');
  442. }
  443. /**
  444. * Reducer function for a single participant.
  445. *
  446. * @param {IParticipant|undefined} state - Participant to be modified.
  447. * @param {Object} action - Action object.
  448. * @param {string} action.type - Type of action.
  449. * @param {IParticipant} action.participant - Information about participant to be
  450. * added/modified.
  451. * @param {JitsiConference} action.conference - Conference instance.
  452. * @private
  453. * @returns {IParticipant}
  454. */
  455. function _participant(state: IParticipant | ILocalParticipant = { id: '' },
  456. action: AnyAction): IParticipant | ILocalParticipant {
  457. switch (action.type) {
  458. case SET_LOADABLE_AVATAR_URL:
  459. case PARTICIPANT_UPDATED: {
  460. const { participant } = action; // eslint-disable-line no-shadow
  461. const newState = { ...state };
  462. for (const key in participant) {
  463. if (participant.hasOwnProperty(key)
  464. && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
  465. === -1) {
  466. // @ts-ignore
  467. newState[key] = participant[key];
  468. }
  469. }
  470. return newState;
  471. }
  472. }
  473. return state;
  474. }
  475. /**
  476. * Reduces a specific redux action of type {@link PARTICIPANT_JOINED} in the
  477. * feature base/participants.
  478. *
  479. * @param {Action} action - The redux action of type {@code PARTICIPANT_JOINED}
  480. * to reduce.
  481. * @private
  482. * @returns {Object} The new participant derived from the payload of the
  483. * specified {@code action} to be added into the redux state of the feature
  484. * base/participants after the reduction of the specified
  485. * {@code action}.
  486. */
  487. function _participantJoined({ participant }: { participant: IParticipant; }) {
  488. const {
  489. avatarURL,
  490. botType,
  491. dominantSpeaker,
  492. email,
  493. fakeParticipant,
  494. isReplacing,
  495. loadableAvatarUrl,
  496. local,
  497. name,
  498. pinned,
  499. presence,
  500. role,
  501. sources
  502. } = participant;
  503. let { conference, id } = participant;
  504. if (local) {
  505. // conference
  506. //
  507. // XXX The local participant is not identified in association with a
  508. // JitsiConference because it is identified by the very fact that it is
  509. // the local participant.
  510. conference = undefined;
  511. // id
  512. id || (id = LOCAL_PARTICIPANT_DEFAULT_ID);
  513. }
  514. return {
  515. avatarURL,
  516. botType,
  517. conference,
  518. dominantSpeaker: dominantSpeaker || false,
  519. email,
  520. fakeParticipant,
  521. id,
  522. isReplacing,
  523. loadableAvatarUrl,
  524. local: local || false,
  525. name,
  526. pinned: pinned || false,
  527. presence,
  528. role: role || PARTICIPANT_ROLE.NONE,
  529. sources
  530. };
  531. }
  532. /**
  533. * Updates a specific property for a participant.
  534. *
  535. * @param {State} state - The redux state.
  536. * @param {string} id - The ID of the participant.
  537. * @param {string} property - The property to update.
  538. * @param {*} value - The new value.
  539. * @returns {boolean} - True if a participant was updated and false otherwise.
  540. */
  541. function _updateParticipantProperty(state: IParticipantsState, id: string, property: string, value: boolean) {
  542. const { remote, local, localScreenShare } = state;
  543. if (remote.has(id)) {
  544. remote.set(id, set(remote.get(id) ?? {
  545. id: '',
  546. name: ''
  547. }, property as keyof IParticipant, value));
  548. return true;
  549. } else if (local?.id === id || local?.id === 'local') {
  550. // The local participant's ID can chance from something to "local" when
  551. // not in a conference.
  552. state.local = set(local, property as keyof ILocalParticipant, value);
  553. return true;
  554. } else if (localScreenShare?.id === id) {
  555. state.localScreenShare = set(localScreenShare, property as keyof IParticipant, value);
  556. return true;
  557. }
  558. return false;
  559. }