Send call messages with conversationJobQueue

Co-authored-by: trevor-signal <trevor@signal.org>
This commit is contained in:
Scott Nonnenberg 2024-04-16 14:55:09 -07:00 committed by GitHub
parent 72169820eb
commit 783c71999a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 457 additions and 392 deletions

View file

@ -23,9 +23,7 @@ import { generateAci } from '../types/ServiceId';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
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 type { SafetyNumberProps } from './SafetyNumberChangeDialog';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { ThemeType } from '../types/Util';
import { StorySendMode } from '../types/Stories'; import { StorySendMode } from '../types/Stories';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -69,7 +67,6 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
declineCall: action('decline-call'), declineCall: action('decline-call'),
getGroupCallVideoFrameSource: (_: string, demuxId: number) => getGroupCallVideoFrameSource: (_: string, demuxId: number) =>
fakeGetGroupCallVideoFrameSource(demuxId), fakeGetGroupCallVideoFrameSource(demuxId),
getPreferredBadge: () => undefined,
getPresentingSources: action('get-presenting-sources'), getPresentingSources: action('get-presenting-sources'),
hangUpActiveCall: action('hang-up-active-call'), hangUpActiveCall: action('hang-up-active-call'),
hasInitialLoadCompleted: true, hasInitialLoadCompleted: true,
@ -78,7 +75,6 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
callLink: undefined, callLink: undefined,
isGroupCallRaiseHandEnabled: true, isGroupCallRaiseHandEnabled: true,
isGroupCallReactionsEnabled: true, isGroupCallReactionsEnabled: true,
keyChangeOk: action('key-change-ok'),
me: { me: {
...getDefaultConversation({ ...getDefaultConversation({
color: AvatarColors[0], color: AvatarColors[0],
@ -92,7 +88,6 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
renderDeviceSelection: () => <div />, renderDeviceSelection: () => <div />,
renderEmojiPicker: () => <>EmojiPicker</>, renderEmojiPicker: () => <>EmojiPicker</>,
renderReactionPicker: () => <div />, renderReactionPicker: () => <div />,
renderSafetyNumberViewer: (_: SafetyNumberProps) => <div />,
sendGroupCallRaiseHand: action('send-group-call-raise-hand'), sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
sendGroupCallReaction: action('send-group-call-reaction'), sendGroupCallReaction: action('send-group-call-reaction'),
setGroupCallVideoRequest: action('set-group-call-video-request'), setGroupCallVideoRequest: action('set-group-call-video-request'),
@ -108,7 +103,6 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
stopRingtone: action('stop-ringtone'), stopRingtone: action('stop-ringtone'),
switchToPresentationView: action('switch-to-presentation-view'), switchToPresentationView: action('switch-to-presentation-view'),
switchFromPresentationView: action('switch-from-presentation-view'), switchFromPresentationView: action('switch-from-presentation-view'),
theme: ThemeType.light,
toggleParticipants: action('toggle-participants'), toggleParticipants: action('toggle-participants'),
togglePip: action('toggle-pip'), togglePip: action('toggle-pip'),
toggleScreenRecordingPermissionsDialog: action( toggleScreenRecordingPermissionsDialog: action(
@ -155,7 +149,6 @@ export function OngoingGroupCall(): JSX.Element {
...getCommonActiveCallData(), ...getCommonActiveCallData(),
callMode: CallMode.Group, callMode: CallMode.Group,
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [],
conversationsByDemuxId: new Map<number, ConversationType>(), conversationsByDemuxId: new Map<number, ConversationType>(),
deviceCount: 0, deviceCount: 0,
joinState: GroupCallJoinState.Joined, joinState: GroupCallJoinState.Joined,
@ -232,35 +225,3 @@ export function CallRequestNeeded(): JSX.Element {
/> />
); );
} }
export function GroupCallSafetyNumberChanged(): JSX.Element {
return (
<CallManager
{...createProps({
activeCall: {
...getCommonActiveCallData(),
callMode: CallMode.Group,
connectionState: GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [
{
...getDefaultConversation({
title: 'Aaron',
}),
},
],
conversationsByDemuxId: new Map<number, ConversationType>(),
deviceCount: 0,
joinState: GroupCallJoinState.Joined,
localDemuxId: 1,
maxDevices: 5,
groupMembers: [],
isConversationTooBigToRing: false,
peekedParticipants: [],
raisedHands: new Set<number>(),
remoteParticipants: [],
remoteAudioLevels: new Map<number, number>(),
},
})}
/>
);
}

View file

@ -11,8 +11,6 @@ import { CallingParticipantsList } from './CallingParticipantsList';
import { CallingSelectPresentingSourcesModal } from './CallingSelectPresentingSourcesModal'; import { CallingSelectPresentingSourcesModal } from './CallingSelectPresentingSourcesModal';
import { CallingPip } from './CallingPip'; import { CallingPip } from './CallingPip';
import { IncomingCallBar } from './IncomingCallBar'; import { IncomingCallBar } from './IncomingCallBar';
import type { SafetyNumberProps } from './SafetyNumberChangeDialog';
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
import type { import type {
ActiveCallType, ActiveCallType,
CallingConversationType, CallingConversationType,
@ -28,13 +26,11 @@ import {
GroupCallJoinState, GroupCallJoinState,
} from '../types/Calling'; } from '../types/Calling';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { import type {
AcceptCallType, AcceptCallType,
CancelCallType, CancelCallType,
DeclineCallType, DeclineCallType,
GroupCallParticipantInfoType, GroupCallParticipantInfoType,
KeyChangeOkType,
SendGroupCallRaiseHandType, SendGroupCallRaiseHandType,
SendGroupCallReactionType, SendGroupCallReactionType,
SetGroupCallVideoRequestType, SetGroupCallVideoRequestType,
@ -46,7 +42,7 @@ import type {
} from '../state/ducks/calling'; } from '../state/ducks/calling';
import { CallLinkRestrictions } from '../types/CallLink'; import { CallLinkRestrictions } from '../types/CallLink';
import type { CallLinkType } from '../types/CallLink'; import type { CallLinkType } from '../types/CallLink';
import type { LocalizerType, ThemeType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { CallingToastProvider } from './CallingToast'; import { CallingToastProvider } from './CallingToast';
import type { SmartReactionPicker } from '../state/smart/ReactionPicker'; import type { SmartReactionPicker } from '../state/smart/ReactionPicker';
@ -89,15 +85,12 @@ export type PropsType = {
conversationId: string, conversationId: string,
demuxId: number demuxId: number
) => VideoFrameSource; ) => VideoFrameSource;
getPreferredBadge: PreferredBadgeSelectorType;
getPresentingSources: () => void; getPresentingSources: () => void;
incomingCall: DirectIncomingCall | GroupIncomingCall | null; incomingCall: DirectIncomingCall | GroupIncomingCall | null;
keyChangeOk: (_: KeyChangeOkType) => void;
renderDeviceSelection: () => JSX.Element; renderDeviceSelection: () => JSX.Element;
renderReactionPicker: ( renderReactionPicker: (
props: React.ComponentProps<typeof SmartReactionPicker> props: React.ComponentProps<typeof SmartReactionPicker>
) => JSX.Element; ) => JSX.Element;
renderSafetyNumberViewer: (props: SafetyNumberProps) => JSX.Element;
startCall: (payload: StartCallType) => void; startCall: (payload: StartCallType) => void;
toggleParticipants: () => void; toggleParticipants: () => void;
acceptCall: (_: AcceptCallType) => void; acceptCall: (_: AcceptCallType) => void;
@ -131,7 +124,6 @@ export type PropsType = {
switchToPresentationView: () => void; switchToPresentationView: () => void;
switchFromPresentationView: () => void; switchFromPresentationView: () => void;
hangUpActiveCall: (reason: string) => void; hangUpActiveCall: (reason: string) => void;
theme: ThemeType;
togglePip: () => void; togglePip: () => void;
toggleScreenRecordingPermissionsDialog: () => unknown; toggleScreenRecordingPermissionsDialog: () => unknown;
toggleSettings: () => void; toggleSettings: () => void;
@ -168,16 +160,13 @@ function ActiveCallManager({
i18n, i18n,
isGroupCallRaiseHandEnabled, isGroupCallRaiseHandEnabled,
isGroupCallReactionsEnabled, isGroupCallReactionsEnabled,
keyChangeOk,
getGroupCallVideoFrameSource, getGroupCallVideoFrameSource,
getPreferredBadge,
getPresentingSources, getPresentingSources,
me, me,
openSystemPreferencesAction, openSystemPreferencesAction,
renderDeviceSelection, renderDeviceSelection,
renderEmojiPicker, renderEmojiPicker,
renderReactionPicker, renderReactionPicker,
renderSafetyNumberViewer,
sendGroupCallRaiseHand, sendGroupCallRaiseHand,
sendGroupCallReaction, sendGroupCallReaction,
setGroupCallVideoRequest, setGroupCallVideoRequest,
@ -191,7 +180,6 @@ function ActiveCallManager({
startCall, startCall,
switchToPresentationView, switchToPresentationView,
switchFromPresentationView, switchFromPresentationView,
theme,
toggleParticipants, toggleParticipants,
togglePip, togglePip,
toggleScreenRecordingPermissionsDialog, toggleScreenRecordingPermissionsDialog,
@ -263,10 +251,6 @@ function ActiveCallManager({
} }
}, [callLink, showToast]); }, [callLink, showToast]);
const onSafetyNumberDialogCancel = useCallback(() => {
hangUpActiveCall('safety number dialog cancel');
}, [hangUpActiveCall]);
let isCallFull: boolean; let isCallFull: boolean;
let showCallLobby: boolean; let showCallLobby: boolean;
let groupMembers: let groupMembers:
@ -463,26 +447,6 @@ function ActiveCallManager({
participants={groupCallParticipantsForParticipantsList} participants={groupCallParticipantsForParticipantsList}
/> />
))} ))}
{isGroupOrAdhocActiveCall(activeCall) &&
activeCall.conversationsWithSafetyNumberChanges.length ? (
<SafetyNumberChangeDialog
confirmText={i18n('icu:continueCall')}
contacts={[
{
story: undefined,
contacts: activeCall.conversationsWithSafetyNumberChanges,
},
]}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
onCancel={onSafetyNumberDialogCancel}
onConfirm={() => {
keyChangeOk({ conversationId: activeCall.conversation.id });
}}
renderSafetyNumber={renderSafetyNumberViewer}
theme={theme}
/>
) : null}
</> </>
); );
} }
@ -499,7 +463,6 @@ export function CallManager({
closeNeedPermissionScreen, closeNeedPermissionScreen,
declineCall, declineCall,
getGroupCallVideoFrameSource, getGroupCallVideoFrameSource,
getPreferredBadge,
getPresentingSources, getPresentingSources,
hangUpActiveCall, hangUpActiveCall,
hasInitialLoadCompleted, hasInitialLoadCompleted,
@ -508,7 +471,6 @@ export function CallManager({
isConversationTooBigToRing, isConversationTooBigToRing,
isGroupCallRaiseHandEnabled, isGroupCallRaiseHandEnabled,
isGroupCallReactionsEnabled, isGroupCallReactionsEnabled,
keyChangeOk,
me, me,
notifyForCall, notifyForCall,
openSystemPreferencesAction, openSystemPreferencesAction,
@ -517,7 +479,6 @@ export function CallManager({
renderDeviceSelection, renderDeviceSelection,
renderEmojiPicker, renderEmojiPicker,
renderReactionPicker, renderReactionPicker,
renderSafetyNumberViewer,
sendGroupCallRaiseHand, sendGroupCallRaiseHand,
sendGroupCallReaction, sendGroupCallReaction,
setGroupCallVideoRequest, setGroupCallVideoRequest,
@ -533,7 +494,6 @@ export function CallManager({
stopRingtone, stopRingtone,
switchFromPresentationView, switchFromPresentationView,
switchToPresentationView, switchToPresentationView,
theme,
toggleParticipants, toggleParticipants,
togglePip, togglePip,
toggleScreenRecordingPermissionsDialog, toggleScreenRecordingPermissionsDialog,
@ -594,20 +554,17 @@ export function CallManager({
changeCallView={changeCallView} changeCallView={changeCallView}
closeNeedPermissionScreen={closeNeedPermissionScreen} closeNeedPermissionScreen={closeNeedPermissionScreen}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
getPreferredBadge={getPreferredBadge}
getPresentingSources={getPresentingSources} getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall} hangUpActiveCall={hangUpActiveCall}
i18n={i18n} i18n={i18n}
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled} isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled}
isGroupCallReactionsEnabled={isGroupCallReactionsEnabled} isGroupCallReactionsEnabled={isGroupCallReactionsEnabled}
keyChangeOk={keyChangeOk}
me={me} me={me}
openSystemPreferencesAction={openSystemPreferencesAction} openSystemPreferencesAction={openSystemPreferencesAction}
pauseVoiceNotePlayer={pauseVoiceNotePlayer} pauseVoiceNotePlayer={pauseVoiceNotePlayer}
renderDeviceSelection={renderDeviceSelection} renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker} renderReactionPicker={renderReactionPicker}
renderSafetyNumberViewer={renderSafetyNumberViewer}
sendGroupCallRaiseHand={sendGroupCallRaiseHand} sendGroupCallRaiseHand={sendGroupCallRaiseHand}
sendGroupCallReaction={sendGroupCallReaction} sendGroupCallReaction={sendGroupCallReaction}
setGroupCallVideoRequest={setGroupCallVideoRequest} setGroupCallVideoRequest={setGroupCallVideoRequest}
@ -621,7 +578,6 @@ export function CallManager({
startCall={startCall} startCall={startCall}
switchFromPresentationView={switchFromPresentationView} switchFromPresentationView={switchFromPresentationView}
switchToPresentationView={switchToPresentationView} switchToPresentationView={switchToPresentationView}
theme={theme}
toggleParticipants={toggleParticipants} toggleParticipants={toggleParticipants}
togglePip={togglePip} togglePip={togglePip}
toggleScreenRecordingPermissionsDialog={ toggleScreenRecordingPermissionsDialog={

View file

@ -124,7 +124,6 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
callMode: CallMode.Group as CallMode.Group, callMode: CallMode.Group as CallMode.Group,
connectionState: connectionState:
overrideProps.connectionState || GroupCallConnectionState.Connected, overrideProps.connectionState || GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [],
conversationsByDemuxId: getConversationsByDemuxId(overrideProps), conversationsByDemuxId: getConversationsByDemuxId(overrideProps),
joinState: GroupCallJoinState.Joined, joinState: GroupCallJoinState.Joined,
localDemuxId: LOCAL_DEMUX_ID, localDemuxId: LOCAL_DEMUX_ID,

View file

@ -131,7 +131,6 @@ export function GroupCall(args: PropsType): JSX.Element {
...getCommonActiveCallData({}), ...getCommonActiveCallData({}),
callMode: CallMode.Group as CallMode.Group, callMode: CallMode.Group as CallMode.Group,
connectionState: GroupCallConnectionState.Connected, connectionState: GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [],
conversationsByDemuxId: new Map<number, ConversationType>(), conversationsByDemuxId: new Map<number, ConversationType>(),
groupMembers: times(3, () => getDefaultConversation()), groupMembers: times(3, () => getDefaultConversation()),
isConversationTooBigToRing: false, isConversationTooBigToRing: false,

View file

@ -12,7 +12,9 @@ import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { JOB_STATUS, JobQueue } from './JobQueue'; import { JOB_STATUS, JobQueue } from './JobQueue';
import { sendNormalMessage } from './helpers/sendNormalMessage'; import { sendNormalMessage } from './helpers/sendNormalMessage';
import { sendCallingMessage } from './helpers/sendCallingMessage';
import { sendDirectExpirationTimerUpdate } from './helpers/sendDirectExpirationTimerUpdate'; import { sendDirectExpirationTimerUpdate } from './helpers/sendDirectExpirationTimerUpdate';
import { sendGroupCallUpdate } from './helpers/sendGroupCallUpdate';
import { sendGroupUpdate } from './helpers/sendGroupUpdate'; import { sendGroupUpdate } from './helpers/sendGroupUpdate';
import { sendDeleteForEveryone } from './helpers/sendDeleteForEveryone'; import { sendDeleteForEveryone } from './helpers/sendDeleteForEveryone';
import { sendDeleteStoryForEveryone } from './helpers/sendDeleteStoryForEveryone'; import { sendDeleteStoryForEveryone } from './helpers/sendDeleteStoryForEveryone';
@ -51,9 +53,11 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
// Note: generally, we only want to add to this list. If you do need to change one of // Note: generally, we only want to add to this list. If you do need to change one of
// these values, you'll likely need to write a database migration. // these values, you'll likely need to write a database migration.
export const conversationQueueJobEnum = z.enum([ export const conversationQueueJobEnum = z.enum([
'CallingMessage',
'DeleteForEveryone', 'DeleteForEveryone',
'DeleteStoryForEveryone', 'DeleteStoryForEveryone',
'DirectExpirationTimerUpdate', 'DirectExpirationTimerUpdate',
'GroupCallUpdate',
'GroupUpdate', 'GroupUpdate',
'NormalMessage', 'NormalMessage',
'NullMessage', 'NullMessage',
@ -67,6 +71,17 @@ export const conversationQueueJobEnum = z.enum([
]); ]);
type ConversationQueueJobEnum = z.infer<typeof conversationQueueJobEnum>; type ConversationQueueJobEnum = z.infer<typeof conversationQueueJobEnum>;
const callingMessageJobDataSchema = z.object({
type: z.literal(conversationQueueJobEnum.enum.CallingMessage),
conversationId: z.string(),
protoBase64: z.string(),
urgent: z.boolean(),
// These two are group-only
recipients: z.array(serviceIdSchema).optional(),
isPartialSend: z.boolean().optional(),
});
export type CallingMessageJobData = z.infer<typeof callingMessageJobDataSchema>;
const deleteForEveryoneJobDataSchema = z.object({ const deleteForEveryoneJobDataSchema = z.object({
type: z.literal(conversationQueueJobEnum.enum.DeleteForEveryone), type: z.literal(conversationQueueJobEnum.enum.DeleteForEveryone),
conversationId: z.string(), conversationId: z.string(),
@ -108,6 +123,16 @@ export type ExpirationTimerUpdateJobData = z.infer<
typeof expirationTimerUpdateJobDataSchema typeof expirationTimerUpdateJobDataSchema
>; >;
const groupCallUpdateJobDataSchema = z.object({
type: z.literal(conversationQueueJobEnum.enum.GroupCallUpdate),
conversationId: z.string(),
eraId: z.string(),
urgent: z.boolean(),
});
export type GroupCallUpdateJobData = z.infer<
typeof groupCallUpdateJobDataSchema
>;
const groupUpdateJobDataSchema = z.object({ const groupUpdateJobDataSchema = z.object({
type: z.literal(conversationQueueJobEnum.enum.GroupUpdate), type: z.literal(conversationQueueJobEnum.enum.GroupUpdate),
conversationId: z.string(), conversationId: z.string(),
@ -208,9 +233,11 @@ const receiptsJobDataSchema = z.object({
export type ReceiptsJobData = z.infer<typeof receiptsJobDataSchema>; export type ReceiptsJobData = z.infer<typeof receiptsJobDataSchema>;
export const conversationQueueJobDataSchema = z.union([ export const conversationQueueJobDataSchema = z.union([
callingMessageJobDataSchema,
deleteForEveryoneJobDataSchema, deleteForEveryoneJobDataSchema,
deleteStoryForEveryoneJobDataSchema, deleteStoryForEveryoneJobDataSchema,
expirationTimerUpdateJobDataSchema, expirationTimerUpdateJobDataSchema,
groupCallUpdateJobDataSchema,
groupUpdateJobDataSchema, groupUpdateJobDataSchema,
normalMessageSendJobDataSchema, normalMessageSendJobDataSchema,
nullMessageJobDataSchema, nullMessageJobDataSchema,
@ -239,6 +266,9 @@ const MAX_RETRY_TIME = durations.DAY;
const MAX_ATTEMPTS = exponentialBackoffMaxAttempts(MAX_RETRY_TIME); const MAX_ATTEMPTS = exponentialBackoffMaxAttempts(MAX_RETRY_TIME);
function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean { function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean {
if (type === 'CallingMessage') {
return true;
}
if (type === 'DeleteForEveryone') { if (type === 'DeleteForEveryone') {
return true; return true;
} }
@ -248,6 +278,9 @@ function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean {
if (type === 'DirectExpirationTimerUpdate') { if (type === 'DirectExpirationTimerUpdate') {
return true; return true;
} }
if (type === 'GroupCallUpdate') {
return true;
}
if (type === 'GroupUpdate') { if (type === 'GroupUpdate') {
return true; return true;
} }
@ -263,6 +296,9 @@ function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean {
if (type === 'Reaction') { if (type === 'Reaction') {
return false; return false;
} }
if (type === 'Receipts') {
return false;
}
if (type === 'ResendRequest') { if (type === 'ResendRequest') {
return false; return false;
} }
@ -277,9 +313,6 @@ function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean {
if (type === 'Story') { if (type === 'Story') {
return true; return true;
} }
if (type === 'Receipts') {
return false;
}
throw missingCaseError(type); throw missingCaseError(type);
} }
@ -785,6 +818,9 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
try { try {
switch (type) { switch (type) {
case jobSet.CallingMessage:
await sendCallingMessage(conversation, jobBundle, data);
break;
case jobSet.DeleteForEveryone: case jobSet.DeleteForEveryone:
await sendDeleteForEveryone(conversation, jobBundle, data); await sendDeleteForEveryone(conversation, jobBundle, data);
break; break;
@ -794,6 +830,9 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
case jobSet.DirectExpirationTimerUpdate: case jobSet.DirectExpirationTimerUpdate:
await sendDirectExpirationTimerUpdate(conversation, jobBundle, data); await sendDirectExpirationTimerUpdate(conversation, jobBundle, data);
break; break;
case jobSet.GroupCallUpdate:
await sendGroupCallUpdate(conversation, jobBundle, data);
break;
case jobSet.GroupUpdate: case jobSet.GroupUpdate:
await sendGroupUpdate(conversation, jobBundle, data); await sendGroupUpdate(conversation, jobBundle, data);
break; break;

View file

@ -0,0 +1,40 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isNotNil } from '../../util/isNotNil';
import type { LoggerType } from '../../types/Logging';
import type { ServiceIdString } from '../../types/ServiceId';
export function getValidRecipients(
recipients: Array<string>,
options: {
logId: string;
log: LoggerType;
}
): Array<ServiceIdString> {
const { log, logId } = options;
return recipients
.map(id => {
const recipient = window.ConversationController.get(id);
if (!recipient) {
return undefined;
}
if (recipient.isUnregistered()) {
log.warn(
`${logId}: dropping unregistered recipient ${recipient.idForLogging()}`
);
return undefined;
}
if (recipient.isBlocked()) {
log.warn(
`${logId}: dropping blocked recipient ${recipient.idForLogging()}`
);
return undefined;
}
return recipient.getSendTarget();
})
.filter(isNotNil);
}

View file

@ -0,0 +1,149 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { handleMessageSend } from '../../util/handleMessageSend';
import { getSendOptions } from '../../util/getSendOptions';
import {
isDirectConversation,
isGroup,
} from '../../util/whatTypeOfConversation';
import { SignalService as Proto } from '../../protobuf';
import {
handleMultipleSendErrors,
maybeExpandErrors,
} from './handleMultipleSendErrors';
import type { ConversationModel } from '../../models/conversations';
import type {
ConversationQueueJobBundle,
CallingMessageJobData,
} from '../conversationJobQueue';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import {
OutgoingIdentityKeyError,
UnregisteredUserError,
} from '../../textsecure/Errors';
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds';
import { sendContentMessageToGroup } from '../../util/sendToGroup';
import * as Bytes from '../../Bytes';
import { getValidRecipients } from './getValidRecipients';
export async function sendCallingMessage(
conversation: ConversationModel,
{
isFinalAttempt,
messaging,
shouldContinue,
timestamp,
timeRemaining,
log,
}: ConversationQueueJobBundle,
data: CallingMessageJobData
): Promise<void> {
const logId = `sendCallingMessage(${conversation.idForLogging()}.${timestamp})`;
if (!shouldContinue) {
log.info(`${logId}: Ran out of time. Giving up.`);
return;
}
log.info(`${logId}: Starting send`);
if (
isDirectConversation(conversation.attributes) &&
isConversationUnregistered(conversation.attributes)
) {
log.warn(`${logId}: Direct conversation is unregistered; refusing to send`);
return;
}
const {
protoBase64,
urgent,
recipients: jobRecipients,
isPartialSend,
} = data;
const recipients = getValidRecipients(
jobRecipients || conversation.getRecipients(),
{ log, logId }
);
const untrustedServiceIds = getUntrustedConversationServiceIds(recipients);
if (untrustedServiceIds.length) {
window.reduxActions.conversations.conversationStoppedByMissingVerification({
conversationId: conversation.id,
untrustedServiceIds,
});
throw new Error(
`${logId}: Blocked because ${untrustedServiceIds.length} conversation(s) were untrusted. Failing this attempt.`
);
}
if (recipients.length === 0) {
log.warn(`${logId}: Giving up because there are no valid recipients.`);
return;
}
const sendType = 'callingMessage';
const sendOptions = await getSendOptions(conversation.attributes);
const callingMessage = Proto.CallingMessage.decode(
Bytes.fromBase64(protoBase64)
);
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
try {
if (isGroup(conversation.attributes)) {
await handleMessageSend(
sendContentMessageToGroup({
contentHint: ContentHint.DEFAULT,
contentMessage: new Proto.Content({ callingMessage }),
isPartialSend,
messageId: undefined,
recipients,
sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType,
timestamp,
urgent,
}),
{ messageIds: [], sendType }
);
} else {
const sendTarget = conversation.getSendTarget();
if (!sendTarget) {
log.error(`${logId}: Direct conversation send target is falsy`);
return;
}
await handleMessageSend(
messaging.sendCallingMessage(
sendTarget,
callingMessage,
timestamp,
urgent,
sendOptions
),
{ messageIds: [], sendType }
);
}
} catch (error: unknown) {
if (
error instanceof OutgoingIdentityKeyError ||
error instanceof UnregisteredUserError
) {
log.info(
`${logId}: Send failure was OutgoingIdentityKeyError or UnregisteredUserError. Cancelling job.`
);
return;
}
await handleMultipleSendErrors({
errors: maybeExpandErrors(error),
isFinalAttempt,
log,
timeRemaining,
toThrow: error,
});
}
}

View file

@ -0,0 +1,110 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { getSendOptions } from '../../util/getSendOptions';
import { isGroup } from '../../util/whatTypeOfConversation';
import { SignalService as Proto } from '../../protobuf';
import {
handleMultipleSendErrors,
maybeExpandErrors,
} from './handleMultipleSendErrors';
import type { ConversationModel } from '../../models/conversations';
import type {
ConversationQueueJobBundle,
GroupCallUpdateJobData,
} from '../conversationJobQueue';
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds';
import { sendToGroup } from '../../util/sendToGroup';
import { wrapWithSyncMessageSend } from '../../util/wrapWithSyncMessageSend';
import { getValidRecipients } from './getValidRecipients';
export async function sendGroupCallUpdate(
conversation: ConversationModel,
{
isFinalAttempt,
shouldContinue,
timestamp,
timeRemaining,
log,
}: ConversationQueueJobBundle,
data: GroupCallUpdateJobData
): Promise<void> {
const { eraId, urgent } = data;
const logId = `sendCallUpdate(${conversation.idForLogging()}.${eraId})`;
if (!shouldContinue) {
log.info(`${logId}: Ran out of time. Giving up.`);
return;
}
log.info(`${logId}: Starting send`);
if (!isGroup(conversation.attributes)) {
log.warn(`${logId}: Conversation is not a group; refusing to send`);
return;
}
const recipients = getValidRecipients(conversation.getRecipients(), {
log,
logId,
});
const untrustedServiceIds = getUntrustedConversationServiceIds(recipients);
if (untrustedServiceIds.length) {
window.reduxActions.conversations.conversationStoppedByMissingVerification({
conversationId: conversation.id,
untrustedServiceIds,
});
throw new Error(
`${logId}: Blocked because ${untrustedServiceIds.length} conversation(s) were untrusted. Failing this attempt.`
);
}
if (recipients.length === 0) {
log.warn(`${logId}: Giving up because there are no valid recipients.`);
return;
}
const sendType = 'callingMessage';
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
const groupV2 = conversation.getGroupV2Info();
const sendOptions = await getSendOptions(conversation.attributes);
if (!groupV2) {
log.error(`${logId}: Conversation lacks groupV2 info!`);
return;
}
try {
await wrapWithSyncMessageSend({
conversation,
logId,
messageIds: [],
send: () =>
conversation.queueJob(logId, () =>
sendToGroup({
contentHint: ContentHint.DEFAULT,
groupSendOptions: {
groupCallUpdate: { eraId },
groupV2,
timestamp,
},
messageId: undefined,
sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType,
urgent,
})
),
sendType,
timestamp,
});
} catch (error: unknown) {
await handleMultipleSendErrors({
errors: maybeExpandErrors(error),
isFinalAttempt,
log,
timeRemaining,
toThrow: error,
});
}
}

View file

@ -11,7 +11,6 @@ import {
import { wrapWithSyncMessageSend } from '../../util/wrapWithSyncMessageSend'; import { wrapWithSyncMessageSend } from '../../util/wrapWithSyncMessageSend';
import * as Bytes from '../../Bytes'; import * as Bytes from '../../Bytes';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { isNotNil } from '../../util/isNotNil';
import { ourProfileKeyService } from '../../services/ourProfileKey'; import { ourProfileKeyService } from '../../services/ourProfileKey';
import type { ConversationModel } from '../../models/conversations'; import type { ConversationModel } from '../../models/conversations';
@ -22,6 +21,7 @@ import type {
} from '../conversationJobQueue'; } from '../conversationJobQueue';
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds'; import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds';
import { sendToGroup } from '../../util/sendToGroup'; import { sendToGroup } from '../../util/sendToGroup';
import { getValidRecipients } from './getValidRecipients';
// Note: because we don't have a recipient map, if some sends fail, we will resend this // Note: because we don't have a recipient map, if some sends fail, we will resend this
// message to folks that got it on the first go-round. This is okay, because receivers // message to folks that got it on the first go-round. This is okay, because receivers
@ -55,28 +55,7 @@ export async function sendGroupUpdate(
const { groupChangeBase64, recipients: jobRecipients, revision } = data; const { groupChangeBase64, recipients: jobRecipients, revision } = data;
const recipients = jobRecipients const recipients = getValidRecipients(jobRecipients, { log, logId });
.map(id => {
const recipient = window.ConversationController.get(id);
if (!recipient) {
return undefined;
}
if (recipient.isUnregistered()) {
log.warn(
`${logId}: dropping unregistered recipient ${recipient.idForLogging()}`
);
return undefined;
}
if (recipient.isBlocked()) {
log.warn(
`${logId}: dropping blocked recipient ${recipient.idForLogging()}`
);
return undefined;
}
return recipient.getSendTarget();
})
.filter(isNotNil);
const untrustedServiceIds = getUntrustedConversationServiceIds(recipients); const untrustedServiceIds = getUntrustedConversationServiceIds(recipients);
if (untrustedServiceIds.length) { if (untrustedServiceIds.length) {

View file

@ -3008,15 +3008,9 @@ export class ConversationModel extends window.Backbone
'addKeyChange' 'addKeyChange'
); );
const isUntrusted = await this.isUntrusted();
this.trigger('newmessage', model); this.trigger('newmessage', model);
const serviceId = this.getServiceId(); const serviceId = this.getServiceId();
// Group calls are always with folks that have a serviceId
if (isUntrusted && isAciString(serviceId)) {
window.reduxActions.calling.keyChanged({ aci: serviceId });
}
if (isDirectConversation(this.attributes)) { if (isDirectConversation(this.attributes)) {
window.reduxActions?.safetyNumber.clearSafetyNumber(this.id); window.reduxActions?.safetyNumber.clearSafetyNumber(this.id);

View file

@ -81,9 +81,7 @@ import { dropNull } from '../util/dropNull';
import { getOwn } from '../util/getOwn'; import { getOwn } from '../util/getOwn';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary'; import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
import { handleMessageSend } from '../util/handleMessageSend';
import { fetchMembershipProof, getMembershipList } from '../groups'; import { fetchMembershipProof, getMembershipList } from '../groups';
import { wrapWithSyncMessageSend } from '../util/wrapWithSyncMessageSend';
import type { ProcessedEnvelope } from '../textsecure/Types.d'; import type { ProcessedEnvelope } from '../textsecure/Types.d';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp'; import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp';
@ -94,7 +92,6 @@ import {
REQUESTED_VIDEO_FRAMERATE, REQUESTED_VIDEO_FRAMERATE,
} from '../calling/constants'; } from '../calling/constants';
import { callingMessageToProto } from '../util/callingMessageToProto'; import { callingMessageToProto } from '../util/callingMessageToProto';
import { getSendOptions } from '../util/getSendOptions';
import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions'; import { requestMicrophonePermissions } from '../util/requestMicrophonePermissions';
import OS from '../util/os/osMain'; import OS from '../util/os/osMain';
import { SignalService as Proto } from '../protobuf'; import { SignalService as Proto } from '../protobuf';
@ -107,7 +104,6 @@ import {
} from './notifications'; } from './notifications';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { assertDev, strictAssert } from '../util/assert'; import { assertDev, strictAssert } from '../util/assert';
import { sendContentMessageToGroup, sendToGroup } from '../util/sendToGroup';
import { import {
formatLocalDeviceState, formatLocalDeviceState,
formatPeekInfo, formatPeekInfo,
@ -130,13 +126,14 @@ import {
} from '../util/callDisposition'; } from '../util/callDisposition';
import { isNormalNumber } from '../util/isNormalNumber'; import { isNormalNumber } from '../util/isNormalNumber';
import { LocalCallEvent } from '../types/CallDisposition'; import { LocalCallEvent } from '../types/CallDisposition';
import { isServiceIdString } from '../types/ServiceId'; import { isServiceIdString, type ServiceIdString } from '../types/ServiceId';
import { isInSystemContacts } from '../util/isInSystemContacts'; import { isInSystemContacts } from '../util/isInSystemContacts';
import { import {
getRoomIdFromRootKey, getRoomIdFromRootKey,
getCallLinkAuthCredentialPresentation, getCallLinkAuthCredentialPresentation,
} from '../util/callLinks'; } from '../util/callLinks';
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled'; import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
import { conversationJobQueue } from '../jobs/conversationJobQueue';
const { const {
processGroupCallRingCancellation, processGroupCallRingCancellation,
@ -1430,53 +1427,36 @@ export class CallingClass {
private async sendGroupCallUpdateMessage( private async sendGroupCallUpdateMessage(
conversationId: string, conversationId: string,
eraId: string eraId: string
): Promise<void> { ): Promise<boolean> {
const conversation = window.ConversationController.get(conversationId); const conversation = window.ConversationController.get(conversationId);
if (!conversation) { if (!conversation) {
log.error( log.error('sendGroupCallUpdateMessage: Conversation not found!');
'Unable to send group call update message for non-existent conversation' return false;
);
return;
} }
const logId = `sendGroupCallUpdateMessage/${conversation.idForLogging()}`;
const groupV2 = conversation.getGroupV2Info(); const groupV2 = conversation.getGroupV2Info();
const sendOptions = await getSendOptions(conversation.attributes);
if (!groupV2) { if (!groupV2) {
log.error( log.error(`${logId}: Conversation lacks groupV2 info!`);
'Unable to send group call update message for conversation that lacks groupV2 info' return false;
);
return;
} }
const timestamp = Date.now(); try {
await conversationJobQueue.add({
// We "fire and forget" because sending this message is non-essential. type: 'GroupCallUpdate',
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; conversationId: conversation.id,
wrapWithSyncMessageSend({ eraId,
conversation, urgent: true,
logId: `sendToGroup/groupCallUpdate/${conversationId}-${eraId}`, });
messageIds: [], return true;
send: () => } catch (err) {
conversation.queueJob('sendGroupCallUpdateMessage', () => log.error(
sendToGroup({ `${logId}: Failed to queue call update:`,
contentHint: ContentHint.DEFAULT, Errors.toLogFormat(err)
groupSendOptions: { );
groupCallUpdate: { eraId }, return false;
groupV2, }
timestamp,
},
messageId: undefined,
sendOptions,
sendTarget: conversation.toSenderKeyTarget(),
sendType: 'callingMessage',
urgent: true,
})
),
sendType: 'callingMessage',
timestamp,
}).catch(err => {
log.error('Failed to send group call update:', Errors.toLogFormat(err));
});
} }
async acceptDirectCall( async acceptDirectCall(
@ -2109,50 +2089,71 @@ export class CallingClass {
private async handleSendCallMessageToGroup( private async handleSendCallMessageToGroup(
groupIdBytes: Buffer, groupIdBytes: Buffer,
data: Buffer, data: Buffer,
urgency: CallMessageUrgency urgency: CallMessageUrgency,
): Promise<void> { overrideRecipients: Array<Buffer> = []
): Promise<boolean> {
const groupId = groupIdBytes.toString('base64'); const groupId = groupIdBytes.toString('base64');
const conversation = window.ConversationController.get(groupId); const conversation = window.ConversationController.get(groupId);
if (!conversation) { if (!conversation) {
log.error('handleSendCallMessageToGroup(): could not find conversation'); log.error('handleSendCallMessageToGroup(): could not find conversation');
return; return false;
} }
const timestamp = Date.now();
const callingMessage = new CallingMessage();
callingMessage.opaque = new OpaqueMessage();
callingMessage.opaque.data = data;
const contentMessage = new Proto.Content();
contentMessage.callingMessage = callingMessageToProto(
callingMessage,
urgency
);
// If this message isn't droppable, we'll wake up recipient devices. The important one // If this message isn't droppable, we'll wake up recipient devices. The important one
// is the first message to start the call. // is the first message to start the call.
const urgent = urgency === CallMessageUrgency.HandleImmediately; const urgent = urgency === CallMessageUrgency.HandleImmediately;
// We "fire and forget" because sending this message is non-essential. try {
// We also don't sync this message. let recipients: Array<ServiceIdString> = [];
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; let isPartialSend = false;
await conversation.queueJob('handleSendCallMessageToGroup', async () => if (overrideRecipients.length > 0) {
handleMessageSend( // Send only to the overriding recipients.
sendContentMessageToGroup({ overrideRecipients.forEach(recipient => {
contentHint: ContentHint.DEFAULT, const serviceId = bytesToUuid(recipient);
contentMessage, if (!serviceId) {
isPartialSend: false, log.error(
messageId: undefined, 'handleSendCallMessageToGroup(): missing recipient serviceId'
recipients: conversation.getRecipients(), );
sendOptions: await getSendOptions(conversation.attributes), } else {
sendTarget: conversation.toSenderKeyTarget(), assertDev(
sendType: 'callingMessage', isServiceIdString(serviceId),
timestamp, 'remoteServiceId is not a serviceId'
urgent, );
}), recipients.push(serviceId);
{ messageIds: [], sendType: 'callingMessage' } }
) });
); isPartialSend = true;
} else {
// Send to all members in the group.
recipients = conversation.getRecipients();
}
const callingMessage = new CallingMessage();
callingMessage.opaque = new OpaqueMessage();
callingMessage.opaque.data = data;
const proto = callingMessageToProto(callingMessage, urgency);
const protoBytes = Proto.CallingMessage.encode(proto).finish();
const protoBase64 = Bytes.toBase64(protoBytes);
await conversationJobQueue.add({
type: 'CallingMessage',
conversationId: conversation.id,
protoBase64,
urgent,
isPartialSend,
recipients,
});
log.info('handleSendCallMessageToGroup() completed successfully');
return true;
} catch (err) {
const errorString = Errors.toLogFormat(err);
log.error(
`handleSendCallMessageToGroup() failed to queue job: ${errorString}`
);
return false;
}
} }
private async handleGroupCallRingUpdate( private async handleGroupCallRingUpdate(
@ -2275,15 +2276,14 @@ export class CallingClass {
message: CallingMessage, message: CallingMessage,
urgency?: CallMessageUrgency urgency?: CallMessageUrgency
): Promise<boolean> { ): Promise<boolean> {
const conversation = window.ConversationController.get(remoteUserId); assertDev(
const sendOptions = conversation isServiceIdString(remoteUserId),
? await getSendOptions(conversation.attributes) 'remoteUserId is not a service id'
: undefined; );
const conversation = window.ConversationController.getOrCreate(
if (!window.textsecure.messaging) { remoteUserId,
log.warn('handleOutgoingSignaling() returning false; offline'); 'private'
return false; );
}
// We want 1:1 call initiate messages to wake up recipient devices, but not others // We want 1:1 call initiate messages to wake up recipient devices, but not others
const urgent = const urgent =
@ -2291,32 +2291,23 @@ export class CallingClass {
Boolean(message.offer); Boolean(message.offer);
try { try {
assertDev( const proto = callingMessageToProto(message, urgency);
isServiceIdString(remoteUserId), const protoBytes = Proto.CallingMessage.encode(proto).finish();
'remoteUserId is not a service id' const protoBase64 = Bytes.toBase64(protoBytes);
);
const result = await handleMessageSend(
window.textsecure.messaging.sendCallingMessage(
remoteUserId,
callingMessageToProto(message, urgency),
urgent,
sendOptions
),
{ messageIds: [], sendType: 'callingMessage' }
);
if (result && result.errors && result.errors.length) { await conversationJobQueue.add({
throw result.errors[0]; type: 'CallingMessage',
} conversationId: conversation.id,
protoBase64,
urgent,
});
log.info('handleOutgoingSignaling() completed successfully');
return true; return true;
} catch (err) { } catch (err) {
if (err && err.errors && err.errors.length > 0) { const errorString = Errors.toLogFormat(err);
log.error(`handleOutgoingSignaling() failed: ${err.errors[0].reason}`); log.error(
} else { `handleOutgoingSignaling() failed to queue job: ${errorString}`
log.error('handleOutgoingSignaling() failed'); );
}
return false; return false;
} }
} }

View file

@ -152,7 +152,6 @@ export type ActiveCallStateType = {
pip: boolean; pip: boolean;
presentingSource?: PresentedSource; presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>; presentingSourcesAvailable?: Array<PresentableSource>;
safetyNumberChangedAcis: Array<AciString>;
settingsDialogOpen: boolean; settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean; showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean; showParticipantsList: boolean;
@ -249,14 +248,6 @@ type HangUpActionPayloadType = ReadonlyDeep<{
conversationId: string; conversationId: string;
}>; }>;
type KeyChangedType = ReadonlyDeep<{
aci: AciString;
}>;
export type KeyChangeOkType = ReadonlyDeep<{
conversationId: string;
}>;
export type IncomingDirectCallType = ReadonlyDeep<{ export type IncomingDirectCallType = ReadonlyDeep<{
conversationId: string; conversationId: string;
isVideoCall: boolean; isVideoCall: boolean;
@ -579,8 +570,6 @@ const GROUP_CALL_REACTIONS_EXPIRED = 'calling/GROUP_CALL_REACTIONS_EXPIRED';
const HANG_UP = 'calling/HANG_UP'; const HANG_UP = 'calling/HANG_UP';
const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL'; const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL';
const INCOMING_GROUP_CALL = 'calling/INCOMING_GROUP_CALL'; const INCOMING_GROUP_CALL = 'calling/INCOMING_GROUP_CALL';
const MARK_CALL_TRUSTED = 'calling/MARK_CALL_TRUSTED';
const MARK_CALL_UNTRUSTED = 'calling/MARK_CALL_UNTRUSTED';
const OUTGOING_CALL = 'calling/OUTGOING_CALL'; const OUTGOING_CALL = 'calling/OUTGOING_CALL';
const PEEK_GROUP_CALL_FULFILLED = 'calling/PEEK_GROUP_CALL_FULFILLED'; const PEEK_GROUP_CALL_FULFILLED = 'calling/PEEK_GROUP_CALL_FULFILLED';
const RAISE_HAND_GROUP_CALL = 'calling/RAISE_HAND_GROUP_CALL'; const RAISE_HAND_GROUP_CALL = 'calling/RAISE_HAND_GROUP_CALL';
@ -725,19 +714,6 @@ type IncomingGroupCallActionType = ReadonlyDeep<{
payload: IncomingGroupCallType; payload: IncomingGroupCallType;
}>; }>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type KeyChangedActionType = {
type: 'calling/MARK_CALL_UNTRUSTED';
payload: {
safetyNumberChangedAcis: Array<AciString>;
};
};
type KeyChangeOkActionType = ReadonlyDeep<{
type: 'calling/MARK_CALL_TRUSTED';
payload: null;
}>;
type SendGroupCallRaiseHandActionType = ReadonlyDeep<{ type SendGroupCallRaiseHandActionType = ReadonlyDeep<{
type: 'calling/RAISE_HAND_GROUP_CALL'; type: 'calling/RAISE_HAND_GROUP_CALL';
payload: SendGroupCallRaiseHandType; payload: SendGroupCallRaiseHandType;
@ -865,8 +841,6 @@ export type CallingActionType =
| HangUpActionType | HangUpActionType
| IncomingDirectCallActionType | IncomingDirectCallActionType
| IncomingGroupCallActionType | IncomingGroupCallActionType
| KeyChangedActionType
| KeyChangeOkActionType
| OutgoingCallActionType | OutgoingCallActionType
| PeekGroupCallFulfilledActionType | PeekGroupCallFulfilledActionType
| RefreshIODevicesActionType | RefreshIODevicesActionType
@ -1267,56 +1241,6 @@ function hangUpActiveCall(
}; };
} }
function keyChanged(
payload: KeyChangedType
): ThunkAction<void, RootStateType, unknown, KeyChangedActionType> {
return (dispatch, getState) => {
const state = getState();
const { activeCallState } = state.calling;
const activeCall = getActiveCall(state.calling);
if (!activeCall || !activeCallState) {
return;
}
if (isGroupOrAdhocCallState(activeCall)) {
const acisChanged = new Set(activeCallState.safetyNumberChangedAcis);
// Iterate over each participant to ensure that the service id passed in
// matches one of the participants in the group call.
activeCall.remoteParticipants.forEach(participant => {
if (participant.aci === payload.aci) {
acisChanged.add(participant.aci);
}
});
const safetyNumberChangedAcis = Array.from(acisChanged);
if (safetyNumberChangedAcis.length) {
dispatch({
type: MARK_CALL_UNTRUSTED,
payload: {
safetyNumberChangedAcis,
},
});
}
}
};
}
function keyChangeOk(
payload: KeyChangeOkType
): ThunkAction<void, RootStateType, unknown, KeyChangeOkActionType> {
return dispatch => {
calling.resendGroupCallMediaKeys(payload.conversationId);
dispatch({
type: MARK_CALL_TRUSTED,
payload: null,
});
};
}
function sendGroupCallRaiseHand( function sendGroupCallRaiseHand(
payload: SendGroupCallRaiseHandType payload: SendGroupCallRaiseHandType
): ThunkAction<void, RootStateType, unknown, SendGroupCallRaiseHandActionType> { ): ThunkAction<void, RootStateType, unknown, SendGroupCallRaiseHandActionType> {
@ -2059,8 +1983,6 @@ export const actions = {
groupCallRaisedHandsChange, groupCallRaisedHandsChange,
groupCallStateChange, groupCallStateChange,
hangUpActiveCall, hangUpActiveCall,
keyChangeOk,
keyChanged,
onOutgoingVideoCallInConversation, onOutgoingVideoCallInConversation,
onOutgoingAudioCallInConversation, onOutgoingAudioCallInConversation,
openSystemPreferencesAction, openSystemPreferencesAction,
@ -2284,7 +2206,6 @@ export function reducer(
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
pip: false, pip: false,
safetyNumberChangedAcis: [],
settingsDialogOpen: false, settingsDialogOpen: false,
showParticipantsList: false, showParticipantsList: false,
outgoingRing, outgoingRing,
@ -2314,7 +2235,6 @@ export function reducer(
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
pip: false, pip: false,
safetyNumberChangedAcis: [],
settingsDialogOpen: false, settingsDialogOpen: false,
showParticipantsList: false, showParticipantsList: false,
outgoingRing: true, outgoingRing: true,
@ -2343,7 +2263,6 @@ export function reducer(
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
pip: false, pip: false,
safetyNumberChangedAcis: [],
settingsDialogOpen: false, settingsDialogOpen: false,
showParticipantsList: false, showParticipantsList: false,
outgoingRing: false, outgoingRing: false,
@ -2505,7 +2424,6 @@ export function reducer(
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
pip: false, pip: false,
safetyNumberChangedAcis: [],
settingsDialogOpen: false, settingsDialogOpen: false,
showParticipantsList: false, showParticipantsList: false,
outgoingRing: true, outgoingRing: true,
@ -3182,42 +3100,5 @@ export function reducer(
}; };
} }
if (action.type === MARK_CALL_UNTRUSTED) {
const { activeCallState } = state;
if (!activeCallState) {
log.warn('Cannot mark call as untrusted when there is no active call');
return state;
}
const { safetyNumberChangedAcis } = action.payload;
return {
...state,
activeCallState: {
...activeCallState,
pip: false,
safetyNumberChangedAcis,
settingsDialogOpen: false,
showParticipantsList: false,
},
};
}
if (action.type === MARK_CALL_TRUSTED) {
const { activeCallState } = state;
if (!activeCallState) {
log.warn('Cannot mark call as trusted when there is no active call');
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
safetyNumberChangedAcis: [],
},
};
}
return state; return state;
} }

View file

@ -127,7 +127,7 @@ import { missingCaseError } from '../../util/missingCaseError';
import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue'; import { viewSyncJobQueue } from '../../jobs/viewSyncJobQueue';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { isIncoming, processBodyRanges } from '../selectors/message'; import { isIncoming, processBodyRanges } from '../selectors/message';
import { getActiveCallState } from '../selectors/calling'; import { getActiveCall, getActiveCallState } from '../selectors/calling';
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage'; import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
import type { ShowToastActionType } from './toast'; import type { ShowToastActionType } from './toast';
import { SHOW_TOAST } from './toast'; import { SHOW_TOAST } from './toast';
@ -2433,7 +2433,15 @@ export function cancelConversationVerification(
}); });
// Start the blocked conversation queues up again // Start the blocked conversation queues up again
const activeCall = getActiveCall(state);
conversationIdsBlocked.forEach(conversationId => { conversationIdsBlocked.forEach(conversationId => {
if (
activeCall &&
activeCall.conversationId === conversationId &&
activeCall.callMode === CallMode.Direct
) {
calling.hangup(conversationId, 'canceled conversation verification');
}
conversationJobQueue.resolveVerificationWaiter(conversationId); conversationJobQueue.resolveVerificationWaiter(conversationId);
}); });
}; };

View file

@ -9,7 +9,6 @@ import type {
GroupIncomingCall, GroupIncomingCall,
} from '../../components/CallManager'; } from '../../components/CallManager';
import { CallManager } from '../../components/CallManager'; import { CallManager } from '../../components/CallManager';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { isConversationTooBigToRing as getIsConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing'; import { isConversationTooBigToRing as getIsConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { calling as callingService } from '../../services/calling'; import { calling as callingService } from '../../services/calling';
@ -47,16 +46,14 @@ import type { ConversationType } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast'; import { useToastActions } from '../ducks/toast';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { getHasInitialLoadCompleted } from '../selectors/app'; import { getHasInitialLoadCompleted } from '../selectors/app';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { import {
getAvailableCameras, getAvailableCameras,
getCallLinkSelector, getCallLinkSelector,
getIncomingCall, getIncomingCall,
} from '../selectors/calling'; } from '../selectors/calling';
import { getConversationSelector, getMe } from '../selectors/conversations'; import { getConversationSelector, getMe } from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection'; import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { renderEmojiPicker } from './renderEmojiPicker'; import { renderEmojiPicker } from './renderEmojiPicker';
import { renderReactionPicker } from './renderReactionPicker'; import { renderReactionPicker } from './renderReactionPicker';
@ -64,10 +61,6 @@ function renderDeviceSelection(): JSX.Element {
return <SmartCallingDeviceSelection />; return <SmartCallingDeviceSelection />;
} }
function renderSafetyNumberViewer(props: SafetyNumberProps): JSX.Element {
return <SmartSafetyNumberViewer {...props} />;
}
const getGroupCallVideoFrameSource = const getGroupCallVideoFrameSource =
callingService.getGroupCallVideoFrameSource.bind(callingService); callingService.getGroupCallVideoFrameSource.bind(callingService);
@ -216,7 +209,6 @@ const mapStateToActiveCallProp = (
} satisfies ActiveDirectCallType; } satisfies ActiveDirectCallType;
case CallMode.Group: case CallMode.Group:
case CallMode.Adhoc: { case CallMode.Adhoc: {
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
const groupMembers: Array<ConversationType> = []; const groupMembers: Array<ConversationType> = [];
const remoteParticipants: Array<GroupCallRemoteParticipantType> = []; const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
const peekedParticipants: Array<ConversationType> = []; const peekedParticipants: Array<ConversationType> = [];
@ -290,22 +282,6 @@ const mapStateToActiveCallProp = (
} }
}); });
for (
let i = 0;
i < activeCallState.safetyNumberChangedAcis.length;
i += 1
) {
const aci = activeCallState.safetyNumberChangedAcis[i];
const remoteConversation = conversationSelectorByAci(aci);
if (!remoteConversation) {
log.error('Remote participant has no corresponding conversation');
continue;
}
conversationsWithSafetyNumberChanges.push(remoteConversation);
}
for (let i = 0; i < peekInfo.acis.length; i += 1) { for (let i = 0; i < peekInfo.acis.length; i += 1) {
const peekedParticipantAci = peekInfo.acis[i]; const peekedParticipantAci = peekInfo.acis[i];
@ -323,7 +299,6 @@ const mapStateToActiveCallProp = (
...baseResult, ...baseResult,
callMode: call.callMode, callMode: call.callMode,
connectionState: call.connectionState, connectionState: call.connectionState,
conversationsWithSafetyNumberChanges,
conversationsByDemuxId, conversationsByDemuxId,
deviceCount: peekInfo.deviceCount, deviceCount: peekInfo.deviceCount,
groupMembers, groupMembers,
@ -422,11 +397,9 @@ const mapStateToIncomingCallProp = (
export const SmartCallManager = memo(function SmartCallManager() { export const SmartCallManager = memo(function SmartCallManager() {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const activeCall = useSelector(mapStateToActiveCallProp); const activeCall = useSelector(mapStateToActiveCallProp);
const callLink = useSelector(mapStateToCallLinkProp); const callLink = useSelector(mapStateToCallLinkProp);
const incomingCall = useSelector(mapStateToIncomingCallProp); const incomingCall = useSelector(mapStateToIncomingCallProp);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const availableCameras = useSelector(getAvailableCameras); const availableCameras = useSelector(getAvailableCameras);
const hasInitialLoadCompleted = useSelector(getHasInitialLoadCompleted); const hasInitialLoadCompleted = useSelector(getHasInitialLoadCompleted);
const me = useSelector(getMe); const me = useSelector(getMe);
@ -439,7 +412,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
closeNeedPermissionScreen, closeNeedPermissionScreen,
getPresentingSources, getPresentingSources,
cancelCall, cancelCall,
keyChangeOk,
startCall, startCall,
toggleParticipants, toggleParticipants,
acceptCall, acceptCall,
@ -478,7 +450,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
closeNeedPermissionScreen={closeNeedPermissionScreen} closeNeedPermissionScreen={closeNeedPermissionScreen}
declineCall={declineCall} declineCall={declineCall}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource} getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
getPreferredBadge={getPreferredBadge}
getPresentingSources={getPresentingSources} getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall} hangUpActiveCall={hangUpActiveCall}
hasInitialLoadCompleted={hasInitialLoadCompleted} hasInitialLoadCompleted={hasInitialLoadCompleted}
@ -487,7 +458,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
isConversationTooBigToRing={isConversationTooBigToRing} isConversationTooBigToRing={isConversationTooBigToRing}
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled()} isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled()}
isGroupCallReactionsEnabled={isGroupCallReactionsEnabled()} isGroupCallReactionsEnabled={isGroupCallReactionsEnabled()}
keyChangeOk={keyChangeOk}
me={me} me={me}
notifyForCall={notifyForCall} notifyForCall={notifyForCall}
openSystemPreferencesAction={openSystemPreferencesAction} openSystemPreferencesAction={openSystemPreferencesAction}
@ -496,7 +466,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
renderDeviceSelection={renderDeviceSelection} renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker} renderReactionPicker={renderReactionPicker}
renderSafetyNumberViewer={renderSafetyNumberViewer}
sendGroupCallRaiseHand={sendGroupCallRaiseHand} sendGroupCallRaiseHand={sendGroupCallRaiseHand}
sendGroupCallReaction={sendGroupCallReaction} sendGroupCallReaction={sendGroupCallReaction}
setGroupCallVideoRequest={setGroupCallVideoRequest} setGroupCallVideoRequest={setGroupCallVideoRequest}
@ -512,7 +481,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
stopRingtone={stopRingtone} stopRingtone={stopRingtone}
switchFromPresentationView={switchFromPresentationView} switchFromPresentationView={switchFromPresentationView}
switchToPresentationView={switchToPresentationView} switchToPresentationView={switchToPresentationView}
theme={theme}
toggleParticipants={toggleParticipants} toggleParticipants={toggleParticipants}
togglePip={togglePip} togglePip={togglePip}
toggleScreenRecordingPermissionsDialog={ toggleScreenRecordingPermissionsDialog={

View file

@ -70,7 +70,6 @@ describe('calling duck', () => {
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedAcis: [],
outgoingRing: true, outgoingRing: true,
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,
@ -153,7 +152,6 @@ describe('calling duck', () => {
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedAcis: [],
outgoingRing: false, outgoingRing: false,
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,
@ -482,7 +480,6 @@ describe('calling duck', () => {
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedAcis: [],
outgoingRing: false, outgoingRing: false,
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,
@ -577,7 +574,6 @@ describe('calling duck', () => {
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedAcis: [],
outgoingRing: false, outgoingRing: false,
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,
@ -1198,7 +1194,6 @@ describe('calling duck', () => {
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedAcis: [],
outgoingRing: false, outgoingRing: false,
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,
@ -1887,7 +1882,6 @@ describe('calling duck', () => {
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedAcis: [],
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,
outgoingRing: true, outgoingRing: true,
@ -2195,7 +2189,6 @@ describe('calling duck', () => {
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedAcis: [],
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,
outgoingRing: true, outgoingRing: true,

View file

@ -69,7 +69,6 @@ describe('state/selectors/calling', () => {
localAudioLevel: 0, localAudioLevel: 0,
viewMode: CallViewMode.Paginated, viewMode: CallViewMode.Paginated,
showParticipantsList: false, showParticipantsList: false,
safetyNumberChangedAcis: [],
outgoingRing: true, outgoingRing: true,
pip: false, pip: false,
settingsDialogOpen: false, settingsDialogOpen: false,

View file

@ -1722,11 +1722,11 @@ export default class MessageSender {
async sendCallingMessage( async sendCallingMessage(
serviceId: ServiceIdString, serviceId: ServiceIdString,
callingMessage: Readonly<Proto.ICallingMessage>, callingMessage: Readonly<Proto.ICallingMessage>,
timestamp: number,
urgent: boolean, urgent: boolean,
options?: Readonly<SendOptionsType> options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> { ): Promise<CallbackResultType> {
const recipients = [serviceId]; const recipients = [serviceId];
const finalTimestamp = Date.now();
const contentMessage = new Proto.Content(); const contentMessage = new Proto.Content();
contentMessage.callingMessage = callingMessage; contentMessage.callingMessage = callingMessage;
@ -1736,13 +1736,13 @@ export default class MessageSender {
addPniSignatureMessageToProto({ addPniSignatureMessageToProto({
conversation, conversation,
proto: contentMessage, proto: contentMessage,
reason: `sendCallingMessage(${finalTimestamp})`, reason: `sendCallingMessage(${timestamp})`,
}); });
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message; const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendMessageProtoAndWait({ return this.sendMessageProtoAndWait({
timestamp: finalTimestamp, timestamp,
recipients, recipients,
proto: contentMessage, proto: contentMessage,
contentHint: ContentHint.DEFAULT, contentHint: ContentHint.DEFAULT,

View file

@ -90,7 +90,6 @@ export type ActiveGroupCallType = ActiveCallBaseType & {
callMode: CallMode.Group | CallMode.Adhoc; callMode: CallMode.Group | CallMode.Adhoc;
connectionState: GroupCallConnectionState; connectionState: GroupCallConnectionState;
conversationsByDemuxId: ConversationsByDemuxIdType; conversationsByDemuxId: ConversationsByDemuxIdType;
conversationsWithSafetyNumberChanges: Array<ConversationType>;
joinState: GroupCallJoinState; joinState: GroupCallJoinState;
localDemuxId: number | undefined; localDemuxId: number | undefined;
maxDevices: number; maxDevices: number;