"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-7555/react/features/base/participants/reducer.ts" (28 Sep 2023, 20884 Bytes) of package /linux/misc/jitsi-meet-7555.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

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