init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { resetSearchCriteria } from '../../actions.native';
import SpeakerStatsList from './SpeakerStatsList';
import SpeakerStatsSearch from './SpeakerStatsSearch';
import style from './styles';
/**
* Component that renders the list of speaker stats.
*
* @returns {React$Element<any>}
*/
const SpeakerStats = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(resetSearchCriteria());
}, []);
return (
<JitsiScreen
style = { style.speakerStatsContainer }>
<SpeakerStatsSearch />
<SpeakerStatsList />
</JitsiScreen>
);
};
export default SpeakerStats;

View File

@@ -0,0 +1,50 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { SPEAKERSTATS_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import AbstractSpeakerStatsButton from '../AbstractSpeakerStatsButton';
/**
* Implementation of a button for opening speaker stats dialog.
*/
class SpeakerStatsButton extends AbstractSpeakerStatsButton {
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
sendAnalytics(createToolbarEvent('speaker.stats'));
return navigate(screen.conference.speakerStats);
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code SpeakerStatsButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* visible: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, SPEAKERSTATS_ENABLED, true);
return {
visible: enabled
};
}
export default translate(connect(_mapStateToProps)(SpeakerStatsButton));

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { Text, View, ViewStyle } from 'react-native';
import Avatar from '../../../base/avatar/components/Avatar';
import StatelessAvatar from '../../../base/avatar/components/native/StatelessAvatar';
import { getInitials } from '../../../base/avatar/functions';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import TimeElapsed from './TimeElapsed';
import style from './styles';
interface IProps {
/**
* The name of the participant.
*/
displayName: string;
/**
* The total milliseconds the participant has been dominant speaker.
*/
dominantSpeakerTime: number;
/**
* True if the participant is no longer in the meeting.
*/
hasLeft: boolean;
/**
* True if the participant is currently the dominant speaker.
*/
isDominantSpeaker: boolean;
/**
* The id of the user.
*/
participantId: string;
}
const SpeakerStatsItem = (props: IProps) =>
(
<View
key = { props.participantId }
style = { style.speakerStatsItemContainer as ViewStyle }>
<View style = { style.speakerStatsAvatar }>
{
props.hasLeft ? (
<StatelessAvatar
color = { BaseTheme.palette.ui05 }
initials = { getInitials(props.displayName) }
size = { BaseTheme.spacing[5] } />
) : (
<Avatar
className = 'userAvatar'
participantId = { props.participantId }
size = { BaseTheme.spacing[5] } />
)
}
</View>
<View style = { style.speakerStatsNameTime as ViewStyle } >
<Text style = { [ style.speakerStatsText, props.hasLeft && style.speakerStatsLeft ] }>
{props.displayName}
</Text>
<TimeElapsed
style = { [
style.speakerStatsText,
style.speakerStatsTime,
props.isDominantSpeaker && style.speakerStatsDominant,
props.hasLeft && style.speakerStatsLeft
] }
time = { props.dominantSpeakerTime } />
</View>
</View>
);
export default SpeakerStatsItem;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { View } from 'react-native';
import abstractSpeakerStatsList from '../AbstractSpeakerStatsList';
import SpeakerStatsItem from './SpeakerStatsItem';
/**
* Component that renders the list of speaker stats.
*
* @returns {React$Element<any>}
*/
const SpeakerStatsList = () => {
const items = abstractSpeakerStatsList(SpeakerStatsItem);
return (
<View>
{items}
</View>
);
};
export default SpeakerStatsList;

View File

@@ -0,0 +1,48 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { withTheme } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { IconSearch } from '../../../base/icons/svg';
import Input from '../../../base/ui/components/native/Input';
import { escapeRegexp } from '../../../base/util/helpers';
import { initSearch } from '../../actions.native';
import { isSpeakerStatsSearchDisabled } from '../../functions';
import styles from './styles';
/**
* React component for display an individual user's speaker stats.
*
* @returns {React$Element<any>}
*/
const SpeakerStatsSearch = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [ searchQuery, setSearchQuery ] = useState('');
const onSearch = useCallback((criteria = '') => {
dispatch(initSearch(escapeRegexp(criteria)));
setSearchQuery(escapeRegexp(criteria));
}, [ dispatch ]);
const disableSpeakerStatsSearch = useSelector(isSpeakerStatsSearchDisabled);
if (disableSpeakerStatsSearch) {
return null;
}
return (
<Input
accessibilityLabel = { t('speakerStats.searchHint') }
clearable = { true }
customStyles = {{ container: styles.customContainer }}
icon = { IconSearch }
onChange = { onSearch }
placeholder = { t('speakerStats.search') }
value = { searchQuery } />
);
};
export default withTheme(SpeakerStatsSearch);

View File

@@ -0,0 +1,50 @@
import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { Text } from 'react-native';
import { translate } from '../../../base/i18n/functions';
import { createLocalizedTime } from '../timeFunctions';
/**
* The type of the React {@code Component} props of {@link TimeElapsed}.
*/
interface IProps extends WithTranslation {
/**
* Style for text.
*/
style: Object;
/**
* The milliseconds to be converted into a human-readable format.
*/
time: number;
}
/**
* React component for displaying total time elapsed. Converts a total count of
* milliseconds into a more humanized form: "# hours, # minutes, # seconds".
* With a time of 0, "0s" will be displayed.
*
* @augments Component
*/
class TimeElapsed extends PureComponent<IProps> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { style, time, t } = this.props;
const timeElapsed = createLocalizedTime(time, t);
return (
<Text style = { style }>
{ timeElapsed }
</Text>
);
}
}
export default translate(TimeElapsed);

View File

@@ -0,0 +1,55 @@
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
export default {
customContainer: {
marginVertical: BaseTheme.spacing[2]
},
speakerStatsContainer: {
flexDirection: 'column',
flex: 1,
height: 'auto',
paddingHorizontal: BaseTheme.spacing[3],
backgroundColor: BaseTheme.palette.ui01
},
speakerStatsItemContainer: {
flexDirection: 'row',
alignSelf: 'stretch',
height: BaseTheme.spacing[9],
alignItems: 'center'
},
speakerStatsAvatar: {
width: BaseTheme.spacing[5],
height: BaseTheme.spacing[5],
marginRight: BaseTheme.spacing[3]
},
speakerStatsNameTime: {
flexDirection: 'row',
flex: 1,
justifyContent: 'space-between',
alignItems: 'center'
},
speakerStatsText: {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01
},
speakerStatsTime: {
paddingHorizontal: 4,
paddingVertical: 2,
borderRadius: 4
},
speakerStatsDominant: {
backgroundColor: BaseTheme.palette.success02
},
speakerStatsLeft: {
color: BaseTheme.palette.text03
}
};