mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-31 03:53:31 +01:00
Add group call participant menu to participant grid tiles
Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
@@ -3991,6 +3991,11 @@ button.module-image__border-overlay:focus {
|
||||
}
|
||||
}
|
||||
|
||||
&__dropdown button {
|
||||
// Matches CallingAudioIndicator
|
||||
background: rgba(variables.$color-gray-80, 0.7);
|
||||
}
|
||||
|
||||
&--hand-raised &__footer {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -479,6 +479,7 @@ function ActiveCallManager({
|
||||
isCallLinkAdmin={isCallLinkAdmin}
|
||||
me={me}
|
||||
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||
renderCallingParticipantMenu={renderCallingParticipantMenu}
|
||||
renderReactionPicker={renderReactionPicker}
|
||||
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
||||
sendGroupCallReaction={sendGroupCallReaction}
|
||||
|
||||
@@ -37,6 +37,7 @@ import type { CallingImageDataCache } from './CallManager.dom.tsx';
|
||||
import { MINUTE } from '../util/durations/index.std.ts';
|
||||
import { strictAssert } from '../util/assert.std.ts';
|
||||
import { generateAci } from '../test-helpers/serviceIdUtils.std.ts';
|
||||
import { renderCallingParticipantMenu } from './CallingParticipantMenu.dom.stories.tsx';
|
||||
|
||||
const { sample, shuffle, times } = lodash;
|
||||
|
||||
@@ -248,6 +249,7 @@ const createProps = (
|
||||
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||
renderReactionPicker: () => <div />,
|
||||
cancelPresenting: action('cancel-presenting'),
|
||||
renderCallingParticipantMenu,
|
||||
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||
|
||||
@@ -110,6 +110,7 @@ import {
|
||||
PIP_MAXIMUM_LOCAL_VIDEO_HEIGHT_MULTIPLIER,
|
||||
PIP_MINIMUM_LOCAL_VIDEO_HEIGHT_MULTIPLIER,
|
||||
} from './CallingPip.dom.tsx';
|
||||
import type { PropsType as SmartCallingParticipantMenuProps } from '../state/smart/CallingParticipantMenu.preload.tsx';
|
||||
|
||||
const { isEqual, noop } = lodash;
|
||||
|
||||
@@ -130,6 +131,9 @@ export type PropsType = {
|
||||
isCallLinkAdmin: boolean;
|
||||
me: ConversationType;
|
||||
openSystemPreferencesAction: () => unknown;
|
||||
readonly renderCallingParticipantMenu: (
|
||||
props: SmartCallingParticipantMenuProps
|
||||
) => React.JSX.Element;
|
||||
renderReactionPicker: (
|
||||
props: React.ComponentProps<typeof SmartReactionPicker>
|
||||
) => React.JSX.Element;
|
||||
@@ -220,6 +224,7 @@ export function CallScreen({
|
||||
isCallLinkAdmin,
|
||||
me,
|
||||
openSystemPreferencesAction,
|
||||
renderCallingParticipantMenu,
|
||||
renderReactionPicker,
|
||||
setGroupCallVideoRequest,
|
||||
sendGroupCallRaiseHand,
|
||||
@@ -972,6 +977,7 @@ export function CallScreen({
|
||||
case CallMode.Adhoc:
|
||||
remoteParticipantsElement = (
|
||||
<GroupCallRemoteParticipants
|
||||
callConversationId={conversation.id}
|
||||
callViewMode={activeCall.viewMode}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
imageDataCache={imageDataCache}
|
||||
@@ -980,6 +986,7 @@ export function CallScreen({
|
||||
remoteParticipants={activeCall.remoteParticipants}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
remoteAudioLevels={activeCall.remoteAudioLevels}
|
||||
renderCallingParticipantMenu={renderCallingParticipantMenu}
|
||||
isCallReconnecting={isReconnecting}
|
||||
onClickRaisedHand={
|
||||
raisedHandsCount > 0
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AxoButton } from '../axo/AxoButton.dom.tsx';
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
export default {
|
||||
title: 'CallingParticipantMenu',
|
||||
title: 'Components/CallingParticipantMenu',
|
||||
excludeStories: ['renderCallingParticipantMenu'],
|
||||
} satisfies Meta;
|
||||
|
||||
@@ -23,11 +23,13 @@ const defaultProps: CallingParticipantMenuProps = {
|
||||
i18n,
|
||||
renderer: 'AxoContextMenu',
|
||||
isMuteAudioDisabled: false,
|
||||
onBlockFromCall: action('on-block-from-call'),
|
||||
onMuteAudio: action('on-mute-audio'),
|
||||
onUnmuteAudio: null,
|
||||
onViewProfile: action('on-view-profile'),
|
||||
onGoToChat: action('on-go-to-chat'),
|
||||
onRemoveFromCall: action('on-remove-from-call'),
|
||||
participantTitle: 'Participant Name',
|
||||
children: <div>Menu</div>,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { type ReactNode } from 'react';
|
||||
import React, { useState, type ReactNode } from 'react';
|
||||
import type { LocalizerType } from '../types/I18N.std.ts';
|
||||
import { AxoMenuBuilder } from '../axo/AxoMenuBuilder.dom.tsx';
|
||||
import { AxoTheme } from '../axo/AxoTheme.dom.tsx';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog.dom.tsx';
|
||||
import { strictAssert } from '../util/assert.std.ts';
|
||||
|
||||
export type CallingParticipantMenuProps = Readonly<{
|
||||
align: AxoMenuBuilder.Align;
|
||||
side: AxoMenuBuilder.Side;
|
||||
align?: AxoMenuBuilder.Align;
|
||||
side?: AxoMenuBuilder.Side;
|
||||
i18n: LocalizerType;
|
||||
renderer: AxoMenuBuilder.Renderer;
|
||||
isMuteAudioDisabled: boolean;
|
||||
participantTitle: string | undefined;
|
||||
onBlockFromCall: (() => void) | null;
|
||||
onMuteAudio: (() => void) | null;
|
||||
onUnmuteAudio: (() => void) | null;
|
||||
onViewProfile: (() => void) | null;
|
||||
@@ -26,13 +30,18 @@ export function CallingParticipantMenu({
|
||||
i18n,
|
||||
renderer,
|
||||
isMuteAudioDisabled,
|
||||
onBlockFromCall,
|
||||
onMuteAudio,
|
||||
onUnmuteAudio,
|
||||
onViewProfile,
|
||||
onGoToChat,
|
||||
onRemoveFromCall,
|
||||
participantTitle,
|
||||
children,
|
||||
}: CallingParticipantMenuProps): React.JSX.Element {
|
||||
const [removeFromCallModalVisible, setRemoveFromCallModalVisible] =
|
||||
useState(false);
|
||||
|
||||
return (
|
||||
<AxoTheme.Override theme="force-dark">
|
||||
<AxoMenuBuilder.Root
|
||||
@@ -74,16 +83,57 @@ export function CallingParticipantMenu({
|
||||
{i18n('icu:CallingParticipantMenu__GoToChat')}
|
||||
</AxoMenuBuilder.Item>
|
||||
)}
|
||||
{onRemoveFromCall && (
|
||||
{onBlockFromCall && onRemoveFromCall && (
|
||||
<AxoMenuBuilder.Item
|
||||
symbol="minus-circle"
|
||||
onSelect={onRemoveFromCall}
|
||||
onSelect={() => setRemoveFromCallModalVisible(true)}
|
||||
>
|
||||
{i18n('icu:CallingParticipantMenu__RemoveFromCall')}
|
||||
</AxoMenuBuilder.Item>
|
||||
)}
|
||||
</AxoMenuBuilder.Content>
|
||||
</AxoMenuBuilder.Root>
|
||||
{removeFromCallModalVisible && (
|
||||
<ConfirmationDialog
|
||||
dialogName="CallingAdhocCallInfo.removeClientDialog"
|
||||
moduleClassName="CallingAdhocCallInfo__RemoveClientDialog"
|
||||
actions={[
|
||||
{
|
||||
action: () => {
|
||||
strictAssert(
|
||||
onBlockFromCall,
|
||||
'onBlockFromCall prop is required'
|
||||
);
|
||||
onBlockFromCall();
|
||||
},
|
||||
style: 'negative',
|
||||
text: i18n(
|
||||
'icu:CallingAdhocCallInfo__RemoveClientDialogButton--block'
|
||||
),
|
||||
},
|
||||
{
|
||||
action: () => {
|
||||
strictAssert(
|
||||
onRemoveFromCall,
|
||||
'onRemoveFromCall prop is required'
|
||||
);
|
||||
onRemoveFromCall();
|
||||
},
|
||||
style: 'negative',
|
||||
text: i18n(
|
||||
'icu:CallingAdhocCallInfo__RemoveClientDialogButton--remove'
|
||||
),
|
||||
},
|
||||
]}
|
||||
cancelText={i18n('icu:cancel')}
|
||||
i18n={i18n}
|
||||
onClose={() => setRemoveFromCallModalVisible(false)}
|
||||
>
|
||||
{i18n('icu:CallingAdhocCallInfo__RemoveClientDialogBody', {
|
||||
name: participantTitle ?? '',
|
||||
})}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</AxoTheme.Override>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { FRAME_BUFFER_SIZE } from '../calling/constants.std.ts';
|
||||
import type { CallingImageDataCache } from './CallManager.dom.tsx';
|
||||
import { MINUTE } from '../util/durations/index.std.ts';
|
||||
import { generateAci } from '../test-helpers/serviceIdUtils.std.ts';
|
||||
import { renderCallingParticipantMenu } from './CallingParticipantMenu.dom.stories.tsx';
|
||||
|
||||
const { memoize, times } = lodash;
|
||||
|
||||
@@ -53,6 +54,7 @@ const defaultProps = {
|
||||
onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'),
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
remoteParticipantsCount: 1,
|
||||
renderCallingParticipantMenu,
|
||||
};
|
||||
|
||||
// This component is usually rendered on a call screen.
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { LocalizerType } from '../types/Util.std.ts';
|
||||
import type { GroupCallRemoteParticipantType } from '../types/Calling.std.ts';
|
||||
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant.dom.tsx';
|
||||
import type { CallingImageDataCache } from './CallManager.dom.tsx';
|
||||
import type { PropsType as SmartCallingParticipantMenuProps } from '../state/smart/CallingParticipantMenu.preload.tsx';
|
||||
|
||||
const OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD = 20;
|
||||
const OVERFLOW_SCROLL_BUTTON_RATIO = 0.75;
|
||||
@@ -17,6 +18,7 @@ const OVERFLOW_SCROLL_BUTTON_RATIO = 0.75;
|
||||
export const OVERFLOW_PARTICIPANT_WIDTH = 107;
|
||||
|
||||
export type PropsType = {
|
||||
callConversationId?: string;
|
||||
getFrameBuffer: () => Uint8Array<ArrayBuffer>;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
i18n: LocalizerType;
|
||||
@@ -31,9 +33,13 @@ export type PropsType = {
|
||||
overflowedParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
|
||||
remoteAudioLevels: Map<number, number>;
|
||||
remoteParticipantsCount: number;
|
||||
renderCallingParticipantMenu: (
|
||||
props: SmartCallingParticipantMenuProps
|
||||
) => React.JSX.Element;
|
||||
};
|
||||
|
||||
export function GroupCallOverflowArea({
|
||||
callConversationId,
|
||||
getFrameBuffer,
|
||||
getGroupCallVideoFrameSource,
|
||||
imageDataCache,
|
||||
@@ -45,6 +51,7 @@ export function GroupCallOverflowArea({
|
||||
overflowedParticipants,
|
||||
remoteAudioLevels,
|
||||
remoteParticipantsCount,
|
||||
renderCallingParticipantMenu,
|
||||
}: PropsType): React.JSX.Element | null {
|
||||
const overflowRef = useRef<HTMLDivElement | null>(null);
|
||||
const [overflowScrollTop, setOverflowScrollTop] = useState(0);
|
||||
@@ -123,6 +130,7 @@ export function GroupCallOverflowArea({
|
||||
>
|
||||
{overflowedParticipants.map(remoteParticipant => (
|
||||
<GroupCallRemoteParticipant
|
||||
callConversationId={callConversationId}
|
||||
key={remoteParticipant.demuxId}
|
||||
getFrameBuffer={getFrameBuffer}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
@@ -137,6 +145,7 @@ export function GroupCallOverflowArea({
|
||||
)}
|
||||
remoteParticipant={remoteParticipant}
|
||||
remoteParticipantsCount={remoteParticipantsCount}
|
||||
renderCallingParticipantMenu={renderCallingParticipantMenu}
|
||||
isActiveSpeakerInSpeakerView={false}
|
||||
isCallReconnecting={isCallReconnecting}
|
||||
isInOverflow
|
||||
|
||||
@@ -31,6 +31,10 @@ import { Theme } from '../util/theme.std.ts';
|
||||
import { isOlderThan } from '../util/timestamp.std.ts';
|
||||
import type { CallingImageDataCache } from './CallManager.dom.tsx';
|
||||
import { usePrevious } from '../hooks/usePrevious.std.ts';
|
||||
import type { PropsType as SmartCallingParticipantMenuProps } from '../state/smart/CallingParticipantMenu.preload.tsx';
|
||||
import type { AxoMenuBuilder } from '../axo/AxoMenuBuilder.dom.tsx';
|
||||
import { AxoIconButton } from '../axo/AxoIconButton.dom.tsx';
|
||||
import { tw } from '../axo/tw.dom.tsx';
|
||||
|
||||
const { debounce, noop } = lodash;
|
||||
|
||||
@@ -42,6 +46,7 @@ const DELAY_TO_SHOW_MISSING_MEDIA_KEYS = 5000;
|
||||
const CONTAINER_TRANSITION_TIME = 200;
|
||||
|
||||
type BasePropsType = {
|
||||
callConversationId?: string;
|
||||
getFrameBuffer: () => Uint8Array<ArrayBuffer>;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
i18n: LocalizerType;
|
||||
@@ -54,6 +59,9 @@ type BasePropsType = {
|
||||
onVisibilityChanged?: (demuxId: number, isVisible: boolean) => unknown;
|
||||
remoteParticipant: GroupCallRemoteParticipantType;
|
||||
remoteParticipantsCount: number;
|
||||
renderCallingParticipantMenu?: (
|
||||
props: SmartCallingParticipantMenuProps
|
||||
) => React.JSX.Element;
|
||||
};
|
||||
|
||||
type InPipPropsType = {
|
||||
@@ -78,6 +86,7 @@ export type PropsType = BasePropsType &
|
||||
export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||
function GroupCallRemoteParticipantInner(props) {
|
||||
const {
|
||||
callConversationId,
|
||||
getFrameBuffer,
|
||||
getGroupCallVideoFrameSource,
|
||||
imageDataCache,
|
||||
@@ -85,6 +94,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||
onClickRaisedHand,
|
||||
onVisibilityChanged,
|
||||
remoteParticipantsCount,
|
||||
renderCallingParticipantMenu,
|
||||
isActiveSpeakerInSpeakerView,
|
||||
isCallReconnecting,
|
||||
isInOverflow,
|
||||
@@ -97,6 +107,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||
avatarUrl,
|
||||
color,
|
||||
demuxId,
|
||||
id: participantConversationId,
|
||||
hasAvatar,
|
||||
hasRemoteAudio,
|
||||
hasRemoteVideo,
|
||||
@@ -534,6 +545,31 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||
titleNoDefault,
|
||||
]);
|
||||
|
||||
const maybeWrapWithParticipantMenu = useCallback(
|
||||
(renderer: AxoMenuBuilder.Renderer, children: ReactNode): ReactNode => {
|
||||
if (renderCallingParticipantMenu) {
|
||||
return renderCallingParticipantMenu({
|
||||
callConversationId,
|
||||
participantConversationId,
|
||||
demuxId,
|
||||
hasAudio: hasRemoteAudio,
|
||||
renderer,
|
||||
children,
|
||||
align: isInOverflow ? 'end' : 'start',
|
||||
});
|
||||
}
|
||||
return children;
|
||||
},
|
||||
[
|
||||
callConversationId,
|
||||
demuxId,
|
||||
hasRemoteAudio,
|
||||
isInOverflow,
|
||||
participantConversationId,
|
||||
renderCallingParticipantMenu,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showErrorDialog && (
|
||||
@@ -548,69 +584,97 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||
{errorDialogBody}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'module-ongoing-call__group-call-remote-participant',
|
||||
isSpeaking &&
|
||||
!isActiveSpeakerInSpeakerView &&
|
||||
remoteParticipantsCount > 1 &&
|
||||
'module-ongoing-call__group-call-remote-participant--speaking',
|
||||
isHandRaised &&
|
||||
'module-ongoing-call__group-call-remote-participant--hand-raised',
|
||||
isOnTop &&
|
||||
'module-ongoing-call__group-call-remote-participant--is-on-top'
|
||||
)}
|
||||
ref={intersectionRef}
|
||||
style={containerStyles}
|
||||
>
|
||||
{!props.isInPip && (
|
||||
<>
|
||||
<CallingAudioIndicator
|
||||
hasAudio={hasRemoteAudio}
|
||||
audioLevel={props.audioLevel}
|
||||
shouldShowSpeaking={isSpeaking}
|
||||
{maybeWrapWithParticipantMenu(
|
||||
'AxoContextMenu',
|
||||
<div
|
||||
className={classNames(
|
||||
'module-ongoing-call__group-call-remote-participant',
|
||||
tw('group'),
|
||||
isSpeaking &&
|
||||
!isActiveSpeakerInSpeakerView &&
|
||||
remoteParticipantsCount > 1 &&
|
||||
'module-ongoing-call__group-call-remote-participant--speaking',
|
||||
isHandRaised &&
|
||||
'module-ongoing-call__group-call-remote-participant--hand-raised',
|
||||
isOnTop &&
|
||||
'module-ongoing-call__group-call-remote-participant--is-on-top'
|
||||
)}
|
||||
ref={intersectionRef}
|
||||
style={containerStyles}
|
||||
>
|
||||
{!props.isInPip && (
|
||||
<>
|
||||
{renderCallingParticipantMenu && (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-ongoing-call__group-call-remote-participant__dropdown',
|
||||
tw(
|
||||
'absolute inset-s-2 top-2 legacy-z-index-base',
|
||||
'opacity-0 group-hover:opacity-100 group-data-focused:opacity-100',
|
||||
'has-data-[axo-dropdownmenu-state="open"]:opacity-100'
|
||||
)
|
||||
)}
|
||||
>
|
||||
{maybeWrapWithParticipantMenu(
|
||||
'AxoDropdownMenu',
|
||||
<AxoIconButton.Root
|
||||
variant="floating-secondary"
|
||||
size="sm"
|
||||
symbol="chevron-down"
|
||||
label={i18n(
|
||||
'icu:CallingParticipantListItem__ContextMenuButton'
|
||||
)}
|
||||
tooltip={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<CallingAudioIndicator
|
||||
hasAudio={hasRemoteAudio}
|
||||
audioLevel={props.audioLevel}
|
||||
shouldShowSpeaking={isSpeaking}
|
||||
/>
|
||||
<div className="module-ongoing-call__group-call-remote-participant__footer">
|
||||
{footerInfoElement}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{wantsToShowVideo && (
|
||||
<canvas
|
||||
className={classNames(
|
||||
'module-ongoing-call__group-call-remote-participant__remote-video',
|
||||
isCallReconnecting &&
|
||||
'module-ongoing-call__group-call-remote-participant__remote-video--reconnecting'
|
||||
)}
|
||||
style={{
|
||||
...canvasStyles,
|
||||
// If we want to show video but don't have any yet, we still render the
|
||||
// canvas invisibly. This lets us render frame data immediately without
|
||||
// having to juggle anything.
|
||||
...(hasVideoToShow ? {} : { display: 'none' }),
|
||||
}}
|
||||
ref={canvasEl => {
|
||||
remoteVideoRef.current = canvasEl;
|
||||
if (canvasEl) {
|
||||
canvasContextRef.current = canvasEl.getContext('2d', {
|
||||
alpha: false,
|
||||
});
|
||||
} else {
|
||||
canvasContextRef.current = null;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="module-ongoing-call__group-call-remote-participant__footer">
|
||||
{footerInfoElement}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{wantsToShowVideo && (
|
||||
<canvas
|
||||
className={classNames(
|
||||
'module-ongoing-call__group-call-remote-participant__remote-video',
|
||||
isCallReconnecting &&
|
||||
'module-ongoing-call__group-call-remote-participant__remote-video--reconnecting'
|
||||
)}
|
||||
style={{
|
||||
...canvasStyles,
|
||||
// If we want to show video but don't have any yet, we still render the
|
||||
// canvas invisibly. This lets us render frame data immediately without
|
||||
// having to juggle anything.
|
||||
...(hasVideoToShow ? {} : { display: 'none' }),
|
||||
}}
|
||||
ref={canvasEl => {
|
||||
remoteVideoRef.current = canvasEl;
|
||||
if (canvasEl) {
|
||||
canvasContextRef.current = canvasEl.getContext('2d', {
|
||||
alpha: false,
|
||||
});
|
||||
} else {
|
||||
canvasContextRef.current = null;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{noVideoNode && (
|
||||
<CallBackgroundBlur
|
||||
avatarUrl={isBlocked ? undefined : avatarUrl}
|
||||
className="module-ongoing-call__group-call-remote-participant-background"
|
||||
>
|
||||
{noVideoNode}
|
||||
</CallBackgroundBlur>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{noVideoNode && (
|
||||
<CallBackgroundBlur
|
||||
avatarUrl={isBlocked ? undefined : avatarUrl}
|
||||
className="module-ongoing-call__group-call-remote-participant-background"
|
||||
>
|
||||
{noVideoNode}
|
||||
</CallBackgroundBlur>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants.std.ts';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver.dom.tsx';
|
||||
import { strictAssert } from '../util/assert.std.ts';
|
||||
import type { CallingImageDataCache } from './CallManager.dom.tsx';
|
||||
import type { PropsType as SmartCallingParticipantMenuProps } from '../state/smart/CallingParticipantMenu.preload.tsx';
|
||||
|
||||
const { clamp, chunk, maxBy, flatten, noop } = lodash;
|
||||
|
||||
@@ -62,6 +63,7 @@ type ParticipantTileType =
|
||||
| PaginationButtonType;
|
||||
|
||||
type PropsType = {
|
||||
callConversationId?: string;
|
||||
callViewMode: CallViewMode;
|
||||
getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
|
||||
i18n: LocalizerType;
|
||||
@@ -74,6 +76,9 @@ type PropsType = {
|
||||
speakerHeight: number
|
||||
) => void;
|
||||
remoteAudioLevels: Map<number, number>;
|
||||
renderCallingParticipantMenu: (
|
||||
props: SmartCallingParticipantMenuProps
|
||||
) => React.JSX.Element;
|
||||
onClickRaisedHand?: () => void;
|
||||
};
|
||||
|
||||
@@ -115,6 +120,7 @@ enum VideoRequestMode {
|
||||
// 5. Lay out this arrangement on the screen.
|
||||
|
||||
export function GroupCallRemoteParticipants({
|
||||
callConversationId,
|
||||
callViewMode,
|
||||
getGroupCallVideoFrameSource,
|
||||
imageDataCache,
|
||||
@@ -124,6 +130,7 @@ export function GroupCallRemoteParticipants({
|
||||
remoteParticipants,
|
||||
setGroupCallVideoRequest,
|
||||
remoteAudioLevels,
|
||||
renderCallingParticipantMenu,
|
||||
onClickRaisedHand,
|
||||
}: PropsType): React.JSX.Element {
|
||||
const [gridDimensions, setGridDimensions] = useState<Dimensions>({
|
||||
@@ -357,6 +364,7 @@ export function GroupCallRemoteParticipants({
|
||||
|
||||
return (
|
||||
<GroupCallRemoteParticipant
|
||||
callConversationId={callConversationId}
|
||||
key={tile.demuxId}
|
||||
getFrameBuffer={getFrameBuffer}
|
||||
imageDataCache={imageDataCache}
|
||||
@@ -370,6 +378,7 @@ export function GroupCallRemoteParticipants({
|
||||
top={top}
|
||||
width={renderedWidth}
|
||||
remoteParticipantsCount={remoteParticipants.length}
|
||||
renderCallingParticipantMenu={renderCallingParticipantMenu}
|
||||
isActiveSpeakerInSpeakerView={isInSpeakerView}
|
||||
isCallReconnecting={isCallReconnecting}
|
||||
joinedAt={joinedAt}
|
||||
@@ -531,6 +540,7 @@ export function GroupCallRemoteParticipants({
|
||||
|
||||
{shouldShowOverflow && overflowedParticipants.length > 0 ? (
|
||||
<GroupCallOverflowArea
|
||||
callConversationId={callConversationId}
|
||||
getFrameBuffer={getFrameBuffer}
|
||||
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
|
||||
imageDataCache={imageDataCache}
|
||||
@@ -542,6 +552,7 @@ export function GroupCallRemoteParticipants({
|
||||
overflowedParticipants={overflowedParticipants}
|
||||
remoteAudioLevels={remoteAudioLevels}
|
||||
remoteParticipantsCount={remoteParticipants.length}
|
||||
renderCallingParticipantMenu={renderCallingParticipantMenu}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -27,8 +27,8 @@ export type PropsType = {
|
||||
participantConversationId?: string;
|
||||
demuxId?: number;
|
||||
hasAudio: boolean;
|
||||
align: AxoMenuBuilder.Align;
|
||||
side: AxoMenuBuilder.Side;
|
||||
align?: AxoMenuBuilder.Align;
|
||||
side?: AxoMenuBuilder.Side;
|
||||
renderer: AxoMenuBuilder.Renderer;
|
||||
children: ReactNode;
|
||||
};
|
||||
@@ -74,8 +74,10 @@ export const SmartCallingParticipantMenu = memo(
|
||||
|
||||
const { showContactModal } = useGlobalModalActions();
|
||||
const { showConversation } = useConversationsActions();
|
||||
const { setLocalAudio, removeClient, sendRemoteMute } = useCallingActions();
|
||||
const { blockClient, setLocalAudio, removeClient, sendRemoteMute } =
|
||||
useCallingActions();
|
||||
|
||||
let participantTitle: string | undefined;
|
||||
let onMuteAudio: (() => void) | null;
|
||||
let onUnmuteAudio: (() => void) | null;
|
||||
let onViewProfile: (() => void) | null;
|
||||
@@ -113,12 +115,14 @@ export const SmartCallingParticipantMenu = memo(
|
||||
contactId: participantConversation.id,
|
||||
conversationId: callConversationId,
|
||||
});
|
||||
participantTitle = participantConversation.title;
|
||||
} else {
|
||||
onGoToChat = null;
|
||||
onViewProfile = null;
|
||||
}
|
||||
}
|
||||
|
||||
let onBlockFromCall: (() => void) | null = null;
|
||||
let onRemoveFromCall: (() => void) | null = null;
|
||||
if (activeCallState?.callMode === CallMode.Adhoc) {
|
||||
const callLink = callLinkSelector(activeCallState.conversationId);
|
||||
@@ -128,6 +132,7 @@ export const SmartCallingParticipantMenu = memo(
|
||||
demuxId !== undefined &&
|
||||
demuxId !== localDemuxId
|
||||
) {
|
||||
onBlockFromCall = () => blockClient({ demuxId });
|
||||
onRemoveFromCall = () => removeClient({ demuxId });
|
||||
}
|
||||
}
|
||||
@@ -139,6 +144,8 @@ export const SmartCallingParticipantMenu = memo(
|
||||
renderer={renderer}
|
||||
i18n={i18n}
|
||||
isMuteAudioDisabled={!hasAudio}
|
||||
participantTitle={participantTitle}
|
||||
onBlockFromCall={onBlockFromCall}
|
||||
onMuteAudio={onMuteAudio}
|
||||
onUnmuteAudio={onUnmuteAudio}
|
||||
onGoToChat={onGoToChat}
|
||||
|
||||
Reference in New Issue
Block a user