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

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