Files
jitsi-meet/react/features/display-name/components/web/DisplayName.tsx
Hristo Terezov d06b847319 feat(pip): Add Picture-in-Picture support for Electron
Implements Picture-in-Picture functionality for the Electron wrapper to maintain video engagement when users are not actively focused on the conference window. This feature addresses the need to keep users visually connected to the conference even when multitasking.

Key features:
- Automatic PiP mode activation and deactivation based on user interaction
- Displays large video participant's stream or renders their avatar on canvas when video unavailable
- Provides audio/video mute controls via MediaSession API directly in PiP window
- Adds API events (_pip-requested) for Electron wrapper integration

Implementation includes new pip feature module with Redux architecture, canvas-based avatar rendering with custom backgrounds support, and integration with existing mute/unmute logic. Depends on jitsi-meet-electron-sdk#479 for proper user gesture handling in Electron.
2025-12-04 16:04:10 -06:00

161 lines
4.7 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import {
getParticipantById,
getParticipantDisplayName
} from '../../../base/participants/functions';
import { updateSettings } from '../../../base/settings/actions';
import Tooltip from '../../../base/tooltip/components/Tooltip';
import { getIndicatorsTooltipPosition } from '../../../filmstrip/functions.web';
import { appendSuffix } from '../../functions';
import { getDisplayNameColor } from './styles';
/**
* The type of the React {@code Component} props of {@link DisplayName}.
*/
interface IProps {
/**
* Whether or not the display name should be editable on click.
*/
allowEditing: boolean;
/**
* A string to append to the displayName, if provided.
*/
displayNameSuffix: string;
/**
* The ID attribute to add to the component. Useful for global querying for
* the component by legacy components and torture tests.
*/
elementID: string;
/**
* The ID of the participant whose name is being displayed.
*/
participantID: string;
/**
* The type of thumbnail.
*/
thumbnailType?: string;
}
const useStyles = makeStyles()(theme => {
return {
displayName: {
...theme.typography.labelBold,
color: getDisplayNameColor(theme),
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
},
editDisplayName: {
outline: 'none',
border: 'none',
background: 'none',
boxShadow: 'none',
padding: 0,
...theme.typography.labelBold,
color: getDisplayNameColor(theme)
}
};
});
const DisplayName = ({
allowEditing,
displayNameSuffix,
elementID,
participantID,
thumbnailType
}: IProps) => {
const { classes } = useStyles();
const configuredDisplayName = useSelector((state: IReduxState) =>
getParticipantById(state, participantID))?.name ?? '';
const nameToDisplay = useSelector((state: IReduxState) => getParticipantDisplayName(state, participantID));
const [ editDisplayNameValue, setEditDisplayNameValue ] = useState('');
const [ isEditing, setIsEditing ] = useState(false);
const dispatch = useDispatch();
const { t } = useTranslation();
const nameInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (isEditing && nameInputRef.current) {
nameInputRef.current.select();
}
}, [ isEditing ]);
const onClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
}, []);
const onChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setEditDisplayNameValue(event.target.value);
}, []);
const onSubmit = useCallback(() => {
dispatch(updateSettings({
displayName: editDisplayNameValue
}));
setEditDisplayNameValue('');
setIsEditing(false);
nameInputRef.current = null;
}, [ editDisplayNameValue, nameInputRef ]);
const onKeyDown = useCallback((event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
onSubmit();
}
}, [ onSubmit ]);
const onStartEditing = useCallback((e: React.MouseEvent) => {
if (allowEditing) {
e.stopPropagation();
setIsEditing(true);
setEditDisplayNameValue(configuredDisplayName);
}
}, [ allowEditing ]);
if (allowEditing && isEditing) {
return (
<input
autoFocus = { true }
className = { classes.editDisplayName }
id = 'editDisplayName'
onBlur = { onSubmit }
onChange = { onChange }
onClick = { onClick }
onKeyDown = { onKeyDown }
placeholder = { t('defaultNickname') }
ref = { nameInputRef }
spellCheck = { 'false' }
type = 'text'
value = { editDisplayNameValue } />
);
}
return (
<Tooltip
content = { appendSuffix(nameToDisplay, displayNameSuffix) }
position = { getIndicatorsTooltipPosition(thumbnailType) }>
<span
className = { `displayname ${classes.displayName}` }
id = { elementID }
onClick = { onStartEditing }>
{appendSuffix(nameToDisplay, displayNameSuffix)}
</span>
</Tooltip>
);
};
export default DisplayName;