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", () => {