Handles safety number changes while in a call

This commit is contained in:
Josh Perez 2020-12-08 14:37:04 -05:00 committed by GitHub
parent 561baf6309
commit 318013e83d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 387 additions and 162 deletions

View File

@ -425,6 +425,10 @@
"message": "Call Anyway",
"description": "Used on a warning dialog to make it clear that it might be risky to call the conversation."
},
"continueCall": {
"message": "Continue Call",
"description": "Used on a warning dialog to make it clear that it might be risky to continue the group call."
},
"noLongerVerified": {
"message": "Your safety number with $name$ has changed and is no longer verified. Click to show.",
"description": "Shown in conversation banner when user's safety number has changed, but they were previously verified.",

View File

@ -904,7 +904,7 @@
// state we had before.
return false;
},
async isUntrusted(identifier) {
isUntrusted(identifier) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to set verified for undefined/null key');
}

View File

@ -17,7 +17,9 @@ import {
} from '../types/Calling';
import { ConversationTypeType } from '../state/ducks/conversations';
import { Colors, ColorType } from '../types/Colors';
import { getDefaultConversation } from '../util/getDefaultConversation';
import { setup as setupI18n } from '../../js/modules/i18n';
import { Props as SafetyNumberViewerProps } from '../state/smart/SafetyNumberViewer';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
@ -68,12 +70,16 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
getGroupCallVideoFrameSource: noop as any,
hangUp: action('hang-up'),
i18n,
keyChangeOk: action('key-change-ok'),
me: {
...getDefaultConversation({
color: select('Caller color', Colors, 'ultramarine' as ColorType),
title: text('Caller Title', 'Morty Smith'),
}),
uuid: 'cb0dd0c8-7393-41e9-a0aa-d631c4109541',
color: select('Caller color', Colors, 'ultramarine' as ColorType),
title: text('Caller Title', 'Morty Smith'),
},
renderDeviceSelection: () => <div />,
renderSafetyNumberViewer: (_: SafetyNumberViewerProps) => <div />,
setGroupCallVideoRequest: action('set-group-call-video-request'),
setLocalAudio: action('set-local-audio'),
setLocalPreview: action('set-local-preview'),
@ -110,6 +116,7 @@ story.add('Ongoing Group Call', () => (
...getCommonActiveCallData(),
callMode: CallMode.Group,
connectionState: GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [],
deviceCount: 0,
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
@ -145,3 +152,27 @@ story.add('Call Request Needed', () => (
})}
/>
));
story.add('Group call - Safety Number Changed', () => (
<CallManager
{...createProps({
activeCall: {
...getCommonActiveCallData(),
callMode: CallMode.Group,
connectionState: GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [
{
...getDefaultConversation({
title: 'Aaron',
}),
},
],
deviceCount: 0,
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
peekedParticipants: [],
remoteParticipants: [],
},
})}
/>
));

View File

@ -8,6 +8,10 @@ import { CallingLobby } from './CallingLobby';
import { CallingParticipantsList } from './CallingParticipantsList';
import { CallingPip } from './CallingPip';
import { IncomingCallBar } from './IncomingCallBar';
import {
SafetyNumberChangeDialog,
SafetyNumberProps,
} from './SafetyNumberChangeDialog';
import {
ActiveCallType,
CallEndedReason,
@ -24,6 +28,7 @@ import {
DeclineCallType,
DirectCallStateType,
HangUpType,
KeyChangeOkType,
SetGroupCallVideoRequestType,
SetLocalAudioType,
SetLocalPreviewType,
@ -32,9 +37,12 @@ import {
StartCallType,
} from '../state/ducks/calling';
import { LocalizerType } from '../types/Util';
import { ColorType } from '../types/Colors';
import { missingCaseError } from '../util/missingCaseError';
interface MeType extends ConversationType {
uuid: string;
}
export interface PropsType {
activeCall?: ActiveCallType;
availableCameras: Array<MediaDeviceInfo>;
@ -48,21 +56,15 @@ export interface PropsType {
call: DirectCallStateType;
conversation: ConversationType;
};
keyChangeOk: (_: KeyChangeOkType) => void;
renderDeviceSelection: () => JSX.Element;
renderSafetyNumberViewer: (props: SafetyNumberProps) => JSX.Element;
startCall: (payload: StartCallType) => void;
toggleParticipants: () => void;
acceptCall: (_: AcceptCallType) => void;
declineCall: (_: DeclineCallType) => void;
i18n: LocalizerType;
me: {
avatarPath?: string;
color?: ColorType;
name?: string;
phoneNumber?: string;
profileName?: string;
title: string;
uuid: string;
};
me: MeType;
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
@ -84,9 +86,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
closeNeedPermissionScreen,
hangUp,
i18n,
keyChangeOk,
getGroupCallVideoFrameSource,
me,
renderDeviceSelection,
renderSafetyNumberViewer,
setGroupCallVideoRequest,
setLocalAudio,
setLocalPreview,
@ -203,6 +207,7 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
<CallingParticipantsList
i18n={i18n}
onClose={toggleParticipants}
ourUuid={me.uuid}
participants={peekedParticipants}
/>
) : null}
@ -233,13 +238,11 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
...participant,
hasAudio: participant.hasRemoteAudio,
hasVideo: participant.hasRemoteVideo,
isSelf: false,
})),
{
...me,
hasAudio: hasLocalAudio,
hasVideo: hasLocalVideo,
isSelf: true,
},
]
: [];
@ -268,9 +271,25 @@ const ActiveCallManager: React.FC<ActiveCallManagerPropsType> = ({
<CallingParticipantsList
i18n={i18n}
onClose={toggleParticipants}
ourUuid={me.uuid}
participants={groupCallParticipantsForParticipantsList}
/>
) : null}
{activeCall.callMode === CallMode.Group &&
activeCall.conversationsWithSafetyNumberChanges.length ? (
<SafetyNumberChangeDialog
confirmText={i18n('continueCall')}
contacts={activeCall.conversationsWithSafetyNumberChanges}
i18n={i18n}
onCancel={() => {
hangUp({ conversationId: activeCall.conversation.id });
}}
onConfirm={() => {
keyChangeOk({ conversationId: activeCall.conversation.id });
}}
renderSafetyNumber={renderSafetyNumberViewer}
/>
) : null}
</>
);
};

View File

@ -12,13 +12,14 @@ import {
CallState,
GroupCallConnectionState,
GroupCallJoinState,
GroupCallPeekedParticipantType,
GroupCallRemoteParticipantType,
} from '../types/Calling';
import { ConversationType } from '../state/ducks/conversations';
import { Colors } from '../types/Colors';
import { CallScreen, PropsType } from './CallScreen';
import { setup as setupI18n } from '../../js/modules/i18n';
import { missingCaseError } from '../util/missingCaseError';
import { getDefaultConversation } from '../util/getDefaultConversation';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
@ -50,7 +51,7 @@ interface DirectCallOverrideProps extends OverridePropsBase {
interface GroupCallOverrideProps extends OverridePropsBase {
callMode: CallMode.Group;
connectionState?: GroupCallConnectionState;
peekedParticipants?: Array<GroupCallPeekedParticipantType>;
peekedParticipants?: Array<ConversationType>;
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
}
@ -83,6 +84,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
callMode: CallMode.Group as CallMode.Group,
connectionState:
overrideProps.connectionState || GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [],
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
deviceCount: (overrideProps.remoteParticipants || []).length,
@ -240,14 +242,15 @@ story.add('Group call - 1', () => (
callMode: CallMode.Group,
remoteParticipants: [
{
uuid: '72fa60e5-25fb-472d-8a56-e56867c57dda',
demuxId: 0,
hasRemoteAudio: true,
hasRemoteVideo: true,
isBlocked: false,
isSelf: false,
title: 'Tyler',
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: false,
uuid: '72fa60e5-25fb-472d-8a56-e56867c57dda',
title: 'Tyler',
}),
},
],
})}
@ -260,34 +263,37 @@ story.add('Group call - Many', () => (
callMode: CallMode.Group,
remoteParticipants: [
{
uuid: '094586f5-8fc2-4ce2-a152-2dfcc99f4630',
demuxId: 0,
hasRemoteAudio: true,
hasRemoteVideo: true,
isBlocked: false,
isSelf: false,
title: 'Amy',
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: false,
title: 'Amy',
uuid: '094586f5-8fc2-4ce2-a152-2dfcc99f4630',
}),
},
{
uuid: 'cb5bdb24-4cbb-4650-8a7a-1a2807051e74',
demuxId: 1,
hasRemoteAudio: true,
hasRemoteVideo: true,
isBlocked: false,
isSelf: true,
title: 'Bob',
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: false,
title: 'Bob',
uuid: 'cb5bdb24-4cbb-4650-8a7a-1a2807051e74',
}),
},
{
uuid: '2d7d13ae-53dc-4a51-8dc7-976cd85e0b57',
demuxId: 2,
hasRemoteAudio: true,
hasRemoteVideo: true,
isBlocked: true,
isSelf: false,
title: 'Alice',
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: true,
title: 'Alice',
uuid: '2d7d13ae-53dc-4a51-8dc7-976cd85e0b57',
}),
},
],
})}
@ -301,14 +307,15 @@ story.add('Group call - reconnecting', () => (
connectionState: GroupCallConnectionState.Reconnecting,
remoteParticipants: [
{
uuid: '33871c64-0c22-45ce-8aa4-0ec237ac4a31',
demuxId: 0,
hasRemoteAudio: true,
hasRemoteVideo: true,
isBlocked: false,
isSelf: false,
title: 'Tyler',
videoAspectRatio: 1.3,
...getDefaultConversation({
isBlocked: false,
title: 'Tyler',
uuid: '33871c64-0c22-45ce-8aa4-0ec237ac4a31',
}),
},
],
})}

View File

@ -11,6 +11,7 @@ import { ColorType } from '../types/Colors';
import { CallingLobby, PropsType } from './CallingLobby';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
import { getDefaultConversation } from '../util/getDefaultConversation';
const i18n = setupI18n('en', enMessages);
@ -33,7 +34,10 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
hasLocalVideo: boolean('hasLocalVideo', overrideProps.hasLocalVideo || false),
i18n,
isGroupCall: boolean('isGroupCall', overrideProps.isGroupCall || false),
me: overrideProps.me || { color: 'ultramarine' as ColorType },
me: overrideProps.me || {
color: 'ultramarine' as ColorType,
uuid: generateUuid(),
},
onCallCanceled: action('on-call-canceled'),
onJoinCall: action('on-join-call'),
peekedParticipants: overrideProps.peekedParticipants || [],
@ -48,11 +52,11 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
toggleSettings: action('toggle-settings'),
});
const fakePeekedParticipant = (title: string) => ({
isSelf: false,
title,
uuid: generateUuid(),
});
const fakePeekedParticipant = (title: string) =>
getDefaultConversation({
title,
uuid: generateUuid(),
});
const story = storiesOf('Components/CallingLobby', module);
@ -72,8 +76,9 @@ story.add('No Camera, local avatar', () => {
const props = createProps({
availableCameras: [],
me: {
color: 'ultramarine' as ColorType,
avatarPath: '/fixtures/kitten-4-112-112.jpg',
color: 'ultramarine' as ColorType,
uuid: generateUuid(),
},
});
return <CallingLobby {...props} />;

View File

@ -14,6 +14,7 @@ import { CallingHeader } from './CallingHeader';
import { Spinner } from './Spinner';
import { ColorType } from '../types/Colors';
import { LocalizerType } from '../types/Util';
import { ConversationType } from '../state/ducks/conversations';
export type PropsType = {
availableCameras: Array<MediaDeviceInfo>;
@ -28,15 +29,11 @@ export type PropsType = {
me: {
avatarPath?: string;
color?: ColorType;
uuid: string;
};
onCallCanceled: () => void;
onJoinCall: () => void;
peekedParticipants: Array<{
firstName?: string;
isSelf: boolean;
title: string;
uuid: string;
}>;
peekedParticipants: Array<ConversationType>;
setLocalAudio: (_: SetLocalAudioType) => void;
setLocalVideo: (_: SetLocalVideoType) => void;
setLocalPreview: (_: SetLocalPreviewType) => void;
@ -124,7 +121,7 @@ export const CallingLobby = ({
// device.
// TODO: Improve the "it's you" case; see DESKTOP-926.
const participantNames = peekedParticipants.map(participant =>
participant.isSelf
participant.uuid === me.uuid
? i18n('you')
: participant.firstName || participant.title
);

View File

@ -9,6 +9,7 @@ import { v4 as generateUuid } from 'uuid';
import { CallingParticipantsList, PropsType } from './CallingParticipantsList';
import { Colors } from '../types/Colors';
import { GroupCallRemoteParticipantType } from '../types/Calling';
import { getDefaultConversation } from '../util/getDefaultConversation';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
@ -19,24 +20,26 @@ function createParticipant(
): GroupCallRemoteParticipantType {
const randomColor = Math.floor(Math.random() * Colors.length - 1);
return {
avatarPath: participantProps.avatarPath,
color: Colors[randomColor],
demuxId: 2,
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
isBlocked: Boolean(participantProps.isBlocked),
isSelf: Boolean(participantProps.isSelf),
name: participantProps.name,
profileName: participantProps.title,
title: String(participantProps.title),
videoAspectRatio: 1.3,
uuid: generateUuid(),
...getDefaultConversation({
avatarPath: participantProps.avatarPath,
color: Colors[randomColor],
isBlocked: Boolean(participantProps.isBlocked),
name: participantProps.name,
profileName: participantProps.title,
title: String(participantProps.title),
uuid: generateUuid(),
}),
};
}
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
i18n,
onClose: action('on-close'),
ourUuid: 'cf085e6a-e70b-41ec-a310-c198248af13f',
participants: overrideProps.participants || [],
});
@ -62,7 +65,6 @@ story.add('Many Participants', () => {
const props = createProps({
participants: [
createParticipant({
isSelf: true,
title: 'Son Goku',
}),
createParticipant({

View File

@ -9,10 +9,10 @@ import { Avatar } from './Avatar';
import { ContactName } from './conversation/ContactName';
import { InContactsIcon } from './InContactsIcon';
import { LocalizerType } from '../types/Util';
import { GroupCallPeekedParticipantType } from '../types/Calling';
import { sortByTitle } from '../util/sortByTitle';
import { ConversationType } from '../state/ducks/conversations';
interface ParticipantType extends GroupCallPeekedParticipantType {
interface ParticipantType extends ConversationType {
hasAudio?: boolean;
hasVideo?: boolean;
}
@ -20,11 +20,12 @@ interface ParticipantType extends GroupCallPeekedParticipantType {
export type PropsType = {
readonly i18n: LocalizerType;
readonly onClose: () => void;
readonly ourUuid: string;
readonly participants: Array<ParticipantType>;
};
export const CallingParticipantsList = React.memo(
({ i18n, onClose, participants }: PropsType) => {
({ i18n, onClose, ourUuid, participants }: PropsType) => {
const [root, setRoot] = React.useState<HTMLElement | null>(null);
const sortedParticipants = React.useMemo<Array<ParticipantType>>(
@ -100,7 +101,7 @@ export const CallingParticipantsList = React.memo(
title={participant.title}
size={32}
/>
{participant.isSelf ? (
{participant.uuid === ourUuid ? (
<span className="module-calling-participants-list__name">
{i18n('you')}
</span>

View File

@ -105,6 +105,7 @@ story.add('Group Call', () => {
...getCommonActiveCallData(),
callMode: CallMode.Group as CallMode.Group,
connectionState: GroupCallConnectionState.Connected,
conversationsWithSafetyNumberChanges: [],
joinState: GroupCallJoinState.Joined,
maxDevices: 5,
deviceCount: 0,

View File

@ -9,6 +9,7 @@ import {
GroupCallRemoteParticipant,
PropsType,
} from './GroupCallRemoteParticipant';
import { getDefaultConversation } from '../util/getDefaultConversation';
import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json';
@ -37,12 +38,13 @@ const createProps = (
demuxId: 123,
hasRemoteAudio: false,
hasRemoteVideo: true,
isBlocked: Boolean(isBlocked),
isSelf: false,
title:
'Pablo Diego José Francisco de Paula Juan Nepomuceno María de los Remedios Cipriano de la Santísima Trinidad Ruiz y Picasso',
videoAspectRatio: 1.3,
uuid: '992ed3b9-fc9b-47a9-bdb4-e0c7cbb0fda5',
...getDefaultConversation({
isBlocked: Boolean(isBlocked),
title:
'Pablo Diego José Francisco de Paula Juan Nepomuceno María de los Remedios Cipriano de la Santísima Trinidad Ruiz y Picasso',
uuid: '992ed3b9-fc9b-47a9-bdb4-e0c7cbb0fda5',
}),
},
...overrideProps,
});

View File

@ -10,7 +10,7 @@ import { InContactsIcon } from './InContactsIcon';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
type SafetyNumberProps = {
export type SafetyNumberProps = {
contactID: string;
onClose?: () => void;
};

View File

@ -1194,6 +1194,7 @@ export class ConversationModel extends window.Backbone.Model<
isGroupV1AndDisabled: this.isGroupV1AndDisabled(),
isPinned: this.get('isPinned'),
isMissingMandatoryProfileSharing: this.isMissingRequiredProfileSharing(),
isUntrusted: this.isUntrusted(),
isVerified: this.isVerified(),
lastMessage: {
status: this.get('lastMessageStatus')!,
@ -1809,64 +1810,50 @@ export class ConversationModel extends window.Backbone.Model<
return window.textsecure.storage.protocol.setApproval(this.id, true);
}
async safeIsUntrusted(): Promise<boolean> {
return window.textsecure.storage.protocol
.isUntrusted(this.id)
.catch(() => false);
safeIsUntrusted(): boolean {
try {
return window.textsecure.storage.protocol.isUntrusted(this.id);
} catch (err) {
return false;
}
}
async isUntrusted(): Promise<boolean> {
isUntrusted(): boolean {
if (this.isPrivate()) {
return this.safeIsUntrusted();
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!this.contactCollection!.length) {
return Promise.resolve(false);
return false;
}
return Promise.all(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.contactCollection!.map(contact => {
if (contact.isMe()) {
return false;
}
return contact.safeIsUntrusted();
})
).then(results => window._.any(results, result => result));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.contactCollection!.any(contact => {
if (contact.isMe()) {
return false;
}
return contact.safeIsUntrusted();
});
}
async getUntrusted(): Promise<Backbone.Collection> {
// This is a bit ugly because isUntrusted() is async. Could do the work to cache
// it locally, but we really only need it for this call.
getUntrusted(): Backbone.Collection {
if (this.isPrivate()) {
return this.isUntrusted().then(untrusted => {
if (untrusted) {
return new window.Backbone.Collection([this]);
}
return new window.Backbone.Collection();
});
if (this.isUntrusted()) {
return new window.Backbone.Collection([this]);
}
return new window.Backbone.Collection();
}
return Promise.all(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.contactCollection!.map(contact => {
if (contact.isMe()) {
return [false, contact];
}
return Promise.all([contact.isUntrusted(), contact]);
})
).then(results => {
const filtered = window._.filter(results, result => {
const untrusted = result[0];
return untrusted;
});
return new window.Backbone.Collection(
window._.map(filtered, result => {
const contact = result[1];
return contact;
})
);
const results = this.contactCollection!.map(contact => {
if (contact.isMe()) {
return [false, contact];
}
return [contact.isUntrusted(), contact];
});
return new window.Backbone.Collection(
results.filter(result => result[0]).map(result => result[1])
);
}
getSentMessageCount(): number {
@ -1983,7 +1970,15 @@ export class ConversationModel extends window.Backbone.Model<
})
);
const isUntrusted = await this.isUntrusted();
this.trigger('newmessage', model);
const uuid = this.get('uuid');
// Group calls are always with folks that have a UUID
if (isUntrusted && uuid) {
window.reduxActions.calling.keyChanged({ uuid });
}
}
async addVerifiedChange(

View File

@ -667,6 +667,14 @@ export class CallingClass {
return groupCall.getVideoSource(demuxId);
}
public resendGroupCallMediaKeys(conversationId: string): void {
const groupCall = this.getGroupCall(conversationId);
if (!groupCall) {
throw new Error('Could not find matching call');
}
groupCall.resendMediaKeys();
}
private syncGroupCallToRedux(
conversationId: string,
groupCall: GroupCall

View File

@ -71,9 +71,10 @@ export interface ActiveCallStateType {
joinedAt?: number;
hasLocalAudio: boolean;
hasLocalVideo: boolean;
showParticipantsList: boolean;
pip: boolean;
settingsDialogOpen: boolean;
safetyNumberChangedUuids: Array<string>;
showParticipantsList: boolean;
}
export interface CallsByConversationType {
@ -126,6 +127,14 @@ export type HangUpType = {
conversationId: string;
};
type KeyChangedType = {
uuid: string;
};
export type KeyChangeOkType = {
conversationId: string;
};
export type IncomingCallType = {
conversationId: string;
isVideoCall: boolean;
@ -220,6 +229,8 @@ const DECLINE_CALL = 'calling/DECLINE_CALL';
const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
const HANG_UP = 'calling/HANG_UP';
const INCOMING_CALL = 'calling/INCOMING_CALL';
const MARK_CALL_TRUSTED = 'calling/MARK_CALL_TRUSTED';
const MARK_CALL_UNTRUSTED = 'calling/MARK_CALL_UNTRUSTED';
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
const PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED =
'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
@ -282,6 +293,18 @@ type IncomingCallActionType = {
payload: IncomingCallType;
};
type KeyChangedActionType = {
type: 'calling/MARK_CALL_UNTRUSTED';
payload: {
safetyNumberChangedUuids: Array<string>;
};
};
type KeyChangeOkActionType = {
type: 'calling/MARK_CALL_TRUSTED';
payload: null;
};
type OutgoingCallActionType = {
type: 'calling/OUTGOING_CALL';
payload: StartDirectCallType;
@ -353,6 +376,8 @@ export type CallingActionType =
| GroupCallStateChangeActionType
| HangUpActionType
| IncomingCallActionType
| KeyChangedActionType
| KeyChangeOkActionType
| OutgoingCallActionType
| PeekNotConnectedGroupCallFulfilledActionType
| RefreshIODevicesActionType
@ -509,6 +534,56 @@ function hangUp(payload: HangUpType): HangUpActionType {
};
}
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 (activeCall.callMode === CallMode.Group) {
const uuidsChanged = new Set(activeCallState.safetyNumberChangedUuids);
// Iterate over each participant to ensure that the uuid passed in
// matches one of the participants in the group call.
activeCall.remoteParticipants.forEach(participant => {
if (participant.uuid === payload.uuid) {
uuidsChanged.add(participant.uuid);
}
});
const safetyNumberChangedUuids = Array.from(uuidsChanged);
if (safetyNumberChangedUuids.length) {
dispatch({
type: MARK_CALL_UNTRUSTED,
payload: {
safetyNumberChangedUuids,
},
});
}
}
};
}
function keyChangeOk(
payload: KeyChangeOkType
): ThunkAction<void, RootStateType, unknown, KeyChangeOkActionType> {
return dispatch => {
calling.resendGroupCallMediaKeys(payload.conversationId);
dispatch({
type: MARK_CALL_TRUSTED,
payload: null,
});
};
}
function receiveIncomingCall(
payload: IncomingCallType
): IncomingCallActionType {
@ -789,6 +864,8 @@ export const actions = {
declineCall,
groupCallStateChange,
hangUp,
keyChanged,
keyChangeOk,
receiveIncomingCall,
outgoingCall,
peekNotConnectedGroupCall,
@ -896,9 +973,10 @@ export function reducer(
conversationId: action.payload.conversationId,
hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo,
showParticipantsList: false,
pip: false,
safetyNumberChangedUuids: [],
settingsDialogOpen: false,
showParticipantsList: false,
},
};
}
@ -920,9 +998,10 @@ export function reducer(
conversationId: action.payload.conversationId,
hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo,
showParticipantsList: false,
pip: false,
safetyNumberChangedUuids: [],
settingsDialogOpen: false,
showParticipantsList: false,
},
};
}
@ -939,9 +1018,10 @@ export function reducer(
conversationId: action.payload.conversationId,
hasLocalAudio: true,
hasLocalVideo: action.payload.asVideoCall,
showParticipantsList: false,
pip: false,
safetyNumberChangedUuids: [],
settingsDialogOpen: false,
showParticipantsList: false,
},
};
}
@ -1003,9 +1083,10 @@ export function reducer(
conversationId: action.payload.conversationId,
hasLocalAudio: action.payload.hasLocalAudio,
hasLocalVideo: action.payload.hasLocalVideo,
showParticipantsList: false,
pip: false,
safetyNumberChangedUuids: [],
settingsDialogOpen: false,
showParticipantsList: false,
},
};
}
@ -1327,5 +1408,46 @@ export function reducer(
};
}
if (action.type === MARK_CALL_UNTRUSTED) {
const { activeCallState } = state;
if (!activeCallState) {
window.log.warn(
'Cannot mark call as untrusted when there is no active call'
);
return state;
}
const { safetyNumberChangedUuids } = action.payload;
return {
...state,
activeCallState: {
...activeCallState,
pip: false,
safetyNumberChangedUuids,
settingsDialogOpen: false,
showParticipantsList: false,
},
};
}
if (action.type === MARK_CALL_TRUSTED) {
const { activeCallState } = state;
if (!activeCallState) {
window.log.warn(
'Cannot mark call as trusted when there is no active call'
);
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
safetyNumberChangedUuids: [],
},
};
}
return state;
}

View File

@ -58,6 +58,7 @@ export type ConversationType = {
isBlocked?: boolean;
isGroupV1AndDisabled?: boolean;
isPinned?: boolean;
isUntrusted?: boolean;
isVerified?: boolean;
activeAt?: number;
timestamp?: number;

View File

@ -15,17 +15,24 @@ import { getIncomingCall } from '../selectors/calling';
import {
ActiveCallType,
CallMode,
GroupCallPeekedParticipantType,
GroupCallRemoteParticipantType,
} from '../../types/Calling';
import { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
import {
SmartSafetyNumberViewer,
Props as SafetyNumberViewerProps,
} from './SafetyNumberViewer';
function renderDeviceSelection(): JSX.Element {
return <SmartCallingDeviceSelection />;
}
function renderSafetyNumberViewer(props: SafetyNumberViewerProps): JSX.Element {
return <SmartSafetyNumberViewer {...props} />;
}
const getGroupCallVideoFrameSource = callingService.getGroupCallVideoFrameSource.bind(
callingService
);
@ -89,10 +96,9 @@ const mapStateToActiveCallProp = (
],
};
case CallMode.Group: {
const ourUuid = getUserUuid(state);
const conversationsWithSafetyNumberChanges: Array<ConversationType> = [];
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
const peekedParticipants: Array<GroupCallPeekedParticipantType> = [];
const peekedParticipants: Array<ConversationType> = [];
for (let i = 0; i < call.remoteParticipants.length; i += 1) {
const remoteParticipant = call.remoteParticipants[i];
@ -108,23 +114,33 @@ const mapStateToActiveCallProp = (
}
remoteParticipants.push({
avatarPath: remoteConversation.avatarPath,
color: remoteConversation.color,
...remoteConversation,
demuxId: remoteParticipant.demuxId,
firstName: remoteConversation.firstName,
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
isBlocked: Boolean(remoteConversation.isBlocked),
isSelf: remoteParticipant.uuid === ourUuid,
name: remoteConversation.name,
profileName: remoteConversation.profileName,
speakerTime: remoteParticipant.speakerTime,
title: remoteConversation.title,
uuid: remoteParticipant.uuid,
videoAspectRatio: remoteParticipant.videoAspectRatio,
});
}
for (
let i = 0;
i < activeCallState.safetyNumberChangedUuids.length;
i += 1
) {
const uuid = activeCallState.safetyNumberChangedUuids[i];
const remoteConversation = conversationSelectorByUuid(uuid);
if (!remoteConversation) {
window.log.error(
'Remote participant has no corresponding conversation'
);
continue;
}
conversationsWithSafetyNumberChanges.push(remoteConversation);
}
for (let i = 0; i < call.peekInfo.uuids.length; i += 1) {
const peekedParticipantUuid = call.peekInfo.uuids[i];
@ -138,22 +154,14 @@ const mapStateToActiveCallProp = (
continue;
}
peekedParticipants.push({
avatarPath: peekedConversation.avatarPath,
color: peekedConversation.color,
firstName: peekedConversation.firstName,
isSelf: peekedParticipantUuid === ourUuid,
name: peekedConversation.name,
profileName: peekedConversation.profileName,
title: peekedConversation.title,
uuid: peekedParticipantUuid,
});
peekedParticipants.push(peekedConversation);
}
return {
...baseResult,
callMode: CallMode.Group,
connectionState: call.connectionState,
conversationsWithSafetyNumberChanges,
deviceCount: call.peekInfo.deviceCount,
joinState: call.joinState,
maxDevices: call.peekInfo.maxDevices,
@ -197,6 +205,7 @@ const mapStateToProps = (state: StateType) => ({
uuid: getUserUuid(state),
},
renderDeviceSelection,
renderSafetyNumberViewer,
});
const smart = connect(mapStateToProps, mapDispatchToProps);

View File

@ -9,7 +9,7 @@ import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
type Props = {
export type Props = {
contactID: string;
onClose?: () => void;
};

View File

@ -44,6 +44,7 @@ describe('calling duck', () => {
hasLocalAudio: true,
hasLocalVideo: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
},
@ -98,6 +99,7 @@ describe('calling duck', () => {
hasLocalAudio: true,
hasLocalVideo: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
},
@ -201,6 +203,7 @@ describe('calling duck', () => {
hasLocalAudio: true,
hasLocalVideo: true,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
});
@ -576,6 +579,7 @@ describe('calling duck', () => {
hasLocalAudio: true,
hasLocalVideo: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
});
@ -812,6 +816,7 @@ describe('calling duck', () => {
hasLocalAudio: true,
hasLocalVideo: true,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
});
@ -1046,6 +1051,7 @@ describe('calling duck', () => {
hasLocalAudio: true,
hasLocalVideo: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
});

View File

@ -41,6 +41,7 @@ describe('state/selectors/calling', () => {
hasLocalAudio: true,
hasLocalVideo: false,
showParticipantsList: false,
safetyNumberChangedUuids: [],
pip: false,
settingsDialogOpen: false,
},

2
ts/textsecure.d.ts vendored
View File

@ -123,7 +123,7 @@ export type StorageProtocolType = StorageType & {
clearSignedPreKeysStore: () => Promise<void>;
clearSessionStore: () => Promise<void>;
isTrustedIdentity: () => void;
isUntrusted: (id: string) => Promise<boolean>;
isUntrusted: (id: string) => boolean;
storePreKey: (keyId: number, keyPair: KeyPairType) => Promise<void>;
storeSignedPreKey: (
keyId: number,

View File

@ -1,7 +1,6 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ColorType } from './Colors';
import { ConversationType } from '../state/ducks/conversations';
// These are strings (1) for the database (2) for Storybook.
@ -19,6 +18,7 @@ interface ActiveCallBaseType {
pip: boolean;
settingsDialogOpen: boolean;
showParticipantsList: boolean;
showSafetyNumberDialog?: boolean;
}
interface ActiveDirectCallType extends ActiveCallBaseType {
@ -36,10 +36,11 @@ interface ActiveDirectCallType extends ActiveCallBaseType {
interface ActiveGroupCallType extends ActiveCallBaseType {
callMode: CallMode.Group;
connectionState: GroupCallConnectionState;
conversationsWithSafetyNumberChanges: Array<ConversationType>;
joinState: GroupCallJoinState;
maxDevices: number;
deviceCount: number;
peekedParticipants: Array<GroupCallPeekedParticipantType>;
peekedParticipants: Array<ConversationType>;
remoteParticipants: Array<GroupCallRemoteParticipantType>;
}
@ -94,23 +95,10 @@ export enum GroupCallJoinState {
Joined = 2,
}
export interface GroupCallPeekedParticipantType {
avatarPath?: string;
color?: ColorType;
firstName?: string;
isSelf: boolean;
name?: string;
profileName?: string;
title: string;
uuid: string;
}
export interface GroupCallRemoteParticipantType
extends GroupCallPeekedParticipantType {
export interface GroupCallRemoteParticipantType extends ConversationType {
demuxId: number;
hasRemoteAudio: boolean;
hasRemoteVideo: boolean;
isBlocked: boolean;
speakerTime?: number;
videoAspectRatio: number;
}

View File

@ -0,0 +1,24 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { v4 as generateUuid } from 'uuid';
import { ConversationType } from '../state/ducks/conversations';
export function getDefaultConversation(
overrideProps: Partial<ConversationType>
): ConversationType {
if (window.STORYBOOK_ENV !== 'react') {
throw new Error('getDefaultConversation is for storybook only');
}
return {
id: 'guid-1',
lastUpdated: Date.now(),
markedUnread: Boolean(overrideProps.markedUnread),
e164: '+1300555000',
title: 'Alice',
type: 'direct' as const,
uuid: generateUuid(),
...overrideProps,
};
}

View File

@ -14400,7 +14400,7 @@
"rule": "React-useRef",
"path": "ts/components/CallingLobby.tsx",
"line": " const localVideoRef = React.useRef(null);",
"lineNumber": 67,
"lineNumber": 64,
"reasonCategory": "usageTrusted",
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Used to get the local video element for rendering."

View File

@ -2992,7 +2992,7 @@ Whisper.ConversationView = Whisper.View.extend({
return unverifiedContacts;
}
const untrustedContacts = await this.model.getUntrusted();
const untrustedContacts = this.model.getUntrusted();
if (options.force) {
if (untrustedContacts.length) {

2
ts/window.d.ts vendored
View File

@ -479,6 +479,8 @@ declare global {
getServerTrustRoot: () => WhatIsThis;
readyForUpdates: () => void;
STORYBOOK_ENV?: string;
// Flags
isGroupCallingEnabled: () => boolean;
}