From ec9041937ffa77aceeb7958a637c1e27fe4fd026 Mon Sep 17 00:00:00 2001 From: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:12:49 -0700 Subject: [PATCH] Consider own join time for group call missing media key check Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> --- ts/components/CallScreen.stories.tsx | 3 +- ts/components/CallScreen.tsx | 4 +-- ts/components/CallingPip.stories.tsx | 3 +- ts/components/CallingPipRemoteVideo.tsx | 1 + .../GroupCallOverflowArea.stories.tsx | 2 ++ ts/components/GroupCallOverflowArea.tsx | 3 ++ .../GroupCallRemoteParticipant.stories.tsx | 4 ++- ts/components/GroupCallRemoteParticipant.tsx | 13 +++++-- ts/components/GroupCallRemoteParticipants.tsx | 4 +++ ts/state/ducks/calling.ts | 24 ++++++++----- ts/test-electron/state/ducks/calling_test.ts | 36 +++++++++++++++++++ 11 files changed, 82 insertions(+), 15 deletions(-) diff --git a/ts/components/CallScreen.stories.tsx b/ts/components/CallScreen.stories.tsx index 68d0adef5278..98fc8f65e647 100644 --- a/ts/components/CallScreen.stories.tsx +++ b/ts/components/CallScreen.stories.tsx @@ -34,6 +34,7 @@ import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGr import enMessages from '../../_locales/en/messages.json'; import { CallingToastProvider, useCallingToasts } from './CallingToast'; import type { CallingImageDataCache } from './CallManager'; +import { MINUTE } from '../util/durations'; const MAX_PARTICIPANTS = 75; const LOCAL_DEMUX_ID = 1; @@ -158,7 +159,7 @@ const createActiveCallProp = ( overrideProps: DirectCallOverrideProps | GroupCallOverrideProps ) => { const baseResult = { - joinedAt: Date.now(), + joinedAt: Date.now() - MINUTE, conversation, hasLocalAudio: overrideProps.hasLocalAudio ?? false, hasLocalVideo: overrideProps.hasLocalVideo ?? false, diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 34848b9ffd59..50e86f6e7b02 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -660,8 +660,7 @@ export function CallScreen({ /> ); } - // joinedAt is only available for direct calls - if (isConnected) { + if (isConnected && activeCall.callMode === CallMode.Direct) { return ; } if (hasLocalVideo) { @@ -713,6 +712,7 @@ export function CallScreen({ getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} imageDataCache={imageDataCache} i18n={i18n} + joinedAt={activeCall.joinedAt} remoteParticipants={activeCall.remoteParticipants} setGroupCallVideoRequest={setGroupCallVideoRequest} remoteAudioLevels={activeCall.remoteAudioLevels} diff --git a/ts/components/CallingPip.stories.tsx b/ts/components/CallingPip.stories.tsx index 1faa7c021abe..28a26d05311b 100644 --- a/ts/components/CallingPip.stories.tsx +++ b/ts/components/CallingPip.stories.tsx @@ -21,6 +21,7 @@ import { getDefaultConversation } from '../test-both/helpers/getDefaultConversat import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource'; import { setupI18n } from '../util/setupI18n'; import enMessages from '../../_locales/en/messages.json'; +import { MINUTE } from '../util/durations'; const i18n = setupI18n('en', enMessages); @@ -47,7 +48,7 @@ const getCommonActiveCallData = (overrides: Overrides) => ({ hasLocalVideo: overrides.hasLocalVideo ?? false, localAudioLevel: overrides.localAudioLevel ?? 0, viewMode: overrides.viewMode ?? CallViewMode.Paginated, - joinedAt: Date.now(), + joinedAt: Date.now() - MINUTE, outgoingRing: true, pip: true, settingsDialogOpen: false, diff --git a/ts/components/CallingPipRemoteVideo.tsx b/ts/components/CallingPipRemoteVideo.tsx index 60d61d8f9209..978345ac7331 100644 --- a/ts/components/CallingPipRemoteVideo.tsx +++ b/ts/components/CallingPipRemoteVideo.tsx @@ -188,6 +188,7 @@ export function CallingPipRemoteVideo({ imageDataCache={imageDataCache} i18n={i18n} isInPip + joinedAt={activeCall.joinedAt} remoteParticipant={activeGroupCallSpeaker} remoteParticipantsCount={activeCall.remoteParticipants.length} isActiveSpeakerInSpeakerView={false} diff --git a/ts/components/GroupCallOverflowArea.stories.tsx b/ts/components/GroupCallOverflowArea.stories.tsx index 6b16d48d740f..ae0e4a8e7056 100644 --- a/ts/components/GroupCallOverflowArea.stories.tsx +++ b/ts/components/GroupCallOverflowArea.stories.tsx @@ -14,6 +14,7 @@ import { FRAME_BUFFER_SIZE } from '../calling/constants'; import enMessages from '../../_locales/en/messages.json'; import { generateAci } from '../types/ServiceId'; import type { CallingImageDataCache } from './CallManager'; +import { MINUTE } from '../util/durations'; const MAX_PARTICIPANTS = 32; @@ -48,6 +49,7 @@ const defaultProps = { imageDataCache: React.createRef(), i18n, isCallReconnecting: false, + joinedAt: new Date().getTime() - MINUTE, onParticipantVisibilityChanged: action('onParticipantVisibilityChanged'), remoteAudioLevels: new Map(), remoteParticipantsCount: 1, diff --git a/ts/components/GroupCallOverflowArea.tsx b/ts/components/GroupCallOverflowArea.tsx index 71036da15667..b7163c89a3a6 100644 --- a/ts/components/GroupCallOverflowArea.tsx +++ b/ts/components/GroupCallOverflowArea.tsx @@ -22,6 +22,7 @@ export type PropsType = { i18n: LocalizerType; imageDataCache: React.RefObject; isCallReconnecting: boolean; + joinedAt: number | null; onClickRaisedHand?: () => void; onParticipantVisibilityChanged: ( demuxId: number, @@ -38,6 +39,7 @@ export function GroupCallOverflowArea({ imageDataCache, i18n, isCallReconnecting, + joinedAt, onClickRaisedHand, onParticipantVisibilityChanged, overflowedParticipants, @@ -138,6 +140,7 @@ export function GroupCallOverflowArea({ isActiveSpeakerInSpeakerView={false} isCallReconnecting={isCallReconnecting} isInOverflow + joinedAt={joinedAt} /> ))} diff --git a/ts/components/GroupCallRemoteParticipant.stories.tsx b/ts/components/GroupCallRemoteParticipant.stories.tsx index 384a1155904e..850c974ef4cf 100644 --- a/ts/components/GroupCallRemoteParticipant.stories.tsx +++ b/ts/components/GroupCallRemoteParticipant.stories.tsx @@ -12,6 +12,7 @@ import { setupI18n } from '../util/setupI18n'; import { generateAci } from '../types/ServiceId'; import enMessages from '../../_locales/en/messages.json'; import type { CallingImageDataCache } from './CallManager'; +import { MINUTE } from '../util/durations'; const i18n = setupI18n('en', enMessages); @@ -79,6 +80,7 @@ const createProps = ( remoteParticipantsCount: 1, isActiveSpeakerInSpeakerView: false, isCallReconnecting: false, + joinedAt: new Date().getTime() - MINUTE, ...overrideProps, }); @@ -186,7 +188,7 @@ export function NoMediaKeys(): JSX.Element { width: 120, }, { - addedTime: Date.now() - 60 * 1000, + addedTime: Date.now() - MINUTE, hasRemoteAudio: true, mediaKeysReceived: false, } diff --git a/ts/components/GroupCallRemoteParticipant.tsx b/ts/components/GroupCallRemoteParticipant.tsx index 057555d49afd..f6eb2d58c479 100644 --- a/ts/components/GroupCallRemoteParticipant.tsx +++ b/ts/components/GroupCallRemoteParticipant.tsx @@ -44,6 +44,7 @@ type BasePropsType = { isActiveSpeakerInSpeakerView: boolean; isCallReconnecting: boolean; isInOverflow?: boolean; + joinedAt: number | null; onClickRaisedHand?: () => void; onVisibilityChanged?: (demuxId: number, isVisible: boolean) => unknown; remoteParticipant: GroupCallRemoteParticipantType; @@ -82,6 +83,7 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( isActiveSpeakerInSpeakerView, isCallReconnecting, isInOverflow, + joinedAt, } = props; const { @@ -150,10 +152,17 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( const wantsToShowVideo = hasRemoteVideo && !isBlocked && isVisible; 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( !mediaKeysReceived && - addedTime && - isOlderThan(addedTime, DELAY_TO_SHOW_MISSING_MEDIA_KEYS) + timeForMissingMediaKeysCheck && + isOlderThan( + timeForMissingMediaKeysCheck, + DELAY_TO_SHOW_MISSING_MEDIA_KEYS + ) ); const videoFrameSource = useMemo( diff --git a/ts/components/GroupCallRemoteParticipants.tsx b/ts/components/GroupCallRemoteParticipants.tsx index 5c9e21119479..093e1ae9e5e1 100644 --- a/ts/components/GroupCallRemoteParticipants.tsx +++ b/ts/components/GroupCallRemoteParticipants.tsx @@ -63,6 +63,7 @@ type PropsType = { i18n: LocalizerType; imageDataCache: React.RefObject; isCallReconnecting: boolean; + joinedAt: number | null; remoteParticipants: ReadonlyArray; setGroupCallVideoRequest: ( _: Array, @@ -115,6 +116,7 @@ export function GroupCallRemoteParticipants({ imageDataCache, i18n, isCallReconnecting, + joinedAt, remoteParticipants, setGroupCallVideoRequest, remoteAudioLevels, @@ -359,6 +361,7 @@ export function GroupCallRemoteParticipants({ remoteParticipantsCount={remoteParticipants.length} isActiveSpeakerInSpeakerView={isInSpeakerView} isCallReconnecting={isCallReconnecting} + joinedAt={joinedAt} /> ); }); @@ -517,6 +520,7 @@ export function GroupCallRemoteParticipants({ imageDataCache={imageDataCache} i18n={i18n} isCallReconnecting={isCallReconnecting} + joinedAt={joinedAt} onClickRaisedHand={onClickRaisedHand} onParticipantVisibilityChanged={onParticipantVisibilityChanged} overflowedParticipants={overflowedParticipants} diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index 4adc9bff9d22..dcb314d24130 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -3408,14 +3408,22 @@ export function reducer( state.activeCallState?.state === 'Active' && state.activeCallState?.conversationId === conversationId ) { - newActiveCallState = - connectionState === GroupCallConnectionState.NotConnected - ? undefined - : { - ...state.activeCallState, - hasLocalAudio, - hasLocalVideo, - }; + if (connectionState === GroupCallConnectionState.NotConnected) { + newActiveCallState = undefined; + } else { + const joinedAt = + state.activeCallState.joinedAt ?? + (connectionState === GroupCallConnectionState.Connected + ? new Date().getTime() + : null); + + newActiveCallState = { + ...state.activeCallState, + hasLocalAudio, + hasLocalVideo, + joinedAt, + }; + } // The first time we detect call participants in the lobby, check participant count // and mute ourselves if over the threshold. diff --git a/ts/test-electron/state/ducks/calling_test.ts b/ts/test-electron/state/ducks/calling_test.ts index 1a1170695695..e4da90dddaed 100644 --- a/ts/test-electron/state/ducks/calling_test.ts +++ b/ts/test-electron/state/ducks/calling_test.ts @@ -1265,6 +1265,42 @@ describe('calling duck', () => { ); assert.isTrue(result.activeCallState?.hasLocalAudio); 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", () => {