2021-09-09 22:46:41 +00:00
|
|
|
import _ from 'lodash';
|
|
|
|
|
2022-10-26 06:59:21 +00:00
|
|
|
import { IReduxState } from '../app/types';
|
2022-11-22 13:56:37 +00:00
|
|
|
import { getConferenceTimestamp } from '../base/conference/functions';
|
2022-10-26 06:59:21 +00:00
|
|
|
import { PARTICIPANT_ROLE } from '../base/participants/constants';
|
|
|
|
import { getParticipantById } from '../base/participants/functions';
|
2022-11-22 13:56:37 +00:00
|
|
|
import { FaceLandmarks } from '../face-landmarks/types';
|
|
|
|
|
|
|
|
import { THRESHOLD_FIXED_AXIS } from './constants';
|
|
|
|
import { ISpeaker, ISpeakerStats } from './reducer';
|
2021-09-09 22:46:41 +00:00
|
|
|
|
2021-08-13 16:10:05 +00:00
|
|
|
/**
|
|
|
|
* Checks if the speaker stats search is disabled.
|
|
|
|
*
|
2022-10-26 06:59:21 +00:00
|
|
|
* @param {IReduxState} state - The redux state.
|
2021-08-13 16:10:05 +00:00
|
|
|
* @returns {boolean} - True if the speaker stats search is disabled and false otherwise.
|
|
|
|
*/
|
2022-10-26 06:59:21 +00:00
|
|
|
export function isSpeakerStatsSearchDisabled(state: IReduxState) {
|
|
|
|
return state['features/base/config']?.speakerStats?.disableSearch;
|
2022-10-17 20:40:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if the speaker stats is disabled.
|
|
|
|
*
|
2022-10-26 06:59:21 +00:00
|
|
|
* @param {IReduxState} state - The redux state.
|
2022-10-17 20:40:13 +00:00
|
|
|
* @returns {boolean} - True if the speaker stats search is disabled and false otherwise.
|
|
|
|
*/
|
2022-10-26 06:59:21 +00:00
|
|
|
export function isSpeakerStatsDisabled(state: IReduxState) {
|
2022-10-17 20:40:13 +00:00
|
|
|
return state['features/base/config']?.speakerStats?.disabled;
|
2021-08-13 16:10:05 +00:00
|
|
|
}
|
2021-09-09 22:46:41 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets whether participants in speaker stats should be ordered or not, and with what priority.
|
|
|
|
*
|
2022-10-26 06:59:21 +00:00
|
|
|
* @param {IReduxState} state - The redux state.
|
2021-09-09 22:46:41 +00:00
|
|
|
* @returns {Array<string>} - The speaker stats order array or an empty array.
|
|
|
|
*/
|
2022-10-26 06:59:21 +00:00
|
|
|
export function getSpeakerStatsOrder(state: IReduxState) {
|
|
|
|
return state['features/base/config']?.speakerStats?.order ?? [
|
2021-09-09 22:46:41 +00:00
|
|
|
'role',
|
|
|
|
'name',
|
|
|
|
'hasLeft'
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets speaker stats.
|
|
|
|
*
|
2022-10-26 06:59:21 +00:00
|
|
|
* @param {IReduxState} state - The redux state.
|
2021-09-09 22:46:41 +00:00
|
|
|
* @returns {Object} - The speaker stats.
|
|
|
|
*/
|
2022-10-26 06:59:21 +00:00
|
|
|
export function getSpeakerStats(state: IReduxState) {
|
2021-09-09 22:46:41 +00:00
|
|
|
return state['features/speaker-stats']?.stats ?? {};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets speaker stats search criteria.
|
|
|
|
*
|
2022-10-26 06:59:21 +00:00
|
|
|
* @param {IReduxState} state - The redux state.
|
2021-09-24 16:39:24 +00:00
|
|
|
* @returns {string | null} - The search criteria.
|
2021-09-09 22:46:41 +00:00
|
|
|
*/
|
2022-10-26 06:59:21 +00:00
|
|
|
export function getSearchCriteria(state: IReduxState) {
|
2021-09-24 16:39:24 +00:00
|
|
|
return state['features/speaker-stats']?.criteria;
|
2021-09-09 22:46:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets if speaker stats reorder is pending.
|
|
|
|
*
|
2022-10-26 06:59:21 +00:00
|
|
|
* @param {IReduxState} state - The redux state.
|
2021-09-09 22:46:41 +00:00
|
|
|
* @returns {boolean} - The pending reorder flag.
|
|
|
|
*/
|
2022-10-26 06:59:21 +00:00
|
|
|
export function getPendingReorder(state: IReduxState) {
|
2021-09-09 22:46:41 +00:00
|
|
|
return state['features/speaker-stats']?.pendingReorder ?? false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-09-28 07:51:53 +00:00
|
|
|
* Get sorted speaker stats ids based on a configuration setting.
|
2021-09-09 22:46:41 +00:00
|
|
|
*
|
2022-11-22 13:56:37 +00:00
|
|
|
* @param {IState} state - The redux state.
|
|
|
|
* @param {IState} stats - The current speaker stats.
|
|
|
|
* @returns {string[] | undefined} - Ordered speaker stats ids.
|
2021-09-09 22:46:41 +00:00
|
|
|
* @public
|
|
|
|
*/
|
2022-11-22 13:56:37 +00:00
|
|
|
export function getSortedSpeakerStatsIds(state: IReduxState, stats: ISpeakerStats) {
|
2021-09-09 22:46:41 +00:00
|
|
|
const orderConfig = getSpeakerStatsOrder(state);
|
|
|
|
|
|
|
|
if (orderConfig) {
|
|
|
|
const enhancedStats = getEnhancedStatsForOrdering(state, stats, orderConfig);
|
2022-09-28 07:51:53 +00:00
|
|
|
|
|
|
|
return Object.entries(enhancedStats)
|
|
|
|
.sort(([ , a ], [ , b ]) => compareFn(a, b))
|
|
|
|
.map(el => el[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* Compares the order of two participants in the speaker stats list.
|
|
|
|
*
|
2022-11-22 13:56:37 +00:00
|
|
|
* @param {ISpeaker} currentParticipant - The first participant for comparison.
|
|
|
|
* @param {ISpeaker} nextParticipant - The second participant for comparison.
|
2022-09-28 07:51:53 +00:00
|
|
|
* @returns {number} - The sort order of the two participants.
|
|
|
|
*/
|
2022-11-22 13:56:37 +00:00
|
|
|
function compareFn(currentParticipant: ISpeaker, nextParticipant: ISpeaker) {
|
2022-09-28 07:51:53 +00:00
|
|
|
if (orderConfig.includes('hasLeft')) {
|
|
|
|
if (nextParticipant.hasLeft() && !currentParticipant.hasLeft()) {
|
|
|
|
return -1;
|
|
|
|
} else if (currentParticipant.hasLeft() && !nextParticipant.hasLeft()) {
|
|
|
|
return 1;
|
2021-09-09 22:46:41 +00:00
|
|
|
}
|
2022-09-28 07:51:53 +00:00
|
|
|
}
|
2021-09-09 22:46:41 +00:00
|
|
|
|
2022-11-22 13:56:37 +00:00
|
|
|
let result = 0;
|
2021-09-09 22:46:41 +00:00
|
|
|
|
2022-09-28 07:51:53 +00:00
|
|
|
for (const sortCriteria of orderConfig) {
|
|
|
|
switch (sortCriteria) {
|
|
|
|
case 'role':
|
|
|
|
if (!nextParticipant.isModerator && currentParticipant.isModerator) {
|
|
|
|
result = -1;
|
|
|
|
} else if (!currentParticipant.isModerator && nextParticipant.isModerator) {
|
|
|
|
result = 1;
|
|
|
|
} else {
|
|
|
|
result = 0;
|
2021-09-09 22:46:41 +00:00
|
|
|
}
|
2022-09-28 07:51:53 +00:00
|
|
|
break;
|
|
|
|
case 'name':
|
|
|
|
result = (currentParticipant.displayName || '').localeCompare(
|
|
|
|
nextParticipant.displayName || ''
|
|
|
|
);
|
|
|
|
break;
|
2021-09-09 22:46:41 +00:00
|
|
|
}
|
|
|
|
|
2022-09-28 07:51:53 +00:00
|
|
|
if (result !== 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-09-09 22:46:41 +00:00
|
|
|
|
2022-09-28 07:51:53 +00:00
|
|
|
return result;
|
2021-09-09 22:46:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enhance speaker stats to include data needed for ordering.
|
|
|
|
*
|
2022-11-22 13:56:37 +00:00
|
|
|
* @param {IState} state - The redux state.
|
|
|
|
* @param {ISpeakerStats} stats - Speaker stats.
|
2021-09-09 22:46:41 +00:00
|
|
|
* @param {Array<string>} orderConfig - Ordering configuration.
|
2022-11-22 13:56:37 +00:00
|
|
|
* @returns {ISpeakerStats} - Enhanced speaker stats.
|
2021-09-09 22:46:41 +00:00
|
|
|
* @public
|
|
|
|
*/
|
2022-11-22 13:56:37 +00:00
|
|
|
function getEnhancedStatsForOrdering(state: IReduxState, stats: ISpeakerStats, orderConfig: Array<string>) {
|
2021-09-09 22:46:41 +00:00
|
|
|
if (!orderConfig) {
|
|
|
|
return stats;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const id in stats) {
|
|
|
|
if (stats[id].hasOwnProperty('_hasLeft') && !stats[id].hasLeft()) {
|
|
|
|
if (orderConfig.includes('role')) {
|
|
|
|
const participant = getParticipantById(state, stats[id].getUserId());
|
|
|
|
|
|
|
|
stats[id].isModerator = participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return stats;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Filter stats by search criteria.
|
|
|
|
*
|
2022-11-22 13:56:37 +00:00
|
|
|
* @param {IState} state - The redux state.
|
|
|
|
* @param {ISpeakerStats | undefined} stats - The unfiltered stats.
|
2021-09-09 22:46:41 +00:00
|
|
|
*
|
2022-11-22 13:56:37 +00:00
|
|
|
* @returns {ISpeakerStats} - Filtered speaker stats.
|
2021-09-09 22:46:41 +00:00
|
|
|
* @public
|
|
|
|
*/
|
2022-11-22 13:56:37 +00:00
|
|
|
export function filterBySearchCriteria(state: IReduxState, stats?: ISpeakerStats) {
|
|
|
|
const filteredStats = _.cloneDeep(stats ?? getSpeakerStats(state));
|
2021-09-09 22:46:41 +00:00
|
|
|
const criteria = getSearchCriteria(state);
|
|
|
|
|
2021-09-24 16:39:24 +00:00
|
|
|
if (criteria !== null) {
|
2021-09-09 22:46:41 +00:00
|
|
|
const searchRegex = new RegExp(criteria, 'gi');
|
|
|
|
|
|
|
|
for (const id in filteredStats) {
|
|
|
|
if (filteredStats[id].hasOwnProperty('_isLocalStats')) {
|
|
|
|
const name = filteredStats[id].getDisplayName();
|
|
|
|
|
2021-09-24 16:39:24 +00:00
|
|
|
filteredStats[id].hidden = !name || !name.match(searchRegex);
|
2021-09-09 22:46:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return filteredStats;
|
|
|
|
}
|
2021-12-28 14:35:21 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset the hidden speaker stats.
|
|
|
|
*
|
2022-11-22 13:56:37 +00:00
|
|
|
* @param {IState} state - The redux state.
|
|
|
|
* @param {ISpeakerStats | undefined} stats - The unfiltered stats.
|
2021-12-28 14:35:21 +00:00
|
|
|
*
|
|
|
|
* @returns {Object} - Speaker stats.
|
|
|
|
* @public
|
|
|
|
*/
|
2022-11-22 13:56:37 +00:00
|
|
|
export function resetHiddenStats(state: IReduxState, stats?: ISpeakerStats) {
|
|
|
|
const resetStats = _.cloneDeep(stats ?? getSpeakerStats(state));
|
2021-12-28 14:35:21 +00:00
|
|
|
|
|
|
|
for (const id in resetStats) {
|
|
|
|
if (resetStats[id].hidden) {
|
|
|
|
resetStats[id].hidden = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return resetStats;
|
|
|
|
}
|
2022-11-22 13:56:37 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the current duration of the conference.
|
|
|
|
*
|
|
|
|
* @param {IState} state - The redux state.
|
|
|
|
* @returns {number | null} - The duration in milliseconds or null.
|
|
|
|
*/
|
|
|
|
export function getCurrentDuration(state: IReduxState) {
|
|
|
|
const startTimestamp = getConferenceTimestamp(state);
|
|
|
|
|
|
|
|
return startTimestamp ? Date.now() - startTimestamp : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the boundaries of the emotion timeline.
|
|
|
|
*
|
|
|
|
* @param {IState} state - The redux state.
|
|
|
|
* @returns {Object} - The left and right boundaries.
|
|
|
|
*/
|
|
|
|
export function getTimelineBoundaries(state: IReduxState) {
|
|
|
|
const { timelineBoundary, offsetLeft, offsetRight } = state['features/speaker-stats'];
|
|
|
|
const currentDuration = getCurrentDuration(state) ?? 0;
|
|
|
|
const rightBoundary = timelineBoundary ? timelineBoundary : currentDuration;
|
|
|
|
let leftOffset = 0;
|
|
|
|
|
|
|
|
if (rightBoundary > THRESHOLD_FIXED_AXIS) {
|
|
|
|
leftOffset = rightBoundary - THRESHOLD_FIXED_AXIS;
|
|
|
|
}
|
|
|
|
|
|
|
|
const left = offsetLeft + leftOffset;
|
|
|
|
const right = rightBoundary + offsetRight;
|
|
|
|
|
|
|
|
return {
|
|
|
|
left,
|
|
|
|
right
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the conference start time of the face landmarks.
|
|
|
|
*
|
|
|
|
* @param {FaceLandmarks} faceLandmarks - The face landmarks.
|
|
|
|
* @param {number} startTimestamp - The start timestamp of the conference.
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
export function getFaceLandmarksStart(faceLandmarks: FaceLandmarks, startTimestamp: number) {
|
|
|
|
return faceLandmarks.timestamp - startTimestamp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the conference end time of the face landmarks.
|
|
|
|
*
|
|
|
|
* @param {FaceLandmarks} faceLandmarks - The face landmarks.
|
|
|
|
* @param {number} startTimestamp - The start timestamp of the conference.
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
export function getFaceLandmarksEnd(faceLandmarks: FaceLandmarks, startTimestamp: number) {
|
|
|
|
return getFaceLandmarksStart(faceLandmarks, startTimestamp) + faceLandmarks.duration;
|
|
|
|
}
|