您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

reducer.ts 20KB

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