Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  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. 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 { isParticipantModerator } from './functions';
  20. /**
  21. * Participant object.
  22. *
  23. * @typedef {Object} Participant
  24. * @property {string} id - Participant ID.
  25. * @property {string} name - Participant name.
  26. * @property {string} avatar - Path to participant avatar if any.
  27. * @property {string} role - Participant role.
  28. * @property {boolean} local - If true, participant is local.
  29. * @property {boolean} pinned - If true, participant is currently a
  30. * "PINNED_ENDPOINT".
  31. * @property {boolean} dominantSpeaker - If this participant is the dominant
  32. * speaker in the (associated) conference, {@code true}; otherwise,
  33. * {@code false}.
  34. * @property {string} email - Participant email.
  35. */
  36. /**
  37. * The participant properties which cannot be updated through
  38. * {@link PARTICIPANT_UPDATED}. They either identify the participant or can only
  39. * be modified through property-dedicated actions.
  40. *
  41. * @type {string[]}
  42. */
  43. const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
  44. // The following properties identify the participant:
  45. 'conference',
  46. 'id',
  47. 'local',
  48. // The following properties can only be modified through property-dedicated
  49. // actions:
  50. 'dominantSpeaker',
  51. 'pinned'
  52. ];
  53. const DEFAULT_STATE = {
  54. dominantSpeaker: undefined,
  55. everyoneIsModerator: false,
  56. fakeParticipants: new Map(),
  57. haveParticipantWithScreenSharingFeature: false,
  58. local: undefined,
  59. localScreenShare: undefined,
  60. overwrittenNameList: {},
  61. pinnedParticipant: undefined,
  62. raisedHandsQueue: [],
  63. remote: new Map(),
  64. sortedRemoteVirtualScreenshareParticipants: new Map(),
  65. sortedRemoteParticipants: new Map(),
  66. sortedRemoteScreenshares: new Map(),
  67. speakersList: new Map()
  68. };
  69. /**
  70. * Listen for actions which add, remove, or update the set of participants in
  71. * the conference.
  72. *
  73. * @param {Participant[]} state - List of participants to be modified.
  74. * @param {Object} action - Action object.
  75. * @param {string} action.type - Type of action.
  76. * @param {Participant} action.participant - Information about participant to be
  77. * added/removed/modified.
  78. * @returns {Participant[]}
  79. */
  80. ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, action) => {
  81. switch (action.type) {
  82. case PARTICIPANT_ID_CHANGED: {
  83. const { local } = state;
  84. if (local) {
  85. if (action.newValue === 'local' && state.raisedHandsQueue.find(pid => pid.id === local.id)) {
  86. state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== local.id);
  87. }
  88. state.local = {
  89. ...local,
  90. id: action.newValue
  91. };
  92. return {
  93. ...state
  94. };
  95. }
  96. return state;
  97. }
  98. case DOMINANT_SPEAKER_CHANGED: {
  99. const { participant } = action;
  100. const { id, previousSpeakers = [] } = participant;
  101. const { dominantSpeaker, local } = state;
  102. const newSpeakers = [ id, ...previousSpeakers ];
  103. const sortedSpeakersList = [];
  104. for (const speaker of newSpeakers) {
  105. if (speaker !== local?.id) {
  106. const remoteParticipant = state.remote.get(speaker);
  107. remoteParticipant
  108. && sortedSpeakersList.push(
  109. [ speaker, _getDisplayName(state, remoteParticipant.name) ]
  110. );
  111. }
  112. }
  113. // Keep the remote speaker list sorted alphabetically.
  114. sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
  115. // Only one dominant speaker is allowed.
  116. if (dominantSpeaker) {
  117. _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
  118. }
  119. if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
  120. return {
  121. ...state,
  122. dominantSpeaker: id,
  123. speakersList: new Map(sortedSpeakersList)
  124. };
  125. }
  126. delete state.dominantSpeaker;
  127. return {
  128. ...state
  129. };
  130. }
  131. case PIN_PARTICIPANT: {
  132. const { participant } = action;
  133. const { id } = participant;
  134. const { pinnedParticipant } = state;
  135. // Only one pinned participant is allowed.
  136. if (pinnedParticipant) {
  137. _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
  138. }
  139. if (id && _updateParticipantProperty(state, id, 'pinned', true)) {
  140. return {
  141. ...state,
  142. pinnedParticipant: id
  143. };
  144. }
  145. delete state.pinnedParticipant;
  146. return {
  147. ...state
  148. };
  149. }
  150. case SET_LOADABLE_AVATAR_URL:
  151. case PARTICIPANT_UPDATED: {
  152. const { participant } = action;
  153. let { id } = participant;
  154. const { local } = participant;
  155. if (!id && local) {
  156. id = LOCAL_PARTICIPANT_DEFAULT_ID;
  157. }
  158. let newParticipant;
  159. if (state.remote.has(id)) {
  160. newParticipant = _participant(state.remote.get(id), action);
  161. state.remote.set(id, newParticipant);
  162. } else if (id === state.local?.id) {
  163. newParticipant = state.local = _participant(state.local, action);
  164. }
  165. if (newParticipant) {
  166. // everyoneIsModerator calculation:
  167. const isModerator = isParticipantModerator(newParticipant);
  168. if (state.everyoneIsModerator && !isModerator) {
  169. state.everyoneIsModerator = false;
  170. } else if (!state.everyoneIsModerator && isModerator) {
  171. state.everyoneIsModerator = _isEveryoneModerator(state);
  172. }
  173. // haveParticipantWithScreenSharingFeature calculation:
  174. const { features = {} } = participant;
  175. // Currently we use only PARTICIPANT_UPDATED to set a feature to enabled and we never disable it.
  176. if (String(features['screen-sharing']) === 'true') {
  177. state.haveParticipantWithScreenSharingFeature = true;
  178. }
  179. }
  180. return {
  181. ...state
  182. };
  183. }
  184. case SCREENSHARE_PARTICIPANT_NAME_CHANGED: {
  185. const { id, name } = action;
  186. if (state.sortedRemoteVirtualScreenshareParticipants.has(id)) {
  187. state.sortedRemoteVirtualScreenshareParticipants.delete(id);
  188. const sortedRemoteVirtualScreenshareParticipants = [ ...state.sortedRemoteVirtualScreenshareParticipants ];
  189. sortedRemoteVirtualScreenshareParticipants.push([ id, name ]);
  190. sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  191. state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
  192. }
  193. return { ...state };
  194. }
  195. case PARTICIPANT_JOINED: {
  196. const participant = _participantJoined(action);
  197. const {
  198. id,
  199. isFakeParticipant,
  200. isLocalScreenShare,
  201. isVirtualScreenshareParticipant,
  202. name,
  203. pinned
  204. } = participant;
  205. const { pinnedParticipant, dominantSpeaker } = state;
  206. if (pinned) {
  207. if (pinnedParticipant) {
  208. _updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
  209. }
  210. state.pinnedParticipant = id;
  211. }
  212. if (participant.dominantSpeaker) {
  213. if (dominantSpeaker) {
  214. _updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
  215. }
  216. state.dominantSpeaker = id;
  217. }
  218. const isModerator = isParticipantModerator(participant);
  219. const { local, remote } = state;
  220. if (state.everyoneIsModerator && !isModerator) {
  221. state.everyoneIsModerator = false;
  222. } else if (!local && remote.size === 0 && isModerator) {
  223. state.everyoneIsModerator = true;
  224. }
  225. if (participant.local) {
  226. return {
  227. ...state,
  228. local: participant
  229. };
  230. }
  231. if (isLocalScreenShare) {
  232. return {
  233. ...state,
  234. localScreenShare: participant
  235. };
  236. }
  237. state.remote.set(id, participant);
  238. // Insert the new participant.
  239. const displayName = _getDisplayName(state, name);
  240. const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
  241. sortedRemoteParticipants.push([ id, displayName ]);
  242. sortedRemoteParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  243. // The sort order of participants is preserved since Map remembers the original insertion order of the keys.
  244. state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
  245. if (isVirtualScreenshareParticipant) {
  246. const sortedRemoteVirtualScreenshareParticipants = [ ...state.sortedRemoteVirtualScreenshareParticipants ];
  247. sortedRemoteVirtualScreenshareParticipants.push([ id, name ]);
  248. sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
  249. state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
  250. }
  251. if (isFakeParticipant) {
  252. state.fakeParticipants.set(id, participant);
  253. }
  254. return { ...state };
  255. }
  256. case PARTICIPANT_LEFT: {
  257. // XXX A remote participant is uniquely identified by their id in a
  258. // specific JitsiConference instance. The local participant is uniquely
  259. // identified by the very fact that there is only one local participant
  260. // (and the fact that the local participant "joins" at the beginning of
  261. // the app and "leaves" at the end of the app).
  262. const { conference, id } = action.participant;
  263. const {
  264. fakeParticipants,
  265. sortedRemoteVirtualScreenshareParticipants,
  266. remote,
  267. local,
  268. localScreenShare,
  269. dominantSpeaker,
  270. pinnedParticipant
  271. } = state;
  272. let oldParticipant = remote.get(id);
  273. if (oldParticipant && oldParticipant.conference === conference) {
  274. remote.delete(id);
  275. } else if (local?.id === id) {
  276. oldParticipant = state.local;
  277. delete state.local;
  278. } else if (localScreenShare?.id === id) {
  279. oldParticipant = state.local;
  280. delete state.localScreenShare;
  281. } else {
  282. // no participant found
  283. return state;
  284. }
  285. state.sortedRemoteParticipants.delete(id);
  286. state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== id);
  287. if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
  288. state.everyoneIsModerator = _isEveryoneModerator(state);
  289. }
  290. const { features = {} } = oldParticipant || {};
  291. if (state.haveParticipantWithScreenSharingFeature && String(features['screen-sharing']) === 'true') {
  292. const { features: localFeatures = {} } = state.local || {};
  293. if (String(localFeatures['screen-sharing']) !== 'true') {
  294. state.haveParticipantWithScreenSharingFeature = false;
  295. // eslint-disable-next-line no-unused-vars
  296. for (const [ key, participant ] of state.remote) {
  297. const { features: f = {} } = participant;
  298. if (String(f['screen-sharing']) === 'true') {
  299. state.haveParticipantWithScreenSharingFeature = true;
  300. break;
  301. }
  302. }
  303. }
  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. 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. const config = state['features/base/config'];
  366. return name ?? (config?.defaultRemoteDisplayName || 'Fellow Jitster');
  367. }
  368. /**
  369. * Loops through the participants in the state in order to check if all participants are moderators.
  370. *
  371. * @param {Object} state - The local participant redux state.
  372. * @returns {boolean}
  373. */
  374. function _isEveryoneModerator(state) {
  375. if (isParticipantModerator(state.local)) {
  376. // eslint-disable-next-line no-unused-vars
  377. for (const [ k, p ] of state.remote) {
  378. if (!isParticipantModerator(p)) {
  379. return false;
  380. }
  381. }
  382. return true;
  383. }
  384. return false;
  385. }
  386. /**
  387. * Reducer function for a single participant.
  388. *
  389. * @param {Participant|undefined} state - Participant to be modified.
  390. * @param {Object} action - Action object.
  391. * @param {string} action.type - Type of action.
  392. * @param {Participant} action.participant - Information about participant to be
  393. * added/modified.
  394. * @param {JitsiConference} action.conference - Conference instance.
  395. * @private
  396. * @returns {Participant}
  397. */
  398. function _participant(state: Object = {}, action) {
  399. switch (action.type) {
  400. case SET_LOADABLE_AVATAR_URL:
  401. case PARTICIPANT_UPDATED: {
  402. const { participant } = action; // eslint-disable-line no-shadow
  403. const newState = { ...state };
  404. for (const key in participant) {
  405. if (participant.hasOwnProperty(key)
  406. && PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
  407. === -1) {
  408. newState[key] = participant[key];
  409. }
  410. }
  411. return newState;
  412. }
  413. }
  414. return state;
  415. }
  416. /**
  417. * Reduces a specific redux action of type {@link PARTICIPANT_JOINED} in the
  418. * feature base/participants.
  419. *
  420. * @param {Action} action - The redux action of type {@code PARTICIPANT_JOINED}
  421. * to reduce.
  422. * @private
  423. * @returns {Object} The new participant derived from the payload of the
  424. * specified {@code action} to be added into the redux state of the feature
  425. * base/participants after the reduction of the specified
  426. * {@code action}.
  427. */
  428. function _participantJoined({ participant }) {
  429. const {
  430. avatarURL,
  431. botType,
  432. connectionStatus,
  433. dominantSpeaker,
  434. email,
  435. isFakeParticipant,
  436. isVirtualScreenshareParticipant,
  437. isLocalScreenShare,
  438. isReplacing,
  439. isJigasi,
  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. id,
  466. isFakeParticipant,
  467. isVirtualScreenshareParticipant,
  468. isLocalScreenShare,
  469. isReplacing,
  470. isJigasi,
  471. loadableAvatarUrl,
  472. local: local || false,
  473. name,
  474. pinned: pinned || false,
  475. presence,
  476. role: role || PARTICIPANT_ROLE.NONE
  477. };
  478. }
  479. /**
  480. * Updates a specific property for a participant.
  481. *
  482. * @param {State} state - The redux state.
  483. * @param {string} id - The ID of the participant.
  484. * @param {string} property - The property to update.
  485. * @param {*} value - The new value.
  486. * @returns {boolean} - True if a participant was updated and false otherwise.
  487. */
  488. function _updateParticipantProperty(state, id, property, value) {
  489. const { remote, local, localScreenShare } = state;
  490. if (remote.has(id)) {
  491. remote.set(id, set(remote.get(id), property, 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, value);
  497. return true;
  498. } else if (localScreenShare?.id === id) {
  499. state.localScreenShare = set(localScreenShare, property, value);
  500. return true;
  501. }
  502. return false;
  503. }