Consider own join time for group call missing media key check

Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
ayumi-signal 2024-11-01 14:12:49 -07:00 committed by GitHub
parent f9b2261783
commit ec9041937f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 82 additions and 15 deletions

View file

@ -34,6 +34,7 @@ import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGr
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { CallingToastProvider, useCallingToasts } from './CallingToast'; import { CallingToastProvider, useCallingToasts } from './CallingToast';
import type { CallingImageDataCache } from './CallManager'; import type { CallingImageDataCache } from './CallManager';
import { MINUTE } from '../util/durations';
const MAX_PARTICIPANTS = 75; const MAX_PARTICIPANTS = 75;
const LOCAL_DEMUX_ID = 1; const LOCAL_DEMUX_ID = 1;
@ -158,7 +159,7 @@ const createActiveCallProp = (
overrideProps: DirectCallOverrideProps | GroupCallOverrideProps overrideProps: DirectCallOverrideProps | GroupCallOverrideProps
) => { ) => {
const baseResult = { const baseResult = {
joinedAt: Date.now(), joinedAt: Date.now() - MINUTE,
conversation, conversation,
hasLocalAudio: overrideProps.hasLocalAudio ?? false, hasLocalAudio: overrideProps.hasLocalAudio ?? false,
hasLocalVideo: overrideProps.hasLocalVideo ?? false, hasLocalVideo: overrideProps.hasLocalVideo ?? false,

View file

@ -660,8 +660,7 @@ export function CallScreen({
/> />
); );
} }
// joinedAt is only available for direct calls if (isConnected && activeCall.callMode === CallMode.Direct) {
if (isConnected) {
return <CallDuration joinedAt={activeCall.joinedAt} />; return <CallDuration joinedAt={activeCall.joinedAt} />;
} }
if (hasLocalVideo) { if (hasLocalVideo) {
@ -713,6 +712,7 @@ export function CallScreen({
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
imageDataCache={imageDataCache} imageDataCache={imageDataCache}
i18n={i18n} i18n={i18n}
joinedAt={activeCall.joinedAt}
remoteParticipants={activeCall.remoteParticipants} remoteParticipants={activeCall.remoteParticipants}
setGroupCallVideoRequest={setGroupCallVideoRequest} setGroupCallVideoRequest={setGroupCallVideoRequest}
remoteAudioLevels={activeCall.remoteAudioLevels} remoteAudioLevels={activeCall.remoteAudioLevels}

View file

@ -21,6 +21,7 @@ import { getDefaultConversation } from '../test-both/helpers/getDefaultConversat
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource'; import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
import { setupI18n } from '../util/setupI18n'; import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { MINUTE } from '../util/durations';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -47,7 +48,7 @@ const getCommonActiveCallData = (overrides: Overrides) => ({
hasLocalVideo: overrides.hasLocalVideo ?? false, hasLocalVideo: overrides.hasLocalVideo ?? false,
localAudioLevel: overrides.localAudioLevel ?? 0, localAudioLevel: overrides.localAudioLevel ?? 0,
viewMode: overrides.viewMode ?? CallViewMode.Paginated, viewMode: overrides.viewMode ?? CallViewMode.Paginated,
joinedAt: Date.now(), joinedAt: Date.now() - MINUTE,
outgoingRing: true, outgoingRing: true,
pip: true, pip: true,
settingsDialogOpen: false, settingsDialogOpen: false,

View file

@ -188,6 +188,7 @@ export function CallingPipRemoteVideo({
imageDataCache={imageDataCache} imageDataCache={imageDataCache}
i18n={i18n} i18n={i18n}
isInPip isInPip
joinedAt={activeCall.joinedAt}
remoteParticipant={activeGroupCallSpeaker} remoteParticipant={activeGroupCallSpeaker}
remoteParticipantsCount={activeCall.remoteParticipants.length} remoteParticipantsCount={activeCall.remoteParticipants.length}
isActiveSpeakerInSpeakerView={false} isActiveSpeakerInSpeakerView={false}

View file

@ -14,6 +14,7 @@ import { FRAME_BUFFER_SIZE } from '../calling/constants';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { generateAci } from '../types/ServiceId'; import { generateAci } from '../types/ServiceId';
import type { CallingImageDataCache } from './CallManager'; import type { CallingImageDataCache } from './CallManager';
import { MINUTE } from '../util/durations';
const MAX_PARTICIPANTS = 32; const MAX_PARTICIPANTS = 32;
@ -48,6 +49,7 @@ const defaultProps = {
imageDataCache: React.createRef<CallingImageDataCache>(), imageDataCache: React.createRef<CallingImageDataCache>(),
i18n, i18n,
isCallReconnecting: false, isCallReconnecting: false,
joinedAt: new Date().getTime() - MINUTE,
onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'), onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'),
remoteAudioLevels: new Map<number, number>(), remoteAudioLevels: new Map<number, number>(),
remoteParticipantsCount: 1, remoteParticipantsCount: 1,

View file

@ -22,6 +22,7 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
imageDataCache: React.RefObject<CallingImageDataCache>; imageDataCache: React.RefObject<CallingImageDataCache>;
isCallReconnecting: boolean; isCallReconnecting: boolean;
joinedAt: number | null;
onClickRaisedHand?: () => void; onClickRaisedHand?: () => void;
onParticipantVisibilityChanged: ( onParticipantVisibilityChanged: (
demuxId: number, demuxId: number,
@ -38,6 +39,7 @@ export function GroupCallOverflowArea({
imageDataCache, imageDataCache,
i18n, i18n,
isCallReconnecting, isCallReconnecting,
joinedAt,
onClickRaisedHand, onClickRaisedHand,
onParticipantVisibilityChanged, onParticipantVisibilityChanged,
overflowedParticipants, overflowedParticipants,
@ -138,6 +140,7 @@ export function GroupCallOverflowArea({
isActiveSpeakerInSpeakerView={false} isActiveSpeakerInSpeakerView={false}
isCallReconnecting={isCallReconnecting} isCallReconnecting={isCallReconnecting}
isInOverflow isInOverflow
joinedAt={joinedAt}
/> />
))} ))}
</div> </div>

View file

@ -12,6 +12,7 @@ import { setupI18n } from '../util/setupI18n';
import { generateAci } from '../types/ServiceId'; import { generateAci } from '../types/ServiceId';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import type { CallingImageDataCache } from './CallManager'; import type { CallingImageDataCache } from './CallManager';
import { MINUTE } from '../util/durations';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -79,6 +80,7 @@ const createProps = (
remoteParticipantsCount: 1, remoteParticipantsCount: 1,
isActiveSpeakerInSpeakerView: false, isActiveSpeakerInSpeakerView: false,
isCallReconnecting: false, isCallReconnecting: false,
joinedAt: new Date().getTime() - MINUTE,
...overrideProps, ...overrideProps,
}); });
@ -186,7 +188,7 @@ export function NoMediaKeys(): JSX.Element {
width: 120, width: 120,
}, },
{ {
addedTime: Date.now() - 60 * 1000, addedTime: Date.now() - MINUTE,
hasRemoteAudio: true, hasRemoteAudio: true,
mediaKeysReceived: false, mediaKeysReceived: false,
} }

View file

@ -44,6 +44,7 @@ type BasePropsType = {
isActiveSpeakerInSpeakerView: boolean; isActiveSpeakerInSpeakerView: boolean;
isCallReconnecting: boolean; isCallReconnecting: boolean;
isInOverflow?: boolean; isInOverflow?: boolean;
joinedAt: number | null;
onClickRaisedHand?: () => void; onClickRaisedHand?: () => void;
onVisibilityChanged?: (demuxId: number, isVisible: boolean) => unknown; onVisibilityChanged?: (demuxId: number, isVisible: boolean) => unknown;
remoteParticipant: GroupCallRemoteParticipantType; remoteParticipant: GroupCallRemoteParticipantType;
@ -82,6 +83,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
isActiveSpeakerInSpeakerView, isActiveSpeakerInSpeakerView,
isCallReconnecting, isCallReconnecting,
isInOverflow, isInOverflow,
joinedAt,
} = props; } = props;
const { const {
@ -150,10 +152,17 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
const wantsToShowVideo = hasRemoteVideo && !isBlocked && isVisible; const wantsToShowVideo = hasRemoteVideo && !isBlocked && isVisible;
const hasVideoToShow = wantsToShowVideo && hasReceivedVideoRecently; const hasVideoToShow = wantsToShowVideo && hasReceivedVideoRecently;
// Use the later of participant join time (addedTime) vs your join time (joinedAt)
const timeForMissingMediaKeysCheck =
addedTime && joinedAt && addedTime > joinedAt ? addedTime : joinedAt;
const showMissingMediaKeys = Boolean( const showMissingMediaKeys = Boolean(
!mediaKeysReceived && !mediaKeysReceived &&
addedTime && timeForMissingMediaKeysCheck &&
isOlderThan(addedTime, DELAY_TO_SHOW_MISSING_MEDIA_KEYS) isOlderThan(
timeForMissingMediaKeysCheck,
DELAY_TO_SHOW_MISSING_MEDIA_KEYS
)
); );
const videoFrameSource = useMemo( const videoFrameSource = useMemo(

View file

@ -63,6 +63,7 @@ type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
imageDataCache: React.RefObject<CallingImageDataCache>; imageDataCache: React.RefObject<CallingImageDataCache>;
isCallReconnecting: boolean; isCallReconnecting: boolean;
joinedAt: number | null;
remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>; remoteParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
setGroupCallVideoRequest: ( setGroupCallVideoRequest: (
_: Array<GroupCallVideoRequest>, _: Array<GroupCallVideoRequest>,
@ -115,6 +116,7 @@ export function GroupCallRemoteParticipants({
imageDataCache, imageDataCache,
i18n, i18n,
isCallReconnecting, isCallReconnecting,
joinedAt,
remoteParticipants, remoteParticipants,
setGroupCallVideoRequest, setGroupCallVideoRequest,
remoteAudioLevels, remoteAudioLevels,
@ -359,6 +361,7 @@ export function GroupCallRemoteParticipants({
remoteParticipantsCount={remoteParticipants.length} remoteParticipantsCount={remoteParticipants.length}
isActiveSpeakerInSpeakerView={isInSpeakerView} isActiveSpeakerInSpeakerView={isInSpeakerView}
isCallReconnecting={isCallReconnecting} isCallReconnecting={isCallReconnecting}
joinedAt={joinedAt}
/> />
); );
}); });
@ -517,6 +520,7 @@ export function GroupCallRemoteParticipants({
imageDataCache={imageDataCache} imageDataCache={imageDataCache}
i18n={i18n} i18n={i18n}
isCallReconnecting={isCallReconnecting} isCallReconnecting={isCallReconnecting}
joinedAt={joinedAt}
onClickRaisedHand={onClickRaisedHand} onClickRaisedHand={onClickRaisedHand}
onParticipantVisibilityChanged={onParticipantVisibilityChanged} onParticipantVisibilityChanged={onParticipantVisibilityChanged}
overflowedParticipants={overflowedParticipants} overflowedParticipants={overflowedParticipants}

View file

@ -3408,14 +3408,22 @@ export function reducer(
state.activeCallState?.state === 'Active' && state.activeCallState?.state === 'Active' &&
state.activeCallState?.conversationId === conversationId state.activeCallState?.conversationId === conversationId
) { ) {
newActiveCallState = if (connectionState === GroupCallConnectionState.NotConnected) {
connectionState === GroupCallConnectionState.NotConnected newActiveCallState = undefined;
? undefined } else {
: { const joinedAt =
state.activeCallState.joinedAt ??
(connectionState === GroupCallConnectionState.Connected
? new Date().getTime()
: null);
newActiveCallState = {
...state.activeCallState, ...state.activeCallState,
hasLocalAudio, hasLocalAudio,
hasLocalVideo, hasLocalVideo,
joinedAt,
}; };
}
// The first time we detect call participants in the lobby, check participant count // The first time we detect call participants in the lobby, check participant count
// and mute ourselves if over the threshold. // and mute ourselves if over the threshold.

View file

@ -1265,6 +1265,42 @@ describe('calling duck', () => {
); );
assert.isTrue(result.activeCallState?.hasLocalAudio); assert.isTrue(result.activeCallState?.hasLocalAudio);
assert.isTrue(result.activeCallState?.hasLocalVideo); assert.isTrue(result.activeCallState?.hasLocalVideo);
assert.isNumber(result.activeCallState?.joinedAt);
});
it('keeps existing activeCallState.joinedAt', () => {
const joinedAt = new Date().getTime() - 1000;
const result = reducer(
{
...stateWithActiveGroupCall,
activeCallState: {
...stateWithActiveDirectCall.activeCallState,
joinedAt,
},
},
getAction({
callMode: CallMode.Group,
conversationId: 'fake-group-call-conversation-id',
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joined,
localDemuxId: 1,
hasLocalAudio: true,
hasLocalVideo: true,
peekInfo: {
acis: [],
pendingAcis: [],
maxDevices: 16,
deviceCount: 0,
},
remoteParticipants: [],
})
);
strictAssert(
result.activeCallState?.state === 'Active',
'state is active'
);
assert.equal(result.activeCallState?.joinedAt, joinedAt);
}); });
it("doesn't stop ringing if nobody is in the call", () => { it("doesn't stop ringing if nobody is in the call", () => {