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:
parent
f9b2261783
commit
ec9041937f
11 changed files with 82 additions and 15 deletions
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
Loading…
Reference in a new issue