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

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