Calling missing media keys indicator

This commit is contained in:
ayumi-signal 2024-01-23 11:08:21 -08:00 committed by GitHub
parent 436ee1a18f
commit d97aa68716
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 280 additions and 84 deletions

View file

@ -326,6 +326,7 @@ export function GroupCall1(): JSX.Element {
hasRemoteAudio: true,
hasRemoteVideo: true,
isHandRaised: false,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
@ -353,6 +354,7 @@ export function GroupCallYourHandRaised(): JSX.Element {
hasRemoteAudio: true,
hasRemoteVideo: true,
isHandRaised: false,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
@ -372,26 +374,32 @@ export function GroupCallYourHandRaised(): JSX.Element {
const PARTICIPANT_EMOJIS = ['❤️', '🤔', '✨', '😂', '🦄'] as const;
// We generate these upfront so that the list is stable when you move the slider.
const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
aci: generateAci(),
demuxId: index,
hasRemoteAudio: index % 3 !== 0,
hasRemoteVideo: index % 4 !== 0,
isHandRaised: (index - 3) % 10 === 0,
presenting: false,
sharingScreen: false,
videoAspectRatio: Math.random() < 0.7 ? 1.3 : Math.random() * 0.4 + 0.6,
...getDefaultConversationWithServiceId({
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
title: `Participant ${
(index - 2) % 4 === 0
? PARTICIPANT_EMOJIS[
Math.floor((index - 2) / 4) % PARTICIPANT_EMOJIS.length
]
: ''
} ${index + 1}`,
}),
}));
const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => {
const mediaKeysReceived = (index + 1) % 20 !== 0;
return {
aci: generateAci(),
addedTime: Date.now() - 60000,
demuxId: index,
hasRemoteAudio: mediaKeysReceived ? index % 3 !== 0 : false,
hasRemoteVideo: mediaKeysReceived ? index % 4 !== 0 : false,
isHandRaised: (index - 3) % 10 === 0,
mediaKeysReceived,
presenting: false,
sharingScreen: false,
videoAspectRatio: Math.random() < 0.7 ? 1.3 : Math.random() * 0.4 + 0.6,
...getDefaultConversationWithServiceId({
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
title: `Participant ${
(index - 2) % 4 === 0
? PARTICIPANT_EMOJIS[
Math.floor((index - 2) / 4) % PARTICIPANT_EMOJIS.length
]
: ''
} ${index + 1}`,
}),
};
});
export function GroupCallManyPaginated(): JSX.Element {
const props = createProps({
@ -471,6 +479,7 @@ export function GroupCallReconnecting(): JSX.Element {
hasRemoteAudio: true,
hasRemoteVideo: true,
isHandRaised: false,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
@ -793,3 +802,38 @@ function useHandRaiser(
}, [frequency, call, max, min]);
return call;
}
export function GroupCallSomeoneMissingMediaKeys(): JSX.Element {
return (
<CallScreen
{...createProps({
callMode: CallMode.Group,
remoteParticipants: allRemoteParticipants
.slice(0, 5)
.map((participant, index) => ({
...participant,
addedTime: index === 1 ? Date.now() - 60000 : undefined,
hasRemoteAudio: false,
hasRemoteVideo: false,
mediaKeysReceived: index !== 1,
})),
})}
/>
);
}
export function GroupCallSomeoneBlocked(): JSX.Element {
return (
<CallScreen
{...createProps({
callMode: CallMode.Group,
remoteParticipants: allRemoteParticipants
.slice(0, 5)
.map((participant, index) => ({
...participant,
isBlocked: index === 1,
})),
})}
/>
);
}

View file

@ -26,6 +26,7 @@ function createParticipant(
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
isHandRaised: Boolean(participantProps.isHandRaised),
mediaKeysReceived: Boolean(participantProps.mediaKeysReceived),
presenting: Boolean(participantProps.presenting),
sharingScreen: Boolean(participantProps.sharingScreen),
videoAspectRatio: 1.3,

View file

@ -24,6 +24,7 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
hasRemoteAudio: index % 3 !== 0,
hasRemoteVideo: index % 4 !== 0,
isHandRaised: (index - 2) % 8 === 0,
mediaKeysReceived: (index + 1) % 20 !== 0,
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,

View file

@ -35,13 +35,17 @@ const getFrameBuffer = memoize(() => Buffer.alloc(FRAME_BUFFER_SIZE));
const createProps = (
overrideProps: OverridePropsType,
{
addedTime,
isBlocked = false,
hasRemoteAudio = false,
presenting = false,
isHandRaised = false,
hasRemoteAudio = false,
mediaKeysReceived = true,
presenting = false,
}: {
addedTime?: number;
isBlocked?: boolean;
hasRemoteAudio?: boolean;
mediaKeysReceived?: boolean;
presenting?: boolean;
isHandRaised?: boolean;
} = {}
@ -54,10 +58,12 @@ const createProps = (
audioLevel: 0,
remoteParticipant: {
aci: generateAci(),
addedTime,
demuxId: 123,
hasRemoteAudio,
hasRemoteVideo: true,
isHandRaised,
mediaKeysReceived,
presenting,
sharingScreen: false,
videoAspectRatio: 1.3,
@ -165,3 +171,24 @@ export function Blocked(): JSX.Element {
/>
);
}
export function NoMediaKeys(): JSX.Element {
return (
<GroupCallRemoteParticipant
{...createProps(
{
isInPip: false,
height: 120,
left: 0,
top: 0,
width: 120,
},
{
addedTime: Date.now() - 60 * 1000,
hasRemoteAudio: true,
mediaKeysReceived: false,
}
)}
/>
);
}

View file

@ -27,9 +27,12 @@ import { ContactName } from './conversation/ContactName';
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants';
import { useValueAtFixedRate } from '../hooks/useValueAtFixedRate';
import { Theme } from '../util/theme';
import { isOlderThan } from '../util/timestamp';
const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 10000;
const MAX_TIME_TO_SHOW_STALE_SCREENSHARE_FRAMES = 60000;
const DELAY_TO_SHOW_MISSING_MEDIA_KEYS = 5000;
type BasePropsType = {
getFrameBuffer: () => Buffer;
@ -77,6 +80,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
const {
acceptedMessageRequest,
addedTime,
avatarPath,
color,
demuxId,
@ -85,6 +89,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
isHandRaised,
isBlocked,
isMe,
mediaKeysReceived,
profileName,
sharedGroupNames,
sharingScreen,
@ -102,7 +107,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
const [isWide, setIsWide] = useState<boolean>(
videoAspectRatio ? videoAspectRatio >= 1 : true
);
const [showBlockInfo, setShowBlockInfo] = useState(false);
const [showErrorDialog, setShowErrorDialog] = useState(false);
// We have some state (`hasReceivedVideoRecently`) and this ref. We can't have a
// single state value like `lastReceivedVideoAt` because (1) it won't automatically
@ -129,6 +134,11 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
const wantsToShowVideo = hasRemoteVideo && !isBlocked && isVisible;
const hasVideoToShow = wantsToShowVideo && hasReceivedVideoRecently;
const showMissingMediaKeys = Boolean(
!mediaKeysReceived &&
addedTime &&
isOlderThan(addedTime, DELAY_TO_SHOW_MISSING_MEDIA_KEYS)
);
const videoFrameSource = useMemo(
() => getGroupCallVideoFrameSource(demuxId),
@ -293,29 +303,94 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
}
}
let noVideoNode: ReactNode;
let errorDialogTitle: ReactNode;
let errorDialogBody = '';
if (!hasVideoToShow) {
const showDialogButton = (
<button
type="button"
className="module-ongoing-call__group-call-remote-participant__more-info"
onClick={() => {
setShowErrorDialog(true);
}}
>
{i18n('icu:moreInfo')}
</button>
);
if (isBlocked) {
noVideoNode = (
<>
<i className="module-ongoing-call__group-call-remote-participant__error-icon module-ongoing-call__group-call-remote-participant__error-icon--blocked" />
{showDialogButton}
</>
);
errorDialogTitle = (
<div className="module-ongoing-call__group-call-remote-participant__more-info-modal-title">
<Intl
i18n={i18n}
id="icu:calling__you-have-blocked"
components={{
name: <ContactName key="name" title={title} />,
}}
/>
</div>
);
errorDialogBody = i18n('icu:calling__block-info');
} else if (showMissingMediaKeys) {
noVideoNode = (
<>
<i className="module-ongoing-call__group-call-remote-participant__error-icon module-ongoing-call__group-call-remote-participant__error-icon--missing-media-keys" />
<div className="module-ongoing-call__group-call-remote-participant__error">
{i18n('icu:calling__missing-media-keys', { name: title })}
</div>
{showDialogButton}
</>
);
errorDialogTitle = (
<div className="module-ongoing-call__group-call-remote-participant__more-info-modal-title">
<Intl
i18n={i18n}
id="icu:calling__missing-media-keys"
components={{
name: <ContactName key="name" title={title} />,
}}
/>
</div>
);
errorDialogBody = i18n('icu:calling__missing-media-keys-info');
} else {
noVideoNode = (
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
badge={undefined}
color={color || AvatarColors[0]}
noteToSelf={false}
conversationType="direct"
i18n={i18n}
isMe={isMe}
profileName={profileName}
title={title}
sharedGroupNames={sharedGroupNames}
size={avatarSize}
/>
);
}
}
return (
<>
{showBlockInfo && (
{showErrorDialog && (
<ConfirmationDialog
dialogName="GroupCallRemoteParticipant.blockInfo"
cancelText={i18n('icu:ok')}
i18n={i18n}
onClose={() => {
setShowBlockInfo(false);
}}
title={
<div className="module-ongoing-call__group-call-remote-participant__blocked--modal-title">
<Intl
i18n={i18n}
id="icu:calling__you-have-blocked"
components={{
name: <ContactName key="name" title={title} />,
}}
/>
</div>
}
onClose={() => setShowErrorDialog(false)}
theme={Theme.Dark}
title={errorDialogTitle}
>
{i18n('icu:calling__block-info')}
{errorDialogBody}
</ConfirmationDialog>
)}
@ -372,40 +447,12 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
}}
/>
)}
{!hasVideoToShow && (
{noVideoNode && (
<CallBackgroundBlur
avatarPath={avatarPath}
className="module-ongoing-call__group-call-remote-participant-background"
>
{isBlocked ? (
<>
<i className="module-ongoing-call__group-call-remote-participant__blocked" />
<button
type="button"
className="module-ongoing-call__group-call-remote-participant__blocked--info"
onClick={() => {
setShowBlockInfo(true);
}}
>
{i18n('icu:moreInfo')}
</button>
</>
) : (
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
badge={undefined}
color={color || AvatarColors[0]}
noteToSelf={false}
conversationType="direct"
i18n={i18n}
isMe={isMe}
profileName={profileName}
title={title}
sharedGroupNames={sharedGroupNames}
size={avatarSize}
/>
)}
{noVideoNode}
</CallBackgroundBlur>
)}
</div>

View file

@ -1086,11 +1086,14 @@ export class CallingClass {
aci = '00000000-0000-4000-8000-000000000000';
}
assertDev(isAciString(aci), 'remote participant aci must be a aci');
return {
aci,
addedTime: normalizeGroupCallTimestamp(remoteDeviceState.addedTime),
demuxId: remoteDeviceState.demuxId,
hasRemoteAudio: !remoteDeviceState.audioMuted,
hasRemoteVideo: !remoteDeviceState.videoMuted,
mediaKeysReceived: remoteDeviceState.mediaKeysReceived,
presenting: Boolean(remoteDeviceState.presenting),
sharingScreen: Boolean(remoteDeviceState.sharingScreen),
speakerTime: normalizeGroupCallTimestamp(

View file

@ -76,9 +76,11 @@ export type GroupCallPeekInfoType = ReadonlyDeep<{
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type GroupCallParticipantInfoType = {
aci: AciString;
addedTime?: number;
demuxId: number;
hasRemoteAudio: boolean;
hasRemoteVideo: boolean;
mediaKeysReceived: boolean;
presenting: boolean;
sharingScreen: boolean;
speakerTime?: number;

View file

@ -246,10 +246,12 @@ const mapStateToActiveCallProp = (
remoteParticipants.push({
...remoteConversation,
aci: remoteParticipant.aci,
addedTime: remoteParticipant.addedTime,
demuxId: remoteParticipant.demuxId,
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
isHandRaised: raisedHands.has(remoteParticipant.demuxId),
mediaKeysReceived: remoteParticipant.mediaKeysReceived,
presenting: remoteParticipant.presenting,
sharingScreen: remoteParticipant.sharingScreen,
speakerTime: remoteParticipant.speakerTime,

View file

@ -118,6 +118,7 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
@ -906,6 +907,7 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
@ -935,6 +937,7 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
@ -966,6 +969,7 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
@ -993,6 +997,7 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
@ -1037,6 +1042,7 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
@ -1089,6 +1095,7 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
@ -1128,6 +1135,7 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
@ -1160,6 +1168,7 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
@ -1204,6 +1213,7 @@ describe('calling duck', () => {
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 16 / 9,
@ -1882,6 +1892,7 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
@ -1908,6 +1919,7 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
@ -1954,6 +1966,7 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
@ -2004,6 +2017,7 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,
@ -2048,6 +2062,7 @@ describe('calling duck', () => {
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
mediaKeysReceived: true,
presenting: false,
sharingScreen: false,
videoAspectRatio: 4 / 3,

View file

@ -156,10 +156,12 @@ export enum GroupCallJoinState {
export type GroupCallRemoteParticipantType = ConversationType & {
aci: AciString;
addedTime?: number;
demuxId: number;
hasRemoteAudio: boolean;
hasRemoteVideo: boolean;
isHandRaised: boolean;
mediaKeysReceived: boolean;
presenting: boolean;
sharingScreen: boolean;
speakerTime?: number;